From 61094188f6f466e2ed9c30b3988af8597f4aa9b6 Mon Sep 17 00:00:00 2001 From: muamars Date: Wed, 18 Mar 2026 14:47:06 +0700 Subject: [PATCH] update: fixing detail penjemputan dan offline pages --- .../DetailController.cs | 707 ++++++++++-------- Data/detail-penjemputan.json | 38 - Models/DetailPenjemputanModels.cs | 56 +- Program.cs | 10 + Services/DetailPenjemputanService.cs | 419 +++++------ Services/FileDetailPenjemputanStore.cs | 340 +++++++++ Services/IDetailPenjemputanStore.cs | 14 + .../Shared/Partials/_Scripts.cshtml | 2 +- wwwroot/driver/css/watch.css | 144 +--- .../driver/js/detail-penjemputan-non-tps.js | 433 ++++++----- wwwroot/driver/js/detail-penjemputan-tps.js | 532 +++++++------ .../json/detail-penjemputan-non-tps.json | 6 +- wwwroot/driver/manifest.json | 18 +- wwwroot/driver/serviceworker.js | 49 +- 14 files changed, 1626 insertions(+), 1142 deletions(-) create mode 100644 Services/FileDetailPenjemputanStore.cs create mode 100644 Services/IDetailPenjemputanStore.cs diff --git a/Controllers/SpjDriverUpstController/DetailController.cs b/Controllers/SpjDriverUpstController/DetailController.cs index b995409..b9a0e2a 100644 --- a/Controllers/SpjDriverUpstController/DetailController.cs +++ b/Controllers/SpjDriverUpstController/DetailController.cs @@ -31,27 +31,168 @@ namespace eSPJ.Controllers.SpjDriverUpstController _env = env; } - private static string ResolveDraftKey(string? draftKey, string? sessionKey, string? spjDetailId = null, string? lokasiAngkutId = null) + private static string SanitizePathSegment(string? value, string fallback = "umum") { - var rawKey = !string.IsNullOrWhiteSpace(draftKey) - ? draftKey - : !string.IsNullOrWhiteSpace(sessionKey) - ? sessionKey - : $"non-tps-{spjDetailId}-{lokasiAngkutId}"; + var safe = string.Concat((value ?? string.Empty).Trim().Select(c => + char.IsLetterOrDigit(c) || c == '-' || c == '_' + ? c + : '-')); - return string.Concat((rawKey ?? string.Empty).Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_')); + safe = Regex.Replace(safe, "-+", "-").Trim('-'); + return string.IsNullOrWhiteSpace(safe) ? fallback : safe; } - private string GetUploadDirectory(string dateFolder) + private string GetUploadDirectory(string dateFolder, string? nomorSpj = null, string? namaTps = null) { - var uploadDir = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", dateFolder); + var uploadDir = Path.Combine( + _env.ContentRootPath, + "uploads", + "penjemputan", + dateFolder, + SanitizePathSegment(nomorSpj, "spj-umum"), + SanitizePathSegment(namaTps, "tps-1")); Directory.CreateDirectory(uploadDir); return uploadDir; } - private static string BuildUploadUrl(string dateFolder, string fileName) + private static string BuildUploadUrl(string dateFolder, string fileName, string? nomorSpj = null, string? namaTps = null) { - return $"/uploads/penjemputan/{dateFolder}/{fileName}"; + var spjFolder = SanitizePathSegment(nomorSpj, "spj-umum"); + var tpsFolder = SanitizePathSegment(namaTps, "tps-1"); + return $"/uploads/penjemputan/{dateFolder}/{spjFolder}/{tpsFolder}/{fileName}"; + } + + private async Task FindExistingRecordAsync(string? nomorSpj, string? spjDetailId, string? lokasiAngkutId, string? namaTps) + { + if (string.IsNullOrWhiteSpace(nomorSpj)) + { + return null; + } + + return await _detailService.GetRecordDetailAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps); + } + + private static List MapRecordTimbangan(List? items) + { + return (items ?? new List()) + .Select(item => new RecordTimbanganItem + { + Berat = item.Berat?.FirstOrDefault() ?? 0, + JenisSampah = item.JenisSampah != null && item.JenisSampah.Count > 0 + ? item.JenisSampah[0].ToString() + : "Residu", + FotoFileName = item.FotoFileName ?? string.Empty, + Uploaded = item.IsUploaded, + OcrInfo = item.IsUploaded ? "Foto dari server." : "OCR: belum diproses." + }) + .ToList(); + } + + private static RecordSaveRequest BuildRecordSaveRequest(TpsData? existingRecord, string? nomorSpj, string? namaTps, string? spjDetailId, string? lokasiAngkutId) + { + return new RecordSaveRequest + { + NomorSpj = nomorSpj ?? existingRecord?.NomorSpj ?? string.Empty, + NamaTps = namaTps ?? existingRecord?.Name ?? string.Empty, + SpjDetailId = spjDetailId ?? existingRecord?.SpjDetailId ?? string.Empty, + LokasiAngkutId = lokasiAngkutId ?? existingRecord?.LokasiAngkutId ?? string.Empty, + Latitude = existingRecord?.Latitude ?? string.Empty, + Longitude = existingRecord?.Longitude ?? string.Empty, + AlamatJalan = existingRecord?.AlamatJalan ?? string.Empty, + WaktuKedatangan = existingRecord?.WaktuKedatangan ?? string.Empty, + FotoKedatanganFileNames = existingRecord?.FotoKedatangan != null ? new List(existingRecord.FotoKedatangan) : new List(), + FotoKedatanganUploaded = existingRecord?.FotoKedatanganUploaded ?? false, + Timbangan = MapRecordTimbangan(existingRecord?.Timbangan), + TotalOrganik = existingRecord?.TotalOrganik ?? 0, + TotalAnorganik = existingRecord?.TotalAnorganik ?? 0, + TotalResidu = existingRecord?.TotalResidu ?? 0, + TotalTimbangan = existingRecord?.TotalTimbangan ?? 0, + FotoPetugasFileNames = existingRecord?.FotoPetugas != null ? new List(existingRecord.FotoPetugas) : new List(), + FotoPetugasUploaded = existingRecord?.FotoPetugasUploaded ?? false, + NamaPetugas = existingRecord?.NamaPetugas ?? string.Empty, + IsSubmit = existingRecord?.IsSubmit ?? false, + }; + } + + private static void RecalculateTotals(RecordSaveRequest request) + { + var timbangan = request.Timbangan ?? new List(); + + request.TotalOrganik = timbangan + .Where(item => string.Equals(item.JenisSampah, "Organik", StringComparison.OrdinalIgnoreCase)) + .Sum(item => item.Berat); + + request.TotalAnorganik = timbangan + .Where(item => string.Equals(item.JenisSampah, "Anorganik", StringComparison.OrdinalIgnoreCase)) + .Sum(item => item.Berat); + + request.TotalResidu = timbangan + .Where(item => string.Equals(item.JenisSampah, "Residu", StringComparison.OrdinalIgnoreCase)) + .Sum(item => item.Berat); + + request.TotalTimbangan = timbangan.Sum(item => item.Berat); + } + + private async Task SaveUploadedRecordAsync( + bool isTps, + string? nomorSpj, + string? namaTps, + string? spjDetailId, + string? lokasiAngkutId, + Action applyChanges) + { + var existingRecord = await FindExistingRecordAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps); + var request = BuildRecordSaveRequest(existingRecord, nomorSpj, namaTps, spjDetailId, lokasiAngkutId); + + applyChanges(request); + RecalculateTotals(request); + + return isTps + ? await _detailService.SaveRecordTpsAsync(request) + : await _detailService.SaveRecordNonTpsAsync(request); + } + + private void ApplyNoCacheHeaders() + { + Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"; + Response.Headers["Pragma"] = "no-cache"; + Response.Headers["Expires"] = "0"; + } + + private async Task ResolveRecordSaveRequestAsync() + { + Request.EnableBuffering(); + + if (Request.Body.CanSeek) + { + Request.Body.Position = 0; + } + + using var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); + var rawBody = await reader.ReadToEndAsync(); + + if (Request.Body.CanSeek) + { + Request.Body.Position = 0; + } + + if (string.IsNullOrWhiteSpace(rawBody)) + { + return null; + } + + try + { + return JsonSerializer.Deserialize(rawBody, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + } + catch (JsonException ex) + { + _logger.LogWarning(ex, "Gagal parse body save-record. Body: {RawBody}", rawBody); + return null; + } } [HttpGet("")] @@ -89,14 +230,96 @@ namespace eSPJ.Controllers.SpjDriverUpstController return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesaiTanpaTps.cshtml"); } + [HttpGet("api/submitted")] + [IgnoreAntiforgeryToken] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public async Task GetSubmittedBySpj([FromQuery] string nomorSpj) + { + ApplyNoCacheHeaders(); + + if (string.IsNullOrWhiteSpace(nomorSpj)) + { + return BadRequest(new { success = false, message = "nomorSpj wajib diisi." }); + } + + var items = await _detailService.GetSubmittedByNomorSpjAsync(nomorSpj); + return Ok(new { success = true, items }); + } + + [HttpGet("api/records")] + [IgnoreAntiforgeryToken] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public async Task GetRecordsBySpj([FromQuery] string nomorSpj) + { + ApplyNoCacheHeaders(); + + if (string.IsNullOrWhiteSpace(nomorSpj)) + { + return BadRequest(new { success = false, message = "nomorSpj wajib diisi." }); + } + + var items = await _detailService.GetRecordsByNomorSpjAsync(nomorSpj); + return Ok(new { success = true, items }); + } + + [HttpGet("api/submitted/detail")] + [IgnoreAntiforgeryToken] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public async Task GetSubmittedDetail( + [FromQuery] string nomorSpj, + [FromQuery] string? spjDetailId = null, + [FromQuery] string? lokasiAngkutId = null, + [FromQuery] string? namaTps = null) + { + ApplyNoCacheHeaders(); + + if (string.IsNullOrWhiteSpace(nomorSpj)) + { + return BadRequest(new { success = false, message = "nomorSpj wajib diisi." }); + } + + var item = await _detailService.GetSubmittedDetailAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps); + return Ok(new { success = true, hasData = item != null, item }); + } + + [HttpGet("api/records/detail")] + [IgnoreAntiforgeryToken] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public async Task GetRecordDetail( + [FromQuery] string nomorSpj, + [FromQuery] string? spjDetailId = null, + [FromQuery] string? lokasiAngkutId = null, + [FromQuery] string? namaTps = null) + { + ApplyNoCacheHeaders(); + + if (string.IsNullOrWhiteSpace(nomorSpj)) + { + return BadRequest(new { success = false, message = "nomorSpj wajib diisi." }); + } + + var item = await _detailService.GetRecordDetailAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps); + return Ok(new { success = true, hasData = item != null, item }); + } + [HttpPost("")] [ValidateAntiForgeryToken] public async Task Submit([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 _detailService.SubmitPenjemputanAsync(request); - + + if (isAjaxRequest) + { + return result.Success + ? Ok(result) + : BadRequest(result); + } + if (result.Success) { TempData["Success"] = result.Message; @@ -111,94 +334,54 @@ namespace eSPJ.Controllers.SpjDriverUpstController catch (Exception ex) { _logger.LogError(ex, "Error submitting 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(Index)); } } - [HttpPost("save-draft-non-tps")] + [HttpPost("save-record-non-tps")] [IgnoreAntiforgeryToken] - public async Task SaveDraftNonTps([FromBody] DraftSaveRequest request) + public async Task SaveRecordNonTps() { + var request = await ResolveRecordSaveRequestAsync(); + if (request == null) - return BadRequest(new DraftSaveResponse { Success = false, Message = "Request tidak valid." }); + return BadRequest(new RecordSaveResponse { Success = false, Message = "Request tidak valid." }); - request.DraftKey = ResolveDraftKey(request.DraftKey, request.SessionKey, request.SpjDetailId, request.LokasiAngkutId); - request.SessionKey = request.DraftKey; - if (string.IsNullOrWhiteSpace(request.DraftKey)) - return BadRequest(new DraftSaveResponse { Success = false, Message = "Draft key tidak valid." }); - - var result = await _detailService.SaveDraftNonTpsAsync(request); + var result = await _detailService.SaveRecordNonTpsAsync(request); return result.Success ? Ok(result) : StatusCode(500, result); } - [HttpGet("load-draft-non-tps")] + [HttpPost("save-record")] [IgnoreAntiforgeryToken] - public async Task LoadDraftNonTps([FromQuery] string? draftKey = null, [FromQuery] string? sessionKey = null) + public async Task SaveRecord() { - var key = ResolveDraftKey(draftKey, sessionKey); - if (string.IsNullOrWhiteSpace(key)) - return Ok(new DraftLoadResponse { Success = true, HasDraft = false, Message = "Draft key kosong." }); - var result = await _detailService.LoadDraftNonTpsAsync(key); - return Ok(result); - } + var request = await ResolveRecordSaveRequestAsync(); - [HttpDelete("delete-draft-non-tps")] - [IgnoreAntiforgeryToken] - public async Task DeleteDraftNonTps([FromQuery] string? draftKey = null, [FromQuery] string? sessionKey = null) - { - var key = ResolveDraftKey(draftKey, sessionKey); - if (string.IsNullOrWhiteSpace(key)) - return Ok(new { success = false }); - var ok = await _detailService.DeleteDraftNonTpsAsync(key); - return Ok(new { success = ok }); - } - - [HttpPost("save-draft")] - [IgnoreAntiforgeryToken] - public async Task SaveDraft([FromBody] DraftSaveRequest request) - { if (request == null) - return BadRequest(new DraftSaveResponse { Success = false, Message = "Request tidak valid." }); + return BadRequest(new RecordSaveResponse { Success = false, Message = "Request tidak valid." }); - request.DraftKey = ResolveDraftKey(request.DraftKey, request.SessionKey, request.SpjDetailId, request.LokasiAngkutId); - request.SessionKey = request.DraftKey; - if (string.IsNullOrWhiteSpace(request.DraftKey)) - return BadRequest(new DraftSaveResponse { Success = false, Message = "Draft key tidak valid." }); - - var result = await _detailService.SaveDraftTpsAsync(request); + var result = await _detailService.SaveRecordTpsAsync(request); return result.Success ? Ok(result) : StatusCode(500, result); } - [HttpGet("load-draft")] - [IgnoreAntiforgeryToken] - public async Task LoadDraft([FromQuery] string? draftKey = null, [FromQuery] string? sessionKey = null) - { - var key = ResolveDraftKey(draftKey, sessionKey); - if (string.IsNullOrWhiteSpace(key)) - return Ok(new DraftLoadResponse { Success = true, HasDraft = false, Message = "Draft key kosong." }); - var result = await _detailService.LoadDraftTpsAsync(key); - return Ok(result); - } - - [HttpDelete("delete-draft")] - [IgnoreAntiforgeryToken] - public async Task DeleteDraft([FromQuery] string? draftKey = null, [FromQuery] string? sessionKey = null) - { - var key = ResolveDraftKey(draftKey, sessionKey); - if (string.IsNullOrWhiteSpace(key)) - return Ok(new { success = false }); - var ok = await _detailService.DeleteDraftTpsAsync(key); - return Ok(new { success = ok }); - } - [HttpPost("upload-foto-kedatangan-non-tps")] [IgnoreAntiforgeryToken] public async Task UploadFotoKedatanganNonTps( [FromForm] List? FotoKedatangan, - [FromForm] string? DraftKey, - [FromForm] string? SessionKey, + [FromForm] string? NomorSpj, + [FromForm] string? NamaTps, [FromForm] string? SpjDetailId, [FromForm] string? LokasiAngkutId, [FromForm] string? WaktuKedatangan, @@ -210,7 +393,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController return BadRequest(new { success = false, message = "Tidak ada foto." }); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); - var uploadDir = GetUploadDirectory(dateFolder); + var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps); var fileNames = new List(); foreach (var file in FotoKedatangan) @@ -223,42 +406,27 @@ namespace eSPJ.Controllers.SpjDriverUpstController await file.CopyToAsync(stream); fileNames.Add(name); } - var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList(); + var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList(); - var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); - if (!string.IsNullOrWhiteSpace(resolvedDraftKey)) - { - var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey); - var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; - draft.FotoKedatanganFileNames = fileUrls; - draft.FotoKedatanganUploaded = true; - if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId; - if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId; - if (!string.IsNullOrWhiteSpace(WaktuKedatangan)) draft.WaktuKedatangan = WaktuKedatangan; - if (!string.IsNullOrWhiteSpace(Latitude)) draft.Latitude = Latitude; - if (!string.IsNullOrWhiteSpace(Longitude)) draft.Longitude = Longitude; - if (!string.IsNullOrWhiteSpace(AlamatJalan)) draft.AlamatJalan = AlamatJalan; - await _detailService.SaveDraftNonTpsAsync(new DraftSaveRequest + var saveResult = await SaveUploadedRecordAsync( + isTps: false, + nomorSpj: NomorSpj, + namaTps: NamaTps, + spjDetailId: SpjDetailId, + lokasiAngkutId: LokasiAngkutId, + applyChanges: request => { - DraftKey = resolvedDraftKey, - SessionKey = resolvedDraftKey, - LokasiAngkutId = draft.LokasiAngkutId, - SpjDetailId = draft.SpjDetailId, - Latitude = draft.Latitude, - Longitude = draft.Longitude, - AlamatJalan = draft.AlamatJalan, - WaktuKedatangan = draft.WaktuKedatangan, - FotoKedatanganFileNames = fileUrls, - FotoKedatanganUploaded = true, - Timbangan = draft.Timbangan, - TotalOrganik = draft.TotalOrganik, - TotalAnorganik = draft.TotalAnorganik, - TotalResidu = draft.TotalResidu, - TotalTimbangan = draft.TotalTimbangan, - FotoPetugasFileNames = draft.FotoPetugasFileNames, - FotoPetugasUploaded = draft.FotoPetugasUploaded, - NamaPetugas = draft.NamaPetugas + 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." }); @@ -268,8 +436,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController [IgnoreAntiforgeryToken] public async Task UploadFotoTimbanganNonTps( [FromForm] IFormFile? FotoTimbangan, - [FromForm] string? DraftKey, - [FromForm] string? SessionKey, + [FromForm] string? NomorSpj, + [FromForm] string? NamaTps, [FromForm] string? SpjDetailId, [FromForm] string? LokasiAngkutId, [FromForm] int ItemIndex, @@ -280,7 +448,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController return BadRequest(new { success = false, message = "Tidak ada foto." }); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); - var uploadDir = GetUploadDirectory(dateFolder); + var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps); var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant(); var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant(); @@ -289,45 +457,33 @@ namespace eSPJ.Controllers.SpjDriverUpstController var filePath = Path.Combine(uploadDir, name); await using var stream = new FileStream(filePath, FileMode.Create); await FotoTimbangan.CopyToAsync(stream); - var fileUrl = BuildUploadUrl(dateFolder, name); + var fileUrl = BuildUploadUrl(dateFolder, name, NomorSpj, NamaTps); - var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); - if (!string.IsNullOrWhiteSpace(resolvedDraftKey)) - { - var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey); - var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; - while (draft.Timbangan.Count <= ItemIndex) - draft.Timbangan.Add(new DraftTimbanganItem()); - if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId; - if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId; - draft.Timbangan[ItemIndex] = new DraftTimbanganItem + var saveResult = await SaveUploadedRecordAsync( + isTps: false, + nomorSpj: NomorSpj, + namaTps: NamaTps, + spjDetailId: SpjDetailId, + lokasiAngkutId: LokasiAngkutId, + applyChanges: request => { - FotoFileName = fileUrl, - JenisSampah = JenisSampah ?? "Residu", - Berat = Berat, - Uploaded = true - }; - await _detailService.SaveDraftNonTpsAsync(new DraftSaveRequest - { - DraftKey = resolvedDraftKey, - SessionKey = resolvedDraftKey, - LokasiAngkutId = draft.LokasiAngkutId, - SpjDetailId = draft.SpjDetailId, - Latitude = draft.Latitude, - Longitude = draft.Longitude, - AlamatJalan = draft.AlamatJalan, - WaktuKedatangan = draft.WaktuKedatangan, - FotoKedatanganFileNames = draft.FotoKedatanganFileNames, - FotoKedatanganUploaded = draft.FotoKedatanganUploaded, - Timbangan = draft.Timbangan, - TotalOrganik = draft.TotalOrganik, - TotalAnorganik = draft.TotalAnorganik, - TotalResidu = draft.TotalResidu, - TotalTimbangan = draft.TotalTimbangan, - FotoPetugasFileNames = draft.FotoPetugasFileNames, - FotoPetugasUploaded = draft.FotoPetugasUploaded, - NamaPetugas = draft.NamaPetugas + while (request.Timbangan.Count <= ItemIndex) + { + request.Timbangan.Add(new RecordTimbanganItem()); + } + + request.Timbangan[ItemIndex] = new RecordTimbanganItem + { + FotoFileName = fileUrl, + JenisSampah = JenisSampah ?? "Residu", + Berat = Berat, + Uploaded = true + }; }); + + if (!saveResult.Success) + { + return StatusCode(500, new { success = false, message = saveResult.Message }); } return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." }); @@ -337,8 +493,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController [IgnoreAntiforgeryToken] public async Task UploadFotoPetugasNonTps( [FromForm] List? FotoPetugas, - [FromForm] string? DraftKey, - [FromForm] string? SessionKey, + [FromForm] string? NomorSpj, + [FromForm] string? NamaTps, [FromForm] string? SpjDetailId, [FromForm] string? LokasiAngkutId, [FromForm] string? NamaPetugas) @@ -347,7 +503,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController return BadRequest(new { success = false, message = "Tidak ada foto." }); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); - var uploadDir = GetUploadDirectory(dateFolder); + var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps); var fileNames = new List(); foreach (var file in FotoPetugas) @@ -360,39 +516,24 @@ namespace eSPJ.Controllers.SpjDriverUpstController await file.CopyToAsync(stream); fileNames.Add(name); } - var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList(); + var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList(); - var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); - if (!string.IsNullOrWhiteSpace(resolvedDraftKey)) - { - var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey); - var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; - draft.FotoPetugasFileNames = fileUrls; - draft.FotoPetugasUploaded = true; - if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId; - if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId; - if (!string.IsNullOrWhiteSpace(NamaPetugas)) draft.NamaPetugas = NamaPetugas; - await _detailService.SaveDraftNonTpsAsync(new DraftSaveRequest + var saveResult = await SaveUploadedRecordAsync( + isTps: false, + nomorSpj: NomorSpj, + namaTps: NamaTps, + spjDetailId: SpjDetailId, + lokasiAngkutId: LokasiAngkutId, + applyChanges: request => { - DraftKey = resolvedDraftKey, - SessionKey = resolvedDraftKey, - LokasiAngkutId = draft.LokasiAngkutId, - SpjDetailId = draft.SpjDetailId, - Latitude = draft.Latitude, - Longitude = draft.Longitude, - AlamatJalan = draft.AlamatJalan, - WaktuKedatangan = draft.WaktuKedatangan, - FotoKedatanganFileNames = draft.FotoKedatanganFileNames, - FotoKedatanganUploaded = draft.FotoKedatanganUploaded, - Timbangan = draft.Timbangan, - TotalOrganik = draft.TotalOrganik, - TotalAnorganik = draft.TotalAnorganik, - TotalResidu = draft.TotalResidu, - TotalTimbangan = draft.TotalTimbangan, - FotoPetugasFileNames = fileUrls, - FotoPetugasUploaded = true, - NamaPetugas = draft.NamaPetugas + 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." }); @@ -402,8 +543,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController [IgnoreAntiforgeryToken] public async Task UploadFotoKedatangan( [FromForm] List? FotoKedatangan, - [FromForm] string? DraftKey, - [FromForm] string? SessionKey, + [FromForm] string? NomorSpj, + [FromForm] string? NamaTps, [FromForm] string? SpjDetailId, [FromForm] string? LokasiAngkutId, [FromForm] string? WaktuKedatangan, @@ -415,7 +556,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController return BadRequest(new { success = false, message = "Tidak ada foto." }); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); - var uploadDir = GetUploadDirectory(dateFolder); + var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps); var fileNames = new List(); foreach (var file in FotoKedatangan) @@ -428,42 +569,27 @@ namespace eSPJ.Controllers.SpjDriverUpstController await file.CopyToAsync(stream); fileNames.Add(name); } - var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList(); + var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList(); - var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); - if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps)) - { - var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps); - var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; - draft.FotoKedatanganFileNames = fileUrls; - draft.FotoKedatanganUploaded = true; - if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId; - if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId; - if (!string.IsNullOrWhiteSpace(WaktuKedatangan)) draft.WaktuKedatangan = WaktuKedatangan; - if (!string.IsNullOrWhiteSpace(Latitude)) draft.Latitude = Latitude; - if (!string.IsNullOrWhiteSpace(Longitude)) draft.Longitude = Longitude; - if (!string.IsNullOrWhiteSpace(AlamatJalan)) draft.AlamatJalan = AlamatJalan; - await _detailService.SaveDraftTpsAsync(new DraftSaveRequest + var saveResult = await SaveUploadedRecordAsync( + isTps: true, + nomorSpj: NomorSpj, + namaTps: NamaTps, + spjDetailId: SpjDetailId, + lokasiAngkutId: LokasiAngkutId, + applyChanges: request => { - DraftKey = resolvedDraftKeyTps, - SessionKey = resolvedDraftKeyTps, - LokasiAngkutId = draft.LokasiAngkutId, - SpjDetailId = draft.SpjDetailId, - Latitude = draft.Latitude, - Longitude = draft.Longitude, - AlamatJalan = draft.AlamatJalan, - WaktuKedatangan = draft.WaktuKedatangan, - FotoKedatanganFileNames = fileUrls, - FotoKedatanganUploaded = true, - Timbangan = draft.Timbangan, - TotalOrganik = draft.TotalOrganik, - TotalAnorganik = draft.TotalAnorganik, - TotalResidu = draft.TotalResidu, - TotalTimbangan = draft.TotalTimbangan, - FotoPetugasFileNames = draft.FotoPetugasFileNames, - FotoPetugasUploaded = draft.FotoPetugasUploaded, - NamaPetugas = draft.NamaPetugas + 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." }); @@ -473,8 +599,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController [IgnoreAntiforgeryToken] public async Task UploadFotoTimbangan( [FromForm] IFormFile? FotoTimbangan, - [FromForm] string? DraftKey, - [FromForm] string? SessionKey, + [FromForm] string? NomorSpj, + [FromForm] string? NamaTps, [FromForm] string? SpjDetailId, [FromForm] string? LokasiAngkutId, [FromForm] int ItemIndex, @@ -485,7 +611,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController return BadRequest(new { success = false, message = "Tidak ada foto." }); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); - var uploadDir = GetUploadDirectory(dateFolder); + var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps); var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant(); var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant(); @@ -494,45 +620,33 @@ namespace eSPJ.Controllers.SpjDriverUpstController var filePath = Path.Combine(uploadDir, name); await using var stream = new FileStream(filePath, FileMode.Create); await FotoTimbangan.CopyToAsync(stream); - var fileUrl = BuildUploadUrl(dateFolder, name); + var fileUrl = BuildUploadUrl(dateFolder, name, NomorSpj, NamaTps); - var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); - if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps)) - { - var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps); - var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; - while (draft.Timbangan.Count <= ItemIndex) - draft.Timbangan.Add(new DraftTimbanganItem()); - if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId; - if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId; - draft.Timbangan[ItemIndex] = new DraftTimbanganItem + var saveResult = await SaveUploadedRecordAsync( + isTps: true, + nomorSpj: NomorSpj, + namaTps: NamaTps, + spjDetailId: SpjDetailId, + lokasiAngkutId: LokasiAngkutId, + applyChanges: request => { - FotoFileName = fileUrl, - JenisSampah = JenisSampah ?? "Residu", - Berat = Berat, - Uploaded = true - }; - await _detailService.SaveDraftTpsAsync(new DraftSaveRequest - { - DraftKey = resolvedDraftKeyTps, - SessionKey = resolvedDraftKeyTps, - LokasiAngkutId = draft.LokasiAngkutId, - SpjDetailId = draft.SpjDetailId, - Latitude = draft.Latitude, - Longitude = draft.Longitude, - AlamatJalan = draft.AlamatJalan, - WaktuKedatangan = draft.WaktuKedatangan, - FotoKedatanganFileNames = draft.FotoKedatanganFileNames, - FotoKedatanganUploaded = draft.FotoKedatanganUploaded, - Timbangan = draft.Timbangan, - TotalOrganik = draft.TotalOrganik, - TotalAnorganik = draft.TotalAnorganik, - TotalResidu = draft.TotalResidu, - TotalTimbangan = draft.TotalTimbangan, - FotoPetugasFileNames = draft.FotoPetugasFileNames, - FotoPetugasUploaded = draft.FotoPetugasUploaded, - NamaPetugas = draft.NamaPetugas + while (request.Timbangan.Count <= ItemIndex) + { + request.Timbangan.Add(new RecordTimbanganItem()); + } + + request.Timbangan[ItemIndex] = new RecordTimbanganItem + { + FotoFileName = fileUrl, + JenisSampah = JenisSampah ?? "Residu", + Berat = Berat, + Uploaded = true + }; }); + + if (!saveResult.Success) + { + return StatusCode(500, new { success = false, message = saveResult.Message }); } return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." }); @@ -542,8 +656,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController [IgnoreAntiforgeryToken] public async Task UploadFotoPetugas( [FromForm] List? FotoPetugas, - [FromForm] string? DraftKey, - [FromForm] string? SessionKey, + [FromForm] string? NomorSpj, + [FromForm] string? NamaTps, [FromForm] string? SpjDetailId, [FromForm] string? LokasiAngkutId, [FromForm] string? NamaPetugas) @@ -552,7 +666,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController return BadRequest(new { success = false, message = "Tidak ada foto." }); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); - var uploadDir = GetUploadDirectory(dateFolder); + var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps); var fileNames = new List(); foreach (var file in FotoPetugas) @@ -565,39 +679,24 @@ namespace eSPJ.Controllers.SpjDriverUpstController await file.CopyToAsync(stream); fileNames.Add(name); } - var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList(); + var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList(); - var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); - if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps)) - { - var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps); - var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; - draft.FotoPetugasFileNames = fileUrls; - draft.FotoPetugasUploaded = true; - if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId; - if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId; - if (!string.IsNullOrWhiteSpace(NamaPetugas)) draft.NamaPetugas = NamaPetugas; - await _detailService.SaveDraftTpsAsync(new DraftSaveRequest + var saveResult = await SaveUploadedRecordAsync( + isTps: true, + nomorSpj: NomorSpj, + namaTps: NamaTps, + spjDetailId: SpjDetailId, + lokasiAngkutId: LokasiAngkutId, + applyChanges: request => { - DraftKey = resolvedDraftKeyTps, - SessionKey = resolvedDraftKeyTps, - LokasiAngkutId = draft.LokasiAngkutId, - SpjDetailId = draft.SpjDetailId, - Latitude = draft.Latitude, - Longitude = draft.Longitude, - AlamatJalan = draft.AlamatJalan, - WaktuKedatangan = draft.WaktuKedatangan, - FotoKedatanganFileNames = draft.FotoKedatanganFileNames, - FotoKedatanganUploaded = draft.FotoKedatanganUploaded, - Timbangan = draft.Timbangan, - TotalOrganik = draft.TotalOrganik, - TotalAnorganik = draft.TotalAnorganik, - TotalResidu = draft.TotalResidu, - TotalTimbangan = draft.TotalTimbangan, - FotoPetugasFileNames = fileUrls, - FotoPetugasUploaded = true, - NamaPetugas = draft.NamaPetugas + 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." }); @@ -660,31 +759,9 @@ namespace eSPJ.Controllers.SpjDriverUpstController - Jawab hanya angka dengan format 2 digit desimal pakai titik (contoh: 54.45). - Jika tidak terbaca jawab: UNREADABLE - Fokus pada angka layar LED merah yang menyala. - - Saya berikan 3 contoh foto timbangan yang benar: - Foto 1 = 75.23 - Foto 2 = 79.86 - Foto 3 = 54.45 - - Sekarang baca angka pada foto terakhir." - }, - - new - { - type = "image_url", - image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888384/Notes_-_2026-02-11_08.52.31_wonhbm.jpg" } - }, - - new - { - type = "image_url", - image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888429/Notes_-_2026-02-11_08.52.34_xairzy.jpg" } - }, - - new - { - type = "image_url", - image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888473/ChatGPT_Image_Feb_11_2026_03_00_33_PM_ujhdlw.png" } + - Abaikan refleksi atau pantulan cahaya yang mungkin muncul di layar. + - Abaikan timestamp seperti tanggal, jam, atau informasi lain yang biasanya muncul di layar timbangan. + " }, new @@ -702,7 +779,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController var request = new HttpRequestMessage(HttpMethod.Post, "https://openrouter.ai/api/v1/chat/completions"); request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {apiKey}"); request.Headers.TryAddWithoutValidation("Accept", "application/json"); - request.Headers.TryAddWithoutValidation("HTTP-Referer", "https://yourdomain.com"); + request.Headers.TryAddWithoutValidation("HTTP-Referer", "https://pesapakawan.dinaslhdki.id"); request.Headers.TryAddWithoutValidation("X-Title", "eSPJ OCR Timbangan"); request.Content = new StringContent(json, Encoding.UTF8, "application/json"); diff --git a/Data/detail-penjemputan.json b/Data/detail-penjemputan.json index 8a316ba..e69de29 100644 --- a/Data/detail-penjemputan.json +++ b/Data/detail-penjemputan.json @@ -1,38 +0,0 @@ -[ - { - "Name": "TPS A", - "Index": 0, - "Latitude": "-6.481670", - "Longitude": "106.854000", - "AlamatJalan": "Cibinong, Bogor, West Java, Java, 16911, Indonesia", - "WaktuKedatangan": "16/03/2026, 09.37.44", - "FotoKedatangan": [ - "kedatangan_ff132727-8942-402e-a612-ac3436e905b9.png" - ], - "FotoKedatanganUploaded": true, - "Timbangan": [ - { - "FotoFileName": "timbangan_0de03ef1-dbdd-4643-a0e6-ac7670b12679.png", - "Berat": [ - 75.23 - ], - "LokasiAngkut": [], - "JenisSampah": [ - 2 - ], - "IsUploaded": true, - "WaktuUpload": "2026-03-16T09:38:37.1840709+07:00" - } - ], - "TotalOrganik": 0, - "TotalAnorganik": 0, - "TotalResidu": 75.23, - "TotalTimbangan": 75.23, - "FotoPetugas": [ - "petugas_dffeaabf-4f30-41dd-9281-e2c33164e2c6.jpg" - ], - "FotoPetugasUploaded": true, - "NamaPetugas": "usmannn", - "Submitted": true - } -] \ No newline at end of file diff --git a/Models/DetailPenjemputanModels.cs b/Models/DetailPenjemputanModels.cs index 7305e19..13eda7c 100644 --- a/Models/DetailPenjemputanModels.cs +++ b/Models/DetailPenjemputanModels.cs @@ -19,6 +19,9 @@ namespace eSPJ.Models public class TpsData { + public string NomorSpj { get; set; } = string.Empty; + public string LokasiAngkutId { get; set; } = string.Empty; + public string SpjDetailId { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public int Index { get; set; } public string Latitude { get; set; } = string.Empty; @@ -35,11 +38,16 @@ namespace eSPJ.Models public List FotoPetugas { get; set; } = new(); public bool FotoPetugasUploaded { get; set; } public string NamaPetugas { get; set; } = string.Empty; - public bool Submitted { get; set; } + public bool IsSubmit { get; set; } + public DateTime UpdatedAt { get; set; } = DateTime.Now; + public DateTime? SubmittedAt { get; set; } } public class DetailPenjemputanRequest { + public string NomorSpj { get; set; } = string.Empty; + public string LokasiAngkutId { get; set; } = string.Empty; + public string SpjDetailId { get; set; } = string.Empty; public string TpsName { get; set; } = string.Empty; public string Latitude { get; set; } = string.Empty; public string Longitude { get; set; } = string.Empty; @@ -77,7 +85,7 @@ namespace eSPJ.Models public string Message { get; set; } = string.Empty; } - public class DraftTimbanganItem + public class RecordTimbanganItem { public decimal Berat { get; set; } public string JenisSampah { get; set; } = "Residu"; @@ -86,33 +94,10 @@ namespace eSPJ.Models public string OcrInfo { get; set; } = string.Empty; } - public class DraftPenjemputanNonTps + public class RecordSaveRequest { - public string SessionKey { get; set; } = string.Empty; - public string LokasiAngkutId { get; set; } = string.Empty; - public string SpjDetailId { 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 List FotoKedatanganFileNames { 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 FotoPetugasFileNames { get; set; } = new(); - public bool FotoPetugasUploaded { get; set; } - public string NamaPetugas { get; set; } = string.Empty; - public bool Submitted { get; set; } - public DateTime UpdatedAt { get; set; } = DateTime.Now; - } - - public class DraftSaveRequest - { - public string DraftKey { get; set; } = string.Empty; - public string SessionKey { get; set; } = string.Empty; + public string NomorSpj { get; set; } = string.Empty; + public string NamaTps { get; set; } = string.Empty; public string LokasiAngkutId { get; set; } = string.Empty; public string SpjDetailId { get; set; } = string.Empty; public string Latitude { get; set; } = string.Empty; @@ -121,7 +106,7 @@ namespace eSPJ.Models public string WaktuKedatangan { get; set; } = string.Empty; public bool FotoKedatanganUploaded { get; set; } public List FotoKedatanganFileNames { get; set; } = new(); - public List Timbangan { get; set; } = new(); + public List Timbangan { get; set; } = new(); public decimal TotalOrganik { get; set; } public decimal TotalAnorganik { get; set; } public decimal TotalResidu { get; set; } @@ -129,21 +114,12 @@ namespace eSPJ.Models public bool FotoPetugasUploaded { get; set; } public List FotoPetugasFileNames { get; set; } = new(); public string NamaPetugas { get; set; } = string.Empty; + public bool IsSubmit { get; set; } } - public class DraftSaveResponse + public class RecordSaveResponse { public bool Success { get; set; } public string Message { get; set; } = string.Empty; - public string? DraftKey { get; set; } - public string? SessionKey { get; set; } - } - - public class DraftLoadResponse - { - public bool Success { get; set; } - public bool HasDraft { get; set; } - public DraftPenjemputanNonTps? Draft { get; set; } - public string Message { get; set; } = string.Empty; } } diff --git a/Program.cs b/Program.cs index ac2a093..57cee76 100644 --- a/Program.cs +++ b/Program.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.FileProviders; using eSPJ.Services; var builder = WebApplication.CreateBuilder(args); @@ -7,6 +8,7 @@ builder.Services.AddControllersWithViews(); builder.Services.AddHttpClient(); // Register custom services +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -21,6 +23,11 @@ if (!app.Environment.IsDevelopment()) } app.UseHttpsRedirection(); +app.UseStaticFiles(new StaticFileOptions +{ + FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "uploads")), + RequestPath = "/uploads" +}); app.Use(async (context, next) => { if (context.Request.Path.Equals("/driver/serviceworker.js", StringComparison.OrdinalIgnoreCase)) @@ -28,6 +35,9 @@ app.Use(async (context, next) => context.Response.OnStarting(() => { context.Response.Headers["Service-Worker-Allowed"] = "/upst"; + context.Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"; + context.Response.Headers["Pragma"] = "no-cache"; + context.Response.Headers["Expires"] = "0"; return Task.CompletedTask; }); } diff --git a/Services/DetailPenjemputanService.cs b/Services/DetailPenjemputanService.cs index 75ad9a4..50da88f 100644 --- a/Services/DetailPenjemputanService.cs +++ b/Services/DetailPenjemputanService.cs @@ -1,60 +1,57 @@ -using System.Text.Json; using eSPJ.Models; namespace eSPJ.Services { public class DetailPenjemputanService { - private readonly string _dataFilePath; + private readonly IDetailPenjemputanStore _store; private readonly IWebHostEnvironment _env; private readonly ILogger _logger; public DetailPenjemputanService( IWebHostEnvironment env, + IDetailPenjemputanStore store, ILogger logger) { _env = env; + _store = store; _logger = logger; - _dataFilePath = Path.Combine(_env.ContentRootPath, "Data", "detail-penjemputan.json"); + } + + private static string SanitizePathSegment(string? value, string fallback = "umum") + { + 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> GetAllTpsDataAsync() { - try - { - if (!File.Exists(_dataFilePath)) - { - return new List(); - } + return await _store.GetSubmittedAsync(); + } - 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> GetRecordsByNomorSpjAsync(string nomorSpj) + { + return await _store.GetByNomorSpjAsync(nomorSpj); } public async Task SaveTpsDataAsync(List data) { try { - var directory = Path.GetDirectoryName(_dataFilePath); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + foreach (var item in data) { - Directory.CreateDirectory(directory); + await _store.SaveSubmittedAsync(item); } - - var options = new JsonSerializerOptions - { - WriteIndented = true - }; - - var json = JsonSerializer.Serialize(data, options); - await File.WriteAllTextAsync(_dataFilePath, json); return true; } catch (Exception ex) @@ -68,7 +65,6 @@ namespace eSPJ.Services { try { - // Validate request if (string.IsNullOrEmpty(request.TpsName)) { return new DetailPenjemputanResponse @@ -78,33 +74,6 @@ namespace eSPJ.Services }; } - 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 @@ -114,47 +83,72 @@ namespace eSPJ.Services }; } + var existingRecord = await GetRecordDetailAsync(request.NomorSpj, request.SpjDetailId, request.LokasiAngkutId, request.TpsName); + var now = DateTime.Now; var datePart = now.ToString("yyyy-MM-dd"); - var uploadPath = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", datePart); - var uploadBaseUrl = $"/uploads/penjemputan/{datePart}"; + var spjFolder = SanitizePathSegment(request.NomorSpj, "spj-umum"); + var tpsFolder = SanitizePathSegment(request.TpsName, "tps-1"); + 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 = 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 - }; + var tpsData = existingRecord != null + ? CloneRecord(existingRecord) + : new TpsData(); - // Save foto kedatangan - foreach (var file in request.FotoKedatangan) + 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.TotalTimbangan = request.TotalTimbangan; + tpsData.TotalOrganik = request.TotalOrganik; + tpsData.TotalAnorganik = request.TotalAnorganik; + tpsData.TotalResidu = request.TotalResidu; + tpsData.NamaPetugas = request.NamaPetugas; + tpsData.IsSubmit = true; + tpsData.SubmittedAt ??= DateTime.Now; + tpsData.UpdatedAt = DateTime.Now; + + if (request.FotoKedatangan != null && request.FotoKedatangan.Any()) { - var fileName = $"kedatangan_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; - var filePath = Path.Combine(uploadPath, fileName); - using (var stream = new FileStream(filePath, FileMode.Create)) + tpsData.FotoKedatangan = new List(); + foreach (var file in request.FotoKedatangan) { - await file.CopyToAsync(stream); + 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.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" + }; } - // Save foto timbangan - if (request.FotoTimbangan != null && request.BeratTimbangan != null && request.JenisSampahList != null) + if (request.FotoTimbangan != null && request.FotoTimbangan.Any() && request.BeratTimbangan != null && request.JenisSampahList != null) { + tpsData.Timbangan = new List(); for (int i = 0; i < request.FotoTimbangan.Count; i++) { var file = request.FotoTimbangan[i]; @@ -174,7 +168,7 @@ namespace eSPJ.Services tpsData.Timbangan.Add(new TimbanganItem { FotoFileName = $"{uploadBaseUrl}/{fileName}", - Berat = new List { (i < request.BeratTimbangan.Count ? request.BeratTimbangan[i] : 0) }, + Berat = new List { i < request.BeratTimbangan.Count ? request.BeratTimbangan[i] : 0 }, LokasiAngkut = new List(), JenisSampah = new List { jenisSampah }, IsUploaded = true, @@ -182,22 +176,49 @@ namespace eSPJ.Services }); } } - - // Save foto petugas - foreach (var file in request.FotoPetugas) + else if (existingRecord?.Timbangan?.Any() == true) { - var fileName = $"petugas_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; - var filePath = Path.Combine(uploadPath, fileName); - using (var stream = new FileStream(filePath, FileMode.Create)) + tpsData.Timbangan = existingRecord.Timbangan; + } + else + { + return new DetailPenjemputanResponse { - await file.CopyToAsync(stream); - } - tpsData.FotoPetugas.Add($"{uploadBaseUrl}/{fileName}"); + Success = false, + Message = "Foto timbangan harus diupload" + }; } - var allData = await GetAllTpsDataAsync(); - allData.Add(tpsData); - await SaveTpsDataAsync(allData); + 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 { @@ -217,128 +238,102 @@ namespace eSPJ.Services } } - private string GetDraftFilePath(string prefix, string sessionKey) + private Task SaveRecordAsync(RecordSaveRequest request) { - var dir = Path.Combine(_env.ContentRootPath, "Data", "drafts"); - if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); - var safe = string.Concat(sessionKey.Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_')); - if (string.IsNullOrEmpty(safe)) safe = "default"; - return Path.Combine(dir, $"draft-{prefix}-{safe}.json"); + return SaveRecordInternalAsync(request); } - private Task SaveDraftAsync(string prefix, DraftSaveRequest request) - { - return SaveDraftInternalAsync(prefix, request); - } - - private async Task SaveDraftInternalAsync(string prefix, DraftSaveRequest request) + private async Task SaveRecordInternalAsync(RecordSaveRequest request) { try { - var filePath = GetDraftFilePath(prefix, request.SessionKey); - var draft = new DraftPenjemputanNonTps + await _store.SaveRecordAsync(request); + + return new RecordSaveResponse { Success = true, Message = "Data tersimpan." }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving penjemputan record"); + return new RecordSaveResponse { Success = false, Message = $"Gagal menyimpan data: {ex.Message}" }; + } + } + + public async Task SaveRecordNonTpsAsync(RecordSaveRequest request) + { + return await SaveRecordAsync(request); + } + + public async Task SaveRecordTpsAsync(RecordSaveRequest request) + { + return await SaveRecordAsync(request); + } + + public async Task> GetSubmittedByNomorSpjAsync(string nomorSpj) + { + var normalizedNomorSpj = (nomorSpj ?? string.Empty).Trim(); + var allData = await _store.GetSubmittedAsync(); + return allData + .Where(item => string.Equals((item.NomorSpj ?? string.Empty).Trim(), normalizedNomorSpj, StringComparison.OrdinalIgnoreCase)) + .OrderBy(item => item.SubmittedAt ?? DateTime.MinValue) + .ToList(); + } + + public async Task GetSubmittedDetailAsync(string nomorSpj, string? spjDetailId = null, string? lokasiAngkutId = null, string? namaTps = null) + { + return await _store.GetSubmittedDetailAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps); + } + + 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))); + } + + 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, + Timbangan = (source.Timbangan ?? new List()).Select(item => new TimbanganItem { - SessionKey = request.SessionKey, - LokasiAngkutId = request.LokasiAngkutId, - SpjDetailId = request.SpjDetailId, - Latitude = request.Latitude, - Longitude = request.Longitude, - AlamatJalan = request.AlamatJalan, - WaktuKedatangan = request.WaktuKedatangan, - FotoKedatanganFileNames = request.FotoKedatanganFileNames, - FotoKedatanganUploaded = request.FotoKedatanganUploaded, - Timbangan = request.Timbangan, - TotalOrganik = request.TotalOrganik, - TotalAnorganik = request.TotalAnorganik, - TotalResidu = request.TotalResidu, - TotalTimbangan = request.TotalTimbangan, - FotoPetugasFileNames = request.FotoPetugasFileNames, - FotoPetugasUploaded = request.FotoPetugasUploaded, - NamaPetugas = request.NamaPetugas, - UpdatedAt = DateTime.Now - }; - - var options = new JsonSerializerOptions { WriteIndented = true }; - var json = JsonSerializer.Serialize(draft, options); - await File.WriteAllTextAsync(filePath, json); - - return new DraftSaveResponse { Success = true, Message = "Draft tersimpan.", DraftKey = request.DraftKey, SessionKey = request.SessionKey }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error saving {Prefix} draft", prefix); - return new DraftSaveResponse { Success = false, Message = $"Gagal menyimpan draft: {ex.Message}" }; - } - } - - public async Task SaveDraftNonTpsAsync(DraftSaveRequest request) - { - return await SaveDraftAsync("non-tps", request); - } - - public async Task SaveDraftTpsAsync(DraftSaveRequest request) - { - return await SaveDraftAsync("tps", request); - } - - public async Task LoadDraftNonTpsAsync(string sessionKey) - { - return await LoadDraftAsync("non-tps", sessionKey); - } - - public async Task LoadDraftTpsAsync(string sessionKey) - { - return await LoadDraftAsync("tps", sessionKey); - } - - private async Task LoadDraftAsync(string prefix, string sessionKey) - { - try - { - var filePath = GetDraftFilePath(prefix, sessionKey); - if (!File.Exists(filePath)) - return new DraftLoadResponse { Success = true, HasDraft = false, Message = "Tidak ada draft." }; - - var json = await File.ReadAllTextAsync(filePath); - var draft = JsonSerializer.Deserialize(json, - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - - if (draft == null) - return new DraftLoadResponse { Success = true, HasDraft = false, Message = "Draft kosong." }; - - return new DraftLoadResponse { Success = true, HasDraft = true, Draft = draft }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error loading {Prefix} draft", prefix); - return new DraftLoadResponse { Success = false, HasDraft = false, Message = $"Gagal memuat draft: {ex.Message}" }; - } - } - - public async Task DeleteDraftNonTpsAsync(string sessionKey) - { - return await DeleteDraftAsync("non-tps", sessionKey); - } - - public async Task DeleteDraftTpsAsync(string sessionKey) - { - return await DeleteDraftAsync("tps", sessionKey); - } - - private async Task DeleteDraftAsync(string prefix, string sessionKey) - { - try - { - var filePath = GetDraftFilePath(prefix, sessionKey); - if (File.Exists(filePath)) File.Delete(filePath); - await Task.CompletedTask; - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting {Prefix} draft", prefix); - return false; - } + FotoFileName = item.FotoFileName, + Berat = new List(item.Berat ?? new List()), + LokasiAngkut = new List(item.LokasiAngkut ?? new List()), + JenisSampah = new List(item.JenisSampah ?? new List()), + IsUploaded = item.IsUploaded, + WaktuUpload = item.WaktuUpload + }).ToList(), + TotalOrganik = source.TotalOrganik, + TotalAnorganik = source.TotalAnorganik, + TotalResidu = source.TotalResidu, + TotalTimbangan = source.TotalTimbangan, + FotoPetugas = new List(source.FotoPetugas ?? new List()), + FotoPetugasUploaded = source.FotoPetugasUploaded, + NamaPetugas = source.NamaPetugas, + IsSubmit = source.IsSubmit, + UpdatedAt = source.UpdatedAt, + SubmittedAt = source.SubmittedAt, + }; } public async Task ProcessOcrTimbanganAsync(IFormFile foto) diff --git a/Services/FileDetailPenjemputanStore.cs b/Services/FileDetailPenjemputanStore.cs new file mode 100644 index 0000000..ad24c94 --- /dev/null +++ b/Services/FileDetailPenjemputanStore.cs @@ -0,0 +1,340 @@ +using System.Text.Json; +using eSPJ.Models; + +namespace eSPJ.Services +{ + public class FileDetailPenjemputanStore : IDetailPenjemputanStore + { + private readonly string _storeFilePath; + private readonly ILogger _logger; + private static readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1); + + public FileDetailPenjemputanStore( + IWebHostEnvironment env, + ILogger logger) + { + _logger = logger; + _storeFilePath = Path.Combine(env.ContentRootPath, "Data", "detail-penjemputan.json"); + } + + public async Task> GetAllAsync() + { + await _fileLock.WaitAsync(); + try + { + return await ReadAllFromDiskAsync(); + } + finally + { + _fileLock.Release(); + } + } + + private async Task> ReadAllFromDiskAsync() + { + try + { + if (!File.Exists(_storeFilePath)) + { + return new List(); + } + + var json = await File.ReadAllTextAsync(_storeFilePath); + if (string.IsNullOrWhiteSpace(json)) + { + return new List(); + } + + var data = JsonSerializer.Deserialize>(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return data?.Where(item => item != null).ToList() ?? new List(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading submitted penjemputan data"); + return new List(); + } + } + + public async Task> GetByNomorSpjAsync(string nomorSpj) + { + var normalizedNomorSpj = (nomorSpj ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(normalizedNomorSpj)) + { + return new List(); + } + + var allData = await GetAllAsync(); + return allData + .Where(item => string.Equals((item.NomorSpj ?? string.Empty).Trim(), normalizedNomorSpj, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(item => item.UpdatedAt) + .GroupBy(GetRecordIdentity, StringComparer.OrdinalIgnoreCase) + .Select(group => group.First()) + .OrderBy(item => item.Name) + .ToList(); + } + + public async Task> GetSubmittedAsync() + { + var allData = await GetAllAsync(); + return allData + .Where(item => item.IsSubmit) + .OrderByDescending(item => item.UpdatedAt) + .GroupBy(GetRecordIdentity, StringComparer.OrdinalIgnoreCase) + .Select(group => group.First()) + .ToList(); + } + + private static string GetRecordIdentity(TpsData item) + { + return string.Join("::", new[] + { + item.NomorSpj?.Trim() ?? string.Empty, + item.SpjDetailId?.Trim() ?? string.Empty, + item.LokasiAngkutId?.Trim() ?? string.Empty, + item.Name?.Trim() ?? string.Empty, + }); + } + + public async Task GetSubmittedDetailAsync(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 GetSubmittedAsync(); + return allData.FirstOrDefault(item => + string.Equals((item.NomorSpj ?? string.Empty).Trim(), normalizedNomorSpj, StringComparison.OrdinalIgnoreCase) && + (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 SaveSubmittedAsync(TpsData data) + { + await _fileLock.WaitAsync(); + try + { + var allData = await ReadAllFromDiskAsync(); + data.IsSubmit = true; + data.UpdatedAt = DateTime.Now; + if (!data.SubmittedAt.HasValue) + { + data.SubmittedAt = DateTime.Now; + } + + var existingIndex = FindRecordIndex(allData, data.NomorSpj, data.SpjDetailId, data.LokasiAngkutId, data.Name); + + if (existingIndex >= 0) + { + allData[existingIndex] = data; + } + else + { + allData.Add(data); + } + + await WriteAllToDiskAsync(allData); + } + finally + { + _fileLock.Release(); + } + } + + public async Task SaveRecordAsync(RecordSaveRequest request) + { + await _fileLock.WaitAsync(); + try + { + var allData = await ReadAllFromDiskAsync(); + var mapped = MapRequestToRecord(request); + mapped.UpdatedAt = DateTime.Now; + + var existingIndex = FindRecordIndex(allData, mapped.NomorSpj, mapped.SpjDetailId, mapped.LokasiAngkutId, mapped.Name); + if (existingIndex >= 0) + { + var existing = allData[existingIndex]; + mapped.NomorSpj = string.IsNullOrWhiteSpace(mapped.NomorSpj) ? existing.NomorSpj : mapped.NomorSpj; + mapped.LokasiAngkutId = string.IsNullOrWhiteSpace(mapped.LokasiAngkutId) ? existing.LokasiAngkutId : mapped.LokasiAngkutId; + mapped.SpjDetailId = string.IsNullOrWhiteSpace(mapped.SpjDetailId) ? existing.SpjDetailId : mapped.SpjDetailId; + mapped.Name = string.IsNullOrWhiteSpace(mapped.Name) ? existing.Name : mapped.Name; + mapped.Latitude = string.IsNullOrWhiteSpace(mapped.Latitude) ? existing.Latitude : mapped.Latitude; + mapped.Longitude = string.IsNullOrWhiteSpace(mapped.Longitude) ? existing.Longitude : mapped.Longitude; + mapped.AlamatJalan = string.IsNullOrWhiteSpace(mapped.AlamatJalan) ? existing.AlamatJalan : mapped.AlamatJalan; + mapped.WaktuKedatangan = string.IsNullOrWhiteSpace(mapped.WaktuKedatangan) ? existing.WaktuKedatangan : mapped.WaktuKedatangan; + mapped.FotoKedatangan = mapped.FotoKedatangan.Count > 0 ? mapped.FotoKedatangan : (existing.FotoKedatangan ?? new List()); + mapped.FotoKedatanganUploaded = mapped.FotoKedatanganUploaded || existing.FotoKedatanganUploaded; + mapped.Timbangan = MergeTimbangan(existing.Timbangan, mapped.Timbangan); + mapped.TotalOrganik = mapped.TotalOrganik != 0 ? mapped.TotalOrganik : existing.TotalOrganik; + mapped.TotalAnorganik = mapped.TotalAnorganik != 0 ? mapped.TotalAnorganik : existing.TotalAnorganik; + mapped.TotalResidu = mapped.TotalResidu != 0 ? mapped.TotalResidu : existing.TotalResidu; + mapped.TotalTimbangan = mapped.TotalTimbangan != 0 ? mapped.TotalTimbangan : existing.TotalTimbangan; + mapped.FotoPetugas = mapped.FotoPetugas.Count > 0 ? mapped.FotoPetugas : (existing.FotoPetugas ?? new List()); + mapped.FotoPetugasUploaded = mapped.FotoPetugasUploaded || existing.FotoPetugasUploaded; + mapped.NamaPetugas = string.IsNullOrWhiteSpace(mapped.NamaPetugas) ? existing.NamaPetugas : mapped.NamaPetugas; + mapped.IsSubmit = existing.IsSubmit || mapped.IsSubmit; + mapped.SubmittedAt = existing.SubmittedAt; + allData[existingIndex] = mapped; + } + else + { + allData.Add(mapped); + } + + await WriteAllToDiskAsync(allData); + } + finally + { + _fileLock.Release(); + } + } + + private static List MergeTimbangan(List? existingItems, List? incomingItems) + { + var existing = existingItems ?? new List(); + var incoming = incomingItems ?? new List(); + + if (incoming.Count == 0) + { + return existing; + } + + var maxCount = Math.Max(existing.Count, incoming.Count); + var merged = new List(maxCount); + + for (var index = 0; index < maxCount; index++) + { + var existingItem = index < existing.Count ? existing[index] : null; + var incomingItem = index < incoming.Count ? incoming[index] : null; + + if (existingItem == null && incomingItem == null) + { + continue; + } + + if (existingItem == null) + { + merged.Add(CloneTimbanganItem(incomingItem!)); + continue; + } + + if (incomingItem == null) + { + merged.Add(CloneTimbanganItem(existingItem)); + continue; + } + + merged.Add(new TimbanganItem + { + FotoFileName = string.IsNullOrWhiteSpace(incomingItem.FotoFileName) + ? existingItem.FotoFileName + : incomingItem.FotoFileName, + Berat = incomingItem.Berat != null && incomingItem.Berat.Count > 0 + ? new List(incomingItem.Berat) + : new List(existingItem.Berat ?? new List()), + LokasiAngkut = incomingItem.LokasiAngkut != null && incomingItem.LokasiAngkut.Count > 0 + ? new List(incomingItem.LokasiAngkut) + : new List(existingItem.LokasiAngkut ?? new List()), + JenisSampah = incomingItem.JenisSampah != null && incomingItem.JenisSampah.Count > 0 + ? new List(incomingItem.JenisSampah) + : new List(existingItem.JenisSampah ?? new List()), + IsUploaded = existingItem.IsUploaded || incomingItem.IsUploaded, + WaktuUpload = incomingItem.WaktuUpload ?? existingItem.WaktuUpload, + }); + } + + return merged; + } + + private static TimbanganItem CloneTimbanganItem(TimbanganItem source) + { + return new TimbanganItem + { + FotoFileName = source.FotoFileName, + Berat = new List(source.Berat ?? new List()), + LokasiAngkut = new List(source.LokasiAngkut ?? new List()), + JenisSampah = new List(source.JenisSampah ?? new List()), + IsUploaded = source.IsUploaded, + WaktuUpload = source.WaktuUpload, + }; + } + + private async Task WriteAllToDiskAsync(List data) + { + var directory = Path.GetDirectoryName(_storeFilePath); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + data = data + .Where(item => item != null) + .OrderByDescending(item => item.UpdatedAt) + .GroupBy(GetRecordIdentity, StringComparer.OrdinalIgnoreCase) + .Select(group => group.First()) + .OrderBy(item => item.Name) + .ToList(); + + var options = new JsonSerializerOptions { WriteIndented = true }; + var json = JsonSerializer.Serialize(data, options); + var tempPath = _storeFilePath + ".tmp"; + await File.WriteAllTextAsync(tempPath, json); + File.Move(tempPath, _storeFilePath, overwrite: true); + } + + private static int FindRecordIndex(List allData, string? nomorSpj, string? spjDetailId, string? lokasiAngkutId, string? namaTps) + { + return allData.FindIndex(item => + item != null && + !string.IsNullOrWhiteSpace(nomorSpj) && + string.Equals(item.NomorSpj, nomorSpj, StringComparison.OrdinalIgnoreCase) && + string.Equals(item.SpjDetailId, spjDetailId ?? string.Empty, StringComparison.OrdinalIgnoreCase) && + string.Equals(item.LokasiAngkutId, lokasiAngkutId ?? string.Empty, StringComparison.OrdinalIgnoreCase) && + string.Equals(item.Name, namaTps ?? string.Empty, StringComparison.OrdinalIgnoreCase)); + } + + private static TpsData MapRequestToRecord(RecordSaveRequest request) + { + return new TpsData + { + NomorSpj = request.NomorSpj, + LokasiAngkutId = request.LokasiAngkutId, + SpjDetailId = request.SpjDetailId, + Name = request.NamaTps, + Latitude = request.Latitude, + Longitude = request.Longitude, + AlamatJalan = request.AlamatJalan, + WaktuKedatangan = request.WaktuKedatangan, + FotoKedatangan = request.FotoKedatanganFileNames ?? new List(), + FotoKedatanganUploaded = request.FotoKedatanganUploaded, + Timbangan = (request.Timbangan ?? new List()) + .Where(item => item != null) + .Select(item => new TimbanganItem + { + FotoFileName = item.FotoFileName, + Berat = new List { item.Berat }, + LokasiAngkut = new List(), + JenisSampah = Enum.TryParse(item.JenisSampah, out var parsedJenis) + ? new List { parsedJenis } + : new List { JenisSampah.Residu }, + IsUploaded = item.Uploaded, + WaktuUpload = DateTime.Now + }).ToList(), + TotalOrganik = request.TotalOrganik, + TotalAnorganik = request.TotalAnorganik, + TotalResidu = request.TotalResidu, + TotalTimbangan = request.TotalTimbangan, + FotoPetugas = request.FotoPetugasFileNames ?? new List(), + FotoPetugasUploaded = request.FotoPetugasUploaded, + NamaPetugas = request.NamaPetugas, + IsSubmit = request.IsSubmit, + UpdatedAt = DateTime.Now, + SubmittedAt = request.IsSubmit ? DateTime.Now : null, + }; + } + } +} diff --git a/Services/IDetailPenjemputanStore.cs b/Services/IDetailPenjemputanStore.cs new file mode 100644 index 0000000..943a493 --- /dev/null +++ b/Services/IDetailPenjemputanStore.cs @@ -0,0 +1,14 @@ +using eSPJ.Models; + +namespace eSPJ.Services +{ + public interface IDetailPenjemputanStore + { + Task> GetAllAsync(); + Task> GetByNomorSpjAsync(string nomorSpj); + Task> GetSubmittedAsync(); + Task GetSubmittedDetailAsync(string nomorSpj, string? spjDetailId = null, string? lokasiAngkutId = null, string? namaTps = null); + Task SaveSubmittedAsync(TpsData data); + Task SaveRecordAsync(RecordSaveRequest request); + } +} diff --git a/Views/Admin/Transport/SpjDriverUpst/Shared/Partials/_Scripts.cshtml b/Views/Admin/Transport/SpjDriverUpst/Shared/Partials/_Scripts.cshtml index 893c633..1dd1b2b 100644 --- a/Views/Admin/Transport/SpjDriverUpst/Shared/Partials/_Scripts.cshtml +++ b/Views/Admin/Transport/SpjDriverUpst/Shared/Partials/_Scripts.cshtml @@ -10,7 +10,7 @@ diff --git a/wwwroot/driver/css/watch.css b/wwwroot/driver/css/watch.css index 436732e..e60ce04 100644 --- a/wwwroot/driver/css/watch.css +++ b/wwwroot/driver/css/watch.css @@ -146,7 +146,6 @@ --blur-md: 12px; --blur-lg: 16px; --blur-xl: 24px; - --blur-3xl: 64px; --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-font-family: var(--font-sans); @@ -359,9 +358,6 @@ .-top-4 { top: calc(var(--spacing) * -4); } - .-top-24 { - top: calc(var(--spacing) * -24); - } .top-0 { top: calc(var(--spacing) * 0); } @@ -404,9 +400,6 @@ .-right-6 { right: calc(var(--spacing) * -6); } - .-right-24 { - right: calc(var(--spacing) * -24); - } .right-0 { right: calc(var(--spacing) * 0); } @@ -434,9 +427,6 @@ .right-full { right: 100%; } - .-bottom-0 { - bottom: calc(var(--spacing) * -0); - } .-bottom-0\.5 { bottom: calc(var(--spacing) * -0.5); } @@ -446,9 +436,6 @@ .-bottom-6 { bottom: calc(var(--spacing) * -6); } - .-bottom-32 { - bottom: calc(var(--spacing) * -32); - } .bottom-0 { bottom: calc(var(--spacing) * 0); } @@ -476,15 +463,9 @@ .bottom-100 { bottom: calc(var(--spacing) * 100); } - .-left-32 { - left: calc(var(--spacing) * -32); - } .left-0 { left: calc(var(--spacing) * 0); } - .left-1 { - left: calc(var(--spacing) * 1); - } .left-1\/2 { left: calc(1/2 * 100%); } @@ -542,6 +523,9 @@ .z-\[100\] { z-index: 100; } + .z-\[9999\] { + z-index: 9999; + } .order-0 { order: 0; } @@ -800,9 +784,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); } @@ -893,18 +874,12 @@ .aspect-square { aspect-ratio: 1 / 1; } - .h-0 { - height: calc(var(--spacing) * 0); - } .h-0\.5 { height: calc(var(--spacing) * 0.5); } .h-1 { height: calc(var(--spacing) * 1); } - .h-1\.5 { - height: calc(var(--spacing) * 1.5); - } .h-2 { height: calc(var(--spacing) * 2); } @@ -977,15 +952,9 @@ .h-64 { height: calc(var(--spacing) * 64); } - .h-72 { - height: calc(var(--spacing) * 72); - } .h-75 { height: calc(var(--spacing) * 75); } - .h-96 { - height: calc(var(--spacing) * 96); - } .h-100 { height: calc(var(--spacing) * 100); } @@ -1016,9 +985,6 @@ .w-1 { width: calc(var(--spacing) * 1); } - .w-1\.5 { - width: calc(var(--spacing) * 1.5); - } .w-1\/3 { width: calc(1/3 * 100%); } @@ -1094,15 +1060,9 @@ .w-64 { width: calc(var(--spacing) * 64); } - .w-72 { - width: calc(var(--spacing) * 72); - } .w-75 { width: calc(var(--spacing) * 75); } - .w-96 { - width: calc(var(--spacing) * 96); - } .w-100 { width: calc(var(--spacing) * 100); } @@ -1121,9 +1081,6 @@ .w-max { width: max-content; } - .max-w-\[260px\] { - max-width: 260px; - } .max-w-full { max-width: 100%; } @@ -1181,10 +1138,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); @@ -1197,10 +1150,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); @@ -1327,13 +1276,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; @@ -1710,9 +1652,6 @@ .border-t-transparent { border-top-color: transparent; } - .border-t-white { - border-top-color: var(--color-white); - } .bg-amber-400 { background-color: var(--color-amber-400); } @@ -1770,9 +1709,6 @@ .bg-blue-600 { background-color: var(--color-blue-600); } - .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)) { @@ -1836,9 +1772,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)) { @@ -1860,9 +1793,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)) { @@ -1905,9 +1835,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)) { @@ -1938,18 +1865,18 @@ background-color: color-mix(in oklab, var(--color-white) 20%, transparent); } } - .bg-white\/30 { - background-color: color-mix(in srgb, #fff 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 30%, transparent); - } - } .bg-white\/70 { background-color: color-mix(in srgb, #fff 70%, transparent); @supports (color: color-mix(in lab, red, red)) { background-color: color-mix(in oklab, var(--color-white) 70%, transparent); } } + .bg-white\/95 { + background-color: color-mix(in srgb, #fff 95%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 95%, transparent); + } + } .bg-yellow-50 { background-color: var(--color-yellow-50); } @@ -2298,6 +2225,9 @@ .py-6 { padding-block: calc(var(--spacing) * 6); } + .py-7 { + padding-block: calc(var(--spacing) * 7); + } .py-12 { padding-block: calc(var(--spacing) * 12); } @@ -2543,10 +2473,6 @@ --tw-tracking: 0.24em; letter-spacing: 0.24em; } - .tracking-\[0\.28em\] { - --tw-tracking: 0.28em; - letter-spacing: 0.28em; - } .tracking-tight { --tw-tracking: var(--tracking-tight); letter-spacing: var(--tracking-tight); @@ -2732,30 +2658,12 @@ .text-white { color: var(--color-white); } - .text-white\/30 { - color: color-mix(in srgb, #fff 30%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 30%, transparent); - } - } - .text-white\/40 { - color: color-mix(in srgb, #fff 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 40%, transparent); - } - } .text-white\/70 { color: color-mix(in srgb, #fff 70%, transparent); @supports (color: color-mix(in lab, red, red)) { color: color-mix(in oklab, var(--color-white) 70%, transparent); } } - .text-white\/80 { - color: color-mix(in srgb, #fff 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 80%, transparent); - } - } .text-white\/90 { color: color-mix(in srgb, #fff 90%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -2816,9 +2724,6 @@ .opacity-60 { opacity: 60%; } - .opacity-70 { - opacity: 70%; - } .opacity-75 { opacity: 75%; } @@ -2879,27 +2784,12 @@ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } - .shadow-black { - --tw-shadow-color: #000; - @supports (color: color-mix(in lab, red, red)) { - --tw-shadow-color: color-mix(in oklab, var(--color-black) var(--tw-shadow-alpha), transparent); - } - } - .shadow-black\/20 { - --tw-shadow-color: color-mix(in srgb, #000 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-black) 20%, transparent) var(--tw-shadow-alpha), transparent); - } - } .shadow-gray-200 { --tw-shadow-color: oklch(92.8% 0.006 264.531); @supports (color: color-mix(in lab, red, red)) { --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)) { @@ -2938,10 +2828,6 @@ --tw-blur: blur(8px); 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,); } - .blur-3xl { - --tw-blur: blur(var(--blur-3xl)); - 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,); - } .drop-shadow { --tw-drop-shadow-size: drop-shadow(0 1px 2px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.1))) drop-shadow(0 1px 1px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.06))); --tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow( 0 1px 1px rgb(0 0 0 / 0.06)); @@ -3592,12 +3478,6 @@ scale: var(--tw-scale-x) var(--tw-scale-y); } } - .active\:shadow-md { - &:active { - --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } .disabled\:cursor-not-allowed { &:disabled { cursor: not-allowed; diff --git a/wwwroot/driver/js/detail-penjemputan-non-tps.js b/wwwroot/driver/js/detail-penjemputan-non-tps.js index 387cc08..5482679 100644 --- a/wwwroot/driver/js/detail-penjemputan-non-tps.js +++ b/wwwroot/driver/js/detail-penjemputan-non-tps.js @@ -8,22 +8,50 @@ document.addEventListener('DOMContentLoaded', async function() { let activeTpsIndex = 0; let tpsData = []; let nomorSpj = 'SPJ/07-2025/PKM/000476'; - let draftRequestKey = ''; - - function buildDraftRequestKey(tps) { - const spjDetailId = (tps?.spjDetailId || '').trim(); - const lokasiAngkutId = (tps?.lokasiAngkutId || '').trim(); - if (!spjDetailId && !lokasiAngkutId) return ''; - return `non-tps-${spjDetailId || 'no-spj'}-${lokasiAngkutId || 'no-lokasi'}`.replace(/[^a-zA-Z0-9_-]/g, ''); - } + 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(autoSaveDraft, 1000); + autoSaveTimer = setTimeout(autoSaveRecord, 500); } function showAutoSaveStatus(msg, isOk = false) { @@ -39,150 +67,176 @@ document.addEventListener('DOMContentLoaded', async function() { if (isOk) setTimeout(() => { autoSaveStatusEl.style.opacity = '0'; }, 2500); } - async function autoSaveDraft() { + 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, + timbangan: (tps.timbangan || []).map(t => ({ + berat: (t.berat && t.berat.length > 0) ? t.berat[0] : 0, + jenisSampah: (t.jenisSampah && t.jenisSampah.length > 0) ? t.jenisSampah[0] : DEFAULT_JENIS, + fotoFileName: t.fotoFileName || '', + uploaded: t.uploaded || false, + ocrInfo: t.ocrInfo || '' + })), + totalOrganik: tps.totalOrganik || 0, + totalAnorganik: tps.totalAnorganik || 0, + totalResidu: tps.totalResidu || 0, + totalTimbangan: tps.totalTimbangan || 0, + fotoPetugasFileNames: tps.fotoPetugasFileNames || [], + fotoPetugasUploaded: tps.fotoPetugasUploaded || false, + namaPetugas: tps.namaPetugas || '' + }; + } + + 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-non-tps', { + 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, + ); - const payload = { - draftKey: draftRequestKey, - lokasiAngkutId: tps.lokasiAngkutId || '', - spjDetailId: tps.spjDetailId || '', - latitude: tps.latitude || '', - longitude: tps.longitude || '', - alamatJalan: tps.alamatJalan || '', - waktuKedatangan: tps.waktuKedatangan || '', - fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [], - fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false, - timbangan: (tps.timbangan || []).map(t => ({ - berat: (t.berat && t.berat.length > 0) ? t.berat[0] : 0, - jenisSampah: (t.jenisSampah && t.jenisSampah.length > 0) ? t.jenisSampah[0] : 'Residu', - fotoFileName: t.fotoFileName || '', - uploaded: t.uploaded || false, - ocrInfo: t.ocrInfo || '' - })), - totalOrganik: tps.totalOrganik || 0, - totalAnorganik: tps.totalAnorganik || 0, - totalResidu: tps.totalResidu || 0, - totalTimbangan: tps.totalTimbangan || 0, - fotoPetugasFileNames: tps.fotoPetugasFileNames || [], - fotoPetugasUploaded: tps.fotoPetugasUploaded || false, - namaPetugas: tps.namaPetugas || '' - }; + 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.totalOrganik = Number(record.totalOrganik ?? record.TotalOrganik ?? tps.totalOrganik) || 0; + tps.totalAnorganik = Number(record.totalAnorganik ?? record.TotalAnorganik ?? tps.totalAnorganik) || 0; + tps.totalResidu = Number(record.totalResidu ?? record.TotalResidu ?? tps.totalResidu) || 0; + tps.totalTimbangan = Number(record.totalTimbangan ?? record.TotalTimbangan ?? tps.totalTimbangan) || 0; + tps.submitted = Boolean(record.isSubmit ?? record.IsSubmit ?? record.submitted ?? record.Submitted ?? tps.submitted); - try { - const res = await fetch('/upst/detail-penjemputan/save-draft-non-tps', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - const data = await res.json(); - showAutoSaveStatus(data.success ? '✓ Draft tersimpan' : '✗ Gagal simpan', data.success); - } catch { - showAutoSaveStatus('✗ Gagal simpan draft'); + const timbangan = record.timbangan || record.Timbangan || []; + if (Array.isArray(timbangan) && timbangan.length > 0) { + tps.timbangan = timbangan.map(item => ({ + file: null, + fotoFileName: item.fotoFileName || item.FotoFileName || '', + berat: [Number(item.berat ?? (Array.isArray(item.Berat) ? item.Berat[0] : item.Berat) ?? 0) || 0], + jenisSampah: [normalizeJenisSampahValue(item.jenisSampah ?? item.JenisSampah)], + lokasiAngkut: [], + uploaded: Boolean(item.uploaded ?? item.isUploaded ?? item.IsUploaded ?? item.fotoFileName ?? item.FotoFileName), + ocrInfo: item.ocrInfo || item.OcrInfo || (item.fotoFileName || item.FotoFileName ? 'Foto dari server.' : 'OCR: diproses.') + })); + } else { + tps.timbangan = []; } - } + } - async function loadDraftFromServer() { - if (!draftRequestKey) return; - try { - const res = await fetch(`/upst/detail-penjemputan/load-draft-non-tps?draftKey=${encodeURIComponent(draftRequestKey)}`); - if (!res.ok) return; - const data = await res.json(); - if (!data.success || !data.hasDraft || !data.draft) return; - - const d = data.draft; - const tps = tpsData[activeTpsIndex]; - if (!tps) return; - - if (d.lokasiAngkutId) tps.lokasiAngkutId = d.lokasiAngkutId; - if (d.spjDetailId) tps.spjDetailId = d.spjDetailId; - if (d.latitude) tps.latitude = d.latitude; - if (d.longitude) tps.longitude = d.longitude; - if (d.alamatJalan) tps.alamatJalan = d.alamatJalan; - if (d.waktuKedatangan) tps.waktuKedatangan = d.waktuKedatangan; - tps.fotoKedatanganFileNames = d.fotoKedatanganFileNames || []; - tps.fotoKedatanganUploaded = d.fotoKedatanganUploaded || false; - tps.fotoPetugasFileNames = d.fotoPetugasFileNames || []; - tps.fotoPetugasUploaded = d.fotoPetugasUploaded || false; - tps.namaPetugas = d.namaPetugas || ''; - tps.totalOrganik = d.totalOrganik || 0; - tps.totalAnorganik = d.totalAnorganik || 0; - tps.totalResidu = d.totalResidu || 0; - tps.totalTimbangan = d.totalTimbangan || 0; - - if (d.timbangan && d.timbangan.length > 0) { - tps.timbangan = d.timbangan.map(t => ({ - file: null, - fotoFileName: t.fotoFileName || '', - berat: [t.berat || 0], - jenisSampah: [t.jenisSampah || 'Residu'], - lokasiAngkut: [], - uploaded: t.uploaded || false, - ocrInfo: t.ocrInfo || 'OCR: diproses.' - })); - } - - patchFormFromDraft(); - showAutoSaveStatus('✓ Draft dipulihkan', true); - } catch (err) { - console.warn('Gagal memuat draft dari server:', err); - } - } - - function patchFormFromDraft() { + async function loadRecordForCurrentSpj() { const tps = tpsData[activeTpsIndex]; - const form = tpsContentContainer.querySelector('form'); - if (!form) return; + if (!tps || !nomorSpj) return; - const hiddenLat = form.querySelector('.tps-latitude'); - const hiddenLng = form.querySelector('.tps-longitude'); - const hiddenAlamat = form.querySelector('.tps-alamat-jalan'); - if (hiddenLat) hiddenLat.value = tps.latitude; - if (hiddenLng) hiddenLng.value = tps.longitude; - if (hiddenAlamat) hiddenAlamat.value = tps.alamatJalan; + 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); - const latDisplay = form.querySelector('.tps-display-latitude'); - const lngDisplay = form.querySelector('.tps-display-longitude'); - const waktuDisplay = form.querySelector('.tps-waktu-kedatangan'); - if (latDisplay && tps.latitude) latDisplay.value = tps.latitude; - if (lngDisplay && tps.longitude) lngDisplay.value = tps.longitude; - if (waktuDisplay && tps.waktuKedatangan) waktuDisplay.value = tps.waktuKedatangan; + try { + const res = await fetch(`${RECORD_DETAIL_ENDPOINT}?${params.toString()}`, { cache: 'no-store' }); + if (!res.ok) return; - const namaPetugasInput = form.querySelector('.tps-nama-petugas'); - if (namaPetugasInput && tps.namaPetugas) namaPetugasInput.value = tps.namaPetugas; + const data = await res.json(); + if (!data.success || !data.hasData || !data.item) return; - refreshKedatanganUploadState(form); - refreshPetugasUploadState(form); - - if (tps.timbangan.length > 0) { - const repeater = form.querySelector('.tps-timbangan-repeater'); - if (repeater) { - repeater.innerHTML = ''; - tps.timbangan.forEach(timb => createTimbanganItem(repeater, timb)); - } + applyServerRecordToTps(data.item); + } catch (error) { + console.warn('Gagal memuat data non-TPS:', error); } - - const previewKedatangan = form.querySelector('.tps-preview-kedatangan'); - if (previewKedatangan) { - if (tps.fotoKedatangan.length > 0) { - renderStoredPhotos(tps.fotoKedatangan, previewKedatangan); - } else if (tps.fotoKedatanganUploaded && tps.fotoKedatanganFileNames.length > 0) { - renderServerImagePreview(tps.fotoKedatanganFileNames, previewKedatangan); - } - } - - const previewPetugas = form.querySelector('.tps-preview-petugas'); - if (previewPetugas) { - if (tps.fotoPetugas.length > 0) { - renderStoredPhotos(tps.fotoPetugas, previewPetugas); - } else if (tps.fotoPetugasUploaded && tps.fotoPetugasFileNames.length > 0) { - renderServerImagePreview(tps.fotoPetugasFileNames, previewPetugas); - } - } - - refreshSubmitButtonState(form); - updateTpsTotalTimbangan(); - } + } const OCR_AREAS = [ { @@ -213,12 +267,11 @@ document.addEventListener('DOMContentLoaded', async function() { const JENIS_SAMPAH = ["Organik", "Anorganik", "Residu"]; const DEFAULT_JENIS = "Residu"; const DETAIL_DATA_URL = "/driver/json/detail-penjemputan-non-tps.json"; - const DEFAULT_TPS_NAME = "Lokasi Pengangkutan 1"; + const DEFAULT_TPS_NAME = "TPS 1"; function isBrowserFile(file) { return file instanceof File; } - function resolveStoredPhoto(file) { return isBrowserFile(file) ? file : null; } @@ -283,7 +336,6 @@ document.addEventListener('DOMContentLoaded', async function() { tpsData[0].name = namaTps; tpsData[0].lokasiAngkutId = detail.lokasiAngkutId || detail.LokasiAngkutID || tpsData[0].lokasiAngkutId; tpsData[0].spjDetailId = detail.spjDetailId || detail.SpjDetailID || tpsData[0].spjDetailId; - draftRequestKey = buildDraftRequestKey(tpsData[0]); } nomorSpj = detail.nomorSpj || nomorSpj; @@ -324,6 +376,23 @@ document.addEventListener('DOMContentLoaded', async function() { 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 = `
@@ -406,11 +475,7 @@ document.addEventListener('DOMContentLoaded', async function() { -
- Batal - -
- ${submitState.canSubmit ? '' : `

${submitState.message}

`} + ${actionMarkup}

`; @@ -479,6 +544,8 @@ document.addEventListener('DOMContentLoaded', async function() { 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"); @@ -490,7 +557,6 @@ document.addEventListener('DOMContentLoaded', async function() { updateWaktuKedatangan(); updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan')); refreshKedatanganUploadState(form); - scheduleAutoSave(); }); fotoPetugasInput.addEventListener('change', function() { @@ -498,12 +564,12 @@ document.addEventListener('DOMContentLoaded', async function() { tps.fotoPetugasUploaded = false; updateMultiPreview(this, form.querySelector('.tps-preview-petugas')); refreshPetugasUploadState(form); - scheduleAutoSave(); }); namaPetugasInput.addEventListener('input', function() { tps.namaPetugas = this.value; refreshPetugasUploadState(form); + scheduleAutoSave(); }); namaPetugasInput.addEventListener('blur', function() { @@ -565,7 +631,6 @@ document.addEventListener('DOMContentLoaded', async function() { if (displayWaktu) displayWaktu.value = formatted; getLocationUpdate(); - scheduleAutoSave(); } function reverseGeocode(lat, lng) { @@ -587,7 +652,6 @@ document.addEventListener('DOMContentLoaded', async function() { if (latInput) latInput.value = lat; if (lngInput) lngInput.value = lng; } - scheduleAutoSave(); }) .catch(() => { tps.latitude = lat; @@ -601,7 +665,6 @@ document.addEventListener('DOMContentLoaded', async function() { if (latInput) latInput.value = lat; if (lngInput) lngInput.value = lng; } - scheduleAutoSave(); }); } @@ -968,7 +1031,6 @@ document.addEventListener('DOMContentLoaded', async function() { formatWeightDisplay(totalAnorganik); if (grandTotalResiduDisplay) grandTotalResiduDisplay.textContent = formatWeightDisplay(totalResidu); - scheduleAutoSave(); } function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) { @@ -1066,6 +1128,10 @@ document.addEventListener('DOMContentLoaded', async function() { } 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.' }; @@ -1105,10 +1171,10 @@ document.addEventListener('DOMContentLoaded', async function() { function refreshSubmitButtonState(form) { const submitButton = form.querySelector('button[type="submit"]'); - if (!submitButton) return; + const tps = tpsData[activeTpsIndex]; + if (tps?.submitted || !submitButton) return; const helperText = form.querySelector(".submit-state-message"); - const tps = tpsData[activeTpsIndex]; const submitState = getSubmitState(tps); submitButton.disabled = !submitState.canSubmit; @@ -1275,7 +1341,6 @@ document.addEventListener('DOMContentLoaded', async function() { if (itemIndex >= 0 && tps.timbangan[itemIndex]) { tps.timbangan[itemIndex].uploaded = false; refreshTimbanganUploadState(item); - scheduleAutoSave(); } } }); @@ -1290,6 +1355,7 @@ document.addEventListener('DOMContentLoaded', async function() { refreshTimbanganUploadState(item); const form = tpsContentContainer.querySelector("form"); if (form) refreshSubmitButtonState(form); + scheduleAutoSave(); }); weightInputDisplay.addEventListener('blur', function() { @@ -1314,7 +1380,6 @@ document.addEventListener('DOMContentLoaded', async function() { syncTimbanganToTpsData(); const form = tpsContentContainer.querySelector('form'); if (form) refreshSubmitButtonState(form); - scheduleAutoSave(); }); removeBtn.addEventListener('click', function() { @@ -1328,7 +1393,6 @@ document.addEventListener('DOMContentLoaded', async function() { updateTpsTotalTimbangan(); syncTimbanganToTpsData(); if (form) refreshSubmitButtonState(form); - scheduleAutoSave(); }); repeater.appendChild(item); @@ -1367,8 +1431,10 @@ document.addEventListener('DOMContentLoaded', async function() { function buildSubmitFormData(tps) { const formData = new FormData(); - formData.append("LokasiAngkutID", tps.lokasiAngkutId || ""); - formData.append("SpjDetailID", tps.spjDetailId || ""); + 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); @@ -1440,7 +1506,8 @@ document.addEventListener('DOMContentLoaded', async function() { const formData = new FormData(); formData.append('FotoTimbangan', tps.timbangan[itemIndex].file); - formData.append('DraftKey', draftRequestKey); + formData.append('NomorSpj', nomorSpj || ''); + formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME); formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('ItemIndex', itemIndex); @@ -1485,7 +1552,8 @@ document.addEventListener('DOMContentLoaded', async function() { const formData = new FormData(); tps.fotoKedatangan.forEach(f => formData.append('FotoKedatangan', f)); - formData.append('DraftKey', draftRequestKey); + 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 || ''); @@ -1533,7 +1601,8 @@ document.addEventListener('DOMContentLoaded', async function() { const formData = new FormData(); tps.fotoPetugas.forEach(f => formData.append('FotoPetugas', f)); - formData.append('DraftKey', draftRequestKey); + 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); @@ -1573,17 +1642,22 @@ document.addEventListener('DOMContentLoaded', async function() { const formData = buildSubmitFormData(tps); try { - const res = await fetch('/upst/detail-penjemputan', { method: 'POST', body: formData }); - if (res.ok || res.redirected) { + const res = await fetch('/upst/detail-penjemputan', { + 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; - if (draftRequestKey) { - await fetch(`/upst/detail-penjemputan/delete-draft-non-tps?draftKey=${encodeURIComponent(draftRequestKey)}`, { method: 'DELETE' }); - } - showToast('Data berhasil disimpan!', 'success'); + showToast(result.message || 'Data berhasil disimpan!', 'success'); setTimeout(() => { window.location.href = '/upst/detail-penjemputan/detail-selesai-tanpa-tps'; }, 1500); } else { - const txt = await res.text(); - showToast('Gagal submit: ' + (txt || res.statusText), 'error'); + showToast(result?.message || 'Gagal submit data.', 'error'); if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Submit'; } } } catch { @@ -1617,10 +1691,15 @@ document.addEventListener('DOMContentLoaded', async function() { nomorSpj = nomorSpjEl.textContent.trim(); } - initializeLocation(); - await loadDetailData(); - await loadDraftFromServer(); - renderTpsForm(); + showLoadingOverlay(); + try { + initializeLocation(); + await loadDetailData(); + await loadRecordForCurrentSpj(); + renderTpsForm(); + } finally { + hideLoadingOverlay(); + } function renderServerImagePreview(fileUrls, container) { container.innerHTML = ''; diff --git a/wwwroot/driver/js/detail-penjemputan-tps.js b/wwwroot/driver/js/detail-penjemputan-tps.js index 75675e0..5433f04 100644 --- a/wwwroot/driver/js/detail-penjemputan-tps.js +++ b/wwwroot/driver/js/detail-penjemputan-tps.js @@ -12,9 +12,8 @@ const DetailPenjemputan = (function () { }; const ENDPOINTS = { - saveDraft: '/upst/detail-penjemputan/save-draft', - loadDraft: '/upst/detail-penjemputan/load-draft', - deleteDraft: '/upst/detail-penjemputan/delete-draft', + saveRecord: '/upst/detail-penjemputan/save-record', + recordsList: '/upst/detail-penjemputan/api/records', uploadKedatangan: '/upst/detail-penjemputan/upload-foto-kedatangan', uploadTimbangan: '/upst/detail-penjemputan/upload-foto-timbangan', uploadPetugas: '/upst/detail-penjemputan/upload-foto-petugas', @@ -31,37 +30,48 @@ const DetailPenjemputan = (function () { }; const STORAGE_KEY = "detailPenjemputanTpsState"; - - function saveState() { - try { - const stateCopy = JSON.parse( - JSON.stringify(state, (key, value) => { - if (value instanceof File) { - return { name: value.name, size: value.size, type: value.type }; - } - return value; - }), - ); - localStorage.setItem(STORAGE_KEY, JSON.stringify(stateCopy)); - } catch (e) { - console.warn("Failed to save state to localStorage:", e); - } + function sanitizeStorageSegment(value) { + return String(value || "default") + .trim() + .replace(/[^a-zA-Z0-9_-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, "") || "default"; } - function loadState() { - try { - const saved = localStorage.getItem(STORAGE_KEY); - if (saved) { - const parsed = JSON.parse(saved); - state = { ...state, ...parsed }; - } - } catch (e) { - console.warn("Failed to load state from localStorage:", e); - } + function getStorageKey(nomorSpj = state.nomorSpj) { + return `${STORAGE_KEY}:${sanitizeStorageSegment(nomorSpj)}`; + } + + function saveState() { + return; + } + + function loadState(nomorSpj = state.nomorSpj) { + return; } function clearState() { - localStorage.removeItem(STORAGE_KEY); + return; + } + + function getTpsIdentity(value) { + if (!value) return ""; + const spjDetailId = value.spjDetailId || value.SpjDetailID || ""; + const lokasiAngkutId = value.lokasiAngkutId || value.LokasiAngkutID || ""; + const name = value.name || value.Name || ""; + return [spjDetailId, lokasiAngkutId, name].filter(Boolean).join("::"); + } + + function getPersistedUiState() { + return null; + } + + function applyPersistedTpsData(persistedUiState) { + return; + } + + function applyPersistedUiState(persistedUiState) { + return; } function isBrowserFile(value) { @@ -215,25 +225,24 @@ const DetailPenjemputan = (function () { }; } - function findMatchingApiTps(apiList, currentTps, index) { - return ( - apiList.find( - (item) => - (currentTps.spjDetailId && - (item.spjDetailId === currentTps.spjDetailId || - item.SpjDetailID === currentTps.spjDetailId)) || - (currentTps.lokasiAngkutId && - (item.lokasiAngkutId === currentTps.lokasiAngkutId || - item.LokasiAngkutID === currentTps.lokasiAngkutId)) || - (item.name || item.Name) === currentTps.name, - ) || apiList[index] - ); + function findMatchingApiTps(apiList, currentTps) { + + return apiList.find( + (item) => + (currentTps.spjDetailId && + (item.spjDetailId === currentTps.spjDetailId || + item.SpjDetailID === currentTps.spjDetailId)) || + (currentTps.lokasiAngkutId && + (item.lokasiAngkutId === currentTps.lokasiAngkutId || + item.LokasiAngkutID === currentTps.lokasiAngkutId)) || + (item.name || item.Name) === currentTps.name, + ) || null; } - function applyApiDraftData(draftData) { - const apiList = Array.isArray(draftData) - ? draftData - : draftData?.tpsData || draftData?.draftPenjemputan || []; + function applyApiRecordData(recordData, { skipRender = false } = {}) { + const apiList = Array.isArray(recordData) + ? recordData + : recordData?.tpsData || []; if (!Array.isArray(apiList) || apiList.length === 0) { return; @@ -244,7 +253,7 @@ const DetailPenjemputan = (function () { } state.tpsData = state.tpsData.map((currentTps, index) => { - const apiTps = findMatchingApiTps(apiList, currentTps, index); + const apiTps = findMatchingApiTps(apiList, currentTps); if (!apiTps) { return currentTps; } @@ -262,6 +271,7 @@ const DetailPenjemputan = (function () { return { ...currentTps, + nomorSpj: apiTps.nomorSpj || apiTps.NomorSpj || currentTps.nomorSpj, name: apiTps.name || apiTps.Name || currentTps.name, lokasiAngkutId: apiTps.lokasiAngkutId || @@ -317,8 +327,10 @@ const DetailPenjemputan = (function () { namaPetugas: apiTps.namaPetugas || apiTps.NamaPetugas || currentTps.namaPetugas, submitted: Boolean( - apiTps.submitted ?? apiTps.Submitted ?? currentTps.submitted, + apiTps.isSubmit ?? apiTps.IsSubmit ?? apiTps.submitted ?? apiTps.Submitted ?? currentTps.submitted, ), + submittedAt: + apiTps.submittedAt || apiTps.SubmittedAt || currentTps.submittedAt, }; }); @@ -329,17 +341,63 @@ const DetailPenjemputan = (function () { ); elements.tpsTabsContainer.style.display = "block"; - if (state.tpsData.length === 1) { - renderSingleForm(); - } else { - renderTabs(); - renderTpsForm(); + if (!skipRender) { + if (state.tpsData.length === 1) { + renderSingleForm(); + } else { + renderTabs(); + renderTpsForm(); + } } updateAllTotals(); saveState(); } + async function loadRecordsForCurrentSpj() { + if (!state.nomorSpj) return; + + try { + const fetchRecords = async () => { + const url = new URL(ENDPOINTS.recordsList, window.location.origin); + url.searchParams.set('nomorSpj', state.nomorSpj); + url.searchParams.set('_ts', Date.now().toString()); + + const res = await fetch(url.toString(), { + cache: 'no-store', + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + Pragma: 'no-cache', + }, + }); + + if (!res.ok) { + return null; + } + + return res.json(); + }; + + let data = await fetchRecords(); + if ( + data?.success && + Array.isArray(data.items) && + data.items.length === 0 + ) { + await new Promise((resolve) => setTimeout(resolve, 250)); + data = await fetchRecords(); + } + + if (!data?.success || !Array.isArray(data.items) || data.items.length === 0) { + return; + } + + applyApiRecordData(data.items, { skipRender: true }); + } catch (error) { + console.warn('Gagal memuat data TPS:', error); + } + } + const elements = { grandTotalDisplay: null, tpsSelectionContainer: null, @@ -355,10 +413,51 @@ const DetailPenjemputan = (function () { let autoSaveTimer = null; let autoSaveStatusEl = null; + let loadingOverlayEl = null; + let isAutoSaving = false; + let pendingAutoSaveIndex = null; + let lastAutoSaveSignature = ""; - async function init(tpsList) { - initElements(); - await initializeLocation(tpsList); + 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 = ''; + } + + async function init(tpsList, nomorSpj = state.nomorSpj) { + state.nomorSpj = nomorSpj || state.nomorSpj; + showLoadingOverlay(); + try { + initElements(); + await initializeLocation(tpsList); + } finally { + hideLoadingOverlay(); + } } function initElements() { @@ -386,13 +485,6 @@ const DetailPenjemputan = (function () { } } - function buildDraftKey(tps) { - const spjDetailId = (tps?.spjDetailId || '').trim(); - const lokasiAngkutId = (tps?.lokasiAngkutId || '').trim(); - if (!spjDetailId && !lokasiAngkutId) return ''; - return `tps-${spjDetailId || 'no-spj'}-${lokasiAngkutId || 'no-lokasi'}`.replace(/[^a-zA-Z0-9_-]/g, ''); - } - function getActiveTps() { return state.tpsData[state.activeTpsIndex] || null; } @@ -415,98 +507,83 @@ const DetailPenjemputan = (function () { if (isOk) setTimeout(() => { statusEl.style.opacity = '0'; }, 2500); } - function scheduleAutoSave() { + function scheduleAutoSave(tpsIndex = state.activeTpsIndex) { clearTimeout(autoSaveTimer); showAutoSaveStatus('menyimpan...'); - autoSaveTimer = setTimeout(autoSaveDraft, 1000); + autoSaveTimer = setTimeout(() => autoSaveRecord(tpsIndex), 1000); } - async function autoSaveDraft() { - const tps = getActiveTps(); - if (!tps || !tps.draftKey) return; + function buildAutoSavePayload(tps) { + return { + nomorSpj: state.nomorSpj || '', + namaTps: 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, + timbangan: (tps.timbangan || []).map(item => ({ + berat: item.weight || 0, + jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS, + fotoFileName: item.fotoFileName || '', + uploaded: item.uploaded || false, + ocrInfo: item.ocrInfo || '' + })), + totalOrganik: tps.totalOrganik || 0, + totalAnorganik: tps.totalAnorganik || 0, + totalResidu: tps.totalResidu || 0, + totalTimbangan: tps.totalTimbangan || 0, + fotoPetugasFileNames: tps.fotoPetugasFileNames || [], + fotoPetugasUploaded: tps.fotoPetugasUploaded || false, + namaPetugas: tps.namaPetugas || '' + }; + } - const payload = { - draftKey: tps.draftKey, - lokasiAngkutId: tps.lokasiAngkutId || '', - spjDetailId: tps.spjDetailId || '', - latitude: tps.latitude || '', - longitude: tps.longitude || '', - alamatJalan: tps.alamatJalan || '', - waktuKedatangan: tps.waktuKedatangan || '', - fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [], - fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false, - timbangan: (tps.timbangan || []).map(item => ({ - berat: item.weight || 0, - jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS, - fotoFileName: item.fotoFileName || '', - uploaded: item.uploaded || false, - ocrInfo: item.ocrInfo || '' - })), - totalOrganik: tps.totalOrganik || 0, - totalAnorganik: tps.totalAnorganik || 0, - totalResidu: tps.totalResidu || 0, - totalTimbangan: tps.totalTimbangan || 0, - fotoPetugasFileNames: tps.fotoPetugasFileNames || [], - fotoPetugasUploaded: tps.fotoPetugasUploaded || false, - namaPetugas: tps.namaPetugas || '' - }; + async function autoSaveRecord(tpsIndex = state.activeTpsIndex) { + const tps = state.tpsData[tpsIndex] || null; + if (!tps) return; + if (tps.submitted) return; + if (isAutoSaving) { + pendingAutoSaveIndex = tpsIndex; + return; + } + + const payload = buildAutoSavePayload(tps); + const payloadSignature = JSON.stringify(payload); + if (payloadSignature === lastAutoSaveSignature) { + showAutoSaveStatus('✓ Data tersimpan', true); + return; + } try { - const res = await fetch(ENDPOINTS.saveDraft, { + isAutoSaving = true; + const res = await fetch(ENDPOINTS.saveRecord, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) + body: payloadSignature }); const data = await res.json(); - showAutoSaveStatus(data.success ? '✓ Draft tersimpan' : '✗ Gagal simpan', data.success); + if (data.success) { + lastAutoSaveSignature = payloadSignature; + } + showAutoSaveStatus(data.success ? '✓ Data tersimpan' : '✗ Gagal simpan', data.success); } catch (_) { - showAutoSaveStatus('✗ Gagal simpan draft'); + showAutoSaveStatus('✗ Gagal simpan data'); + } finally { + isAutoSaving = false; + if (pendingAutoSaveIndex !== null) { + const nextIndex = pendingAutoSaveIndex; + pendingAutoSaveIndex = null; + scheduleAutoSave(nextIndex); + } } } - async function loadDraftForTps(tps) { - if (!tps || !tps.draftKey) return; - - try { - const res = await fetch(`${ENDPOINTS.loadDraft}?draftKey=${encodeURIComponent(tps.draftKey)}`); - if (!res.ok) return; - const data = await res.json(); - if (!data.success || !data.hasDraft || !data.draft) return; - - const draft = data.draft; - tps.lokasiAngkutId = draft.lokasiAngkutId || tps.lokasiAngkutId; - tps.spjDetailId = draft.spjDetailId || tps.spjDetailId; - tps.latitude = draft.latitude || ''; - tps.longitude = draft.longitude || ''; - tps.alamatJalan = draft.alamatJalan || ''; - tps.waktuKedatangan = draft.waktuKedatangan || ''; - tps.fotoKedatanganFileNames = draft.fotoKedatanganFileNames || []; - tps.fotoKedatanganUploaded = draft.fotoKedatanganUploaded || false; - tps.fotoPetugasFileNames = draft.fotoPetugasFileNames || []; - tps.fotoPetugasUploaded = draft.fotoPetugasUploaded || false; - tps.namaPetugas = draft.namaPetugas || ''; - tps.totalOrganik = draft.totalOrganik || 0; - tps.totalAnorganik = draft.totalAnorganik || 0; - tps.totalResidu = draft.totalResidu || 0; - tps.totalTimbangan = draft.totalTimbangan || 0; - tps.timbangan = (draft.timbangan || []).map(item => ({ - file: null, - fotoFileName: item.fotoFileName || '', - weight: item.berat || 0, - jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS, - uploaded: item.uploaded || false, - ocrInfo: item.ocrInfo || 'OCR: belum diproses.' - })); - } catch (error) { - console.warn('Gagal memuat draft TPS:', error); - } - } - - async function loadDraftsForAllTps() { - await Promise.all(state.tpsData.map(loadDraftForTps)); - } - async function initializeLocation(tpsList) { + const persistedUiState = getPersistedUiState(); state.availableTpsList = tpsList || []; if (elements.tpsSelectionContainer) { elements.tpsSelectionContainer.style.display = 'none'; @@ -515,7 +592,7 @@ const DetailPenjemputan = (function () { if (state.availableTpsList.length === 0) { state.selectedTpsList = ['1 Lokasi TPS']; initializeTpsData(state.selectedTpsList); - await loadDraftsForAllTps(); + await loadRecordsForCurrentSpj(); elements.tpsTabsContainer.style.display = 'block'; renderSingleForm(); return; @@ -523,7 +600,9 @@ const DetailPenjemputan = (function () { state.selectedTpsList = [...state.availableTpsList]; initializeTpsData(state.selectedTpsList); - await loadDraftsForAllTps(); + await loadRecordsForCurrentSpj(); + applyPersistedTpsData(persistedUiState); + applyPersistedUiState(persistedUiState); elements.tpsTabsContainer.style.display = 'block'; if (state.selectedTpsList.length === 1) { @@ -540,7 +619,6 @@ const DetailPenjemputan = (function () { index: index, lokasiAngkutId: typeof tpsItem === 'string' ? '' : (tpsItem?.lokasiAngkutId || tpsItem?.LokasiAngkutID || ''), spjDetailId: typeof tpsItem === 'string' ? '' : (tpsItem?.spjDetailId || tpsItem?.SpjDetailID || ''), - draftKey: '', latitude: '', longitude: '', alamatJalan: '', @@ -558,7 +636,7 @@ const DetailPenjemputan = (function () { fotoPetugasUploaded: false, namaPetugas: '', submitted: false - })).map(tps => ({ ...tps, draftKey: buildDraftKey(tps) })); + })); state.hasRequestedLocation = new Array(tpsNames.length).fill(false); } @@ -596,7 +674,7 @@ const DetailPenjemputan = (function () { } initializeTpsData(state.selectedTpsList); - await loadDraftsForAllTps(); + await loadRecordsForCurrentSpj(); elements.tpsSelectionContainer.style.display = 'none'; elements.tpsTabsContainer.style.display = 'block'; @@ -654,9 +732,10 @@ const DetailPenjemputan = (function () { renderTabs(); renderTpsForm(); - if (!state.hasRequestedLocation[index]) { + const switchedTps = state.tpsData[index]; + if (!state.hasRequestedLocation[index] && !switchedTps?.submitted) { state.hasRequestedLocation[index] = true; - getLocationUpdate(); + getLocationUpdate(index); } updateAllTotals(); @@ -667,6 +746,22 @@ const DetailPenjemputan = (function () { const showTpsName = state.selectedTpsList.length > 1 || state.availableTpsList.length > 0; const submitState = getSubmitState(tps); + const actionMarkup = tps.submitted + ? `
+ + + + + Data ${showTpsName ? tps.name + ' ' : ''}sudah disubmit + +
` + : ` +
+ Batal + +
+ ${submitState.canSubmit ? '' : `

${submitState.message}

`} + `; elements.tpsContentContainer.innerHTML = `
@@ -684,11 +779,7 @@ const DetailPenjemputan = (function () { ${renderSection2Timbangan(tps, showTpsName)} ${renderSection3Petugas(tps)} -
- Batal - -
- ${submitState.canSubmit ? '' : `

${submitState.message}

`} + ${actionMarkup}

`; @@ -782,6 +873,7 @@ const DetailPenjemputan = (function () { function attachTpsFormListeners() { const form = elements.tpsContentContainer.querySelector("form"); const tps = state.tpsData[state.activeTpsIndex]; + if (tps.submitted) return; const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan"); const fotoPetugasInput = form.querySelector(".tps-foto-petugas"); @@ -791,10 +883,9 @@ const DetailPenjemputan = (function () { fotoKedatanganInput.addEventListener('change', function() { tps.fotoKedatangan = Array.from(this.files); tps.fotoKedatanganUploaded = false; - updateWaktuKedatangan(); + updateWaktuKedatangan(tps.index); updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan')); refreshKedatanganUploadState(form); - scheduleAutoSave(); }); fotoPetugasInput.addEventListener('change', function() { @@ -802,24 +893,23 @@ const DetailPenjemputan = (function () { tps.fotoPetugasUploaded = false; updateMultiPreview(this, form.querySelector('.tps-preview-petugas')); refreshPetugasUploadState(form); - scheduleAutoSave(); }); namaPetugasInput.addEventListener('input', function() { tps.namaPetugas = this.value; refreshPetugasUploadState(form); + scheduleAutoSave(tps.index); }); namaPetugasInput.addEventListener('blur', function() { tps.namaPetugas = this.value; - scheduleAutoSave(); + scheduleAutoSave(tps.index); }); btnAddTimbangan.addEventListener('click', function() { createTimbanganItem(form.querySelector('.tps-timbangan-repeater')); syncTimbanganToTpsData(); refreshSubmitButtonState(form); - scheduleAutoSave(); }); const btnUploadKedatangan = form.querySelector( @@ -887,7 +977,7 @@ const DetailPenjemputan = (function () { item.className = "rounded-xl border border-gray-200 overflow-hidden bg-black"; - const imageUrl = URL.createObjectURL(file); + const imageUrl = getStoredPhotoUrl(file); item.innerHTML = `
Preview ${index + 1} @@ -904,8 +994,9 @@ const DetailPenjemputan = (function () { }); } - function updateWaktuKedatangan() { - const tps = state.tpsData[state.activeTpsIndex]; + function updateWaktuKedatangan(tpsIndex = state.activeTpsIndex) { + const tps = state.tpsData[tpsIndex]; + if (!tps) return; const now = new Date(); const formatted = now.toLocaleString("id-ID", { day: "2-digit", @@ -917,22 +1008,24 @@ const DetailPenjemputan = (function () { }); tps.waktuKedatangan = formatted; - const form = elements.tpsContentContainer.querySelector("form"); - const displayWaktu = form.querySelector(".tps-waktu-kedatangan"); - if (displayWaktu) displayWaktu.value = formatted; + if (tpsIndex === state.activeTpsIndex) { + const form = elements.tpsContentContainer.querySelector("form"); + const displayWaktu = form?.querySelector(".tps-waktu-kedatangan"); + if (displayWaktu) displayWaktu.value = formatted; + } - getLocationUpdate(); + getLocationUpdate(tpsIndex); saveState(); } - function getLocationUpdate() { + function getLocationUpdate(tpsIndex = state.activeTpsIndex) { 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); + reverseGeocode(lat, lng, tpsIndex); }, function () { console.log("Lokasi tidak diizinkan"); @@ -940,28 +1033,30 @@ const DetailPenjemputan = (function () { ); } - function reverseGeocode(lat, lng) { - const tps = state.tpsData[state.activeTpsIndex]; + function reverseGeocode(lat, lng, tpsIndex = state.activeTpsIndex) { + const tps = state.tpsData[tpsIndex]; + if (!tps) return; 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); + updateTpsLocation(lat, lng, address, tpsIndex); }) .catch(() => { - updateTpsLocation(lat, lng, `${lat}, ${lng}`); + updateTpsLocation(lat, lng, `${lat}, ${lng}`, tpsIndex); }); } - function updateTpsLocation(lat, lng, address) { - const tps = state.tpsData[state.activeTpsIndex]; + function updateTpsLocation(lat, lng, address, tpsIndex = state.activeTpsIndex) { + const tps = state.tpsData[tpsIndex]; + if (!tps) return; tps.latitude = lat; tps.longitude = lng; tps.alamatJalan = address; - const form = elements.tpsContentContainer.querySelector('form'); + const form = tpsIndex === state.activeTpsIndex ? elements.tpsContentContainer.querySelector('form') : null; if (form) { const latInput = form.querySelector('.tps-display-latitude'); const lngInput = form.querySelector('.tps-display-longitude'); @@ -969,7 +1064,6 @@ const DetailPenjemputan = (function () { if (lngInput) lngInput.value = lng; } - scheduleAutoSave(); } function updateMultiPreview(input, previewContainer) { @@ -1010,12 +1104,13 @@ const DetailPenjemputan = (function () { 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')); + const imageUrl = getStoredPhotoUrl(url); + const isUrl = Boolean(imageUrl) && (imageUrl.startsWith('/') || imageUrl.startsWith('http')); if (isUrl) { item.innerHTML = `
- Foto ${index + 1} + Foto ${index + 1}
`; } else { @@ -1036,8 +1131,8 @@ const DetailPenjemputan = (function () { const weight = existingData ? (existingData.weight || 0) : 0; const jenisSampah = existingData ? (existingData.jenisSampah || CONFIG.DEFAULT_JENIS) : CONFIG.DEFAULT_JENIS; - const hasFileBlob = Boolean(existingData?.file); - const hasFile = Boolean(existingData?.file || existingData?.fotoFileName); + const hasFileBlob = isBrowserFile(existingData?.file); + const hasFile = Boolean(hasStoredPhoto(existingData?.file) || existingData?.fotoFileName); const isUploaded = Boolean(existingData?.uploaded); const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'); @@ -1047,7 +1142,7 @@ const DetailPenjemputan = (function () {
-
+
Preview foto timbangan

${ocrInfoText}

@@ -1078,11 +1173,12 @@ const DetailPenjemputan = (function () { const jenisSampahSelect = item.querySelector(".input-jenis-sampah"); const removeBtn = item.querySelector(".btn-remove-timbangan"); - if (existingData && existingData.file) { - const localUrl = URL.createObjectURL(existingData.file); + if (existingData && hasStoredPhoto(existingData.file)) { + const localUrl = getStoredPhotoUrl(existingData.file); previewImage.src = localUrl; previewWrap.classList.remove('hidden'); previewImage.onload = function() { + if (!isBrowserFile(resolveStoredPhoto(existingData.file))) return; URL.revokeObjectURL(localUrl); }; } else if (existingData && existingData.fotoFileName && existingData.fotoFileName.startsWith('/')) { @@ -1120,7 +1216,6 @@ const DetailPenjemputan = (function () { tps.timbangan[itemIndex].fotoFileName = ''; refreshTimbanganUploadState(item); } - scheduleAutoSave(); } }); @@ -1134,7 +1229,7 @@ const DetailPenjemputan = (function () { refreshTimbanganUploadState(item); const form = elements.tpsContentContainer.querySelector('form'); if (form) refreshSubmitButtonState(form); - scheduleAutoSave(); + scheduleAutoSave(state.activeTpsIndex); }); weightInputDisplay.addEventListener("blur", function () { @@ -1158,7 +1253,6 @@ const DetailPenjemputan = (function () { syncTimbanganToTpsData(); const form = elements.tpsContentContainer.querySelector('form'); if (form) refreshSubmitButtonState(form); - scheduleAutoSave(); }); removeBtn.addEventListener('click', function() { @@ -1176,7 +1270,6 @@ const DetailPenjemputan = (function () { updateTpsTotalTimbangan(); syncTimbanganToTpsData(); if (form) refreshSubmitButtonState(form); - scheduleAutoSave(); }); repeater.appendChild(item); @@ -1277,6 +1370,10 @@ const DetailPenjemputan = (function () { } 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 terlebih dahulu.' }; @@ -1318,10 +1415,10 @@ const DetailPenjemputan = (function () { function refreshSubmitButtonState(form) { const submitButton = form.querySelector('button[type="submit"]'); - if (!submitButton) return; + const tps = state.tpsData[state.activeTpsIndex]; + if (tps?.submitted || !submitButton) return; let messageEl = form.querySelector(".submit-state-message"); - const tps = state.tpsData[state.activeTpsIndex]; const submitState = getSubmitState(tps); submitButton.disabled = !submitState.canSubmit; @@ -1825,7 +1922,8 @@ const DetailPenjemputan = (function () { const formData = new FormData(); formData.append('FotoTimbangan', timbanganItem.file); - formData.append('DraftKey', tps.draftKey || ''); + formData.append('NomorSpj', state.nomorSpj || ''); + formData.append('NamaTps', tps.name || 'TPS'); formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('ItemIndex', itemIndex); @@ -1863,7 +1961,7 @@ const DetailPenjemputan = (function () { syncTimbanganToTpsData(); const form = elements.tpsContentContainer.querySelector('form'); if (form) refreshSubmitButtonState(form); - scheduleAutoSave(); + scheduleAutoSave(tps.index); } async function uploadFotoKedatangan() { @@ -1882,7 +1980,8 @@ const DetailPenjemputan = (function () { const formData = new FormData(); tps.fotoKedatangan.forEach(file => formData.append('FotoKedatangan', file)); - formData.append('DraftKey', tps.draftKey || ''); + formData.append('NomorSpj', state.nomorSpj || ''); + formData.append('NamaTps', tps.name || 'TPS'); formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('WaktuKedatangan', tps.waktuKedatangan || ''); @@ -1902,7 +2001,7 @@ const DetailPenjemputan = (function () { if (fotoInput) fotoInput.value = ''; refreshKedatanganUploadState(form); } - scheduleAutoSave(); + scheduleAutoSave(tps.index); } else { showToast(data.message || 'Gagal upload foto kedatangan.', 'error'); if (btn) { @@ -1939,7 +2038,8 @@ const DetailPenjemputan = (function () { const formData = new FormData(); tps.fotoPetugas.forEach(file => formData.append('FotoPetugas', file)); - formData.append('DraftKey', tps.draftKey || ''); + formData.append('NomorSpj', state.nomorSpj || ''); + formData.append('NamaTps', tps.name || 'TPS'); formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('NamaPetugas', tps.namaPetugas || ''); @@ -1956,7 +2056,7 @@ const DetailPenjemputan = (function () { if (fotoInput) fotoInput.value = ''; refreshPetugasUploadState(form); } - scheduleAutoSave(); + scheduleAutoSave(tps.index); } else { showToast(data.message || 'Gagal upload foto petugas.', 'error'); if (btn) { @@ -1975,8 +2075,9 @@ const DetailPenjemputan = (function () { function buildSubmitFormData(tps) { const formData = new FormData(); - formData.append("LokasiAngkutID", tps.lokasiAngkutId || ""); - formData.append("SpjDetailID", tps.spjDetailId || ""); + formData.append("NomorSpj", state.nomorSpj || ""); + formData.append("LokasiAngkutId", tps.lokasiAngkutId || ""); + formData.append("SpjDetailId", tps.spjDetailId || ""); formData.append("TpsName", tps.name); formData.append("Latitude", tps.latitude); formData.append("Longitude", tps.longitude); @@ -2053,16 +2154,18 @@ const DetailPenjemputan = (function () { try { const response = await fetch(ENDPOINTS.submit, { method: 'POST', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json' + }, body: formData }); - if (response.ok || response.redirected) { - markTpsSubmitted(tps); - if (tps.draftKey) { - await fetch(`${ENDPOINTS.deleteDraft}?draftKey=${encodeURIComponent(tps.draftKey)}`, { method: 'DELETE' }); - } + const result = await response.json().catch(() => null); - showToast(`Data ${tps.name} berhasil disimpan!`, 'success'); + if (response.ok && result?.success) { + markTpsSubmitted(tps); + showToast(result.message || `Data ${tps.name} berhasil disimpan!`, 'success'); const allSubmitted = state.tpsData.every(item => item.submitted); if (allSubmitted) { @@ -2074,8 +2177,7 @@ const DetailPenjemputan = (function () { renderTpsForm(); } } else { - const errorText = await response.text(); - showToast(errorText || 'Gagal menyimpan data. Silakan coba lagi.', 'error'); + showToast(result?.message || 'Gagal menyimpan data. Silakan coba lagi.', 'error'); if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = `Submit${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}`; @@ -2139,7 +2241,7 @@ const DetailPenjemputan = (function () { return { init: init, - hydrateFromApi: applyApiDraftData, + hydrateFromApi: applyApiRecordData, setNomorSpj: function (nomorSpj) { state.nomorSpj = nomorSpj; saveState(); @@ -2148,8 +2250,17 @@ const DetailPenjemputan = (function () { })(); document.addEventListener('DOMContentLoaded', async function() { + const nomorSpjEl = document.querySelector('.text-gray-600.font-mono'); + const nomorSpj = nomorSpjEl ? nomorSpjEl.textContent.trim() : 'SPJ/07-2025/PKM/000476'; + try { - const response = await fetch('/driver/json/tps-list.json'); + const response = await fetch(`/driver/json/tps-list.json?_ts=${Date.now()}`, { + cache: 'no-store', + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + Pragma: 'no-cache' + } + }); const data = await response.json(); const tpsList = data.tpsList.map(tps => ({ name: tps.name, @@ -2157,16 +2268,13 @@ document.addEventListener('DOMContentLoaded', async function() { spjDetailId: tps.spjDetailId, id: tps.id })); - await DetailPenjemputan.init(tpsList); + await DetailPenjemputan.init(tpsList, nomorSpj); } catch (error) { console.error('Error loading TPS list:', error); } - - const platNomorEl = document.getElementById('plat-nomor'); - if (platNomorEl) { - const nomorSpjEl = document.querySelector('.text-gray-600.font-mono'); - if (nomorSpjEl) { - DetailPenjemputan.setNomorSpj(nomorSpjEl.textContent.trim()); - } + + const platNomorEl = document.getElementById('plat-nomor'); + if (platNomorEl && nomorSpjEl) { + DetailPenjemputan.setNomorSpj(nomorSpj); } }); diff --git a/wwwroot/driver/json/detail-penjemputan-non-tps.json b/wwwroot/driver/json/detail-penjemputan-non-tps.json index c2f91f5..daf02d2 100644 --- a/wwwroot/driver/json/detail-penjemputan-non-tps.json +++ b/wwwroot/driver/json/detail-penjemputan-non-tps.json @@ -2,11 +2,11 @@ "detailPenjemputan": { "namaTps": "TPS A", "namaPerusahaan": "CV Tri Berkah Sejahtera", - "nomorSpj": "SPJ/07-2025/PKM/000476", + "nomorSpj": "SPJ/07-2025/PKM/000500", "platNomor": "B 9632 TOR", "nomorPintu": "JRC 005", "alamat": "Kp. Pertanian II Rt.004 Rw.001 Kel. Klender Kec, Duren Sawit, Kota Adm. Jakarta Timur 13470", - "lokasiAngkutId": "", - "spjDetailId": "" + "lokasiAngkutId": "LOK001", + "spjDetailId": "SPJ001" } } \ No newline at end of file diff --git a/wwwroot/driver/manifest.json b/wwwroot/driver/manifest.json index 778c965..1481436 100644 --- a/wwwroot/driver/manifest.json +++ b/wwwroot/driver/manifest.json @@ -1,8 +1,8 @@ { "id": "/upst", - "name": "eSPJ - Surat Perjalanan Dinas", - "short_name": "eSPJ", - "description": "Aplikasi pengelolaan Surat Perjalanan Dinas yang modern dan efisien", + "name": "PKM UPST - SPJ Driver UPST", + "short_name": "PKM UPST", + "description": "Aplikasi pengelolaan SPJ untuk driver UPST", "start_url": "/upst", "display": "standalone", "background_color": "#ffffff", @@ -27,17 +27,17 @@ ], "shortcuts": [ { - "name": "Home UPST", - "short_name": "Home", + "name": "Beranda", + "short_name": "Beranda", "description": "Buka dashboard utama UPST", "url": "/upst", "icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }] }, { - "name": "Halaman Kosong", - "short_name": "Kosong", - "description": "Buka halaman alternatif UPST", - "url": "/upst/kosong", + "name": "Riwayat Perjalanan", + "short_name": "Riwayat", + "description": "Buka halaman riwayat perjalanan UPST", + "url": "/upst/riwayat", "icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }] } ] diff --git a/wwwroot/driver/serviceworker.js b/wwwroot/driver/serviceworker.js index 704d7ff..a78a7c5 100644 --- a/wwwroot/driver/serviceworker.js +++ b/wwwroot/driver/serviceworker.js @@ -1,4 +1,4 @@ -const CACHE_NAME = "espj-upst-pwa-v1"; +const CACHE_NAME = "espj-upst-pwa-v2"; const OFFLINE_URL = "/driver/offline.html"; const PRECACHE_URLS = [ "/upst", @@ -12,7 +12,27 @@ const PRECACHE_URLS = [ self.addEventListener("install", (event) => { event.waitUntil( - caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS)) + caches.open(CACHE_NAME).then(async (cache) => { + const results = await Promise.allSettled( + PRECACHE_URLS.map(async (url) => { + const response = await fetch(url, { cache: "no-store" }); + if (!response.ok) { + throw new Error(`Gagal precache: ${url} (${response.status})`); + } + + await cache.put(url, response.clone()); + }) + ); + + const offlineCached = results.some( + (result, index) => + PRECACHE_URLS[index] === OFFLINE_URL && result.status === "fulfilled" + ); + + if (!offlineCached) { + throw new Error(`Offline page gagal diprecache: ${OFFLINE_URL}`); + } + }) ); self.skipWaiting(); }); @@ -44,6 +64,16 @@ function isAuthPath(url) { return AUTH_PATHS.some((path) => url.pathname.startsWith(path)); } +const BYPASS_CACHE_PATHS = [ + "/upst/detail-penjemputan/api/", + "/uploads/penjemputan/", + "/driver/json/" +]; + +function shouldBypassCache(url) { + return BYPASS_CACHE_PATHS.some((path) => url.pathname.startsWith(path)); +} + self.addEventListener("fetch", (event) => { const { request } = event; const url = new URL(request.url); @@ -60,6 +90,19 @@ self.addEventListener("fetch", (event) => { return; } + if (shouldBypassCache(url)) { + event.respondWith( + fetch(new Request(request, { cache: "no-store" })).catch(async () => { + if (request.mode === "navigate") { + return (await caches.match(OFFLINE_URL)) || Response.error(); + } + + return Response.error(); + }) + ); + return; + } + if (request.mode === "navigate") { event.respondWith( fetch(request) @@ -72,7 +115,7 @@ self.addEventListener("fetch", (event) => { }) .catch(async () => { const cachedPage = await caches.match(request); - return cachedPage || caches.match(OFFLINE_URL); + return cachedPage || (await caches.match(OFFLINE_URL)) || Response.error(); }) ); return;