diff --git a/Controllers/SpjDriverUpstController/DetailController.cs b/Controllers/SpjDriverUpstController/DetailController.cs index 92daf74..b995409 100644 --- a/Controllers/SpjDriverUpstController/DetailController.cs +++ b/Controllers/SpjDriverUpstController/DetailController.cs @@ -15,17 +15,43 @@ namespace eSPJ.Controllers.SpjDriverUpstController private readonly IConfiguration _configuration; private readonly DetailPenjemputanService _detailService; private readonly ILogger _logger; + private readonly IWebHostEnvironment _env; public DetailPenjemputanController( IHttpClientFactory httpClientFactory, IConfiguration configuration, DetailPenjemputanService detailService, - ILogger logger) + ILogger logger, + IWebHostEnvironment env) { _httpClientFactory = httpClientFactory; _configuration = configuration; _detailService = detailService; _logger = logger; + _env = env; + } + + private static string ResolveDraftKey(string? draftKey, string? sessionKey, string? spjDetailId = null, string? lokasiAngkutId = null) + { + var rawKey = !string.IsNullOrWhiteSpace(draftKey) + ? draftKey + : !string.IsNullOrWhiteSpace(sessionKey) + ? sessionKey + : $"non-tps-{spjDetailId}-{lokasiAngkutId}"; + + return string.Concat((rawKey ?? string.Empty).Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_')); + } + + private string GetUploadDirectory(string dateFolder) + { + var uploadDir = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", dateFolder); + Directory.CreateDirectory(uploadDir); + return uploadDir; + } + + private static string BuildUploadUrl(string dateFolder, string fileName) + { + return $"/uploads/penjemputan/{dateFolder}/{fileName}"; } [HttpGet("")] @@ -90,6 +116,494 @@ namespace eSPJ.Controllers.SpjDriverUpstController } } + [HttpPost("save-draft-non-tps")] + [IgnoreAntiforgeryToken] + public async Task SaveDraftNonTps([FromBody] DraftSaveRequest request) + { + if (request == null) + return BadRequest(new DraftSaveResponse { 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); + return result.Success ? Ok(result) : StatusCode(500, result); + } + + [HttpGet("load-draft-non-tps")] + [IgnoreAntiforgeryToken] + public async Task LoadDraftNonTps([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.LoadDraftNonTpsAsync(key); + return Ok(result); + } + + [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." }); + + 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); + 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? SpjDetailId, + [FromForm] string? LokasiAngkutId, + [FromForm] string? WaktuKedatangan, + [FromForm] string? Latitude, + [FromForm] string? Longitude, + [FromForm] string? AlamatJalan) + { + if (FotoKedatangan == null || FotoKedatangan.Count == 0) + return BadRequest(new { success = false, message = "Tidak ada foto." }); + + var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); + var uploadDir = GetUploadDirectory(dateFolder); + + var fileNames = new List(); + foreach (var file in FotoKedatangan) + { + if (file.Length == 0) continue; + var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); + var name = $"kedatangan_{Guid.NewGuid()}{ext}"; + var path = Path.Combine(uploadDir, name); + await using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + fileNames.Add(name); + } + var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).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 + { + 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 + }); + } + + return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." }); + } + + [HttpPost("upload-foto-timbangan-non-tps")] + [IgnoreAntiforgeryToken] + public async Task UploadFotoTimbanganNonTps( + [FromForm] IFormFile? FotoTimbangan, + [FromForm] string? DraftKey, + [FromForm] string? SessionKey, + [FromForm] string? SpjDetailId, + [FromForm] string? LokasiAngkutId, + [FromForm] int ItemIndex, + [FromForm] string? JenisSampah, + [FromForm] decimal Berat) + { + if (FotoTimbangan == null || FotoTimbangan.Length == 0) + return BadRequest(new { success = false, message = "Tidak ada foto." }); + + var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); + var uploadDir = GetUploadDirectory(dateFolder); + + var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant(); + var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant(); + var beratStr = Berat.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture).Replace('.', '_'); + var name = $"timbangan{ItemIndex + 1}-{jenisSafe}-{beratStr}{ext}"; + 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 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 + { + 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 + }); + } + + return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." }); + } + + [HttpPost("upload-foto-petugas-non-tps")] + [IgnoreAntiforgeryToken] + public async Task UploadFotoPetugasNonTps( + [FromForm] List? FotoPetugas, + [FromForm] string? DraftKey, + [FromForm] string? SessionKey, + [FromForm] string? SpjDetailId, + [FromForm] string? LokasiAngkutId, + [FromForm] string? NamaPetugas) + { + if (FotoPetugas == null || FotoPetugas.Count == 0) + return BadRequest(new { success = false, message = "Tidak ada foto." }); + + var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); + var uploadDir = GetUploadDirectory(dateFolder); + + var fileNames = new List(); + foreach (var file in FotoPetugas) + { + if (file.Length == 0) continue; + var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); + var name = $"petugas_{Guid.NewGuid()}{ext}"; + var path = Path.Combine(uploadDir, name); + await using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + fileNames.Add(name); + } + var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).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 + { + 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 + }); + } + + return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." }); + } + + [HttpPost("upload-foto-kedatangan")] + [IgnoreAntiforgeryToken] + public async Task UploadFotoKedatangan( + [FromForm] List? FotoKedatangan, + [FromForm] string? DraftKey, + [FromForm] string? SessionKey, + [FromForm] string? SpjDetailId, + [FromForm] string? LokasiAngkutId, + [FromForm] string? WaktuKedatangan, + [FromForm] string? Latitude, + [FromForm] string? Longitude, + [FromForm] string? AlamatJalan) + { + if (FotoKedatangan == null || FotoKedatangan.Count == 0) + return BadRequest(new { success = false, message = "Tidak ada foto." }); + + var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); + var uploadDir = GetUploadDirectory(dateFolder); + + var fileNames = new List(); + foreach (var file in FotoKedatangan) + { + if (file.Length == 0) continue; + var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); + var name = $"kedatangan_{Guid.NewGuid()}{ext}"; + var path = Path.Combine(uploadDir, name); + await using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + fileNames.Add(name); + } + var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).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 + { + 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 + }); + } + + return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." }); + } + + [HttpPost("upload-foto-timbangan")] + [IgnoreAntiforgeryToken] + public async Task UploadFotoTimbangan( + [FromForm] IFormFile? FotoTimbangan, + [FromForm] string? DraftKey, + [FromForm] string? SessionKey, + [FromForm] string? SpjDetailId, + [FromForm] string? LokasiAngkutId, + [FromForm] int ItemIndex, + [FromForm] string? JenisSampah, + [FromForm] decimal Berat) + { + if (FotoTimbangan == null || FotoTimbangan.Length == 0) + return BadRequest(new { success = false, message = "Tidak ada foto." }); + + var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); + var uploadDir = GetUploadDirectory(dateFolder); + + var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant(); + var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant(); + var beratStr = Berat.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture).Replace('.', '_'); + var name = $"timbangan{ItemIndex + 1}-{jenisSafe}-{beratStr}{ext}"; + 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 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 + { + 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 + }); + } + + return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." }); + } + + [HttpPost("upload-foto-petugas")] + [IgnoreAntiforgeryToken] + public async Task UploadFotoPetugas( + [FromForm] List? FotoPetugas, + [FromForm] string? DraftKey, + [FromForm] string? SessionKey, + [FromForm] string? SpjDetailId, + [FromForm] string? LokasiAngkutId, + [FromForm] string? NamaPetugas) + { + if (FotoPetugas == null || FotoPetugas.Count == 0) + return BadRequest(new { success = false, message = "Tidak ada foto." }); + + var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); + var uploadDir = GetUploadDirectory(dateFolder); + + var fileNames = new List(); + foreach (var file in FotoPetugas) + { + if (file.Length == 0) continue; + var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); + var name = $"petugas_{Guid.NewGuid()}{ext}"; + var path = Path.Combine(uploadDir, name); + await using var stream = new FileStream(path, FileMode.Create); + await file.CopyToAsync(stream); + fileNames.Add(name); + } + var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).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 + { + 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 + }); + } + + return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." }); + } + + [HttpPost("ocr-timbangan")] [IgnoreAntiforgeryToken] public async Task OcrTimbangan(IFormFile? Foto) diff --git a/Controllers/SpjDriverUpstController/HistoryController.cs b/Controllers/SpjDriverUpstController/HistoryController.cs index 574b564..e049567 100644 --- a/Controllers/SpjDriverUpstController/HistoryController.cs +++ b/Controllers/SpjDriverUpstController/HistoryController.cs @@ -1,10 +1,17 @@ using Microsoft.AspNetCore.Mvc; +using eSPJ.Services; namespace eSPJ.Controllers.SpjDriverUpstController { [Route("upst/history")] public class HistoryController : Controller { + private readonly HistoryService _historyService; + + public HistoryController(HistoryService historyService) + { + _historyService = historyService; + } [HttpGet("")] public IActionResult Index() @@ -12,6 +19,26 @@ namespace eSPJ.Controllers.SpjDriverUpstController return View("~/Views/Admin/Transport/SpjDriverUpst/History/Index.cshtml"); } + [HttpGet("api")] + public async Task GetHistory([FromQuery] string? fromDate = null, [FromQuery] string? toDate = null, [FromQuery] int page = 1, [FromQuery] int pageSize = 5) + { + DateOnly? parsedFromDate = null; + DateOnly? parsedToDate = null; + + if (!string.IsNullOrWhiteSpace(fromDate) && DateOnly.TryParse(fromDate, out var from)) + { + parsedFromDate = from; + } + + if (!string.IsNullOrWhiteSpace(toDate) && DateOnly.TryParse(toDate, out var to)) + { + parsedToDate = to; + } + + var result = await _historyService.GetUpstHistoryAsync(parsedFromDate, parsedToDate, page, pageSize <= 0 ? 5 : pageSize); + return Ok(result); + } + [HttpGet("details/{id}")] public IActionResult Details(int id) { diff --git a/Data/detail-penjemputan.json b/Data/detail-penjemputan.json index fe51488..8a316ba 100644 --- a/Data/detail-penjemputan.json +++ b/Data/detail-penjemputan.json @@ -1 +1,38 @@ -[] +[ + { + "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/Data/history-upst.json b/Data/history-upst.json new file mode 100644 index 0000000..88f05f9 --- /dev/null +++ b/Data/history-upst.json @@ -0,0 +1,110 @@ +[ + { + "id": 1, + "noSpj": "SPJ/07-2025/PKM/000478", + "plat": "B 5678 ABC", + "kode": "JRC 007", + "tujuan": "Bantar Gebang", + "status": "In Progress", + "tanggalWaktu": "2025-07-28T16:45:00" + }, + { + "id": 2, + "noSpj": "SPJ/07-2025/PKM/000476", + "plat": "B 9632 TOR", + "kode": "JRC 005", + "tujuan": "RDF Rorotan", + "status": "Completed", + "tanggalWaktu": "2025-07-27T14:30:00" + }, + { + "id": 3, + "noSpj": "SPJ/07-2025/PKM/000477", + "plat": "B 1234 XYZ", + "kode": "JRC 006", + "tujuan": "RDF Pesanggarahan", + "status": "Completed", + "tanggalWaktu": "2025-07-26T09:15:00" + }, + { + "id": 4, + "noSpj": "SPJ/07-2025/PKM/000479", + "plat": "B 9876 DEF", + "kode": "JRC 008", + "tujuan": "RDF Sunter", + "status": "Completed", + "tanggalWaktu": "2025-07-25T11:20:00" + }, + { + "id": 5, + "noSpj": "SPJ/07-2025/PKM/000480", + "plat": "B 4321 GHI", + "kode": "JRC 009", + "tujuan": "Bantar Gebang", + "status": "Completed", + "tanggalWaktu": "2025-07-24T08:45:00" + }, + { + "id": 6, + "noSpj": "SPJ/07-2025/PKM/000481", + "plat": "B 7654 JKL", + "kode": "JRC 010", + "tujuan": "RDF Marunda", + "status": "Completed", + "tanggalWaktu": "2025-07-23T10:10:00" + }, + { + "id": 7, + "noSpj": "SPJ/07-2025/PKM/000482", + "plat": "B 2468 MNO", + "kode": "JRC 011", + "tujuan": "Bantar Gebang", + "status": "Completed", + "tanggalWaktu": "2025-07-22T07:25:00" + }, + { + "id": 8, + "noSpj": "SPJ/07-2025/PKM/000483", + "plat": "B 1357 PQR", + "kode": "JRC 012", + "tujuan": "RDF Rorotan", + "status": "In Progress", + "tanggalWaktu": "2025-07-21T13:05:00" + }, + { + "id": 9, + "noSpj": "SPJ/07-2025/PKM/000484", + "plat": "B 8080 STU", + "kode": "JRC 013", + "tujuan": "RDF Pesanggarahan", + "status": "Completed", + "tanggalWaktu": "2025-07-20T15:40:00" + }, + { + "id": 10, + "noSpj": "SPJ/07-2025/PKM/000485", + "plat": "B 9090 VWX", + "kode": "JRC 014", + "tujuan": "RDF Sunter", + "status": "Completed", + "tanggalWaktu": "2025-07-19T12:00:00" + }, + { + "id": 11, + "noSpj": "SPJ/07-2025/PKM/000486", + "plat": "B 2222 YZA", + "kode": "JRC 015", + "tujuan": "Bantar Gebang", + "status": "Completed", + "tanggalWaktu": "2025-07-18T06:55:00" + }, + { + "id": 12, + "noSpj": "SPJ/07-2025/PKM/000487", + "plat": "B 3333 BCD", + "kode": "JRC 016", + "tujuan": "RDF Marunda", + "status": "Completed", + "tanggalWaktu": "2025-07-18T17:30:00" + } +] diff --git a/Models/DetailPenjemputanModels.cs b/Models/DetailPenjemputanModels.cs index 94f20a3..7305e19 100644 --- a/Models/DetailPenjemputanModels.cs +++ b/Models/DetailPenjemputanModels.cs @@ -76,4 +76,74 @@ namespace eSPJ.Models public string? Raw { get; set; } public string Message { get; set; } = string.Empty; } + + public class DraftTimbanganItem + { + public decimal Berat { get; set; } + public string JenisSampah { get; set; } = "Residu"; + public string FotoFileName { get; set; } = string.Empty; + public bool Uploaded { get; set; } + public string OcrInfo { get; set; } = string.Empty; + } + + public class DraftPenjemputanNonTps + { + 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 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 bool FotoKedatanganUploaded { get; set; } + public List FotoKedatanganFileNames { get; set; } = new(); + 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 bool FotoPetugasUploaded { get; set; } + public List FotoPetugasFileNames { get; set; } = new(); + public string NamaPetugas { get; set; } = string.Empty; + } + + public class DraftSaveResponse + { + 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/Models/HistoryModels.cs b/Models/HistoryModels.cs new file mode 100644 index 0000000..c95eeaa --- /dev/null +++ b/Models/HistoryModels.cs @@ -0,0 +1,24 @@ +namespace eSPJ.Models +{ + public class HistoryItem + { + public int Id { get; set; } + public string NoSpj { get; set; } = string.Empty; + public string Plat { get; set; } = string.Empty; + public string Kode { get; set; } = string.Empty; + public string Tujuan { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public DateTime TanggalWaktu { get; set; } + } + + public class HistoryListResponse + { + public List Items { get; set; } = new(); + public int Page { get; set; } + public int PageSize { get; set; } + public int TotalItems { get; set; } + public int TotalPages { get; set; } + public string? FromDate { get; set; } + public string? ToDate { get; set; } + } +} diff --git a/Program.cs b/Program.cs index 0998118..ac2a093 100644 --- a/Program.cs +++ b/Program.cs @@ -8,6 +8,7 @@ builder.Services.AddHttpClient(); // Register custom services builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); @@ -20,8 +21,20 @@ if (!app.Environment.IsDevelopment()) } app.UseHttpsRedirection(); -app.UseRouting(); +app.Use(async (context, next) => +{ + if (context.Request.Path.Equals("/driver/serviceworker.js", StringComparison.OrdinalIgnoreCase)) + { + context.Response.OnStarting(() => + { + context.Response.Headers["Service-Worker-Allowed"] = "/upst"; + return Task.CompletedTask; + }); + } + await next(); +}); +app.UseRouting(); app.UseAuthorization(); app.MapStaticAssets(); diff --git a/Services/DetailPenjemputanService.cs b/Services/DetailPenjemputanService.cs index 2670723..aafd265 100644 --- a/Services/DetailPenjemputanService.cs +++ b/Services/DetailPenjemputanService.cs @@ -114,9 +114,7 @@ namespace eSPJ.Services }; } - var uploadDateFolder = DateTime.Now.ToString("yyyy-MM-dd"); - var uploadPath = Path.Combine(_env.WebRootPath, "uploads", "penjemputan", uploadDateFolder); - var uploadBaseUrl = $"/uploads/penjemputan/{uploadDateFolder}"; + var uploadPath = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", DateTime.Now.ToString("yyyy-MM-dd")); if (!Directory.Exists(uploadPath)) { Directory.CreateDirectory(uploadPath); @@ -216,6 +214,130 @@ namespace eSPJ.Services } } + private string GetDraftFilePath(string prefix, string sessionKey) + { + 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"); + } + + private Task SaveDraftAsync(string prefix, DraftSaveRequest request) + { + return SaveDraftInternalAsync(prefix, request); + } + + private async Task SaveDraftInternalAsync(string prefix, DraftSaveRequest request) + { + try + { + var filePath = GetDraftFilePath(prefix, request.SessionKey); + var draft = new DraftPenjemputanNonTps + { + 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; + } + } + public async Task ProcessOcrTimbanganAsync(IFormFile foto) { try diff --git a/Services/HistoryService.cs b/Services/HistoryService.cs new file mode 100644 index 0000000..7df48ff --- /dev/null +++ b/Services/HistoryService.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using eSPJ.Models; + +namespace eSPJ.Services +{ + public class HistoryService + { + private readonly string _historyUpstFilePath; + private readonly ILogger _logger; + + public HistoryService(IWebHostEnvironment env, ILogger logger) + { + _logger = logger; + _historyUpstFilePath = Path.Combine(env.ContentRootPath, "Data", "history-upst.json"); + } + + public async Task GetUpstHistoryAsync(DateOnly? fromDate, DateOnly? toDate, int page = 1, int pageSize = 5) + { + page = page < 1 ? 1 : page; + pageSize = pageSize <= 0 ? 5 : pageSize; + + var items = await ReadUpstHistoryAsync(); + var query = items.AsEnumerable(); + + if (fromDate.HasValue) + { + query = query.Where(item => DateOnly.FromDateTime(item.TanggalWaktu) >= fromDate.Value); + } + + if (toDate.HasValue) + { + query = query.Where(item => DateOnly.FromDateTime(item.TanggalWaktu) <= toDate.Value); + } + + var ordered = query + .OrderByDescending(item => item.TanggalWaktu) + .ToList(); + + var totalItems = ordered.Count; + var totalPages = totalItems == 0 ? 1 : (int)Math.Ceiling(totalItems / (double)pageSize); + var safePage = Math.Min(page, totalPages); + var pagedItems = ordered + .Skip((safePage - 1) * pageSize) + .Take(pageSize) + .ToList(); + + return new HistoryListResponse + { + Items = pagedItems, + Page = safePage, + PageSize = pageSize, + TotalItems = totalItems, + TotalPages = totalPages, + FromDate = fromDate?.ToString("yyyy-MM-dd"), + ToDate = toDate?.ToString("yyyy-MM-dd") + }; + } + + private async Task> ReadUpstHistoryAsync() + { + try + { + if (!File.Exists(_historyUpstFilePath)) + { + return new List(); + } + + var json = await File.ReadAllTextAsync(_historyUpstFilePath); + var items = JsonSerializer.Deserialize>(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return items ?? new List(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading UPST history JSON"); + return new List(); + } + } + } +} diff --git a/Views/Admin/Transport/SpjDriverUpst/History/Index.cshtml b/Views/Admin/Transport/SpjDriverUpst/History/Index.cshtml index a4f7aad..0a7c91d 100644 --- a/Views/Admin/Transport/SpjDriverUpst/History/Index.cshtml +++ b/Views/Admin/Transport/SpjDriverUpst/History/Index.cshtml @@ -3,7 +3,7 @@ ViewData["Title"] = "History - DLH"; } -
+
@@ -18,134 +18,58 @@
- - @{ - var spjList = new[] - { - new { - Id = 1, - NoSpj = "SPJ/07-2025/PKM/000478", - Plat = "B 5678 ABC", - Kode = "JRC 007", - Tujuan = "Bantar Gebang", - Status = "In Progress", - Tanggal = "28 Jul 2025", - Waktu = "16:45" - }, - new { - Id = 2, - NoSpj = "SPJ/07-2025/PKM/000476", - Plat = "B 9632 TOR", - Kode = "JRC 005", - Tujuan = "RDF Rorotan", - Status = "Completed", - Tanggal = "27 Jul 2025", - Waktu = "14:30" - }, - new { - Id = 3, - NoSpj = "SPJ/07-2025/PKM/000477", - Plat = "B 1234 XYZ", - Kode = "JRC 006", - Tujuan = "RDF Pesanggarahan", - Status = "Completed", - Tanggal = "26 Jul 2025", - Waktu = "09:15" - }, - new { - Id = 4, - NoSpj = "SPJ/07-2025/PKM/000479", - Plat = "B 9876 DEF", - Kode = "JRC 008", - Tujuan = "RDF Sunter", - Status = "Completed", - Tanggal = "25 Jul 2025", - Waktu = "11:20" - }, - new { - Id = 5, - NoSpj = "SPJ/07-2025/PKM/000480", - Plat = "B 4321 GHI", - Kode = "JRC 009", - Tujuan = "Bantar Gebang", - Status = "Completed", - Tanggal = "24 Jul 2025", - Waktu = "08:45" - } - }; - } - -
- @foreach (var spj in spjList) - { - -
- -
-
-

Nomor Dokumen

-

@spj.NoSpj

-
- - @if (spj.Status == "Completed") - { - - Selesai - - } - else - { - - Proses - - } -
- -
-
- -
-
-
-

@spj.Plat

- @spj.Waktu -
-
- @spj.Kode - @spj.Tanggal -
-
-
- -
-
-
-
- Tujuan Akhir - @spj.Tujuan -
-
-
- -
-
- +
+
+ +
+ + +
+
+ +
+
+ +
+ Memuat riwayat perjalanan... +
+ + + +
+ +
- - - - @*
-
- -
-

Belum Ada Riwayat

-

Riwayat perjalanan Anda akan muncul di sini setelah melakukan perjalanan pertama.

-
*@ -
\ No newline at end of file +
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Views/Admin/Transport/SpjDriverUpst/Home/Index.cshtml b/Views/Admin/Transport/SpjDriverUpst/Home/Index.cshtml index ce9a7f9..d251c38 100644 --- a/Views/Admin/Transport/SpjDriverUpst/Home/Index.cshtml +++ b/Views/Admin/Transport/SpjDriverUpst/Home/Index.cshtml @@ -33,80 +33,95 @@
-
-
-
-
- -
-
-

Lokasi Saat Ini

-

Mendeteksi lokasi...

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

Nomor SPJ

-

SPJ/07-2025/PKM/000476

-
-
- B 9632 TOR -
+
+

Lokasi Saat Ini

+

Mendeteksi lokasi...

- -
-
-
-
-

Tujuan Pembuangan

-

JRC Rorotan

-

(JRC 005)

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

Nomor SPJ

+

SPJ/07-2025/PKM/000476

+
+
+ B 9632 TOR +
+
+ +
+ + +
+
+

Tujuan Pembuangan

+

JRC Rorotan

+ + JRC 005 + +
+ + +
+
+
+ + + + +
+
+
+ +
@@ -300,7 +315,6 @@ document.addEventListener("DOMContentLoaded", function () { getLocationUpdate(); } - // Update Lokasi cuy userLocationEl.addEventListener("click", function () { getLocationUpdate(); }); @@ -358,30 +372,131 @@ document.addEventListener("DOMContentLoaded", function () { let mapInstance = null; let mapBounds = null; let mapInitialized = false; + + let userMarker = null; + let liveLocationWatchId = null; + let lastUserLatLng = null; + let lastKnownHeading = 0; function fitAllLocations() { if (!mapInstance || !mapBounds) return; - mapInstance.invalidateSize(); - if (Array.isArray(mapBounds) && mapBounds.length === 1) { mapInstance.setView(mapBounds[0], 16); return; } + mapInstance.fitBounds(mapBounds, { padding: [24, 24], maxZoom: 17 }); + } - mapInstance.fitBounds(mapBounds, { - padding: [24, 24], - maxZoom: 17 + function toRadians(deg) { + return deg * Math.PI / 180; + } + + function toDegrees(rad) { + return rad * 180 / Math.PI; + } + + function getBearing(from, to) { + const [lat1, lng1] = from; + const [lat2, lng2] = to; + + const phi1 = toRadians(lat1); + const phi2 = toRadians(lat2); + const lambda1 = toRadians(lng1); + const lambda2 = toRadians(lng2); + + const y = Math.sin(lambda2 - lambda1) * Math.cos(phi2); + const x = + Math.cos(phi1) * Math.sin(phi2) - + Math.sin(phi1) * Math.cos(phi2) * Math.cos(lambda2 - lambda1); + + return (toDegrees(Math.atan2(y, x)) + 360) % 360; + } + + function createTruckDivIcon(rotationDeg) { + const carIconHtml = ` +
+ Truck +
`; + + return L.divIcon({ + className: 'bg-transparent border-0', + html: carIconHtml, + iconSize: [40, 40], + iconAnchor: [20, 20], + popupAnchor: [0, -15] }); } + function handleUserPosition(position) { + if (!mapInstance) return; + + const lat = position.coords.latitude; + const lng = position.coords.longitude; + const nextLatLng = [lat, lng]; + + let heading = Number.isFinite(position.coords.heading) + ? position.coords.heading + : null; + + if (!Number.isFinite(heading) && lastUserLatLng) { + heading = getBearing(lastUserLatLng, nextLatLng); + } + + if (Number.isFinite(heading)) { + lastKnownHeading = heading; + } + + const rotationDeg = (lastKnownHeading + 180) % 360; + const truckIcon = createTruckDivIcon(rotationDeg); + + if (userMarker) { + userMarker.setLatLng(nextLatLng); + userMarker.setIcon(truckIcon); + } else { + userMarker = L.marker(nextLatLng, { icon: truckIcon, zIndexOffset: 1000 }) + .addTo(mapInstance) + .bindPopup("
Posisi Anda
Diperbarui otomatis via GPS
"); + } + + lastUserLatLng = nextLatLng; + } + + function updateUserLocationOnMap() { + if (!mapInstance || !("geolocation" in navigator)) return; + + navigator.geolocation.getCurrentPosition( + handleUserPosition, + function (error) { + console.warn("Gagal mengambil GPS untuk live tracking:", error); + }, + { enableHighAccuracy: true, maximumAge: 0, timeout: 15000 } + ); + } + + function startLiveTracking() { + if (!mapInstance || !("geolocation" in navigator) || liveLocationWatchId !== null) return; + + liveLocationWatchId = navigator.geolocation.watchPosition( + handleUserPosition, + function (error) { + console.warn("Gagal mengambil GPS untuk live tracking:", error); + }, + { enableHighAccuracy: true, maximumAge: 0, timeout: 15000 } + ); + } + function ensureLeaflet() { return new Promise((resolve, reject) => { if (window.L && typeof window.L.map === "function") { resolve(); return; } - if (!document.getElementById("leaflet-css")) { const css = document.createElement("link"); css.id = "leaflet-css"; @@ -389,14 +504,12 @@ document.addEventListener("DOMContentLoaded", function () { css.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"; document.head.appendChild(css); } - const existingScript = document.getElementById("leaflet-js"); if (existingScript) { existingScript.addEventListener("load", resolve, { once: true }); existingScript.addEventListener("error", reject, { once: true }); return; } - const script = document.createElement("script"); script.id = "leaflet-js"; script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"; @@ -418,7 +531,6 @@ document.addEventListener("DOMContentLoaded", function () { function normalizeLatLng(item) { const lat = toNumber(item.latitude); const lng = toNumber(item.longitude); - if (isValidLatLng(lat, lng)) return { lat, lng }; if (isValidLatLng(lng, lat)) return { lat: lng, lng: lat }; return null; @@ -446,23 +558,15 @@ document.addEventListener("DOMContentLoaded", function () { async function getRoadRouteLatLngs(latLngs) { if (!Array.isArray(latLngs) || latLngs.length < 2) return null; - try { - const coordString = latLngs - .map(([lat, lng]) => `${lng},${lat}`) - .join(";"); - + const coordString = latLngs.map(([lat, lng]) => `${lng},${lat}`).join(";"); const url = `https://router.project-osrm.org/route/v1/driving/${coordString}?overview=full&geometries=geojson&steps=false&alternatives=false`; const res = await fetch(url); if (!res.ok) return null; - const data = await res.json(); const coords = data?.routes?.[0]?.geometry?.coordinates; if (!Array.isArray(coords) || coords.length < 2) return null; - - return coords - .map(pair => [toNumber(pair?.[1]), toNumber(pair?.[0])]) - .filter(([lat, lng]) => isValidLatLng(lat, lng)); + return coords.map(pair => [toNumber(pair?.[1]), toNumber(pair?.[0])]).filter(([lat, lng]) => isValidLatLng(lat, lng)); } catch { return null; } @@ -471,13 +575,12 @@ document.addEventListener("DOMContentLoaded", function () { async function initMap() { try { if (mapInitialized && mapInstance) { - setTimeout(() => { - fitAllLocations(); - }, 80); + setTimeout(() => fitAllLocations(), 80); return; } await ensureLeaflet(); + const res = await fetch("@Url.Content("~/driver/json/pengangkutan.json")"); const payload = await res.json(); const rows = Array.isArray(payload?.data) ? payload.data : []; @@ -513,13 +616,25 @@ document.addEventListener("DOMContentLoaded", function () { const latLngs = points.map(p => [p.lat, p.lng]); points.forEach((point, index) => { + const popupContent = ` +
+ ${index + 1}. ${point.name || "Lokasi"} + ${point.alamat || "-"} + + BUKA DI GMAPS + +
+ `; + L.marker([point.lat, point.lng], { icon: pickupIcon }) .addTo(mapInstance) - .bindPopup(`${index + 1}. ${point.name || "Lokasi"}
${point.alamat || "-"}`); + .bindPopup(popupContent); }); let routeLatLngs = latLngs; - if (latLngs.length > 1) { const routed = await getRoadRouteLatLngs(latLngs); if (routed && routed.length > 1) { @@ -537,9 +652,9 @@ document.addEventListener("DOMContentLoaded", function () { fitAllLocations(); mapInitialized = true; - setTimeout(() => { - fitAllLocations(); - }, 80); + updateUserLocationOnMap(); + startLiveTracking(); + } catch (err) { mapEl.innerHTML = '
Gagal memuat peta
'; console.error("Map init error:", err); @@ -563,9 +678,17 @@ document.addEventListener("DOMContentLoaded", function () { if (mapsCardPanel && !mapsCardPanel.classList.contains("hidden")) { initMap(); } + + window.addEventListener("beforeunload", function () { + if (liveLocationWatchId !== null && "geolocation" in navigator) { + navigator.geolocation.clearWatch(liveLocationWatchId); + liveLocationWatchId = null; + } + }); }); + + + + + + + \ No newline at end of file diff --git a/Views/Admin/Transport/SpjDriverUpst/Shared/_Layout.cshtml b/Views/Admin/Transport/SpjDriverUpst/Shared/_Layout.cshtml index 1341a48..9d945a4 100644 --- a/Views/Admin/Transport/SpjDriverUpst/Shared/_Layout.cshtml +++ b/Views/Admin/Transport/SpjDriverUpst/Shared/_Layout.cshtml @@ -7,8 +7,8 @@ - - + + @@ -18,12 +18,12 @@ - - + + - - + + @* *@ @@ -40,15 +40,9 @@ @RenderBody() - - - - - + @await Html.PartialAsync("~/Views/Admin/Transport/SpjDriverUpst/Shared/Components/_PWAinstall.cshtml") + @await Html.PartialAsync("~/Views/Admin/Transport/SpjDriverUpst/Shared/Partials/_Scripts.cshtml") + @await RenderSectionAsync("Scripts", required: false) diff --git a/wwwroot/driver/css/watch.css b/wwwroot/driver/css/watch.css index c8ae70d..d75fb2a 100644 --- a/wwwroot/driver/css/watch.css +++ b/wwwroot/driver/css/watch.css @@ -77,11 +77,14 @@ --color-slate-50: oklch(98.4% 0.003 247.858); --color-slate-100: oklch(96.8% 0.007 247.896); --color-slate-200: oklch(92.9% 0.013 255.508); + --color-slate-300: oklch(86.9% 0.022 252.894); --color-slate-400: oklch(70.4% 0.04 256.788); + --color-slate-500: oklch(55.4% 0.046 257.417); --color-slate-600: oklch(44.6% 0.043 257.281); --color-slate-700: oklch(37.2% 0.044 257.287); --color-slate-800: oklch(27.9% 0.041 260.031); --color-slate-900: oklch(20.8% 0.042 265.755); + --color-slate-950: oklch(12.9% 0.042 264.695); --color-gray-50: oklch(98.5% 0.002 247.839); --color-gray-100: oklch(96.7% 0.003 264.542); --color-gray-200: oklch(92.8% 0.006 264.531); @@ -97,6 +100,7 @@ --spacing: 0.25rem; --container-xs: 20rem; --container-sm: 24rem; + --container-md: 28rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; @@ -109,6 +113,8 @@ --text-xl--line-height: calc(1.75 / 1.25); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; @@ -127,6 +133,7 @@ --radius-xl: 0.75rem; --radius-2xl: 1rem; --radius-3xl: 1.5rem; + --radius-4xl: 2rem; --drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15); --ease-in: cubic-bezier(0.4, 0, 1, 1); --ease-out: cubic-bezier(0, 0, 0.2, 1); @@ -324,6 +331,9 @@ .inset-0 { inset: calc(var(--spacing) * 0); } + .inset-x-4 { + inset-inline: calc(var(--spacing) * 4); + } .start-0 { inset-inline-start: calc(var(--spacing) * 0); } @@ -369,9 +379,6 @@ .top-6 { top: calc(var(--spacing) * 6); } - .top-9 { - top: calc(var(--spacing) * 9); - } .top-12 { top: calc(var(--spacing) * 12); } @@ -390,6 +397,9 @@ .-right-1 { right: calc(var(--spacing) * -1); } + .-right-6 { + right: calc(var(--spacing) * -6); + } .right-0 { right: calc(var(--spacing) * 0); } @@ -402,29 +412,32 @@ .right-4 { right: calc(var(--spacing) * 4); } + .right-5 { + right: calc(var(--spacing) * 5); + } .right-6 { right: calc(var(--spacing) * 6); } .right-8 { right: calc(var(--spacing) * 8); } - .right-9 { - right: calc(var(--spacing) * 9); - } .right-16 { right: calc(var(--spacing) * 16); } .right-full { right: 100%; } + .-bottom-0 { + bottom: calc(var(--spacing) * -0); + } .-bottom-0\.5 { bottom: calc(var(--spacing) * -0.5); } .-bottom-1 { bottom: calc(var(--spacing) * -1); } - .-bottom-4 { - bottom: calc(var(--spacing) * -4); + .-bottom-6 { + bottom: calc(var(--spacing) * -6); } .bottom-0 { bottom: calc(var(--spacing) * 0); @@ -435,24 +448,30 @@ .bottom-2 { bottom: calc(var(--spacing) * 2); } + .bottom-5 { + bottom: calc(var(--spacing) * 5); + } .bottom-8 { bottom: calc(var(--spacing) * 8); } .bottom-16 { bottom: calc(var(--spacing) * 16); } + .bottom-28 { + bottom: calc(var(--spacing) * 28); + } .bottom-50 { bottom: calc(var(--spacing) * 50); } .bottom-100 { bottom: calc(var(--spacing) * 100); } - .-left-4 { - left: calc(var(--spacing) * -4); - } .left-0 { left: calc(var(--spacing) * 0); } + .left-1 { + left: calc(var(--spacing) * 1); + } .left-1\/2 { left: calc(1/2 * 100%); } @@ -501,6 +520,12 @@ .z-99 { z-index: 99; } + .z-120 { + z-index: 120; + } + .z-130 { + z-index: 130; + } .z-\[100\] { z-index: 100; } @@ -762,6 +787,12 @@ .-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); + } .mr-2 { margin-right: calc(var(--spacing) * 2); } @@ -798,11 +829,11 @@ .ml-auto { margin-left: auto; } - .line-clamp-3 { + .line-clamp-2 { overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 3; + -webkit-line-clamp: 2; } .\!hidden { display: none !important; @@ -849,6 +880,9 @@ .aspect-square { aspect-ratio: 1 / 1; } + .h-0 { + height: calc(var(--spacing) * 0); + } .h-0\.5 { height: calc(var(--spacing) * 0.5); } @@ -876,6 +910,9 @@ .h-8 { height: calc(var(--spacing) * 8); } + .h-9 { + height: calc(var(--spacing) * 9); + } .h-10 { height: calc(var(--spacing) * 10); } @@ -930,14 +967,11 @@ .h-100 { height: calc(var(--spacing) * 100); } - .h-\[236px\] { - height: 236px; - } .h-\[250px\] { height: 250px; } - .h-\[300px\] { - height: 300px; + .h-\[340px\] { + height: 340px; } .h-auto { height: auto; @@ -1020,6 +1054,9 @@ .w-36 { width: calc(var(--spacing) * 36); } + .w-40 { + width: calc(var(--spacing) * 40); + } .w-48 { width: calc(var(--spacing) * 48); } @@ -1050,6 +1087,12 @@ .w-max { width: max-content; } + .max-w-full { + max-width: 100%; + } + .max-w-md { + max-width: var(--container-md); + } .max-w-sm { max-width: var(--container-sm); } @@ -1059,6 +1102,12 @@ .min-w-0 { min-width: calc(var(--spacing) * 0); } + .min-w-\[150px\] { + min-width: 150px; + } + .min-w-max { + min-width: max-content; + } .flex-1 { flex: 1; } @@ -1095,6 +1144,10 @@ .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); @@ -1107,6 +1160,10 @@ --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); @@ -1209,6 +1266,9 @@ .justify-center { justify-content: center; } + .justify-end { + justify-content: flex-end; + } .gap-0 { gap: calc(var(--spacing) * 0); } @@ -1227,6 +1287,16 @@ .gap-5 { gap: calc(var(--spacing) * 5); } + .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; @@ -1276,6 +1346,13 @@ margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-8 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-x-2 { :where(& > :not(:last-child)) { --tw-space-x-reverse: 0; @@ -1353,8 +1430,11 @@ .rounded-3xl { border-radius: var(--radius-3xl); } - .rounded-\[24px\] { - border-radius: 24px; + .rounded-4xl { + border-radius: var(--radius-4xl); + } + .rounded-\[28px\] { + border-radius: 28px; } .rounded-\[32px\] { border-radius: 32px; @@ -1503,6 +1583,9 @@ .border-gray-400 { border-color: var(--color-gray-400); } + .border-gray-500 { + border-color: var(--color-gray-500); + } .border-green-100 { border-color: var(--color-green-100); } @@ -1515,15 +1598,12 @@ .border-green-400 { border-color: var(--color-green-400); } - .border-green-600\/20 { - border-color: color-mix(in srgb, oklch(62.7% 0.194 149.214) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-green-600) 20%, transparent); - } - } .border-lime-400 { border-color: var(--color-lime-400); } + .border-orange-100 { + border-color: var(--color-orange-100); + } .border-orange-200 { border-color: var(--color-orange-200); } @@ -1560,6 +1640,12 @@ .border-white { border-color: var(--color-white); } + .border-white\/5 { + border-color: color-mix(in srgb, #fff 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-white) 5%, transparent); + } + } .border-white\/10 { border-color: color-mix(in srgb, #fff 10%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1578,12 +1664,6 @@ border-color: color-mix(in oklab, var(--color-white) 30%, transparent); } } - .border-white\/70 { - border-color: color-mix(in srgb, #fff 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-white) 70%, transparent); - } - } .border-yellow-100 { border-color: var(--color-yellow-100); } @@ -1608,6 +1688,12 @@ .bg-black { background-color: var(--color-black); } + .bg-black\/10 { + background-color: color-mix(in srgb, #000 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 10%, transparent); + } + } .bg-black\/20 { background-color: color-mix(in srgb, #000 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1644,6 +1730,12 @@ .bg-blue-500 { background-color: var(--color-blue-500); } + .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)) { @@ -1707,6 +1799,9 @@ .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)) { @@ -1728,6 +1823,9 @@ .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)) { @@ -1752,12 +1850,33 @@ .bg-red-500 { background-color: var(--color-red-500); } + .bg-slate-50 { + background-color: var(--color-slate-50); + } .bg-slate-100 { background-color: var(--color-slate-100); } + .bg-slate-200 { + background-color: var(--color-slate-200); + } + .bg-slate-300 { + background-color: var(--color-slate-300); + } .bg-slate-400 { background-color: var(--color-slate-400); } + .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)) { + background-color: color-mix(in oklab, var(--color-slate-950) 60%, transparent); + } + } .bg-transparent { background-color: transparent; } @@ -1776,6 +1895,12 @@ background-color: color-mix(in oklab, var(--color-white) 10%, transparent); } } + .bg-white\/15 { + background-color: color-mix(in srgb, #fff 15%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 15%, transparent); + } + } .bg-white\/20 { background-color: color-mix(in srgb, #fff 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1788,12 +1913,6 @@ 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); } @@ -2067,6 +2186,9 @@ .p-1 { padding: calc(var(--spacing) * 1); } + .p-1\.5 { + padding: calc(var(--spacing) * 1.5); + } .p-2 { padding: calc(var(--spacing) * 2); } @@ -2118,6 +2240,9 @@ .py-1 { padding-block: calc(var(--spacing) * 1); } + .py-1\.5 { + padding-block: calc(var(--spacing) * 1.5); + } .py-2 { padding-block: calc(var(--spacing) * 2); } @@ -2205,6 +2330,12 @@ .pt-10 { padding-top: calc(var(--spacing) * 10); } + .pt-12 { + padding-top: calc(var(--spacing) * 12); + } + .pr-4 { + padding-right: calc(var(--spacing) * 4); + } .pb-0 { padding-bottom: calc(var(--spacing) * 0); } @@ -2290,6 +2421,10 @@ font-size: var(--text-2xl); line-height: var(--tw-leading, var(--text-2xl--line-height)); } + .text-3xl { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } .text-base { font-size: var(--text-base); line-height: var(--tw-leading, var(--text-base--line-height)); @@ -2363,6 +2498,14 @@ --tw-tracking: 0.3em; letter-spacing: 0.3em; } + .tracking-\[0\.22em\] { + --tw-tracking: 0.22em; + letter-spacing: 0.22em; + } + .tracking-\[0\.24em\] { + --tw-tracking: 0.24em; + letter-spacing: 0.24em; + } .tracking-tight { --tw-tracking: var(--tracking-tight); letter-spacing: var(--tracking-tight); @@ -2395,6 +2538,15 @@ .break-all { word-break: break-all; } + .whitespace-nowrap { + white-space: nowrap; + } + .\!text-white { + color: var(--color-white) !important; + } + .text-amber-500 { + color: var(--color-amber-500); + } .text-amber-600 { color: var(--color-amber-600); } @@ -2425,6 +2577,12 @@ .text-gray-500 { color: var(--color-gray-500); } + .text-gray-500\/80 { + color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-gray-500) 80%, transparent); + } + } .text-gray-600 { color: var(--color-gray-600); } @@ -2437,15 +2595,12 @@ .text-gray-900 { color: var(--color-gray-900); } + .text-green-50 { + color: var(--color-green-50); + } .text-green-100 { color: var(--color-green-100); } - .text-green-100\/70 { - color: color-mix(in srgb, oklch(96.2% 0.044 156.743) 70%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-green-100) 70%, transparent); - } - } .text-green-400 { color: var(--color-green-400); } @@ -2488,6 +2643,9 @@ .text-orange-700 { color: var(--color-orange-700); } + .text-red-400 { + color: var(--color-red-400); + } .text-red-500 { color: var(--color-red-500); } @@ -2506,9 +2664,15 @@ .text-red-800 { color: var(--color-red-800); } + .text-slate-50 { + color: var(--color-slate-50); + } .text-slate-400 { color: var(--color-slate-400); } + .text-slate-500 { + color: var(--color-slate-500); + } .text-slate-600 { color: var(--color-slate-600); } @@ -2518,6 +2682,9 @@ .text-slate-800 { color: var(--color-slate-800); } + .text-slate-900 { + color: var(--color-slate-900); + } .text-transparent { color: transparent; } @@ -2530,6 +2697,12 @@ 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)) { @@ -2638,6 +2811,10 @@ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + 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); } + .ring-1 { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + 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); + } .ring-2 { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + 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); @@ -2652,6 +2829,15 @@ --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)) { + --tw-ring-color: color-mix(in oklab, var(--color-black) 5%, transparent); + } + } .ring-gray-200 { --tw-ring-color: var(--color-gray-200); } @@ -2704,11 +2890,6 @@ .filter { 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,); } - .backdrop-blur { - --tw-backdrop-blur: blur(8px); - -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - } .backdrop-blur-lg { --tw-backdrop-blur: blur(var(--blur-lg)); -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); @@ -2775,6 +2956,10 @@ --tw-duration: 300ms; transition-duration: 300ms; } + .duration-500 { + --tw-duration: 500ms; + transition-duration: 500ms; + } .ease-in { --tw-ease: var(--ease-in); transition-timing-function: var(--ease-in); @@ -2864,6 +3049,11 @@ scale: var(--tw-scale-x) var(--tw-scale-y); } } + .group-active\:rotate-180 { + &:is(:where(.group):active *) { + rotate: 180deg; + } + } .group-active\:bg-gray-50 { &:is(:where(.group):active *) { background-color: var(--color-gray-50); @@ -2974,6 +3164,13 @@ } } } + .hover\:border-green-400 { + &:hover { + @media (hover: hover) { + border-color: var(--color-green-400); + } + } + } .hover\:border-orange-200 { &:hover { @media (hover: hover) { @@ -2995,6 +3192,13 @@ } } } + .hover\:bg-blue-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-blue-700); + } + } + } .hover\:bg-gray-50 { &:hover { @media (hover: hover) { @@ -3065,6 +3269,13 @@ } } } + .hover\:bg-slate-800 { + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-800); + } + } + } .hover\:bg-white\/10 { &:hover { @media (hover: hover) { @@ -3276,6 +3487,23 @@ outline-style: none; } } + .focus-visible\:ring-2 { + &:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + 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); + } + } + .focus-visible\:ring-slate-400 { + &:focus-visible { + --tw-ring-color: var(--color-slate-400); + } + } + .focus-visible\:outline-none { + &:focus-visible { + --tw-outline-style: none; + outline-style: none; + } + } .active\:scale-90 { &:active { --tw-scale-x: 90%; @@ -3292,6 +3520,36 @@ scale: var(--tw-scale-x) var(--tw-scale-y); } } + .disabled\:cursor-not-allowed { + &:disabled { + cursor: not-allowed; + } + } + .disabled\:opacity-50 { + &:disabled { + opacity: 50%; + } + } + .sm\:flex-1 { + @media (width >= 40rem) { + flex: 1; + } + } + .sm\:grid-cols-2 { + @media (width >= 40rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .sm\:flex-row { + @media (width >= 40rem) { + flex-direction: row; + } + } + .sm\:items-center { + @media (width >= 40rem) { + align-items: center; + } + } .lg\:max-w-sm { @media (width >= 64rem) { max-width: var(--container-sm); diff --git a/wwwroot/driver/images/pwa_192.png b/wwwroot/driver/images/pwa_192.png new file mode 100644 index 0000000..ecf9d2e Binary files /dev/null and b/wwwroot/driver/images/pwa_192.png differ diff --git a/wwwroot/driver/images/pwa_512.png b/wwwroot/driver/images/pwa_512.png new file mode 100644 index 0000000..8934036 Binary files /dev/null and b/wwwroot/driver/images/pwa_512.png differ diff --git a/wwwroot/driver/images/truck_icon.png b/wwwroot/driver/images/truck_icon.png new file mode 100644 index 0000000..aa3b56e Binary files /dev/null and b/wwwroot/driver/images/truck_icon.png differ diff --git a/wwwroot/driver/js/detail-penjemputan-non-tps.js b/wwwroot/driver/js/detail-penjemputan-non-tps.js index 0a292d7..1804d59 100644 --- a/wwwroot/driver/js/detail-penjemputan-non-tps.js +++ b/wwwroot/driver/js/detail-penjemputan-non-tps.js @@ -1,269 +1,189 @@ -document.addEventListener("DOMContentLoaded", function () { - const grandTotalDisplay = document.getElementById("grand-total-timbangan"); - const grandTotalOrganikDisplay = document.getElementById( - "grand-total-organik", - ); - const grandTotalAnorganikDisplay = document.getElementById( - "grand-total-anorganik", - ); - const grandTotalResiduDisplay = document.getElementById("grand-total-residu"); - const tpsContentContainer = document.getElementById("tps-content"); +document.addEventListener('DOMContentLoaded', async function() { + const grandTotalDisplay = document.getElementById('grand-total-timbangan'); + const grandTotalOrganikDisplay = document.getElementById('grand-total-organik'); + const grandTotalAnorganikDisplay = document.getElementById('grand-total-anorganik'); + const grandTotalResiduDisplay = document.getElementById('grand-total-residu'); + const tpsContentContainer = document.getElementById('tps-content'); - let activeTpsIndex = 0; - let tpsData = []; - let nomorSpj = "SPJ/07-2025/PKM/000476"; + let activeTpsIndex = 0; + let tpsData = []; + let nomorSpj = 'SPJ/07-2025/PKM/000476'; + let draftRequestKey = ''; - const STORAGE_KEY = "detailPenjemputanNonTpsState"; - - function saveState() { - try { - const stateCopy = JSON.parse( - JSON.stringify({ activeTpsIndex, tpsData, nomorSpj }, (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 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, ''); } - } - function loadState() { - try { - const saved = localStorage.getItem(STORAGE_KEY); - if (saved) { - const parsed = JSON.parse(saved); - if (parsed.tpsData && parsed.tpsData.length > 0) { - tpsData = parsed.tpsData; + let autoSaveTimer = null; + let autoSaveStatusEl = null; + + function scheduleAutoSave() { + clearTimeout(autoSaveTimer); + showAutoSaveStatus('menyimpan...'); + autoSaveTimer = setTimeout(autoSaveDraft, 1000); + } + + function showAutoSaveStatus(msg, isOk = false) { + if (!autoSaveStatusEl) { + autoSaveStatusEl = document.getElementById('auto-save-status'); } - if (parsed.nomorSpj) nomorSpj = parsed.nomorSpj; - } - } catch (e) { - console.warn("Failed to load state from localStorage:", e); - } - } - - function clearState() { - localStorage.removeItem(STORAGE_KEY); - } - - function isBrowserFile(value) { - return typeof File !== "undefined" && value instanceof File; - } - - function getFirstValue(value) { - return Array.isArray(value) ? value[0] : value; - } - - function resolveStoredPhoto(value) { - if (!value) { - return null; + if (!autoSaveStatusEl) return; + autoSaveStatusEl.textContent = msg; + autoSaveStatusEl.className = isOk + ? 'text-[11px] text-green-600 text-center font-medium transition-opacity' + : 'text-[11px] text-amber-500 text-center font-medium transition-opacity'; + autoSaveStatusEl.style.opacity = '1'; + if (isOk) setTimeout(() => { autoSaveStatusEl.style.opacity = '0'; }, 2500); } - if (isBrowserFile(value)) { - return value; + async function autoSaveDraft() { + const tps = tpsData[activeTpsIndex]; + if (!tps) return; + + 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 || '' + }; + + 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'); + } } - if (typeof value === "string") { - return { - url: value, - name: value.split("/").pop() || "Foto", - size: 0, - source: "api", - }; + 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); + } } - if (typeof value !== "object") { - return null; + function patchFormFromDraft() { + const tps = tpsData[activeTpsIndex]; + const form = tpsContentContainer.querySelector('form'); + if (!form) 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 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; + + const namaPetugasInput = form.querySelector('.tps-nama-petugas'); + if (namaPetugasInput && tps.namaPetugas) namaPetugasInput.value = tps.namaPetugas; + + 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)); + } + } + + 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 url = - value.url || - value.fileUrl || - value.path || - value.filePath || - value.src || - value.fotoUrl || - value.photoUrl || - value.fotoFileName || - value.fileName || - ""; - - return { - ...value, - url, - name: - value.name || - value.fileName || - value.file_name || - (url ? url.split("/").pop() : "Foto"), - size: Number(value.size || value.fileSize || 0) || 0, - source: value.source || "api", - }; - } - - function getStoredPhotoUrl(value) { - const photo = resolveStoredPhoto(value); - if (!photo) { - return ""; - } - - if (isBrowserFile(photo)) { - return URL.createObjectURL(photo); - } - - return photo.url || ""; - } - - function getStoredPhotoName(value, fallbackName = "Foto") { - const photo = resolveStoredPhoto(value); - if (!photo) { - return fallbackName; - } - - return isBrowserFile(photo) - ? photo.name || fallbackName - : photo.name || fallbackName; - } - - function getStoredPhotoSize(value) { - const photo = resolveStoredPhoto(value); - if (!photo) { - return 0; - } - - return isBrowserFile(photo) - ? photo.size || 0 - : Number(photo.size || 0) || 0; - } - - function hasStoredPhoto(value) { - const photo = resolveStoredPhoto(value); - if (!photo) { - return false; - } - - return isBrowserFile(photo) || Boolean(photo.url || photo.name); - } - - function normalizePhotoList(value) { - if (!Array.isArray(value)) { - return []; - } - - return value.map(resolveStoredPhoto).filter(Boolean); - } - - function normalizeTimbanganItem(item) { - if (!item) { - return { - file: null, - berat: [0], - jenisSampah: [DEFAULT_JENIS], - lokasiAngkut: [], - uploaded: false, - ocrInfo: "OCR: belum diproses.", - }; - } - - const file = resolveStoredPhoto( - item.file || - item.foto || - item.photo || - item.fotoUrl || - item.photoUrl || - item.fotoFileName, - ); - const berat = Number(getFirstValue(item.berat) ?? item.weight ?? 0) || 0; - const jenisSampah = - getFirstValue(item.jenisSampah) || item.JenisSampah || DEFAULT_JENIS; - const uploaded = Boolean( - item.uploaded ?? - item.isUploaded ?? - item.IsUploaded ?? - hasStoredPhoto(file), - ); - - return { - ...item, - file, - berat: [berat], - jenisSampah: [jenisSampah], - lokasiAngkut: item.lokasiAngkut || [], - uploaded, - ocrInfo: - item.ocrInfo || - item.OcrInfo || - (uploaded ? "Foto dari server." : "OCR: belum diproses."), - }; - } - - function hydrateSingleTpsFromApi(detail) { - const tps = tpsData[0]; - if (!tps || !detail) { - return; - } - - const fotoKedatangan = normalizePhotoList( - detail.fotoKedatangan || detail.FotoKedatangan, - ); - const fotoPetugas = normalizePhotoList( - detail.fotoPetugas || detail.FotoPetugas, - ); - const apiTimbangan = detail.timbangan || detail.Timbangan; - const timbangan = Array.isArray(apiTimbangan) - ? apiTimbangan.map(normalizeTimbanganItem) - : tps.timbangan; - - tps.name = detail.namaTps || detail.tpsName || detail.name || tps.name; - tps.lokasiAngkutId = - detail.lokasiAngkutId || detail.LokasiAngkutID || tps.lokasiAngkutId; - tps.spjDetailId = - detail.spjDetailId || detail.SpjDetailID || tps.spjDetailId; - tps.latitude = detail.latitude || detail.Latitude || tps.latitude; - tps.longitude = detail.longitude || detail.Longitude || tps.longitude; - tps.alamatJalan = - detail.alamatJalan || detail.AlamatJalan || tps.alamatJalan; - tps.waktuKedatangan = - detail.waktuKedatangan || detail.WaktuKedatangan || tps.waktuKedatangan; - tps.fotoKedatangan = fotoKedatangan.length - ? fotoKedatangan - : tps.fotoKedatangan; - tps.fotoKedatanganUploaded = - Boolean(detail.fotoKedatanganUploaded ?? detail.FotoKedatanganUploaded) || - fotoKedatangan.length > 0 || - tps.fotoKedatanganUploaded; - tps.timbangan = timbangan; - tps.totalOrganik = - Number(detail.totalOrganik ?? detail.TotalOrganik ?? tps.totalOrganik) || - 0; - tps.totalAnorganik = - Number( - detail.totalAnorganik ?? detail.TotalAnorganik ?? tps.totalAnorganik, - ) || 0; - tps.totalResidu = - Number(detail.totalResidu ?? detail.TotalResidu ?? tps.totalResidu) || 0; - tps.totalTimbangan = - Number( - detail.totalTimbangan ?? detail.TotalTimbangan ?? tps.totalTimbangan, - ) || 0; - tps.fotoPetugas = fotoPetugas.length ? fotoPetugas : tps.fotoPetugas; - tps.fotoPetugasUploaded = - Boolean(detail.fotoPetugasUploaded ?? detail.FotoPetugasUploaded) || - fotoPetugas.length > 0 || - tps.fotoPetugasUploaded; - tps.namaPetugas = - detail.namaPetugas || detail.NamaPetugas || tps.namaPetugas; - tps.submitted = Boolean( - detail.submitted ?? detail.Submitted ?? tps.submitted, - ); - - saveState(); - } - const OCR_AREAS = [ { id: "A", @@ -295,38 +215,31 @@ document.addEventListener("DOMContentLoaded", function () { const DETAIL_DATA_URL = "/driver/json/detail-penjemputan-non-tps.json"; const DEFAULT_TPS_NAME = "Lokasi Pengangkutan 1"; - function initializeLocation() { - loadState(); - if (tpsData.length > 0) { - renderTpsForm(); - return; + function initializeLocation() { + tpsData = [{ + name: DEFAULT_TPS_NAME, + index: 0, + lokasiAngkutId: '', + spjDetailId: '', + latitude: '', + longitude: '', + alamatJalan: '', + waktuKedatangan: '', + fotoKedatangan: [], + fotoKedatanganFileNames: [], + fotoKedatanganUploaded: false, + timbangan: [], + totalOrganik: 0, + totalAnorganik: 0, + totalResidu: 0, + totalTimbangan: 0, + fotoPetugas: [], + fotoPetugasFileNames: [], + fotoPetugasUploaded: false, + namaPetugas: '', + submitted: false + }]; } - tpsData = [ - { - name: DEFAULT_TPS_NAME, - index: 0, - lokasiAngkutId: "", - spjDetailId: "", - latitude: "", - longitude: "", - alamatJalan: "", - waktuKedatangan: "", - fotoKedatangan: [], - fotoKedatanganUploaded: false, - timbangan: [], - totalOrganik: 0, - totalAnorganik: 0, - totalResidu: 0, - totalTimbangan: 0, - fotoPetugas: [], - fotoPetugasUploaded: false, - namaPetugas: "", - submitted: false, - }, - ]; - - renderTpsForm(); - } async function loadDetailData() { try { @@ -341,24 +254,26 @@ document.addEventListener("DOMContentLoaded", function () { detail.namaTps || detail.tpsName || detail.name || DEFAULT_TPS_NAME; const namaPerusahaan = detail.namaPerusahaan || detail.companyName || ""; - if (tpsData[0]) { - tpsData[0].name = namaTps; - tpsData[0].lokasiAngkutId = - detail.lokasiAngkutId || - detail.LokasiAngkutID || - tpsData[0].lokasiAngkutId; - tpsData[0].spjDetailId = - detail.spjDetailId || detail.SpjDetailID || tpsData[0].spjDetailId; - } + if (tpsData[0]) { + tpsData[0].name = namaTps; + tpsData[0].lokasiAngkutId = detail.lokasiAngkutId || detail.LokasiAngkutID || tpsData[0].lokasiAngkutId; + tpsData[0].spjDetailId = detail.spjDetailId || detail.SpjDetailID || tpsData[0].spjDetailId; + draftRequestKey = buildDraftRequestKey(tpsData[0]); + } - hydrateSingleTpsFromApi(detail); - nomorSpj = detail.nomorSpj || nomorSpj; - applyDetailDataToView(detail, namaTps, namaPerusahaan); - renderTpsForm(); - } catch (error) { - console.warn("Gagal memuat detail penjemputan non-TPS:", error); + nomorSpj = detail.nomorSpj || nomorSpj; + applyDetailDataToView(detail, namaTps, namaPerusahaan); + const _form = tpsContentContainer.querySelector('form'); + if (_form) { + const lokasiInput = _form.querySelector('.tps-lokasi-angkut-id'); + const spjInput = _form.querySelector('.tps-spj-detail-id'); + if (lokasiInput) lokasiInput.value = tpsData[0].lokasiAngkutId; + if (spjInput) spjInput.value = tpsData[0].spjDetailId; + } + } catch (error) { + console.warn('Gagal memuat detail penjemputan non-TPS:', error); + } } - } function applyDetailDataToView(detail, namaTps, namaPerusahaan) { const titleEl = document.getElementById("detail-page-title"); @@ -470,7 +385,8 @@ document.addEventListener("DOMContentLoaded", function () { Batal
- ${submitState.canSubmit ? "" : `

${submitState.message}

`} + ${submitState.canSubmit ? '' : `

${submitState.message}

`} +

`; @@ -484,16 +400,24 @@ document.addEventListener("DOMContentLoaded", function () { const form = tpsContentContainer.querySelector("form"); if (!form) return; - const previewKedatangan = form.querySelector(".tps-preview-kedatangan"); - if (previewKedatangan && tps.fotoKedatangan.length > 0) { - renderStoredPhotos(tps.fotoKedatangan, previewKedatangan); - } + const 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 && tps.fotoPetugas.length > 0) { - renderStoredPhotos(tps.fotoPetugas, previewPetugas); + 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); + } + } } - } function renderStoredPhotos(files, container) { container.innerHTML = ""; @@ -514,10 +438,6 @@ document.addEventListener("DOMContentLoaded", function () {
Preview ${index + 1}
-
-

${index + 1}. ${safeName}

-

${fileSize > 0 ? formatFileSize(fileSize) : "Tersimpan di server"}

-
`; const img = item.querySelector(".preview-multi-image"); @@ -539,28 +459,32 @@ document.addEventListener("DOMContentLoaded", function () { const namaPetugasInput = form.querySelector(".tps-nama-petugas"); const btnAddTimbangan = form.querySelector(".tps-btn-add-timbangan"); - fotoKedatanganInput.addEventListener("change", function () { - tps.fotoKedatangan = Array.from(this.files); - tps.fotoKedatanganUploaded = false; - updateWaktuKedatangan(); - updateMultiPreview(this, form.querySelector(".tps-preview-kedatangan")); - refreshKedatanganUploadState(form); - saveState(); - }); + fotoKedatanganInput.addEventListener('change', function() { + tps.fotoKedatangan = Array.from(this.files); + tps.fotoKedatanganUploaded = false; + updateWaktuKedatangan(); + updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan')); + refreshKedatanganUploadState(form); + scheduleAutoSave(); + }); - fotoPetugasInput.addEventListener("change", function () { - tps.fotoPetugas = Array.from(this.files); - tps.fotoPetugasUploaded = false; - updateMultiPreview(this, form.querySelector(".tps-preview-petugas")); - refreshPetugasUploadState(form); - saveState(); - }); + fotoPetugasInput.addEventListener('change', function() { + tps.fotoPetugas = Array.from(this.files); + tps.fotoPetugasUploaded = false; + updateMultiPreview(this, form.querySelector('.tps-preview-petugas')); + refreshPetugasUploadState(form); + scheduleAutoSave(); + }); - namaPetugasInput.addEventListener("input", function () { - tps.namaPetugas = this.value; - refreshPetugasUploadState(form); - saveState(); - }); + namaPetugasInput.addEventListener('input', function() { + tps.namaPetugas = this.value; + refreshPetugasUploadState(form); + }); + + namaPetugasInput.addEventListener('blur', function() { + tps.namaPetugas = this.value; + scheduleAutoSave(); + }); btnAddTimbangan.addEventListener("click", function () { createTimbanganItem(form.querySelector(".tps-timbangan-repeater")); @@ -695,10 +619,6 @@ document.addEventListener("DOMContentLoaded", function () {
Preview ${index + 1}
-
-

${index + 1}. ${safeName}

-

${formatFileSize(file.size)}

-
`; const img = item.querySelector(".preview-multi-image"); @@ -1026,17 +946,17 @@ document.addEventListener("DOMContentLoaded", function () { saveState(); } - function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) { - if (!hasFile) { - return '

Pilih foto timbangan terlebih dahulu

'; - } - - if (isUploaded) { - return ` + function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) { + if (isUploaded) { + return `
✓ Foto timbangan sudah diupload

Jika ingin revisi, pilih file baru diatas. Status upload akan tereset otomatis.

`; - } + } + + if (!hasFile) { + return '

Pilih foto timbangan terlebih dahulu

'; + } if (!hasValidWeight) { return ` @@ -1114,26 +1034,19 @@ document.addEventListener("DOMContentLoaded", function () { refreshSubmitButtonState(form); } - function isTimbanganItemReady(timbanganItem) { - const weight = - timbanganItem?.berat && timbanganItem.berat.length > 0 - ? timbanganItem.berat[0] - : 0; - return ( - hasStoredPhoto(timbanganItem?.file) && - Boolean(timbanganItem?.uploaded) && - weight > 0 - ); - } - - function getSubmitState(tps) { - if (!tps.fotoKedatangan.length || !tps.fotoKedatanganUploaded) { - return { - canSubmit: false, - message: "Silakan upload foto kedatangan terlebih dahulu.", - }; + function isTimbanganItemReady(timbanganItem) { + const weight = timbanganItem?.berat && timbanganItem.berat.length > 0 ? timbanganItem.berat[0] : 0; + const hasFile = Boolean(timbanganItem?.file) || Boolean(timbanganItem?.fotoFileName); + return hasFile && Boolean(timbanganItem?.uploaded) && weight > 0; } + function getSubmitState(tps) { + if (!tps.fotoKedatanganUploaded) { + if (!tps.fotoKedatangan.length) + return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan.' }; + return { canSubmit: false, message: 'Silakan upload foto kedatangan terlebih dahulu.' }; + } + if (!tps.timbangan.length) { return { canSubmit: false, @@ -1149,12 +1062,11 @@ document.addEventListener("DOMContentLoaded", function () { }; } - if (!tps.fotoPetugas.length || !tps.fotoPetugasUploaded) { - return { - canSubmit: false, - message: "Silakan upload foto petugas terlebih dahulu", - }; - } + if (!tps.fotoPetugasUploaded) { + if (!tps.fotoPetugas.length) + return { canSubmit: false, message: 'Silakan pilih dan upload foto petugas.' }; + return { canSubmit: false, message: 'Silakan upload foto petugas terlebih dahulu.' }; + } if (!tps.namaPetugas.trim()) { return { @@ -1200,21 +1112,18 @@ document.addEventListener("DOMContentLoaded", function () { const stateContainer = item.querySelector(".timbangan-upload-state"); if (!stateContainer) return; - const repeater = item.parentElement; - const itemIndex = repeater - ? Array.from(repeater.children).indexOf(item) - : -1; - const tps = tpsData[activeTpsIndex]; - const currentData = itemIndex >= 0 ? tps.timbangan[itemIndex] : null; - const fileInput = item.querySelector(".input-foto-timbangan"); - const hasFile = hasStoredPhoto(currentData?.file || fileInput?.files?.[0]); - const isUploaded = Boolean(currentData?.uploaded); - const weightInputValue = item.querySelector(".input-berat-timbangan-value"); - const currentWeight = - currentData?.berat && currentData.berat.length > 0 - ? currentData.berat[0] - : parseWeightInput(weightInputValue?.value || "0"); - const hasValidWeight = currentWeight > 0; + const repeater = item.parentElement; + const itemIndex = repeater ? Array.from(repeater.children).indexOf(item) : -1; + const tps = tpsData[activeTpsIndex]; + const currentData = itemIndex >= 0 ? tps.timbangan[itemIndex] : null; + const fileInput = item.querySelector('.input-foto-timbangan'); + const hasFile = Boolean(currentData?.file || fileInput?.files?.[0] || currentData?.fotoFileName); + const isUploaded = Boolean(currentData?.uploaded); + const weightInputValue = item.querySelector('.input-berat-timbangan-value'); + const currentWeight = currentData?.berat && currentData.berat.length > 0 + ? currentData.berat[0] + : parseWeightInput(weightInputValue?.value || '0'); + const hasValidWeight = currentWeight > 0; stateContainer.innerHTML = getTimbanganUploadStateMarkup( hasFile, @@ -1258,24 +1167,12 @@ document.addEventListener("DOMContentLoaded", function () { "timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50"; item.dataset.photoNumber = photoNumber; - const weight = existingData - ? existingData.berat && existingData.berat.length > 0 - ? existingData.berat[0] - : 0 - : 0; - const jenisSampah = existingData - ? existingData.jenisSampah && existingData.jenisSampah.length > 0 - ? existingData.jenisSampah[0] - : DEFAULT_JENIS - : DEFAULT_JENIS; - const hasFile = hasStoredPhoto(existingData && existingData.file); - const isUploaded = Boolean(existingData && existingData.uploaded); - const ocrInfoText = - existingData && existingData.ocrInfo - ? existingData.ocrInfo - : hasFile - ? "OCR: diproses." - : "OCR: belum diproses."; + const weight = existingData ? (existingData.berat && existingData.berat.length > 0 ? existingData.berat[0] : 0) : 0; + const jenisSampah = existingData ? (existingData.jenisSampah && existingData.jenisSampah.length > 0 ? existingData.jenisSampah[0] : DEFAULT_JENIS) : DEFAULT_JENIS; + const hasFileBlob = Boolean(existingData?.file); + const hasFile = Boolean(existingData?.file || existingData?.fotoFileName); + const isUploaded = Boolean(existingData?.uploaded); + const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'); item.innerHTML = `
@@ -1283,7 +1180,7 @@ document.addEventListener("DOMContentLoaded", function () {
-
+
Preview foto timbangan
@@ -1315,26 +1212,21 @@ document.addEventListener("DOMContentLoaded", function () { const jenisSampahSelect = item.querySelector(".input-jenis-sampah"); const removeBtn = item.querySelector(".btn-remove-timbangan"); - if (existingData && hasStoredPhoto(existingData.file)) { - const existingPhoto = resolveStoredPhoto(existingData.file); - const photoUrl = getStoredPhotoUrl(existingPhoto); - previewImage.src = photoUrl; - previewWrap.classList.remove("hidden"); - if (isBrowserFile(existingPhoto)) { - previewImage.onload = function () { - URL.revokeObjectURL(photoUrl); - }; - } - } + if (existingData && existingData.file) { + const localUrl = URL.createObjectURL(existingData.file); + previewImage.src = localUrl; + previewWrap.classList.remove('hidden'); + previewImage.onload = function() { URL.revokeObjectURL(localUrl); }; + } else if (existingData && existingData.fotoFileName && existingData.fotoFileName.startsWith('/')) { + previewImage.src = existingData.fotoFileName; + previewWrap.classList.remove('hidden'); + } - fileInput.addEventListener("change", async function () { - if (fileInput.files && fileInput.files[0]) { - const originalFile = fileInput.files[0]; - const photoNumber = Number( - item.dataset.photoNumber || - Array.from(repeater.children).indexOf(item) + 1, - ); - const watermarkedFile = await applyWatermark(originalFile, photoNumber); + fileInput.addEventListener('change', async function() { + if (fileInput.files && fileInput.files[0]) { + const originalFile = fileInput.files[0]; + const photoNumber = Number(item.dataset.photoNumber || (Array.from(repeater.children).indexOf(item) + 1)); + const watermarkedFile = await applyWatermark(originalFile, photoNumber); const dataTransfer = new DataTransfer(); dataTransfer.items.add(watermarkedFile); @@ -1375,41 +1267,44 @@ document.addEventListener("DOMContentLoaded", function () { if (form) refreshSubmitButtonState(form); }); - weightInputDisplay.addEventListener("blur", function () { - const parsed = parseWeightInput(this.value); - if (parsed > 0) { - this.value = formatWeightDisplay(parsed); - weightInputValue.value = parsed.toFixed(2); - } else { - this.value = ""; - weightInputValue.value = "0.00"; - } - updateTpsTotalTimbangan(); - syncTimbanganToTpsData(); - refreshTimbanganUploadState(item); - const form = tpsContentContainer.querySelector("form"); - if (form) refreshSubmitButtonState(form); - }); + weightInputDisplay.addEventListener('blur', function() { + const parsed = parseWeightInput(this.value); + if (parsed > 0) { + this.value = formatWeightDisplay(parsed); + weightInputValue.value = parsed.toFixed(2); + } else { + this.value = ''; + weightInputValue.value = '0.00'; + } + updateTpsTotalTimbangan(); + syncTimbanganToTpsData(); + refreshTimbanganUploadState(item); + const form = tpsContentContainer.querySelector('form'); + if (form) refreshSubmitButtonState(form); + scheduleAutoSave(); + }); - jenisSampahSelect.addEventListener("change", function () { - updateTpsTotalTimbangan(); - syncTimbanganToTpsData(); - const form = tpsContentContainer.querySelector("form"); - if (form) refreshSubmitButtonState(form); - }); + jenisSampahSelect.addEventListener('change', function() { + updateTpsTotalTimbangan(); + syncTimbanganToTpsData(); + const form = tpsContentContainer.querySelector('form'); + if (form) refreshSubmitButtonState(form); + scheduleAutoSave(); + }); - removeBtn.addEventListener("click", function () { - item.remove(); - const form = tpsContentContainer.querySelector("form"); - const rep = form ? form.querySelector(".tps-timbangan-repeater") : null; - if (rep) { - renumberTimbanganItems(rep); - if (rep.children.length === 0) createTimbanganItem(rep); - } - updateTpsTotalTimbangan(); - syncTimbanganToTpsData(); - if (form) refreshSubmitButtonState(form); - }); + removeBtn.addEventListener('click', function() { + item.remove(); + const form = tpsContentContainer.querySelector('form'); + const rep = form ? form.querySelector('.tps-timbangan-repeater') : null; + if (rep) { + renumberTimbanganItems(rep); + if (rep.children.length === 0) createTimbanganItem(rep); + } + updateTpsTotalTimbangan(); + syncTimbanganToTpsData(); + if (form) refreshSubmitButtonState(form); + scheduleAutoSave(); + }); repeater.appendChild(item); refreshTimbanganUploadState(item); @@ -1425,28 +1320,25 @@ document.addEventListener("DOMContentLoaded", function () { const items = repeater.querySelectorAll(".timbangan-item"); const previousTimbangan = [...tps.timbangan]; - tps.timbangan = []; - items.forEach((item, index) => { - const fileInput = item.querySelector(".input-foto-timbangan"); - const weightValue = item.querySelector(".input-berat-timbangan-value"); - const weight = parseWeightInput(weightValue.value); - const jenisSampah = item.querySelector(".input-jenis-sampah").value; - const ocrInfo = - item.querySelector(".input-ocr-info")?.textContent || - "OCR: belum diproses."; - - tps.timbangan.push({ - file: fileInput.files[0] || previousTimbangan[index]?.file || null, - berat: [weight], - jenisSampah: [jenisSampah], - lokasiAngkut: [], - uploaded: previousTimbangan[index]?.uploaded ?? false, - ocrInfo, - }); - }); - - saveState(); - } + tps.timbangan = []; + items.forEach((item, index) => { + const fileInput = item.querySelector('.input-foto-timbangan'); + const weightValue = item.querySelector('.input-berat-timbangan-value'); + const weight = parseWeightInput(weightValue.value); + const jenisSampah = item.querySelector('.input-jenis-sampah').value; + const ocrInfo = item.querySelector('.input-ocr-info')?.textContent || 'OCR: belum diproses.'; + + tps.timbangan.push({ + file: fileInput.files[0] || previousTimbangan[index]?.file || null, + fotoFileName: previousTimbangan[index]?.fotoFileName || '', + berat: [weight], + jenisSampah: [jenisSampah], + lokasiAngkut: [], + uploaded: previousTimbangan[index]?.uploaded ?? false, + ocrInfo + }); + }); + } function buildSubmitFormData(tps) { const formData = new FormData(); @@ -1493,47 +1385,19 @@ document.addEventListener("DOMContentLoaded", function () { return formData; } - function uploadSingleFotoTimbangan(itemIndex, targetItem = null) { - const tps = tpsData[activeTpsIndex]; - if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) { - alert("Belum ada foto timbangan yang dipilih!"); - return; - } - const weight = - tps.timbangan[itemIndex].berat && - tps.timbangan[itemIndex].berat.length > 0 - ? tps.timbangan[itemIndex].berat[0] - : 0; - if (weight <= 0) { - alert( - "Berat belum valid. Isi manual dulu sebelum upload foto timbangan.", - ); - return; - } - - const _ext = ( - tps.timbangan[itemIndex].file.name.split(".").pop() || "jpg" - ).toLowerCase(); - const _jenis = ( - tps.timbangan[itemIndex].jenisSampah[0] || "residu" - ).toLowerCase(); - const _beratStr = parseFloat(weight.toFixed(2)) - .toString() - .replace(".", "_"); - const _newName = `timbangan${itemIndex + 1}-${_jenis}-${_beratStr}.${_ext}`; - tps.timbangan[itemIndex].file = new File( - [tps.timbangan[itemIndex].file], - _newName, - { - type: tps.timbangan[itemIndex].file.type, - lastModified: tps.timbangan[itemIndex].file.lastModified, - }, - ); - - alert( - `Upload foto timbangan #${itemIndex + 1}\nBerat: ${formatWeightDisplay(weight)} kg\n(Implementasi upload ke server)`, - ); - tps.timbangan[itemIndex].uploaded = true; + async function uploadSingleFotoTimbangan(itemIndex, targetItem = null) { + const tps = tpsData[activeTpsIndex]; + if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) { + showToast('Belum ada foto timbangan yang dipilih!', 'error'); + return; + } + const weight = tps.timbangan[itemIndex].berat && tps.timbangan[itemIndex].berat.length > 0 + ? tps.timbangan[itemIndex].berat[0] + : 0; + if (weight <= 0) { + showToast('Isi berat manual dulu sebelum upload foto timbangan.', 'error'); + return; + } if (!targetItem) { const form = tpsContentContainer.querySelector("form"); @@ -1546,78 +1410,212 @@ document.addEventListener("DOMContentLoaded", function () { targetItem = items[itemIndex] || null; } - if (targetItem) { - refreshTimbanganUploadState(targetItem); + const uploadBtn = targetItem ? targetItem.querySelector('.btn-upload-timbangan') : null; + if (uploadBtn) { uploadBtn.disabled = true; uploadBtn.textContent = 'Mengupload...'; } + + const formData = new FormData(); + formData.append('FotoTimbangan', tps.timbangan[itemIndex].file); + formData.append('DraftKey', draftRequestKey); + formData.append('SpjDetailId', tps.spjDetailId || ''); + formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); + formData.append('ItemIndex', itemIndex); + formData.append('JenisSampah', (tps.timbangan[itemIndex].jenisSampah && tps.timbangan[itemIndex].jenisSampah[0]) || DEFAULT_JENIS); + formData.append('Berat', weight.toFixed(2)); + + try { + const res = await fetch('/upst/detail-penjemputan/upload-foto-timbangan-non-tps', { method: 'POST', body: formData }); + const data = await res.json(); + if (res.ok && data.success) { + tps.timbangan[itemIndex].uploaded = true; + tps.timbangan[itemIndex].fotoFileName = data.fileUrl || data.fileName || ''; + const fotoInputTimb = targetItem?.querySelector('.input-foto-timbangan'); + if (fotoInputTimb) fotoInputTimb.value = ''; + showToast(data.message || `Foto timbangan #${itemIndex + 1} berhasil diupload.`, 'success'); + } else { + showToast(data.message || 'Gagal upload foto timbangan.', 'error'); + if (uploadBtn) { uploadBtn.disabled = false; uploadBtn.textContent = 'Upload Foto Timbangan Ini'; } + } + } catch { + showToast('Koneksi gagal saat upload foto timbangan.', 'error'); + if (uploadBtn) { uploadBtn.disabled = false; uploadBtn.textContent = 'Upload Foto Timbangan Ini'; } + } + + if (targetItem) refreshTimbanganUploadState(targetItem); + syncTimbanganToTpsData(); + const form = tpsContentContainer.querySelector('form'); + if (form) refreshSubmitButtonState(form); + scheduleAutoSave(); } - syncTimbanganToTpsData(); - const form = tpsContentContainer.querySelector("form"); - if (form) refreshSubmitButtonState(form); - saveState(); - } + async function uploadFotoKedatangan() { + const tps = tpsData[activeTpsIndex]; + if (tps.fotoKedatangan.length === 0) { + showToast('Belum ada foto kedatangan yang dipilih!', 'error'); + return; + } - function uploadFotoKedatangan() { - const tps = tpsData[activeTpsIndex]; - if (tps.fotoKedatangan.length === 0) { - alert("Belum ada foto kedatangan yang dipilih!"); - return; + const form = tpsContentContainer.querySelector('form'); + const btn = form ? form.querySelector('.tps-btn-upload-kedatangan') : null; + if (btn) { btn.disabled = true; btn.textContent = 'Mengupload...'; } + + const formData = new FormData(); + tps.fotoKedatangan.forEach(f => formData.append('FotoKedatangan', f)); + formData.append('DraftKey', draftRequestKey); + formData.append('SpjDetailId', tps.spjDetailId || ''); + formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); + formData.append('WaktuKedatangan', tps.waktuKedatangan || ''); + formData.append('Latitude', tps.latitude || ''); + formData.append('Longitude', tps.longitude || ''); + formData.append('AlamatJalan', tps.alamatJalan || ''); + + try { + const res = await fetch('/upst/detail-penjemputan/upload-foto-kedatangan-non-tps', { method: 'POST', body: formData }); + const data = await res.json(); + if (res.ok && data.success) { + tps.fotoKedatanganUploaded = true; + tps.fotoKedatanganFileNames = data.fileUrls || data.fileNames || []; + showToast(data.message || 'Foto kedatangan berhasil diupload.', 'success'); + if (form) { + const fotoInput = form.querySelector('.tps-foto-kedatangan'); + if (fotoInput) fotoInput.value = ''; + refreshKedatanganUploadState(form); + } + scheduleAutoSave(); + } else { + showToast(data.message || 'Gagal upload foto kedatangan.', 'error'); + if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`; } + } + } catch { + showToast('Koneksi gagal saat upload foto kedatangan.', 'error'); + if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`; } + } } - alert( - `Upload ${tps.fotoKedatangan.length} foto kedatangan\n(Implementasi upload ke server)`, - ); - tps.fotoKedatanganUploaded = true; - const form = tpsContentContainer.querySelector("form"); - if (form) refreshKedatanganUploadState(form); - saveState(); - } - function uploadFotoPetugas() { - const tps = tpsData[activeTpsIndex]; - if (tps.fotoPetugas.length === 0) { - alert("Belum ada foto petugas yang dipilih!"); - return; + async function uploadFotoPetugas() { + const tps = tpsData[activeTpsIndex]; + if (tps.fotoPetugas.length === 0) { + showToast('Belum ada foto petugas yang dipilih!', 'error'); + return; + } + if (!tps.namaPetugas.trim()) { + showToast('Nama petugas wajib diisi sebelum upload foto petugas!', 'error'); + return; + } + + const form = tpsContentContainer.querySelector('form'); + const btn = form ? form.querySelector('.tps-btn-upload-petugas:not([disabled])') : null; + if (btn) { btn.disabled = true; btn.textContent = 'Mengupload...'; } + + const formData = new FormData(); + tps.fotoPetugas.forEach(f => formData.append('FotoPetugas', f)); + formData.append('DraftKey', draftRequestKey); + formData.append('SpjDetailId', tps.spjDetailId || ''); + formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); + formData.append('NamaPetugas', tps.namaPetugas); + + try { + const res = await fetch('/upst/detail-penjemputan/upload-foto-petugas-non-tps', { method: 'POST', body: formData }); + const data = await res.json(); + if (res.ok && data.success) { + tps.fotoPetugasUploaded = true; + tps.fotoPetugasFileNames = data.fileUrls || data.fileNames || []; + showToast(data.message || 'Foto petugas berhasil diupload.', 'success'); + if (form) { + const fotoInput = form.querySelector('.tps-foto-petugas'); + if (fotoInput) fotoInput.value = ''; + refreshPetugasUploadState(form); + } + scheduleAutoSave(); + } else { + showToast(data.message || 'Gagal upload foto petugas.', 'error'); + if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`; } + } + } catch { + showToast('Koneksi gagal saat upload foto petugas.', 'error'); + if (btn) { btn.disabled = false; btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`; } + } } - if (!tps.namaPetugas.trim()) { - alert("Nama petugas wajib diisi sebelum upload foto petugas!"); - return; - } - alert( - `Upload ${tps.fotoPetugas.length} foto petugas untuk ${tps.name}\n(Implementasi upload ke server)`, - ); - tps.fotoPetugasUploaded = true; - const form = tpsContentContainer.querySelector("form"); - if (form) refreshPetugasUploadState(form); - saveState(); - } - function submitTpsData() { - const tps = tpsData[activeTpsIndex]; - const submitState = getSubmitState(tps); - if (!submitState.canSubmit) return alert(submitState.message); + async function submitTpsData() { + const tps = tpsData[activeTpsIndex]; + const submitState = getSubmitState(tps); + if (!submitState.canSubmit) { showToast(submitState.message, 'error'); return; } - alert( - `Validasi OK (${tps.name}).\n- Organik: ${formatWeightDisplay(tps.totalOrganik)} kg\n- Anorganik: ${formatWeightDisplay(tps.totalAnorganik)} kg\n- Residu: ${formatWeightDisplay(tps.totalResidu)} kg\n- Total: ${formatWeightDisplay(tps.totalTimbangan)} kg\n- Petugas: ${tps.namaPetugas}`, - ); - tps.submitted = true; - saveState(); - clearState(); + const form = tpsContentContainer.querySelector('form'); + const submitBtn = form ? form.querySelector('button[type="submit"]') : null; + if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Menyimpan...'; } - /* const formData = buildSubmitFormData(tps); - fetch('/upst/detail-penjemputan', { method: 'POST', body: formData }) - .then(response => { - if (response.ok) { - clearState(); - } - }); - */ - } + + try { + const res = await fetch('/upst/detail-penjemputan', { method: 'POST', body: formData }); + if (res.ok || res.redirected) { + tps.submitted = true; + if (draftRequestKey) { + await fetch(`/upst/detail-penjemputan/delete-draft-non-tps?draftKey=${encodeURIComponent(draftRequestKey)}`, { method: 'DELETE' }); + } + showToast('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'); + if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Submit'; } + } + } catch { + showToast('Koneksi gagal saat submit.', 'error'); + if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Submit'; } + } + } + + function showToast(message, type = 'info') { + let container = document.getElementById('espj-toast-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'espj-toast-container'; + container.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:9999;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;'; + document.body.appendChild(container); + } + const toast = document.createElement('div'); + const bg = type === 'success' ? '#16a34a' : type === 'error' ? '#dc2626' : '#2563eb'; + toast.style.cssText = `background:${bg};color:#fff;padding:10px 18px;border-radius:16px;font-size:13px;font-weight:600;box-shadow:0 4px 24px rgba(0,0,0,.18);opacity:0;transition:opacity .25s;max-width:320px;text-align:center;`; + toast.textContent = message; + container.appendChild(toast); + requestAnimationFrame(() => { toast.style.opacity = '1'; }); + setTimeout(() => { + toast.style.opacity = '0'; + setTimeout(() => toast.remove(), 300); + }, 3200); + } const nomorSpjEl = document.querySelector(".text-gray-600.font-mono"); if (nomorSpjEl && nomorSpjEl.textContent) { nomorSpj = nomorSpjEl.textContent.trim(); } - initializeLocation(); - loadDetailData(); + initializeLocation(); + await loadDetailData(); + await loadDraftFromServer(); + renderTpsForm(); + + function renderServerImagePreview(fileUrls, container) { + container.innerHTML = ''; + if (!fileUrls || fileUrls.length === 0) return; + container.className = 'space-y-2'; + fileUrls.forEach((url, index) => { + const item = document.createElement('div'); + item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black'; + const isUrl = typeof url === 'string' && (url.startsWith('/') || url.startsWith('http')); + if (isUrl) { + item.innerHTML = ` +
+ Foto ${index + 1} +
+ `; + } else { + item.className = 'rounded-xl border border-green-200 bg-green-50 h-44 flex items-center justify-center'; + item.innerHTML = `

✓ Foto ${index + 1}

`; + } + container.appendChild(item); + }); + } }); diff --git a/wwwroot/driver/js/detail-penjemputan-tps.js b/wwwroot/driver/js/detail-penjemputan-tps.js index 7040e19..75675e0 100644 --- a/wwwroot/driver/js/detail-penjemputan-tps.js +++ b/wwwroot/driver/js/detail-penjemputan-tps.js @@ -1,36 +1,25 @@ const DetailPenjemputan = (function () { "use strict"; - const CONFIG = { - OCR_AREAS: [ - { - id: "A", - x: 0.34, - y: 0.35, - w: 0.4, - h: 0.11, - color: "border-lime-400 bg-lime-500/15", - }, - { - id: "B", - x: 0.31, - y: 0.33, - w: 0.45, - h: 0.14, - color: "border-amber-300 bg-amber-400/10", - }, - { - id: "C", - x: 0.29, - y: 0.31, - w: 0.49, - h: 0.17, - color: "border-cyan-300 bg-cyan-400/10", - }, - ], - JENIS_SAMPAH: ["Organik", "Anorganik", "Residu"], - DEFAULT_JENIS: "Residu", - }; + const CONFIG = { + OCR_AREAS: [ + { id: 'A', x: 0.34, y: 0.35, w: 0.40, h: 0.11, color: 'border-lime-400 bg-lime-500/15' }, + { id: 'B', x: 0.31, y: 0.33, w: 0.45, h: 0.14, color: 'border-amber-300 bg-amber-400/10' }, + { id: 'C', x: 0.29, y: 0.31, w: 0.49, h: 0.17, color: 'border-cyan-300 bg-cyan-400/10' } + ], + JENIS_SAMPAH: ['Organik', 'Anorganik', 'Residu'], + DEFAULT_JENIS: 'Residu' + }; + + const ENDPOINTS = { + saveDraft: '/upst/detail-penjemputan/save-draft', + loadDraft: '/upst/detail-penjemputan/load-draft', + deleteDraft: '/upst/detail-penjemputan/delete-draft', + uploadKedatangan: '/upst/detail-penjemputan/upload-foto-kedatangan', + uploadTimbangan: '/upst/detail-penjemputan/upload-foto-timbangan', + uploadPetugas: '/upst/detail-penjemputan/upload-foto-petugas', + submit: '/upst/detail-penjemputan' + }; let state = { activeTpsIndex: 0, @@ -364,11 +353,13 @@ const DetailPenjemputan = (function () { totalResiduDisplay: null, }; - function init(tpsList) { - loadState(); - initElements(); - initializeLocation(tpsList); - } + let autoSaveTimer = null; + let autoSaveStatusEl = null; + + async function init(tpsList) { + initElements(); + await initializeLocation(tpsList); + } function initElements() { elements.grandTotalDisplay = document.getElementById( @@ -395,42 +386,145 @@ const DetailPenjemputan = (function () { } } - function initializeLocation(tpsList) { - state.availableTpsList = tpsList || []; - if (elements.tpsSelectionContainer) { - elements.tpsSelectionContainer.style.display = "none"; + 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, ''); } - if (state.tpsData.length > 0) { - state.selectedTpsList = state.tpsData.map((item) => item.name); - state.activeTpsIndex = Math.min( - state.activeTpsIndex, - Math.max(state.tpsData.length - 1, 0), - ); - elements.tpsTabsContainer.style.display = "block"; - - if (state.tpsData.length === 1) { - renderSingleForm(); - } else { - renderTabs(); - renderTpsForm(); - } - - updateAllTotals(); - return; + function getActiveTps() { + return state.tpsData[state.activeTpsIndex] || null; } - if (state.availableTpsList.length === 0) { - state.selectedTpsList = ["1 Lokasi TPS"]; - initializeTpsData(state.selectedTpsList); - elements.tpsTabsContainer.style.display = "block"; - renderSingleForm(); - return; + function getAutoSaveStatusEl() { + if (!autoSaveStatusEl || !document.body.contains(autoSaveStatusEl)) { + autoSaveStatusEl = document.getElementById('auto-save-status'); + } + return autoSaveStatusEl; } - state.selectedTpsList = [...state.availableTpsList]; - initializeTpsData(state.selectedTpsList); - elements.tpsTabsContainer.style.display = "block"; + function showAutoSaveStatus(msg, isOk = false) { + const statusEl = getAutoSaveStatusEl(); + if (!statusEl) return; + statusEl.textContent = msg; + statusEl.className = isOk + ? 'text-[11px] text-green-600 text-center font-medium transition-opacity' + : 'text-[11px] text-amber-500 text-center font-medium transition-opacity'; + statusEl.style.opacity = '1'; + if (isOk) setTimeout(() => { statusEl.style.opacity = '0'; }, 2500); + } + + function scheduleAutoSave() { + clearTimeout(autoSaveTimer); + showAutoSaveStatus('menyimpan...'); + autoSaveTimer = setTimeout(autoSaveDraft, 1000); + } + + async function autoSaveDraft() { + const tps = getActiveTps(); + if (!tps || !tps.draftKey) return; + + 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 || '' + }; + + try { + const res = await fetch(ENDPOINTS.saveDraft, { + 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'); + } + } + + 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) { + state.availableTpsList = tpsList || []; + if (elements.tpsSelectionContainer) { + elements.tpsSelectionContainer.style.display = 'none'; + } + + if (state.availableTpsList.length === 0) { + state.selectedTpsList = ['1 Lokasi TPS']; + initializeTpsData(state.selectedTpsList); + await loadDraftsForAllTps(); + elements.tpsTabsContainer.style.display = 'block'; + renderSingleForm(); + return; + } + + state.selectedTpsList = [...state.availableTpsList]; + initializeTpsData(state.selectedTpsList); + await loadDraftsForAllTps(); + elements.tpsTabsContainer.style.display = 'block'; if (state.selectedTpsList.length === 1) { renderSingleForm(); @@ -440,39 +534,33 @@ const DetailPenjemputan = (function () { } } - function initializeTpsData(tpsNames) { - state.tpsData = tpsNames.map((tpsItem, index) => ({ - name: - typeof tpsItem === "string" - ? tpsItem - : tpsItem?.name || tpsItem?.Name || `TPS ${index + 1}`, - index: index, - lokasiAngkutId: - typeof tpsItem === "string" - ? "" - : tpsItem?.lokasiAngkutId || tpsItem?.LokasiAngkutID || "", - spjDetailId: - typeof tpsItem === "string" - ? "" - : tpsItem?.spjDetailId || tpsItem?.SpjDetailID || "", - latitude: "", - longitude: "", - alamatJalan: "", - waktuKedatangan: "", - fotoKedatangan: [], - fotoKedatanganUploaded: false, - timbangan: [], - totalOrganik: 0, - totalAnorganik: 0, - totalResidu: 0, - totalTimbangan: 0, - fotoPetugas: [], - fotoPetugasUploaded: false, - namaPetugas: "", - submitted: false, - })); - state.hasRequestedLocation = new Array(tpsNames.length).fill(false); - } + function initializeTpsData(tpsNames) { + state.tpsData = tpsNames.map((tpsItem, index) => ({ + name: typeof tpsItem === 'string' ? tpsItem : (tpsItem?.name || tpsItem?.Name || `TPS ${index + 1}`), + index: index, + lokasiAngkutId: typeof tpsItem === 'string' ? '' : (tpsItem?.lokasiAngkutId || tpsItem?.LokasiAngkutID || ''), + spjDetailId: typeof tpsItem === 'string' ? '' : (tpsItem?.spjDetailId || tpsItem?.SpjDetailID || ''), + draftKey: '', + latitude: '', + longitude: '', + alamatJalan: '', + waktuKedatangan: '', + fotoKedatangan: [], + fotoKedatanganFileNames: [], + fotoKedatanganUploaded: false, + timbangan: [], + totalOrganik: 0, + totalAnorganik: 0, + totalResidu: 0, + totalTimbangan: 0, + fotoPetugas: [], + fotoPetugasFileNames: [], + fotoPetugasUploaded: false, + namaPetugas: '', + submitted: false + })).map(tps => ({ ...tps, draftKey: buildDraftKey(tps) })); + state.hasRequestedLocation = new Array(tpsNames.length).fill(false); + } function renderTpsSelection() { elements.tpsCheckboxesContainer.innerHTML = ""; @@ -498,29 +586,28 @@ const DetailPenjemputan = (function () { }); } - function handleConfirmTps() { - const checkboxes = elements.tpsCheckboxesContainer.querySelectorAll( - 'input[type="checkbox"]:checked', - ); - state.selectedTpsList = Array.from(checkboxes).map((cb) => cb.value); - - if (state.selectedTpsList.length === 0) { - alert("Pilih minimal 1 TPS untuk diangkut!"); - return; + async function handleConfirmTps() { + const checkboxes = elements.tpsCheckboxesContainer.querySelectorAll('input[type="checkbox"]:checked'); + state.selectedTpsList = Array.from(checkboxes).map(cb => cb.value); + + if (state.selectedTpsList.length === 0) { + showToast('Pilih minimal 1 TPS untuk diangkut!', 'error'); + return; + } + + initializeTpsData(state.selectedTpsList); + await loadDraftsForAllTps(); + elements.tpsSelectionContainer.style.display = 'none'; + elements.tpsTabsContainer.style.display = 'block'; + + if (state.selectedTpsList.length === 1) { + renderSingleForm(); + } else { + renderTabs(); + renderTpsForm(); + } } - initializeTpsData(state.selectedTpsList); - elements.tpsSelectionContainer.style.display = "none"; - elements.tpsTabsContainer.style.display = "block"; - - if (state.selectedTpsList.length === 1) { - renderSingleForm(); - } else { - renderTabs(); - renderTpsForm(); - } - } - function renderSingleForm() { elements.tpsTabsEl.style.display = "none"; state.activeTpsIndex = 0; @@ -601,7 +688,8 @@ const DetailPenjemputan = (function () { Batal
- ${submitState.canSubmit ? "" : `

${submitState.message}

`} + ${submitState.canSubmit ? '' : `

${submitState.message}

`} +

`; @@ -700,34 +788,39 @@ const DetailPenjemputan = (function () { const namaPetugasInput = form.querySelector(".tps-nama-petugas"); const btnAddTimbangan = form.querySelector(".tps-btn-add-timbangan"); - fotoKedatanganInput.addEventListener("change", function () { - tps.fotoKedatangan = Array.from(this.files); - tps.fotoKedatanganUploaded = false; - updateWaktuKedatangan(); - updateMultiPreview(this, form.querySelector(".tps-preview-kedatangan")); - refreshKedatanganUploadState(form); - saveState(); - }); + fotoKedatanganInput.addEventListener('change', function() { + tps.fotoKedatangan = Array.from(this.files); + tps.fotoKedatanganUploaded = false; + updateWaktuKedatangan(); + updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan')); + refreshKedatanganUploadState(form); + scheduleAutoSave(); + }); - fotoPetugasInput.addEventListener("change", function () { - tps.fotoPetugas = Array.from(this.files); - tps.fotoPetugasUploaded = false; - updateMultiPreview(this, form.querySelector(".tps-preview-petugas")); - refreshPetugasUploadState(form); - saveState(); - }); + fotoPetugasInput.addEventListener('change', function() { + tps.fotoPetugas = Array.from(this.files); + tps.fotoPetugasUploaded = false; + updateMultiPreview(this, form.querySelector('.tps-preview-petugas')); + refreshPetugasUploadState(form); + scheduleAutoSave(); + }); - namaPetugasInput.addEventListener("input", function () { - tps.namaPetugas = this.value; - refreshPetugasUploadState(form); - saveState(); - }); + namaPetugasInput.addEventListener('input', function() { + tps.namaPetugas = this.value; + refreshPetugasUploadState(form); + }); - btnAddTimbangan.addEventListener("click", function () { - createTimbanganItem(form.querySelector(".tps-timbangan-repeater")); - syncTimbanganToTpsData(); - refreshSubmitButtonState(form); - }); + namaPetugasInput.addEventListener('blur', function() { + tps.namaPetugas = this.value; + scheduleAutoSave(); + }); + + btnAddTimbangan.addEventListener('click', function() { + createTimbanganItem(form.querySelector('.tps-timbangan-repeater')); + syncTimbanganToTpsData(); + refreshSubmitButtonState(form); + scheduleAutoSave(); + }); const btnUploadKedatangan = form.querySelector( ".tps-btn-upload-kedatangan", @@ -766,16 +859,24 @@ const DetailPenjemputan = (function () { const form = elements.tpsContentContainer.querySelector("form"); if (!form) return; - const previewKedatangan = form.querySelector(".tps-preview-kedatangan"); - if (previewKedatangan && tps.fotoKedatangan.length > 0) { - renderStoredPhotos(tps.fotoKedatangan, previewKedatangan); - } + const 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 && tps.fotoPetugas.length > 0) { - renderStoredPhotos(tps.fotoPetugas, previewPetugas); + 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); + } + } } - } function renderStoredPhotos(files, container) { container.innerHTML = ""; @@ -786,21 +887,12 @@ const DetailPenjemputan = (function () { item.className = "rounded-xl border border-gray-200 overflow-hidden bg-black"; - const imageUrl = getStoredPhotoUrl(file); - const safeName = getStoredPhotoName(file, `Foto ${index + 1}`).replace( - /"/g, - """, - ); - const fileSize = getStoredPhotoSize(file); - item.innerHTML = ` + const imageUrl = URL.createObjectURL(file); + item.innerHTML = `
Preview ${index + 1}
-
-

${index + 1}. ${safeName}

-

${fileSize > 0 ? formatFileSize(fileSize) : "Tersimpan di server"}

-
- `; + `; const img = item.querySelector(".preview-multi-image"); if (img && isBrowserFile(resolveStoredPhoto(file))) { @@ -863,21 +955,22 @@ const DetailPenjemputan = (function () { }); } - function updateTpsLocation(lat, lng, address) { - const tps = state.tpsData[state.activeTpsIndex]; - tps.latitude = lat; - tps.longitude = lng; - tps.alamatJalan = address; + function updateTpsLocation(lat, lng, address) { + const tps = state.tpsData[state.activeTpsIndex]; + tps.latitude = lat; + tps.longitude = lng; + tps.alamatJalan = address; + + const form = elements.tpsContentContainer.querySelector('form'); + if (form) { + const latInput = form.querySelector('.tps-display-latitude'); + const lngInput = form.querySelector('.tps-display-longitude'); + if (latInput) latInput.value = lat; + if (lngInput) lngInput.value = lng; + } - const form = elements.tpsContentContainer.querySelector("form"); - if (form) { - const latInput = form.querySelector(".tps-display-latitude"); - const lngInput = form.querySelector(".tps-display-longitude"); - if (latInput) latInput.value = lat; - if (lngInput) lngInput.value = lng; + scheduleAutoSave(); } - saveState(); - } function updateMultiPreview(input, previewContainer) { if (!input || !previewContainer) return; @@ -892,56 +985,69 @@ const DetailPenjemputan = (function () { item.className = "rounded-xl border border-gray-200 overflow-hidden bg-black"; - const imageUrl = URL.createObjectURL(file); - const safeName = file.name.replace(/"/g, """); - item.innerHTML = ` + const imageUrl = URL.createObjectURL(file); + item.innerHTML = `
Preview ${index + 1}
-
-

${index + 1}. ${safeName}

-

${formatFileSize(file.size)}

-
`; - const img = item.querySelector(".preview-multi-image"); - if (img) { - img.onload = function () { - URL.revokeObjectURL(imageUrl); - }; - } - previewContainer.appendChild(item); - }); - } + const img = item.querySelector('.preview-multi-image'); + if (img) { + img.onload = function() { + URL.revokeObjectURL(imageUrl); + }; + } + previewContainer.appendChild(item); + }); + } - function createTimbanganItem(repeater, existingData = null) { - const photoNumber = repeater.children.length + 1; + function renderServerImagePreview(fileUrls, container) { + container.innerHTML = ''; + if (!fileUrls || fileUrls.length === 0) return; + container.className = 'space-y-2'; - const item = document.createElement("div"); - item.className = - "timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50"; - item.dataset.photoNumber = photoNumber; + 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 weight = existingData ? existingData.weight : 0; - const jenisSampah = existingData - ? existingData.jenisSampah - : CONFIG.DEFAULT_JENIS; - const hasFile = hasStoredPhoto(existingData && existingData.file); - const isUploaded = Boolean(existingData && existingData.uploaded); - const ocrInfoText = - existingData && existingData.ocrInfo - ? existingData.ocrInfo - : hasFile - ? "OCR: diproses." - : "OCR: belum diproses."; + if (isUrl) { + item.innerHTML = ` +
+ Foto ${index + 1} +
+ `; + } else { + item.className = 'rounded-xl border border-green-200 bg-green-50 h-44 flex items-center justify-center'; + item.innerHTML = `

✓ Foto ${index + 1}

`; + } - item.innerHTML = ` + container.appendChild(item); + }); + } + + function createTimbanganItem(repeater, existingData = null) { + const photoNumber = repeater.children.length + 1; + + const item = document.createElement('div'); + item.className = 'timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50'; + item.dataset.photoNumber = photoNumber; + + const weight = existingData ? (existingData.weight || 0) : 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 isUploaded = Boolean(existingData?.uploaded); + const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'); + + item.innerHTML = `

Item Timbangan #${photoNumber}

-
+
Preview foto timbangan

${ocrInfoText}

@@ -972,62 +1078,64 @@ const DetailPenjemputan = (function () { const jenisSampahSelect = item.querySelector(".input-jenis-sampah"); const removeBtn = item.querySelector(".btn-remove-timbangan"); - if (existingData && hasStoredPhoto(existingData.file)) { - const existingPhoto = resolveStoredPhoto(existingData.file); - const photoUrl = getStoredPhotoUrl(existingPhoto); - previewImage.src = photoUrl; - previewWrap.classList.remove("hidden"); - if (isBrowserFile(existingPhoto)) { - previewImage.onload = function () { - URL.revokeObjectURL(photoUrl); - }; - } - } - - fileInput.addEventListener("change", async function () { - if (fileInput.files && fileInput.files[0]) { - const originalFile = fileInput.files[0]; - - const watermarkedFile = await applyWatermark(originalFile, photoNumber); - - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(watermarkedFile); - fileInput.files = dataTransfer.files; - - const localUrl = URL.createObjectURL(watermarkedFile); - previewImage.src = localUrl; - previewWrap.classList.remove("hidden"); - previewImage.onload = function () { - URL.revokeObjectURL(localUrl); - }; - - await autoFillWeight(watermarkedFile, weightInputDisplay, ocrInfoEl); - const parsed = parseWeightInput(weightInputDisplay.value); - weightInputValue.value = parsed.toFixed(2); - updateTpsTotalTimbangan(); - syncTimbanganToTpsData(); - - const tps = state.tpsData[state.activeTpsIndex]; - const itemIndex = Array.from(repeater.children).indexOf(item); - if (itemIndex >= 0 && tps.timbangan[itemIndex]) { - tps.timbangan[itemIndex].uploaded = false; - refreshTimbanganUploadState(item); - saveState(); + if (existingData && existingData.file) { + const localUrl = URL.createObjectURL(existingData.file); + previewImage.src = localUrl; + previewWrap.classList.remove('hidden'); + previewImage.onload = function() { + URL.revokeObjectURL(localUrl); + }; + } else if (existingData && existingData.fotoFileName && existingData.fotoFileName.startsWith('/')) { + previewImage.src = existingData.fotoFileName; + previewWrap.classList.remove('hidden'); } - } - }); - weightInputDisplay.addEventListener("input", function () { - const cleaned = this.value.replace(/[^0-9.,]/g, ""); - this.value = cleaned; - const parsed = parseWeightInput(cleaned); - weightInputValue.value = parsed.toFixed(2); - updateTpsTotalTimbangan(); - syncTimbanganToTpsData(); - refreshTimbanganUploadState(item); - const form = elements.tpsContentContainer.querySelector("form"); - if (form) refreshSubmitButtonState(form); - }); + fileInput.addEventListener('change', async function() { + if (fileInput.files && fileInput.files[0]) { + const originalFile = fileInput.files[0]; + + const watermarkedFile = await applyWatermark(originalFile, photoNumber); + + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(watermarkedFile); + fileInput.files = dataTransfer.files; + + const localUrl = URL.createObjectURL(watermarkedFile); + previewImage.src = localUrl; + previewWrap.classList.remove('hidden'); + previewImage.onload = function() { + URL.revokeObjectURL(localUrl); + }; + + await autoFillWeight(watermarkedFile, weightInputDisplay, ocrInfoEl); + const parsed = parseWeightInput(weightInputDisplay.value); + weightInputValue.value = parsed.toFixed(2); + updateTpsTotalTimbangan(); + syncTimbanganToTpsData(); + + const tps = state.tpsData[state.activeTpsIndex]; + const itemIndex = Array.from(repeater.children).indexOf(item); + if (itemIndex >= 0 && tps.timbangan[itemIndex]) { + tps.timbangan[itemIndex].uploaded = false; + tps.timbangan[itemIndex].fotoFileName = ''; + refreshTimbanganUploadState(item); + } + scheduleAutoSave(); + } + }); + + weightInputDisplay.addEventListener('input', function() { + const cleaned = this.value.replace(/[^0-9.,]/g, ''); + this.value = cleaned; + const parsed = parseWeightInput(cleaned); + weightInputValue.value = parsed.toFixed(2); + updateTpsTotalTimbangan(); + syncTimbanganToTpsData(); + refreshTimbanganUploadState(item); + const form = elements.tpsContentContainer.querySelector('form'); + if (form) refreshSubmitButtonState(form); + scheduleAutoSave(); + }); weightInputDisplay.addEventListener("blur", function () { const parsed = parseWeightInput(this.value); @@ -1045,31 +1153,31 @@ const DetailPenjemputan = (function () { if (form) refreshSubmitButtonState(form); }); - jenisSampahSelect.addEventListener("change", function () { - updateTpsTotalTimbangan(); - syncTimbanganToTpsData(); - const form = elements.tpsContentContainer.querySelector("form"); - if (form) refreshSubmitButtonState(form); - }); + jenisSampahSelect.addEventListener('change', function() { + updateTpsTotalTimbangan(); + syncTimbanganToTpsData(); + const form = elements.tpsContentContainer.querySelector('form'); + if (form) refreshSubmitButtonState(form); + scheduleAutoSave(); + }); - removeBtn.addEventListener("click", function () { - item.remove(); - const form = elements.tpsContentContainer.querySelector("form"); - const repeater = form - ? form.querySelector(".tps-timbangan-repeater") - : null; - - if (repeater) { - renumberTimbanganItems(repeater); - if (repeater.children.length === 0) { - createTimbanganItem(repeater); - } - } - - updateTpsTotalTimbangan(); - syncTimbanganToTpsData(); - if (form) refreshSubmitButtonState(form); - }); + removeBtn.addEventListener('click', function() { + item.remove(); + const form = elements.tpsContentContainer.querySelector('form'); + const repeater = form ? form.querySelector('.tps-timbangan-repeater') : null; + + if (repeater) { + renumberTimbanganItems(repeater); + if (repeater.children.length === 0) { + createTimbanganItem(repeater); + } + } + + updateTpsTotalTimbangan(); + syncTimbanganToTpsData(); + if (form) refreshSubmitButtonState(form); + scheduleAutoSave(); + }); repeater.appendChild(item); refreshTimbanganUploadState(item); @@ -1164,22 +1272,18 @@ const DetailPenjemputan = (function () { refreshSubmitButtonState(form); } - function isTimbanganItemReady(timbanganItem) { - return ( - hasStoredPhoto(timbanganItem?.file) && - Boolean(timbanganItem?.uploaded) && - (timbanganItem?.weight || 0) > 0 - ); - } - - function getSubmitState(tps) { - if (!tps.fotoKedatangan.length || !tps.fotoKedatanganUploaded) { - return { - canSubmit: false, - message: "Silakan upload foto kedatangan terlebih dahulu.", - }; + function isTimbanganItemReady(timbanganItem) { + return Boolean(timbanganItem?.file || timbanganItem?.fotoFileName) && Boolean(timbanganItem?.uploaded) && (timbanganItem?.weight || 0) > 0; } + function getSubmitState(tps) { + if (!tps.fotoKedatanganUploaded) { + if (!tps.fotoKedatangan.length) { + return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan terlebih dahulu.' }; + } + return { canSubmit: false, message: 'Silakan upload foto kedatangan terlebih dahulu.' }; + } + if (!tps.timbangan.length) { return { canSubmit: false, @@ -1195,12 +1299,12 @@ const DetailPenjemputan = (function () { }; } - if (!tps.fotoPetugas.length || !tps.fotoPetugasUploaded) { - return { - canSubmit: false, - message: "Silakan upload foto petugas terlebih dahulu", - }; - } + if (!tps.fotoPetugasUploaded) { + if (!tps.fotoPetugas.length) { + return { canSubmit: false, message: 'Silakan pilih dan upload foto petugas terlebih dahulu.' }; + } + return { canSubmit: false, message: 'Silakan upload foto petugas terlebih dahulu' }; + } if (!tps.namaPetugas.trim()) { return { @@ -1245,19 +1349,16 @@ const DetailPenjemputan = (function () { const stateContainer = item.querySelector(".timbangan-upload-state"); if (!stateContainer) return; - const repeater = item.parentElement; - const itemIndex = repeater - ? Array.from(repeater.children).indexOf(item) - : -1; - const tps = state.tpsData[state.activeTpsIndex]; - const currentData = itemIndex >= 0 ? tps.timbangan[itemIndex] : null; - const fileInput = item.querySelector(".input-foto-timbangan"); - const hasFile = hasStoredPhoto(currentData?.file || fileInput?.files?.[0]); - const isUploaded = Boolean(currentData?.uploaded); - const weightInputValue = item.querySelector(".input-berat-timbangan-value"); - const currentWeight = - currentData?.weight ?? parseWeightInput(weightInputValue?.value || "0"); - const hasValidWeight = currentWeight > 0; + const repeater = item.parentElement; + const itemIndex = repeater ? Array.from(repeater.children).indexOf(item) : -1; + const tps = state.tpsData[state.activeTpsIndex]; + const currentData = itemIndex >= 0 ? tps.timbangan[itemIndex] : null; + const fileInput = item.querySelector('.input-foto-timbangan'); + const hasFile = Boolean(currentData?.file || fileInput?.files?.[0] || currentData?.fotoFileName); + const isUploaded = Boolean(currentData?.uploaded); + const weightInputValue = item.querySelector('.input-berat-timbangan-value'); + const currentWeight = currentData?.weight ?? parseWeightInput(weightInputValue?.value || '0'); + const hasValidWeight = currentWeight > 0; stateContainer.innerHTML = getTimbanganUploadStateMarkup( hasFile, @@ -1672,125 +1773,205 @@ const DetailPenjemputan = (function () { const form = elements.tpsContentContainer.querySelector("form"); if (!form) return; - const repeater = form.querySelector(".tps-timbangan-repeater"); - const items = repeater.querySelectorAll(".timbangan-item"); - const previousTimbangan = [...tps.timbangan]; - - tps.timbangan = []; - items.forEach((item, index) => { - const fileInput = item.querySelector(".input-foto-timbangan"); - const weightValue = item.querySelector(".input-berat-timbangan-value"); - const jenisSampahSelect = item.querySelector(".input-jenis-sampah"); - const ocrInfo = - item.querySelector(".input-ocr-info")?.textContent || - "OCR: belum diproses."; - const existingData = previousTimbangan[index]; - - tps.timbangan.push({ - file: fileInput.files[0] || (existingData ? existingData.file : null), - weight: parseWeightInput(weightValue.value), - jenisSampah: jenisSampahSelect.value, - uploaded: existingData ? existingData.uploaded : false, - ocrInfo, - }); - }); - - saveState(); - } - - function uploadSingleFotoTimbangan(itemIndex, targetItem = null) { - const tps = state.tpsData[state.activeTpsIndex]; - - if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) { - alert("Belum ada foto timbangan yang dipilih!"); - return; + const repeater = form.querySelector('.tps-timbangan-repeater'); + const items = repeater.querySelectorAll('.timbangan-item'); + const previousTimbangan = [...tps.timbangan]; + + tps.timbangan = []; + items.forEach((item, index) => { + const fileInput = item.querySelector('.input-foto-timbangan'); + const weightValue = item.querySelector('.input-berat-timbangan-value'); + const jenisSampahSelect = item.querySelector('.input-jenis-sampah'); + const ocrInfo = item.querySelector('.input-ocr-info')?.textContent || 'OCR: belum diproses.'; + const existingData = previousTimbangan[index]; + + tps.timbangan.push({ + file: fileInput.files[0] || (existingData ? existingData.file : null), + fotoFileName: existingData ? existingData.fotoFileName || '' : '', + weight: parseWeightInput(weightValue.value), + jenisSampah: jenisSampahSelect.value, + uploaded: existingData ? existingData.uploaded : false, + ocrInfo + }); + }); } - const timbanganItem = tps.timbangan[itemIndex]; - if (timbanganItem.weight <= 0) { - alert( - "Berat belum valid. Isi manual dulu sebelum upload foto timbangan.", - ); - return; - } + async function uploadSingleFotoTimbangan(itemIndex, targetItem = null) { + const tps = state.tpsData[state.activeTpsIndex]; + + if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) { + showToast('Belum ada foto timbangan yang dipilih!', 'error'); + return; + } + + const timbanganItem = tps.timbangan[itemIndex]; + if (timbanganItem.weight <= 0) { + showToast('Berat belum valid. Isi manual dulu sebelum upload foto timbangan.', 'error'); + return; + } - const _ext = ( - timbanganItem.file.name.split(".").pop() || "jpg" - ).toLowerCase(); - const _jenis = timbanganItem.jenisSampah.toLowerCase(); - const _beratStr = parseFloat(timbanganItem.weight.toFixed(2)) - .toString() - .replace(".", "_"); - const _newName = `timbangan${itemIndex + 1}-${_jenis}-${_beratStr}.${_ext}`; - timbanganItem.file = new File([timbanganItem.file], _newName, { - type: timbanganItem.file.type, - lastModified: timbanganItem.file.lastModified, - }); + if (!targetItem) { + const form = elements.tpsContentContainer.querySelector('form'); + const repeater = form ? form.querySelector('.tps-timbangan-repeater') : null; + const items = repeater ? repeater.querySelectorAll('.timbangan-item') : []; + targetItem = items[itemIndex] || null; + } - alert( - `Upload foto timbangan #${itemIndex + 1} untuk ${tps.name}\nJenis: ${timbanganItem.jenisSampah}\nBerat: ${timbanganItem.weight} kg\n(Implementasi upload ke server)`, - ); + const uploadBtn = targetItem ? targetItem.querySelector('.btn-upload-timbangan') : null; + if (uploadBtn) { + uploadBtn.disabled = true; + uploadBtn.textContent = 'Mengupload...'; + } - timbanganItem.uploaded = true; + const formData = new FormData(); + formData.append('FotoTimbangan', timbanganItem.file); + formData.append('DraftKey', tps.draftKey || ''); + formData.append('SpjDetailId', tps.spjDetailId || ''); + formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); + formData.append('ItemIndex', itemIndex); + formData.append('JenisSampah', timbanganItem.jenisSampah || CONFIG.DEFAULT_JENIS); + formData.append('Berat', Number(timbanganItem.weight || 0).toFixed(2)); - if (!targetItem) { - const form = elements.tpsContentContainer.querySelector("form"); - const repeater = form - ? form.querySelector(".tps-timbangan-repeater") - : null; - const items = repeater - ? repeater.querySelectorAll(".timbangan-item") - : []; - targetItem = items[itemIndex] || null; - } + try { + const res = await fetch(ENDPOINTS.uploadTimbangan, { method: 'POST', body: formData }); + const data = await res.json(); + if (res.ok && data.success) { + timbanganItem.uploaded = true; + timbanganItem.fotoFileName = data.fileUrl || data.fileName || ''; + const fotoInputTimb = targetItem?.querySelector('.input-foto-timbangan'); + if (fotoInputTimb) fotoInputTimb.value = ''; + showToast(data.message || `Foto timbangan #${itemIndex + 1} berhasil diupload.`, 'success'); + } else { + showToast(data.message || 'Gagal upload foto timbangan.', 'error'); + if (uploadBtn) { + uploadBtn.disabled = false; + uploadBtn.textContent = 'Upload Foto Timbangan Ini'; + } + } + } catch (_) { + showToast('Koneksi gagal saat upload foto timbangan.', 'error'); + if (uploadBtn) { + uploadBtn.disabled = false; + uploadBtn.textContent = 'Upload Foto Timbangan Ini'; + } + } if (targetItem) { refreshTimbanganUploadState(targetItem); } - syncTimbanganToTpsData(); - const form = elements.tpsContentContainer.querySelector("form"); - if (form) refreshSubmitButtonState(form); - saveState(); - } - - function uploadFotoKedatangan() { - const tps = state.tpsData[state.activeTpsIndex]; - if (tps.fotoKedatangan.length === 0) { - alert("Belum ada foto kedatangan yang dipilih!"); - return; + syncTimbanganToTpsData(); + const form = elements.tpsContentContainer.querySelector('form'); + if (form) refreshSubmitButtonState(form); + scheduleAutoSave(); } - alert( - `Upload ${tps.fotoKedatangan.length} foto kedatangan untuk ${tps.name}\n(Implementasi upload ke server)`, - ); + async function uploadFotoKedatangan() { + const tps = state.tpsData[state.activeTpsIndex]; + if (tps.fotoKedatangan.length === 0) { + showToast('Belum ada foto kedatangan yang dipilih!', 'error'); + return; + } - tps.fotoKedatanganUploaded = true; - const form = elements.tpsContentContainer.querySelector("form"); - if (form) refreshKedatanganUploadState(form); - saveState(); - } + const form = elements.tpsContentContainer.querySelector('form'); + const btn = form ? form.querySelector('.tps-btn-upload-kedatangan') : null; + if (btn) { + btn.disabled = true; + btn.textContent = 'Mengupload...'; + } - function uploadFotoPetugas() { - const tps = state.tpsData[state.activeTpsIndex]; - if (tps.fotoPetugas.length === 0) { - alert("Belum ada foto petugas yang dipilih!"); - return; - } - if (!tps.namaPetugas.trim()) { - alert("Nama petugas wajib diisi sebelum upload foto petugas!"); - return; + const formData = new FormData(); + tps.fotoKedatangan.forEach(file => formData.append('FotoKedatangan', file)); + formData.append('DraftKey', tps.draftKey || ''); + formData.append('SpjDetailId', tps.spjDetailId || ''); + formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); + formData.append('WaktuKedatangan', tps.waktuKedatangan || ''); + formData.append('Latitude', tps.latitude || ''); + formData.append('Longitude', tps.longitude || ''); + formData.append('AlamatJalan', tps.alamatJalan || ''); + + try { + const res = await fetch(ENDPOINTS.uploadKedatangan, { method: 'POST', body: formData }); + const data = await res.json(); + if (res.ok && data.success) { + tps.fotoKedatanganUploaded = true; + tps.fotoKedatanganFileNames = data.fileUrls || data.fileNames || []; + showToast(data.message || 'Foto kedatangan berhasil diupload.', 'success'); + if (form) { + const fotoInput = form.querySelector('.tps-foto-kedatangan'); + if (fotoInput) fotoInput.value = ''; + refreshKedatanganUploadState(form); + } + scheduleAutoSave(); + } else { + showToast(data.message || 'Gagal upload foto kedatangan.', 'error'); + if (btn) { + btn.disabled = false; + btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`; + } + } + } catch (_) { + showToast('Koneksi gagal saat upload foto kedatangan.', 'error'); + if (btn) { + btn.disabled = false; + btn.textContent = `Upload ${tps.fotoKedatangan.length} Foto Kedatangan`; + } + } } - alert( - `Upload ${tps.fotoPetugas.length} foto petugas untuk ${tps.name}\n(Implementasi upload ke server)`, - ); + async function uploadFotoPetugas() { + const tps = state.tpsData[state.activeTpsIndex]; + if (tps.fotoPetugas.length === 0) { + showToast('Belum ada foto petugas yang dipilih!', 'error'); + return; + } + if (!tps.namaPetugas.trim()) { + showToast('Nama petugas wajib diisi sebelum upload foto petugas!', 'error'); + return; + } - tps.fotoPetugasUploaded = true; - const form = elements.tpsContentContainer.querySelector("form"); - if (form) refreshPetugasUploadState(form); - saveState(); - } + const form = elements.tpsContentContainer.querySelector('form'); + const btn = form ? form.querySelector('.tps-btn-upload-petugas:not([disabled])') : null; + if (btn) { + btn.disabled = true; + btn.textContent = 'Mengupload...'; + } + + const formData = new FormData(); + tps.fotoPetugas.forEach(file => formData.append('FotoPetugas', file)); + formData.append('DraftKey', tps.draftKey || ''); + formData.append('SpjDetailId', tps.spjDetailId || ''); + formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); + formData.append('NamaPetugas', tps.namaPetugas || ''); + + try { + const res = await fetch(ENDPOINTS.uploadPetugas, { method: 'POST', body: formData }); + const data = await res.json(); + if (res.ok && data.success) { + tps.fotoPetugasUploaded = true; + tps.fotoPetugasFileNames = data.fileUrls || data.fileNames || []; + showToast(data.message || 'Foto petugas berhasil diupload.', 'success'); + if (form) { + const fotoInput = form.querySelector('.tps-foto-petugas'); + if (fotoInput) fotoInput.value = ''; + refreshPetugasUploadState(form); + } + scheduleAutoSave(); + } else { + showToast(data.message || 'Gagal upload foto petugas.', 'error'); + if (btn) { + btn.disabled = false; + btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`; + } + } + } catch (_) { + showToast('Koneksi gagal saat upload foto petugas.', 'error'); + if (btn) { + btn.disabled = false; + btn.textContent = `Upload ${tps.fotoPetugas.length} Foto Petugas`; + } + } + } function buildSubmitFormData(tps) { const formData = new FormData(); @@ -1852,51 +2033,63 @@ const DetailPenjemputan = (function () { } } - function submitTpsData() { - const tps = state.tpsData[state.activeTpsIndex]; - const submitState = getSubmitState(tps); - if (!submitState.canSubmit) { - alert(submitState.message); - return; - } + async function submitTpsData() { + const tps = state.tpsData[state.activeTpsIndex]; + const submitState = getSubmitState(tps); + if (!submitState.canSubmit) { + showToast(submitState.message, 'error'); + return; + } - // MODE STATIK (aktif sekarang) - // Cuma validasi + tandai TPS selesai, ga kirim ke backend. - markTpsSubmitted(tps); - clearStateIfAllSubmitted(); - alert( - `Validasi ${tps.name} OK. Data belum dikirim ke server (mode statik).`, - ); + const form = elements.tpsContentContainer.querySelector('form'); + const submitBtn = form ? form.querySelector('button[type="submit"]') : null; + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.textContent = `Menyimpan${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}...`; + } - // MODE PRODUCTION (aktifkan kalau backend udah ready mas ebik) - /* const formData = buildSubmitFormData(tps); - fetch('/upst/detail-penjemputan', { - method: 'POST', - body: formData - }) - .then(async response => { - if (response.ok) { + try { + const response = await fetch(ENDPOINTS.submit, { + method: 'POST', + body: formData + }); + + if (response.ok || response.redirected) { markTpsSubmitted(tps); - clearStateIfAllSubmitted(); - alert(`Data ${tps.name} berhasil disimpan!`); - window.location.reload(); + if (tps.draftKey) { + await fetch(`${ENDPOINTS.deleteDraft}?draftKey=${encodeURIComponent(tps.draftKey)}`, { method: 'DELETE' }); + } + + showToast(`Data ${tps.name} berhasil disimpan!`, 'success'); + + const allSubmitted = state.tpsData.every(item => item.submitted); + if (allSubmitted) { + setTimeout(() => { + window.location.href = '/upst/detail-penjemputan/detail-selesai'; + }, 1200); + } else { + renderTabs(); + renderTpsForm(); + } } else { const errorText = await response.text(); - if (response.status === 400) { - alert('Sesi submit tidak valid. Silakan refresh halaman lalu coba lagi.'); - } else { - alert(errorText || 'Gagal menyimpan data. Silakan coba lagi.'); + showToast(errorText || 'Gagal menyimpan data. Silakan coba lagi.', 'error'); + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.textContent = `Submit${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}`; } } - }) - .catch(error => { + } catch (error) { console.error('Error:', error); - alert('Terjadi kesalahan saat menyimpan data.'); - }); - */ - } + showToast('Terjadi kesalahan saat menyimpan data.', 'error'); + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.textContent = `Submit${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}`; + } + } + } function formatWeightDisplay(value) { if (isNaN(value)) return "0,00"; @@ -1914,10 +2107,35 @@ const DetailPenjemputan = (function () { return isNaN(parsed) ? 0 : parsed; } - function formatFileSize(bytes) { - const mb = bytes / (1024 * 1024); - return `${mb.toFixed(2)} MB`; - } + function formatFileSize(bytes) { + const mb = bytes / (1024 * 1024); + return `${mb.toFixed(2)} MB`; + } + + function showToast(message, type = 'info') { + let container = document.getElementById('espj-toast-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'espj-toast-container'; + container.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:9999;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;'; + document.body.appendChild(container); + } + + const toast = document.createElement('div'); + const bg = type === 'success' ? '#16a34a' : type === 'error' ? '#dc2626' : '#2563eb'; + toast.style.cssText = `background:${bg};color:#fff;padding:10px 18px;border-radius:16px;font-size:13px;font-weight:600;box-shadow:0 4px 24px rgba(0,0,0,.18);opacity:0;transition:opacity .25s;max-width:320px;text-align:center;`; + toast.textContent = message; + container.appendChild(toast); + + requestAnimationFrame(() => { + toast.style.opacity = '1'; + }); + + setTimeout(() => { + toast.style.opacity = '0'; + setTimeout(() => toast.remove(), 300); + }, 3200); + } return { init: init, @@ -1929,29 +2147,26 @@ const DetailPenjemputan = (function () { }; })(); -document.addEventListener("DOMContentLoaded", async function () { - try { - const response = await fetch("/driver/json/tps-list.json"); - const data = await response.json(); - const tpsList = data.tpsList.map((tps) => ({ - name: tps.name, - lokasiAngkutId: tps.lokasiAngkutId, - spjDetailId: tps.spjDetailId, - id: tps.id, - })); - DetailPenjemputan.init(tpsList); - if (data.draftPenjemputan || data.tpsData) { - DetailPenjemputan.hydrateFromApi(data.draftPenjemputan || data.tpsData); +document.addEventListener('DOMContentLoaded', async function() { + try { + const response = await fetch('/driver/json/tps-list.json'); + const data = await response.json(); + const tpsList = data.tpsList.map(tps => ({ + name: tps.name, + lokasiAngkutId: tps.lokasiAngkutId, + spjDetailId: tps.spjDetailId, + id: tps.id + })); + await DetailPenjemputan.init(tpsList); + } catch (error) { + console.error('Error loading TPS list:', error); } - } 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) { + const nomorSpjEl = document.querySelector('.text-gray-600.font-mono'); + if (nomorSpjEl) { + DetailPenjemputan.setNomorSpj(nomorSpjEl.textContent.trim()); + } } - } }); diff --git a/wwwroot/driver/js/history-upst.js b/wwwroot/driver/js/history-upst.js new file mode 100644 index 0000000..0fb95b3 --- /dev/null +++ b/wwwroot/driver/js/history-upst.js @@ -0,0 +1,291 @@ +document.addEventListener('DOMContentLoaded', function () { + const pageEl = document.getElementById('history-page'); + if (!pageEl) return; + + const apiUrl = pageEl.dataset.historyApi; + const detailBase = pageEl.dataset.historyDetailBase || '/upst/history/details/__id__'; + + const loadingEl = document.getElementById('history-loading'); + const emptyEl = document.getElementById('history-empty'); + const listEl = document.getElementById('history-list'); + const paginationEl = document.getElementById('history-pagination'); + const pageInfoEl = document.getElementById('history-page-info'); + const totalInfoEl = document.getElementById('history-total-info'); + const pageButtonsEl = document.getElementById('history-page-buttons'); + const prevBtn = document.getElementById('history-prev-page'); + const nextBtn = document.getElementById('history-next-page'); + const fromDateInput = document.getElementById('history-from-date-filter'); + const toDateInput = document.getElementById('history-to-date-filter'); + const applyFilterBtn = document.getElementById('history-apply-filter'); + const resetFilterBtn = document.getElementById('history-reset-filter'); + + const state = { + page: 1, + pageSize: 5, + totalPages: 1, + totalItems: 0, + fromDate: '', + toDate: '' + }; + + function formatTanggal(dateString) { + const date = new Date(dateString); + return new Intl.DateTimeFormat('id-ID', { + day: '2-digit', + month: 'short', + year: 'numeric' + }).format(date); + } + + function formatWaktu(dateString) { + const date = new Date(dateString); + return new Intl.DateTimeFormat('id-ID', { + hour: '2-digit', + minute: '2-digit' + }).format(date); + } + + function getStatusMarkup(status) { + if ((status || '').toLowerCase() === 'completed') { + return 'Selesai'; + } + + return 'Proses'; + } + + function buildDetailUrl(id) { + return detailBase.replace('__id__', String(id)); + } + + function renderItems(items) { + listEl.innerHTML = ''; + + items.forEach(function (item) { + const card = document.createElement('a'); + card.href = buildDetailUrl(item.id); + card.className = 'block group'; + card.innerHTML = ` +
+
+
+

Nomor Dokumen

+

${item.noSpj}

+
+ ${getStatusMarkup(item.status)} +
+ +
+
+ +
+
+
+

${item.plat}

+ ${formatWaktu(item.tanggalWaktu)} +
+
+ ${item.kode} + ${formatTanggal(item.tanggalWaktu)} +
+
+
+ +
+
+
+
+ Tujuan Akhir + ${item.tujuan} +
+
+
+ +
+
+
+ `; + listEl.appendChild(card); + }); + + if (window.lucide) { + window.lucide.createIcons(); + } + } + + function renderPagination() { + const hasItems = state.totalItems > 0; + paginationEl.classList.toggle('hidden', !hasItems); + if (!hasItems) return; + + pageInfoEl.textContent = `Halaman ${state.page} dari ${state.totalPages}`; + totalInfoEl.textContent = `${state.totalItems} data`; + prevBtn.disabled = state.page <= 1; + nextBtn.disabled = state.page >= state.totalPages; + + pageButtonsEl.innerHTML = ''; + const pages = []; + const startPage = Math.max(1, state.page - 2); + const endPage = Math.min(state.totalPages, state.page + 2); + + if (startPage > 1) { + pages.push(1); + if (startPage > 2) pages.push('ellipsis-left'); + } + + for (let page = startPage; page <= endPage; page++) { + pages.push(page); + } + + if (endPage < state.totalPages) { + if (endPage < state.totalPages - 1) pages.push('ellipsis-right'); + pages.push(state.totalPages); + } + + pages.forEach(function (page) { + if (typeof page !== 'number') { + const ellipsis = document.createElement('span'); + ellipsis.className = 'w-10 h-10 flex items-center justify-center text-xs font-black text-gray-400'; + ellipsis.textContent = '...'; + pageButtonsEl.appendChild(ellipsis); + return; + } + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.textContent = String(page); + btn.className = page === state.page + ? 'w-10 h-10 rounded-2xl bg-upst text-white text-xs font-black' + : 'w-10 h-10 rounded-2xl border border-gray-200 text-gray-700 text-xs font-black bg-white'; + btn.addEventListener('click', function () { + if (page === state.page) return; + loadHistory(page); + }); + pageButtonsEl.appendChild(btn); + }); + } + + function setLoading(isLoading) { + loadingEl.classList.toggle('hidden', !isLoading); + if (isLoading) { + emptyEl.classList.add('hidden'); + listEl.innerHTML = ''; + paginationEl.classList.add('hidden'); + } + } + + function showEmpty() { + listEl.innerHTML = ''; + emptyEl.classList.remove('hidden'); + paginationEl.classList.add('hidden'); + if (window.lucide) { + window.lucide.createIcons(); + } + } + + async function loadHistory(page) { + state.page = page || 1; + setLoading(true); + + try { + const params = new URLSearchParams({ + page: String(state.page), + pageSize: String(state.pageSize) + }); + + if (state.fromDate) { + params.set('fromDate', state.fromDate); + } + + if (state.toDate) { + params.set('toDate', state.toDate); + } + + const response = await fetch(`${apiUrl}?${params.toString()}`, { cache: 'no-store' }); + if (!response.ok) { + throw new Error('Gagal memuat history'); + } + + const data = await response.json(); + state.page = data.page || 1; + state.totalPages = data.totalPages || 1; + state.totalItems = data.totalItems || 0; + + loadingEl.classList.add('hidden'); + if (!data.items || data.items.length === 0) { + showEmpty(); + return; + } + + emptyEl.classList.add('hidden'); + renderItems(data.items); + renderPagination(); + } catch (error) { + console.error(error); + loadingEl.classList.add('hidden'); + emptyEl.classList.remove('hidden'); + emptyEl.innerHTML = ` +
+ +
+

Gagal Memuat Riwayat

+

Silakan coba lagi beberapa saat.

+ `; + if (window.lucide) { + window.lucide.createIcons(); + } + } + } + + function applyDateRangeFilter() { + const nextFromDate = fromDateInput?.value || ''; + const nextToDate = toDateInput?.value || ''; + + if (nextFromDate && nextToDate && nextFromDate > nextToDate) { + const temp = nextFromDate; + state.fromDate = nextToDate; + state.toDate = temp; + if (fromDateInput) fromDateInput.value = state.fromDate; + if (toDateInput) toDateInput.value = state.toDate; + } else { + state.fromDate = nextFromDate; + state.toDate = nextToDate; + } + + loadHistory(1); + } + + applyFilterBtn?.addEventListener('click', function () { + applyDateRangeFilter(); + }); + + resetFilterBtn?.addEventListener('click', function () { + state.fromDate = ''; + state.toDate = ''; + if (fromDateInput) fromDateInput.value = ''; + if (toDateInput) toDateInput.value = ''; + loadHistory(1); + }); + + [fromDateInput, toDateInput].forEach(function (input) { + input?.addEventListener('keydown', function (event) { + if (event.key === 'Enter') { + event.preventDefault(); + applyDateRangeFilter(); + } + }); + }); + + prevBtn?.addEventListener('click', function () { + if (state.page > 1) { + loadHistory(state.page - 1); + } + }); + + nextBtn?.addEventListener('click', function () { + if (state.page < state.totalPages) { + loadHistory(state.page + 1); + } + }); + + loadHistory(1); +}); diff --git a/wwwroot/driver/json/pengangkutan.json b/wwwroot/driver/json/pengangkutan.json index fbd9102..2d40542 100644 --- a/wwwroot/driver/json/pengangkutan.json +++ b/wwwroot/driver/json/pengangkutan.json @@ -3,20 +3,20 @@ { "name": "CV Tri Mitra Utama - Shell Radio Dalam", "alamat": "Kp. Pertanian II Rt.004 Rw.001 Kel. Klender Kec, Duren Sawit, Kota Adm. Jakarta Timur 13470", - "longitude": -6.260066361357777, - "latitude": 106.78918653869111 + "longitude": -6.26012668512782, + "latitude": 106.8712511969409 }, { "name": "Jakarta Islamic Hospital", "alamat": "Gang Masjid Baitusalam, Jl. E No.19, RT.10/RW.11, Cipinang, Kec. Pulo Gadung, Kota Jakarta Timur, Daerah Khusus Ibukota Jakarta 13240", - "longitude": -6.168850536704003, - "latitude": 106.87091508600079 + "longitude": -6.2550852145095694, + "latitude": 106.86310593093889 }, { "name": "Puskesmas Kelurahan Klender 1", "alamat": "Jl. Pertanian Tengah No.7, RT.4/RW.2, Klender, Kec. Duren Sawit, Kota Jakarta Timur, Daerah Khusus Ibukota Jakarta 13430", - "longitude": -6.2146980384303845, - "latitude": 106.89777460376108 + "longitude": -6.268768227222534, + "latitude": 106.87026643078956 } ] } diff --git a/wwwroot/driver/manifest.json b/wwwroot/driver/manifest.json index 3853628..778c965 100644 --- a/wwwroot/driver/manifest.json +++ b/wwwroot/driver/manifest.json @@ -1,93 +1,44 @@ { + "id": "/upst", "name": "eSPJ - Surat Perjalanan Dinas", "short_name": "eSPJ", "description": "Aplikasi pengelolaan Surat Perjalanan Dinas yang modern dan efisien", - "start_url": "/", + "start_url": "/upst", "display": "standalone", "background_color": "#ffffff", "theme_color": "#fb923c", "orientation": "portrait-primary", - "scope": "/", + "scope": "/upst", "lang": "id", "dir": "ltr", "icons": [ { - "src": "icon-72.png", - "sizes": "72x72", - "type": "image/png", - "purpose": "any" - }, - { - "src": "icon-96.png", - "sizes": "96x96", - "type": "image/png", - "purpose": "any" - }, - { - "src": "icon-128.png", - "sizes": "128x128", - "type": "image/png", - "purpose": "any" - }, - { - "src": "icon-144.png", - "sizes": "144x144", - "type": "image/png", - "purpose": "any" - }, - { - "src": "icon-152.png", - "sizes": "152x152", - "type": "image/png", - "purpose": "any" - }, - { - "src": "icon-192.png", + "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { - "src": "icon-384.png", - "sizes": "384x384", - "type": "image/png", - "purpose": "any" - }, - { - "src": "icon-512.png", + "src": "/driver/images/pwa_512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } ], - "screenshots": [ - { - "src": "screenshot-wide.png", - "sizes": "1280x720", - "type": "image/png", - "form_factor": "wide" - }, - { - "src": "screenshot-narrow.png", - "sizes": "360x640", - "type": "image/png", - "form_factor": "narrow" - } - ], "shortcuts": [ { - "name": "Login", - "short_name": "Login", - "description": "Masuk ke aplikasi eSPJ", - "url": "/login", - "icons": [{ "src": "icon-96.png", "sizes": "96x96" }] + "name": "Home UPST", + "short_name": "Home", + "description": "Buka dashboard utama UPST", + "url": "/upst", + "icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }] }, { - "name": "Dashboard", - "short_name": "Dashboard", - "description": "Buka dashboard utama", - "url": "/dashboard", - "icons": [{ "src": "icon-96.png", "sizes": "96x96" }] + "name": "Halaman Kosong", + "short_name": "Kosong", + "description": "Buka halaman alternatif UPST", + "url": "/upst/kosong", + "icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }] } ] } diff --git a/wwwroot/driver/offline.html b/wwwroot/driver/offline.html new file mode 100644 index 0000000..6647768 --- /dev/null +++ b/wwwroot/driver/offline.html @@ -0,0 +1,67 @@ + + + + + + + Offline - DLH DKI Jakarta + + + + + + +
+
+
+ +
+ + + +
+
+ +
+

Koneksi Terputus

+

+ Sepertinya perangkatmu sedang tidak terhubung ke internet. Silakan cek koneksi Wi-Fi atau data seluler + kamu. +

+
+ +
+ +
+ +
+
+

DLH DKI Jakarta

+
+
+ + + + \ No newline at end of file diff --git a/wwwroot/driver/serviceworker.js b/wwwroot/driver/serviceworker.js new file mode 100644 index 0000000..fba86a9 --- /dev/null +++ b/wwwroot/driver/serviceworker.js @@ -0,0 +1,77 @@ +const CACHE_NAME = "espj-upst-pwa-v1"; +const OFFLINE_URL = "/driver/offline.html"; +const PRECACHE_URLS = [ + "/upst", + "/upst/kosong", + "/driver/manifest.json", + "/driver/favicon.ico", + "/driver/images/pwa_192.png", + "/driver/images/pwa_512.png", + OFFLINE_URL +]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS)) + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all( + keys + .filter((key) => key !== CACHE_NAME) + .map((key) => caches.delete(key)) + ) + ) + ); + + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + const { request } = event; + const url = new URL(request.url); + + if (request.method !== "GET") { + return; + } + + if (request.mode === "navigate") { + event.respondWith( + fetch(request) + .then((response) => { + const responseClone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, responseClone)); + return response; + }) + .catch(async () => { + const cachedPage = await caches.match(request); + return cachedPage || caches.match(OFFLINE_URL); + }) + ); + return; + } + + if (url.origin !== self.location.origin) { + return; + } + + event.respondWith( + caches.match(request).then((cachedResponse) => { + const networkFetch = fetch(request) + .then((response) => { + if (response && response.ok) { + const responseClone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, responseClone)); + } + return response; + }) + .catch(() => cachedResponse); + + return cachedResponse || networkFetch; + }) + ); +});