update: fixing detail penjemputan dan offline pages

main
muamars 2026-03-18 14:47:06 +07:00
parent 0390d630b3
commit 61094188f6
14 changed files with 1626 additions and 1142 deletions

View File

@ -31,27 +31,168 @@ namespace eSPJ.Controllers.SpjDriverUpstController
_env = env; _env = env;
} }
private static string ResolveDraftKey(string? draftKey, string? sessionKey, string? spjDetailId = null, string? lokasiAngkutId = null) private static string SanitizePathSegment(string? value, string fallback = "umum")
{ {
var rawKey = !string.IsNullOrWhiteSpace(draftKey) var safe = string.Concat((value ?? string.Empty).Trim().Select(c =>
? draftKey char.IsLetterOrDigit(c) || c == '-' || c == '_'
: !string.IsNullOrWhiteSpace(sessionKey) ? c
? sessionKey : '-'));
: $"non-tps-{spjDetailId}-{lokasiAngkutId}";
return string.Concat((rawKey ?? string.Empty).Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_')); safe = Regex.Replace(safe, "-+", "-").Trim('-');
return string.IsNullOrWhiteSpace(safe) ? fallback : safe;
} }
private string GetUploadDirectory(string dateFolder) private string GetUploadDirectory(string dateFolder, string? nomorSpj = null, string? namaTps = null)
{ {
var uploadDir = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", dateFolder); var uploadDir = Path.Combine(
_env.ContentRootPath,
"uploads",
"penjemputan",
dateFolder,
SanitizePathSegment(nomorSpj, "spj-umum"),
SanitizePathSegment(namaTps, "tps-1"));
Directory.CreateDirectory(uploadDir); Directory.CreateDirectory(uploadDir);
return uploadDir; return uploadDir;
} }
private static string BuildUploadUrl(string dateFolder, string fileName) private static string BuildUploadUrl(string dateFolder, string fileName, string? nomorSpj = null, string? namaTps = null)
{ {
return $"/uploads/penjemputan/{dateFolder}/{fileName}"; var spjFolder = SanitizePathSegment(nomorSpj, "spj-umum");
var tpsFolder = SanitizePathSegment(namaTps, "tps-1");
return $"/uploads/penjemputan/{dateFolder}/{spjFolder}/{tpsFolder}/{fileName}";
}
private async Task<TpsData?> FindExistingRecordAsync(string? nomorSpj, string? spjDetailId, string? lokasiAngkutId, string? namaTps)
{
if (string.IsNullOrWhiteSpace(nomorSpj))
{
return null;
}
return await _detailService.GetRecordDetailAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps);
}
private static List<RecordTimbanganItem> MapRecordTimbangan(List<TimbanganItem>? items)
{
return (items ?? new List<TimbanganItem>())
.Select(item => new RecordTimbanganItem
{
Berat = item.Berat?.FirstOrDefault() ?? 0,
JenisSampah = item.JenisSampah != null && item.JenisSampah.Count > 0
? item.JenisSampah[0].ToString()
: "Residu",
FotoFileName = item.FotoFileName ?? string.Empty,
Uploaded = item.IsUploaded,
OcrInfo = item.IsUploaded ? "Foto dari server." : "OCR: belum diproses."
})
.ToList();
}
private static RecordSaveRequest BuildRecordSaveRequest(TpsData? existingRecord, string? nomorSpj, string? namaTps, string? spjDetailId, string? lokasiAngkutId)
{
return new RecordSaveRequest
{
NomorSpj = nomorSpj ?? existingRecord?.NomorSpj ?? string.Empty,
NamaTps = namaTps ?? existingRecord?.Name ?? string.Empty,
SpjDetailId = spjDetailId ?? existingRecord?.SpjDetailId ?? string.Empty,
LokasiAngkutId = lokasiAngkutId ?? existingRecord?.LokasiAngkutId ?? string.Empty,
Latitude = existingRecord?.Latitude ?? string.Empty,
Longitude = existingRecord?.Longitude ?? string.Empty,
AlamatJalan = existingRecord?.AlamatJalan ?? string.Empty,
WaktuKedatangan = existingRecord?.WaktuKedatangan ?? string.Empty,
FotoKedatanganFileNames = existingRecord?.FotoKedatangan != null ? new List<string>(existingRecord.FotoKedatangan) : new List<string>(),
FotoKedatanganUploaded = existingRecord?.FotoKedatanganUploaded ?? false,
Timbangan = MapRecordTimbangan(existingRecord?.Timbangan),
TotalOrganik = existingRecord?.TotalOrganik ?? 0,
TotalAnorganik = existingRecord?.TotalAnorganik ?? 0,
TotalResidu = existingRecord?.TotalResidu ?? 0,
TotalTimbangan = existingRecord?.TotalTimbangan ?? 0,
FotoPetugasFileNames = existingRecord?.FotoPetugas != null ? new List<string>(existingRecord.FotoPetugas) : new List<string>(),
FotoPetugasUploaded = existingRecord?.FotoPetugasUploaded ?? false,
NamaPetugas = existingRecord?.NamaPetugas ?? string.Empty,
IsSubmit = existingRecord?.IsSubmit ?? false,
};
}
private static void RecalculateTotals(RecordSaveRequest request)
{
var timbangan = request.Timbangan ?? new List<RecordTimbanganItem>();
request.TotalOrganik = timbangan
.Where(item => string.Equals(item.JenisSampah, "Organik", StringComparison.OrdinalIgnoreCase))
.Sum(item => item.Berat);
request.TotalAnorganik = timbangan
.Where(item => string.Equals(item.JenisSampah, "Anorganik", StringComparison.OrdinalIgnoreCase))
.Sum(item => item.Berat);
request.TotalResidu = timbangan
.Where(item => string.Equals(item.JenisSampah, "Residu", StringComparison.OrdinalIgnoreCase))
.Sum(item => item.Berat);
request.TotalTimbangan = timbangan.Sum(item => item.Berat);
}
private async Task<RecordSaveResponse> SaveUploadedRecordAsync(
bool isTps,
string? nomorSpj,
string? namaTps,
string? spjDetailId,
string? lokasiAngkutId,
Action<RecordSaveRequest> applyChanges)
{
var existingRecord = await FindExistingRecordAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps);
var request = BuildRecordSaveRequest(existingRecord, nomorSpj, namaTps, spjDetailId, lokasiAngkutId);
applyChanges(request);
RecalculateTotals(request);
return isTps
? await _detailService.SaveRecordTpsAsync(request)
: await _detailService.SaveRecordNonTpsAsync(request);
}
private void ApplyNoCacheHeaders()
{
Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0";
Response.Headers["Pragma"] = "no-cache";
Response.Headers["Expires"] = "0";
}
private async Task<RecordSaveRequest?> ResolveRecordSaveRequestAsync()
{
Request.EnableBuffering();
if (Request.Body.CanSeek)
{
Request.Body.Position = 0;
}
using var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
var rawBody = await reader.ReadToEndAsync();
if (Request.Body.CanSeek)
{
Request.Body.Position = 0;
}
if (string.IsNullOrWhiteSpace(rawBody))
{
return null;
}
try
{
return JsonSerializer.Deserialize<RecordSaveRequest>(rawBody, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch (JsonException ex)
{
_logger.LogWarning(ex, "Gagal parse body save-record. Body: {RawBody}", rawBody);
return null;
}
} }
[HttpGet("")] [HttpGet("")]
@ -89,14 +230,96 @@ namespace eSPJ.Controllers.SpjDriverUpstController
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesaiTanpaTps.cshtml"); return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesaiTanpaTps.cshtml");
} }
[HttpGet("api/submitted")]
[IgnoreAntiforgeryToken]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> GetSubmittedBySpj([FromQuery] string nomorSpj)
{
ApplyNoCacheHeaders();
if (string.IsNullOrWhiteSpace(nomorSpj))
{
return BadRequest(new { success = false, message = "nomorSpj wajib diisi." });
}
var items = await _detailService.GetSubmittedByNomorSpjAsync(nomorSpj);
return Ok(new { success = true, items });
}
[HttpGet("api/records")]
[IgnoreAntiforgeryToken]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> GetRecordsBySpj([FromQuery] string nomorSpj)
{
ApplyNoCacheHeaders();
if (string.IsNullOrWhiteSpace(nomorSpj))
{
return BadRequest(new { success = false, message = "nomorSpj wajib diisi." });
}
var items = await _detailService.GetRecordsByNomorSpjAsync(nomorSpj);
return Ok(new { success = true, items });
}
[HttpGet("api/submitted/detail")]
[IgnoreAntiforgeryToken]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> GetSubmittedDetail(
[FromQuery] string nomorSpj,
[FromQuery] string? spjDetailId = null,
[FromQuery] string? lokasiAngkutId = null,
[FromQuery] string? namaTps = null)
{
ApplyNoCacheHeaders();
if (string.IsNullOrWhiteSpace(nomorSpj))
{
return BadRequest(new { success = false, message = "nomorSpj wajib diisi." });
}
var item = await _detailService.GetSubmittedDetailAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps);
return Ok(new { success = true, hasData = item != null, item });
}
[HttpGet("api/records/detail")]
[IgnoreAntiforgeryToken]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> GetRecordDetail(
[FromQuery] string nomorSpj,
[FromQuery] string? spjDetailId = null,
[FromQuery] string? lokasiAngkutId = null,
[FromQuery] string? namaTps = null)
{
ApplyNoCacheHeaders();
if (string.IsNullOrWhiteSpace(nomorSpj))
{
return BadRequest(new { success = false, message = "nomorSpj wajib diisi." });
}
var item = await _detailService.GetRecordDetailAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps);
return Ok(new { success = true, hasData = item != null, item });
}
[HttpPost("")] [HttpPost("")]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Submit([FromForm] DetailPenjemputanRequest request) public async Task<IActionResult> Submit([FromForm] DetailPenjemputanRequest request)
{ {
var isAjaxRequest = string.Equals(Request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.OrdinalIgnoreCase)
|| Request.Headers.Accept.Any(value => value?.Contains("application/json", StringComparison.OrdinalIgnoreCase) == true);
try try
{ {
var result = await _detailService.SubmitPenjemputanAsync(request); var result = await _detailService.SubmitPenjemputanAsync(request);
if (isAjaxRequest)
{
return result.Success
? Ok(result)
: BadRequest(result);
}
if (result.Success) if (result.Success)
{ {
TempData["Success"] = result.Message; TempData["Success"] = result.Message;
@ -111,94 +334,54 @@ namespace eSPJ.Controllers.SpjDriverUpstController
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error submitting penjemputan data"); _logger.LogError(ex, "Error submitting penjemputan data");
if (isAjaxRequest)
{
return StatusCode(500, new DetailPenjemputanResponse
{
Success = false,
Message = "Terjadi kesalahan saat menyimpan data."
});
}
TempData["Error"] = "Terjadi kesalahan saat menyimpan data."; TempData["Error"] = "Terjadi kesalahan saat menyimpan data.";
return RedirectToAction(nameof(Index)); return RedirectToAction(nameof(Index));
} }
} }
[HttpPost("save-draft-non-tps")] [HttpPost("save-record-non-tps")]
[IgnoreAntiforgeryToken] [IgnoreAntiforgeryToken]
public async Task<IActionResult> SaveDraftNonTps([FromBody] DraftSaveRequest request) public async Task<IActionResult> SaveRecordNonTps()
{ {
var request = await ResolveRecordSaveRequestAsync();
if (request == null) if (request == null)
return BadRequest(new DraftSaveResponse { Success = false, Message = "Request tidak valid." }); return BadRequest(new RecordSaveResponse { Success = false, Message = "Request tidak valid." });
request.DraftKey = ResolveDraftKey(request.DraftKey, request.SessionKey, request.SpjDetailId, request.LokasiAngkutId); var result = await _detailService.SaveRecordNonTpsAsync(request);
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); return result.Success ? Ok(result) : StatusCode(500, result);
} }
[HttpGet("load-draft-non-tps")] [HttpPost("save-record")]
[IgnoreAntiforgeryToken] [IgnoreAntiforgeryToken]
public async Task<IActionResult> LoadDraftNonTps([FromQuery] string? draftKey = null, [FromQuery] string? sessionKey = null) public async Task<IActionResult> SaveRecord()
{ {
var key = ResolveDraftKey(draftKey, sessionKey); var request = await ResolveRecordSaveRequestAsync();
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<IActionResult> 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<IActionResult> SaveDraft([FromBody] DraftSaveRequest request)
{
if (request == null) if (request == null)
return BadRequest(new DraftSaveResponse { Success = false, Message = "Request tidak valid." }); return BadRequest(new RecordSaveResponse { Success = false, Message = "Request tidak valid." });
request.DraftKey = ResolveDraftKey(request.DraftKey, request.SessionKey, request.SpjDetailId, request.LokasiAngkutId); var result = await _detailService.SaveRecordTpsAsync(request);
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); return result.Success ? Ok(result) : StatusCode(500, result);
} }
[HttpGet("load-draft")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> 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<IActionResult> 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")] [HttpPost("upload-foto-kedatangan-non-tps")]
[IgnoreAntiforgeryToken] [IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoKedatanganNonTps( public async Task<IActionResult> UploadFotoKedatanganNonTps(
[FromForm] List<IFormFile>? FotoKedatangan, [FromForm] List<IFormFile>? FotoKedatangan,
[FromForm] string? DraftKey, [FromForm] string? NomorSpj,
[FromForm] string? SessionKey, [FromForm] string? NamaTps,
[FromForm] string? SpjDetailId, [FromForm] string? SpjDetailId,
[FromForm] string? LokasiAngkutId, [FromForm] string? LokasiAngkutId,
[FromForm] string? WaktuKedatangan, [FromForm] string? WaktuKedatangan,
@ -210,7 +393,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
return BadRequest(new { success = false, message = "Tidak ada foto." }); return BadRequest(new { success = false, message = "Tidak ada foto." });
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
var uploadDir = GetUploadDirectory(dateFolder); var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
var fileNames = new List<string>(); var fileNames = new List<string>();
foreach (var file in FotoKedatangan) foreach (var file in FotoKedatangan)
@ -223,42 +406,27 @@ namespace eSPJ.Controllers.SpjDriverUpstController
await file.CopyToAsync(stream); await file.CopyToAsync(stream);
fileNames.Add(name); fileNames.Add(name);
} }
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList(); var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList();
var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); var saveResult = await SaveUploadedRecordAsync(
if (!string.IsNullOrWhiteSpace(resolvedDraftKey)) isTps: false,
{ nomorSpj: NomorSpj,
var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey); namaTps: NamaTps,
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; spjDetailId: SpjDetailId,
draft.FotoKedatanganFileNames = fileUrls; lokasiAngkutId: LokasiAngkutId,
draft.FotoKedatanganUploaded = true; applyChanges: request =>
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, request.WaktuKedatangan = string.IsNullOrWhiteSpace(WaktuKedatangan) ? request.WaktuKedatangan : WaktuKedatangan;
SessionKey = resolvedDraftKey, request.Latitude = string.IsNullOrWhiteSpace(Latitude) ? request.Latitude : Latitude;
LokasiAngkutId = draft.LokasiAngkutId, request.Longitude = string.IsNullOrWhiteSpace(Longitude) ? request.Longitude : Longitude;
SpjDetailId = draft.SpjDetailId, request.AlamatJalan = string.IsNullOrWhiteSpace(AlamatJalan) ? request.AlamatJalan : AlamatJalan;
Latitude = draft.Latitude, request.FotoKedatanganFileNames = fileUrls;
Longitude = draft.Longitude, request.FotoKedatanganUploaded = true;
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
}); });
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
} }
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." }); return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." });
@ -268,8 +436,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController
[IgnoreAntiforgeryToken] [IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoTimbanganNonTps( public async Task<IActionResult> UploadFotoTimbanganNonTps(
[FromForm] IFormFile? FotoTimbangan, [FromForm] IFormFile? FotoTimbangan,
[FromForm] string? DraftKey, [FromForm] string? NomorSpj,
[FromForm] string? SessionKey, [FromForm] string? NamaTps,
[FromForm] string? SpjDetailId, [FromForm] string? SpjDetailId,
[FromForm] string? LokasiAngkutId, [FromForm] string? LokasiAngkutId,
[FromForm] int ItemIndex, [FromForm] int ItemIndex,
@ -280,7 +448,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
return BadRequest(new { success = false, message = "Tidak ada foto." }); return BadRequest(new { success = false, message = "Tidak ada foto." });
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
var uploadDir = GetUploadDirectory(dateFolder); var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant(); var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant();
var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant(); var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant();
@ -289,45 +457,33 @@ namespace eSPJ.Controllers.SpjDriverUpstController
var filePath = Path.Combine(uploadDir, name); var filePath = Path.Combine(uploadDir, name);
await using var stream = new FileStream(filePath, FileMode.Create); await using var stream = new FileStream(filePath, FileMode.Create);
await FotoTimbangan.CopyToAsync(stream); await FotoTimbangan.CopyToAsync(stream);
var fileUrl = BuildUploadUrl(dateFolder, name); var fileUrl = BuildUploadUrl(dateFolder, name, NomorSpj, NamaTps);
var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); var saveResult = await SaveUploadedRecordAsync(
if (!string.IsNullOrWhiteSpace(resolvedDraftKey)) isTps: false,
{ nomorSpj: NomorSpj,
var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey); namaTps: NamaTps,
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; spjDetailId: SpjDetailId,
while (draft.Timbangan.Count <= ItemIndex) lokasiAngkutId: LokasiAngkutId,
draft.Timbangan.Add(new DraftTimbanganItem()); applyChanges: request =>
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
draft.Timbangan[ItemIndex] = new DraftTimbanganItem
{ {
FotoFileName = fileUrl, while (request.Timbangan.Count <= ItemIndex)
JenisSampah = JenisSampah ?? "Residu", {
Berat = Berat, request.Timbangan.Add(new RecordTimbanganItem());
Uploaded = true }
};
await _detailService.SaveDraftNonTpsAsync(new DraftSaveRequest request.Timbangan[ItemIndex] = new RecordTimbanganItem
{ {
DraftKey = resolvedDraftKey, FotoFileName = fileUrl,
SessionKey = resolvedDraftKey, JenisSampah = JenisSampah ?? "Residu",
LokasiAngkutId = draft.LokasiAngkutId, Berat = Berat,
SpjDetailId = draft.SpjDetailId, Uploaded = true
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
}); });
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
} }
return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." }); return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." });
@ -337,8 +493,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController
[IgnoreAntiforgeryToken] [IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoPetugasNonTps( public async Task<IActionResult> UploadFotoPetugasNonTps(
[FromForm] List<IFormFile>? FotoPetugas, [FromForm] List<IFormFile>? FotoPetugas,
[FromForm] string? DraftKey, [FromForm] string? NomorSpj,
[FromForm] string? SessionKey, [FromForm] string? NamaTps,
[FromForm] string? SpjDetailId, [FromForm] string? SpjDetailId,
[FromForm] string? LokasiAngkutId, [FromForm] string? LokasiAngkutId,
[FromForm] string? NamaPetugas) [FromForm] string? NamaPetugas)
@ -347,7 +503,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
return BadRequest(new { success = false, message = "Tidak ada foto." }); return BadRequest(new { success = false, message = "Tidak ada foto." });
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
var uploadDir = GetUploadDirectory(dateFolder); var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
var fileNames = new List<string>(); var fileNames = new List<string>();
foreach (var file in FotoPetugas) foreach (var file in FotoPetugas)
@ -360,39 +516,24 @@ namespace eSPJ.Controllers.SpjDriverUpstController
await file.CopyToAsync(stream); await file.CopyToAsync(stream);
fileNames.Add(name); fileNames.Add(name);
} }
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList(); var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList();
var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); var saveResult = await SaveUploadedRecordAsync(
if (!string.IsNullOrWhiteSpace(resolvedDraftKey)) isTps: false,
{ nomorSpj: NomorSpj,
var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey); namaTps: NamaTps,
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; spjDetailId: SpjDetailId,
draft.FotoPetugasFileNames = fileUrls; lokasiAngkutId: LokasiAngkutId,
draft.FotoPetugasUploaded = true; applyChanges: request =>
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, request.FotoPetugasFileNames = fileUrls;
SessionKey = resolvedDraftKey, request.FotoPetugasUploaded = true;
LokasiAngkutId = draft.LokasiAngkutId, request.NamaPetugas = string.IsNullOrWhiteSpace(NamaPetugas) ? request.NamaPetugas : NamaPetugas;
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
}); });
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
} }
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." }); return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." });
@ -402,8 +543,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController
[IgnoreAntiforgeryToken] [IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoKedatangan( public async Task<IActionResult> UploadFotoKedatangan(
[FromForm] List<IFormFile>? FotoKedatangan, [FromForm] List<IFormFile>? FotoKedatangan,
[FromForm] string? DraftKey, [FromForm] string? NomorSpj,
[FromForm] string? SessionKey, [FromForm] string? NamaTps,
[FromForm] string? SpjDetailId, [FromForm] string? SpjDetailId,
[FromForm] string? LokasiAngkutId, [FromForm] string? LokasiAngkutId,
[FromForm] string? WaktuKedatangan, [FromForm] string? WaktuKedatangan,
@ -415,7 +556,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
return BadRequest(new { success = false, message = "Tidak ada foto." }); return BadRequest(new { success = false, message = "Tidak ada foto." });
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
var uploadDir = GetUploadDirectory(dateFolder); var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
var fileNames = new List<string>(); var fileNames = new List<string>();
foreach (var file in FotoKedatangan) foreach (var file in FotoKedatangan)
@ -428,42 +569,27 @@ namespace eSPJ.Controllers.SpjDriverUpstController
await file.CopyToAsync(stream); await file.CopyToAsync(stream);
fileNames.Add(name); fileNames.Add(name);
} }
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList(); var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList();
var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); var saveResult = await SaveUploadedRecordAsync(
if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps)) isTps: true,
{ nomorSpj: NomorSpj,
var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps); namaTps: NamaTps,
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; spjDetailId: SpjDetailId,
draft.FotoKedatanganFileNames = fileUrls; lokasiAngkutId: LokasiAngkutId,
draft.FotoKedatanganUploaded = true; applyChanges: request =>
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, request.WaktuKedatangan = string.IsNullOrWhiteSpace(WaktuKedatangan) ? request.WaktuKedatangan : WaktuKedatangan;
SessionKey = resolvedDraftKeyTps, request.Latitude = string.IsNullOrWhiteSpace(Latitude) ? request.Latitude : Latitude;
LokasiAngkutId = draft.LokasiAngkutId, request.Longitude = string.IsNullOrWhiteSpace(Longitude) ? request.Longitude : Longitude;
SpjDetailId = draft.SpjDetailId, request.AlamatJalan = string.IsNullOrWhiteSpace(AlamatJalan) ? request.AlamatJalan : AlamatJalan;
Latitude = draft.Latitude, request.FotoKedatanganFileNames = fileUrls;
Longitude = draft.Longitude, request.FotoKedatanganUploaded = true;
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
}); });
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
} }
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." }); return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." });
@ -473,8 +599,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController
[IgnoreAntiforgeryToken] [IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoTimbangan( public async Task<IActionResult> UploadFotoTimbangan(
[FromForm] IFormFile? FotoTimbangan, [FromForm] IFormFile? FotoTimbangan,
[FromForm] string? DraftKey, [FromForm] string? NomorSpj,
[FromForm] string? SessionKey, [FromForm] string? NamaTps,
[FromForm] string? SpjDetailId, [FromForm] string? SpjDetailId,
[FromForm] string? LokasiAngkutId, [FromForm] string? LokasiAngkutId,
[FromForm] int ItemIndex, [FromForm] int ItemIndex,
@ -485,7 +611,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
return BadRequest(new { success = false, message = "Tidak ada foto." }); return BadRequest(new { success = false, message = "Tidak ada foto." });
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
var uploadDir = GetUploadDirectory(dateFolder); var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant(); var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant();
var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant(); var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant();
@ -494,45 +620,33 @@ namespace eSPJ.Controllers.SpjDriverUpstController
var filePath = Path.Combine(uploadDir, name); var filePath = Path.Combine(uploadDir, name);
await using var stream = new FileStream(filePath, FileMode.Create); await using var stream = new FileStream(filePath, FileMode.Create);
await FotoTimbangan.CopyToAsync(stream); await FotoTimbangan.CopyToAsync(stream);
var fileUrl = BuildUploadUrl(dateFolder, name); var fileUrl = BuildUploadUrl(dateFolder, name, NomorSpj, NamaTps);
var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); var saveResult = await SaveUploadedRecordAsync(
if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps)) isTps: true,
{ nomorSpj: NomorSpj,
var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps); namaTps: NamaTps,
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; spjDetailId: SpjDetailId,
while (draft.Timbangan.Count <= ItemIndex) lokasiAngkutId: LokasiAngkutId,
draft.Timbangan.Add(new DraftTimbanganItem()); applyChanges: request =>
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
draft.Timbangan[ItemIndex] = new DraftTimbanganItem
{ {
FotoFileName = fileUrl, while (request.Timbangan.Count <= ItemIndex)
JenisSampah = JenisSampah ?? "Residu", {
Berat = Berat, request.Timbangan.Add(new RecordTimbanganItem());
Uploaded = true }
};
await _detailService.SaveDraftTpsAsync(new DraftSaveRequest request.Timbangan[ItemIndex] = new RecordTimbanganItem
{ {
DraftKey = resolvedDraftKeyTps, FotoFileName = fileUrl,
SessionKey = resolvedDraftKeyTps, JenisSampah = JenisSampah ?? "Residu",
LokasiAngkutId = draft.LokasiAngkutId, Berat = Berat,
SpjDetailId = draft.SpjDetailId, Uploaded = true
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
}); });
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
} }
return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." }); return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." });
@ -542,8 +656,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController
[IgnoreAntiforgeryToken] [IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoPetugas( public async Task<IActionResult> UploadFotoPetugas(
[FromForm] List<IFormFile>? FotoPetugas, [FromForm] List<IFormFile>? FotoPetugas,
[FromForm] string? DraftKey, [FromForm] string? NomorSpj,
[FromForm] string? SessionKey, [FromForm] string? NamaTps,
[FromForm] string? SpjDetailId, [FromForm] string? SpjDetailId,
[FromForm] string? LokasiAngkutId, [FromForm] string? LokasiAngkutId,
[FromForm] string? NamaPetugas) [FromForm] string? NamaPetugas)
@ -552,7 +666,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
return BadRequest(new { success = false, message = "Tidak ada foto." }); return BadRequest(new { success = false, message = "Tidak ada foto." });
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd"); var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
var uploadDir = GetUploadDirectory(dateFolder); var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
var fileNames = new List<string>(); var fileNames = new List<string>();
foreach (var file in FotoPetugas) foreach (var file in FotoPetugas)
@ -565,39 +679,24 @@ namespace eSPJ.Controllers.SpjDriverUpstController
await file.CopyToAsync(stream); await file.CopyToAsync(stream);
fileNames.Add(name); fileNames.Add(name);
} }
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList(); var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList();
var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId); var saveResult = await SaveUploadedRecordAsync(
if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps)) isTps: true,
{ nomorSpj: NomorSpj,
var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps); namaTps: NamaTps,
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty }; spjDetailId: SpjDetailId,
draft.FotoPetugasFileNames = fileUrls; lokasiAngkutId: LokasiAngkutId,
draft.FotoPetugasUploaded = true; applyChanges: request =>
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, request.FotoPetugasFileNames = fileUrls;
SessionKey = resolvedDraftKeyTps, request.FotoPetugasUploaded = true;
LokasiAngkutId = draft.LokasiAngkutId, request.NamaPetugas = string.IsNullOrWhiteSpace(NamaPetugas) ? request.NamaPetugas : NamaPetugas;
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
}); });
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
} }
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." }); return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." });
@ -660,31 +759,9 @@ namespace eSPJ.Controllers.SpjDriverUpstController
- Jawab hanya angka dengan format 2 digit desimal pakai titik (contoh: 54.45). - Jawab hanya angka dengan format 2 digit desimal pakai titik (contoh: 54.45).
- Jika tidak terbaca jawab: UNREADABLE - Jika tidak terbaca jawab: UNREADABLE
- Fokus pada angka layar LED merah yang menyala. - Fokus pada angka layar LED merah yang menyala.
- Abaikan refleksi atau pantulan cahaya yang mungkin muncul di layar.
Saya berikan 3 contoh foto timbangan yang benar: - Abaikan timestamp seperti tanggal, jam, atau informasi lain yang biasanya muncul di layar timbangan.
Foto 1 = 75.23 "
Foto 2 = 79.86
Foto 3 = 54.45
Sekarang baca angka pada foto terakhir."
},
new
{
type = "image_url",
image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888384/Notes_-_2026-02-11_08.52.31_wonhbm.jpg" }
},
new
{
type = "image_url",
image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888429/Notes_-_2026-02-11_08.52.34_xairzy.jpg" }
},
new
{
type = "image_url",
image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888473/ChatGPT_Image_Feb_11_2026_03_00_33_PM_ujhdlw.png" }
}, },
new new
@ -702,7 +779,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
var request = new HttpRequestMessage(HttpMethod.Post, "https://openrouter.ai/api/v1/chat/completions"); var request = new HttpRequestMessage(HttpMethod.Post, "https://openrouter.ai/api/v1/chat/completions");
request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {apiKey}"); request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {apiKey}");
request.Headers.TryAddWithoutValidation("Accept", "application/json"); request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("HTTP-Referer", "https://yourdomain.com"); request.Headers.TryAddWithoutValidation("HTTP-Referer", "https://pesapakawan.dinaslhdki.id");
request.Headers.TryAddWithoutValidation("X-Title", "eSPJ OCR Timbangan"); request.Headers.TryAddWithoutValidation("X-Title", "eSPJ OCR Timbangan");
request.Content = new StringContent(json, Encoding.UTF8, "application/json"); request.Content = new StringContent(json, Encoding.UTF8, "application/json");

View File

@ -1,38 +0,0 @@
[
{
"Name": "TPS A",
"Index": 0,
"Latitude": "-6.481670",
"Longitude": "106.854000",
"AlamatJalan": "Cibinong, Bogor, West Java, Java, 16911, Indonesia",
"WaktuKedatangan": "16/03/2026, 09.37.44",
"FotoKedatangan": [
"kedatangan_ff132727-8942-402e-a612-ac3436e905b9.png"
],
"FotoKedatanganUploaded": true,
"Timbangan": [
{
"FotoFileName": "timbangan_0de03ef1-dbdd-4643-a0e6-ac7670b12679.png",
"Berat": [
75.23
],
"LokasiAngkut": [],
"JenisSampah": [
2
],
"IsUploaded": true,
"WaktuUpload": "2026-03-16T09:38:37.1840709+07:00"
}
],
"TotalOrganik": 0,
"TotalAnorganik": 0,
"TotalResidu": 75.23,
"TotalTimbangan": 75.23,
"FotoPetugas": [
"petugas_dffeaabf-4f30-41dd-9281-e2c33164e2c6.jpg"
],
"FotoPetugasUploaded": true,
"NamaPetugas": "usmannn",
"Submitted": true
}
]

View File

@ -19,6 +19,9 @@ namespace eSPJ.Models
public class TpsData public class TpsData
{ {
public string NomorSpj { get; set; } = string.Empty;
public string LokasiAngkutId { get; set; } = string.Empty;
public string SpjDetailId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public int Index { get; set; } public int Index { get; set; }
public string Latitude { get; set; } = string.Empty; public string Latitude { get; set; } = string.Empty;
@ -35,11 +38,16 @@ namespace eSPJ.Models
public List<string> FotoPetugas { get; set; } = new(); public List<string> FotoPetugas { get; set; } = new();
public bool FotoPetugasUploaded { get; set; } public bool FotoPetugasUploaded { get; set; }
public string NamaPetugas { get; set; } = string.Empty; public string NamaPetugas { get; set; } = string.Empty;
public bool Submitted { get; set; } public bool IsSubmit { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.Now;
public DateTime? SubmittedAt { get; set; }
} }
public class DetailPenjemputanRequest public class DetailPenjemputanRequest
{ {
public string NomorSpj { get; set; } = string.Empty;
public string LokasiAngkutId { get; set; } = string.Empty;
public string SpjDetailId { get; set; } = string.Empty;
public string TpsName { get; set; } = string.Empty; public string TpsName { get; set; } = string.Empty;
public string Latitude { get; set; } = string.Empty; public string Latitude { get; set; } = string.Empty;
public string Longitude { get; set; } = string.Empty; public string Longitude { get; set; } = string.Empty;
@ -77,7 +85,7 @@ namespace eSPJ.Models
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;
} }
public class DraftTimbanganItem public class RecordTimbanganItem
{ {
public decimal Berat { get; set; } public decimal Berat { get; set; }
public string JenisSampah { get; set; } = "Residu"; public string JenisSampah { get; set; } = "Residu";
@ -86,33 +94,10 @@ namespace eSPJ.Models
public string OcrInfo { get; set; } = string.Empty; public string OcrInfo { get; set; } = string.Empty;
} }
public class DraftPenjemputanNonTps public class RecordSaveRequest
{ {
public string SessionKey { get; set; } = string.Empty; public string NomorSpj { get; set; } = string.Empty;
public string LokasiAngkutId { get; set; } = string.Empty; public string NamaTps { 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<string> FotoKedatanganFileNames { get; set; } = new();
public bool FotoKedatanganUploaded { get; set; }
public List<DraftTimbanganItem> 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<string> 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 LokasiAngkutId { get; set; } = string.Empty;
public string SpjDetailId { get; set; } = string.Empty; public string SpjDetailId { get; set; } = string.Empty;
public string Latitude { get; set; } = string.Empty; public string Latitude { get; set; } = string.Empty;
@ -121,7 +106,7 @@ namespace eSPJ.Models
public string WaktuKedatangan { get; set; } = string.Empty; public string WaktuKedatangan { get; set; } = string.Empty;
public bool FotoKedatanganUploaded { get; set; } public bool FotoKedatanganUploaded { get; set; }
public List<string> FotoKedatanganFileNames { get; set; } = new(); public List<string> FotoKedatanganFileNames { get; set; } = new();
public List<DraftTimbanganItem> Timbangan { get; set; } = new(); public List<RecordTimbanganItem> Timbangan { get; set; } = new();
public decimal TotalOrganik { get; set; } public decimal TotalOrganik { get; set; }
public decimal TotalAnorganik { get; set; } public decimal TotalAnorganik { get; set; }
public decimal TotalResidu { get; set; } public decimal TotalResidu { get; set; }
@ -129,21 +114,12 @@ namespace eSPJ.Models
public bool FotoPetugasUploaded { get; set; } public bool FotoPetugasUploaded { get; set; }
public List<string> FotoPetugasFileNames { get; set; } = new(); public List<string> FotoPetugasFileNames { get; set; } = new();
public string NamaPetugas { get; set; } = string.Empty; public string NamaPetugas { get; set; } = string.Empty;
public bool IsSubmit { get; set; }
} }
public class DraftSaveResponse public class RecordSaveResponse
{ {
public bool Success { get; set; } public bool Success { get; set; }
public string Message { get; set; } = string.Empty; 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;
} }
} }

View File

@ -1,3 +1,4 @@
using Microsoft.Extensions.FileProviders;
using eSPJ.Services; using eSPJ.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -7,6 +8,7 @@ builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
// Register custom services // Register custom services
builder.Services.AddScoped<IDetailPenjemputanStore, FileDetailPenjemputanStore>();
builder.Services.AddScoped<DetailPenjemputanService>(); builder.Services.AddScoped<DetailPenjemputanService>();
builder.Services.AddScoped<HistoryService>(); builder.Services.AddScoped<HistoryService>();
@ -21,6 +23,11 @@ if (!app.Environment.IsDevelopment())
} }
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "uploads")),
RequestPath = "/uploads"
});
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
if (context.Request.Path.Equals("/driver/serviceworker.js", StringComparison.OrdinalIgnoreCase)) if (context.Request.Path.Equals("/driver/serviceworker.js", StringComparison.OrdinalIgnoreCase))
@ -28,6 +35,9 @@ app.Use(async (context, next) =>
context.Response.OnStarting(() => context.Response.OnStarting(() =>
{ {
context.Response.Headers["Service-Worker-Allowed"] = "/upst"; context.Response.Headers["Service-Worker-Allowed"] = "/upst";
context.Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0";
context.Response.Headers["Pragma"] = "no-cache";
context.Response.Headers["Expires"] = "0";
return Task.CompletedTask; return Task.CompletedTask;
}); });
} }

View File

@ -1,60 +1,57 @@
using System.Text.Json;
using eSPJ.Models; using eSPJ.Models;
namespace eSPJ.Services namespace eSPJ.Services
{ {
public class DetailPenjemputanService public class DetailPenjemputanService
{ {
private readonly string _dataFilePath; private readonly IDetailPenjemputanStore _store;
private readonly IWebHostEnvironment _env; private readonly IWebHostEnvironment _env;
private readonly ILogger<DetailPenjemputanService> _logger; private readonly ILogger<DetailPenjemputanService> _logger;
public DetailPenjemputanService( public DetailPenjemputanService(
IWebHostEnvironment env, IWebHostEnvironment env,
IDetailPenjemputanStore store,
ILogger<DetailPenjemputanService> logger) ILogger<DetailPenjemputanService> logger)
{ {
_env = env; _env = env;
_store = store;
_logger = logger; _logger = logger;
_dataFilePath = Path.Combine(_env.ContentRootPath, "Data", "detail-penjemputan.json"); }
private static string SanitizePathSegment(string? value, string fallback = "umum")
{
var safe = string.Concat((value ?? string.Empty).Trim().Select(c =>
char.IsLetterOrDigit(c) || c == '-' || c == '_'
? c
: '-'));
while (safe.Contains("--"))
{
safe = safe.Replace("--", "-");
}
safe = safe.Trim('-');
return string.IsNullOrWhiteSpace(safe) ? fallback : safe;
} }
public async Task<List<TpsData>> GetAllTpsDataAsync() public async Task<List<TpsData>> GetAllTpsDataAsync()
{ {
try return await _store.GetSubmittedAsync();
{ }
if (!File.Exists(_dataFilePath))
{
return new List<TpsData>();
}
var json = await File.ReadAllTextAsync(_dataFilePath); public async Task<List<TpsData>> GetRecordsByNomorSpjAsync(string nomorSpj)
var data = JsonSerializer.Deserialize<List<TpsData>>(json); {
return data ?? new List<TpsData>(); return await _store.GetByNomorSpjAsync(nomorSpj);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error reading TPS data from JSON");
return new List<TpsData>();
}
} }
public async Task<bool> SaveTpsDataAsync(List<TpsData> data) public async Task<bool> SaveTpsDataAsync(List<TpsData> data)
{ {
try try
{ {
var directory = Path.GetDirectoryName(_dataFilePath); foreach (var item in data)
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{ {
Directory.CreateDirectory(directory); await _store.SaveSubmittedAsync(item);
} }
var options = new JsonSerializerOptions
{
WriteIndented = true
};
var json = JsonSerializer.Serialize(data, options);
await File.WriteAllTextAsync(_dataFilePath, json);
return true; return true;
} }
catch (Exception ex) catch (Exception ex)
@ -68,7 +65,6 @@ namespace eSPJ.Services
{ {
try try
{ {
// Validate request
if (string.IsNullOrEmpty(request.TpsName)) if (string.IsNullOrEmpty(request.TpsName))
{ {
return new DetailPenjemputanResponse return new DetailPenjemputanResponse
@ -78,33 +74,6 @@ namespace eSPJ.Services
}; };
} }
if (request.FotoKedatangan == null || !request.FotoKedatangan.Any())
{
return new DetailPenjemputanResponse
{
Success = false,
Message = "Foto kedatangan harus diupload"
};
}
if (request.FotoTimbangan == null || !request.FotoTimbangan.Any())
{
return new DetailPenjemputanResponse
{
Success = false,
Message = "Foto timbangan harus diupload"
};
}
if (request.FotoPetugas == null || !request.FotoPetugas.Any())
{
return new DetailPenjemputanResponse
{
Success = false,
Message = "Foto petugas harus diupload"
};
}
if (string.IsNullOrEmpty(request.NamaPetugas)) if (string.IsNullOrEmpty(request.NamaPetugas))
{ {
return new DetailPenjemputanResponse return new DetailPenjemputanResponse
@ -114,47 +83,72 @@ namespace eSPJ.Services
}; };
} }
var existingRecord = await GetRecordDetailAsync(request.NomorSpj, request.SpjDetailId, request.LokasiAngkutId, request.TpsName);
var now = DateTime.Now; var now = DateTime.Now;
var datePart = now.ToString("yyyy-MM-dd"); var datePart = now.ToString("yyyy-MM-dd");
var uploadPath = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", datePart); var spjFolder = SanitizePathSegment(request.NomorSpj, "spj-umum");
var uploadBaseUrl = $"/uploads/penjemputan/{datePart}"; var tpsFolder = SanitizePathSegment(request.TpsName, "tps-1");
var uploadPath = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", datePart, spjFolder, tpsFolder);
var uploadBaseUrl = $"/uploads/penjemputan/{datePart}/{spjFolder}/{tpsFolder}";
if (!Directory.Exists(uploadPath)) if (!Directory.Exists(uploadPath))
{ {
Directory.CreateDirectory(uploadPath); Directory.CreateDirectory(uploadPath);
} }
var tpsData = new TpsData var tpsData = existingRecord != null
{ ? CloneRecord(existingRecord)
Name = request.TpsName, : new TpsData();
Latitude = request.Latitude,
Longitude = request.Longitude,
AlamatJalan = request.AlamatJalan,
WaktuKedatangan = request.WaktuKedatangan,
TotalTimbangan = request.TotalTimbangan,
TotalOrganik = request.TotalOrganik,
TotalAnorganik = request.TotalAnorganik,
TotalResidu = request.TotalResidu,
NamaPetugas = request.NamaPetugas,
Submitted = true,
FotoKedatanganUploaded = true,
FotoPetugasUploaded = true
};
// Save foto kedatangan tpsData.NomorSpj = request.NomorSpj;
foreach (var file in request.FotoKedatangan) tpsData.LokasiAngkutId = request.LokasiAngkutId;
tpsData.SpjDetailId = request.SpjDetailId;
tpsData.Name = request.TpsName;
tpsData.Latitude = request.Latitude;
tpsData.Longitude = request.Longitude;
tpsData.AlamatJalan = request.AlamatJalan;
tpsData.WaktuKedatangan = request.WaktuKedatangan;
tpsData.TotalTimbangan = request.TotalTimbangan;
tpsData.TotalOrganik = request.TotalOrganik;
tpsData.TotalAnorganik = request.TotalAnorganik;
tpsData.TotalResidu = request.TotalResidu;
tpsData.NamaPetugas = request.NamaPetugas;
tpsData.IsSubmit = true;
tpsData.SubmittedAt ??= DateTime.Now;
tpsData.UpdatedAt = DateTime.Now;
if (request.FotoKedatangan != null && request.FotoKedatangan.Any())
{ {
var fileName = $"kedatangan_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; tpsData.FotoKedatangan = new List<string>();
var filePath = Path.Combine(uploadPath, fileName); foreach (var file in request.FotoKedatangan)
using (var stream = new FileStream(filePath, FileMode.Create))
{ {
await file.CopyToAsync(stream); var fileName = $"kedatangan_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var filePath = Path.Combine(uploadPath, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
tpsData.FotoKedatangan.Add($"{uploadBaseUrl}/{fileName}");
} }
tpsData.FotoKedatangan.Add($"{uploadBaseUrl}/{fileName}"); tpsData.FotoKedatanganUploaded = tpsData.FotoKedatangan.Count > 0;
}
else if (existingRecord?.FotoKedatangan?.Any() == true)
{
tpsData.FotoKedatangan = new List<string>(existingRecord.FotoKedatangan);
tpsData.FotoKedatanganUploaded = existingRecord.FotoKedatanganUploaded || tpsData.FotoKedatangan.Count > 0;
}
else
{
return new DetailPenjemputanResponse
{
Success = false,
Message = "Foto kedatangan harus diupload"
};
} }
// Save foto timbangan if (request.FotoTimbangan != null && request.FotoTimbangan.Any() && request.BeratTimbangan != null && request.JenisSampahList != null)
if (request.FotoTimbangan != null && request.BeratTimbangan != null && request.JenisSampahList != null)
{ {
tpsData.Timbangan = new List<TimbanganItem>();
for (int i = 0; i < request.FotoTimbangan.Count; i++) for (int i = 0; i < request.FotoTimbangan.Count; i++)
{ {
var file = request.FotoTimbangan[i]; var file = request.FotoTimbangan[i];
@ -174,7 +168,7 @@ namespace eSPJ.Services
tpsData.Timbangan.Add(new TimbanganItem tpsData.Timbangan.Add(new TimbanganItem
{ {
FotoFileName = $"{uploadBaseUrl}/{fileName}", FotoFileName = $"{uploadBaseUrl}/{fileName}",
Berat = new List<decimal> { (i < request.BeratTimbangan.Count ? request.BeratTimbangan[i] : 0) }, Berat = new List<decimal> { i < request.BeratTimbangan.Count ? request.BeratTimbangan[i] : 0 },
LokasiAngkut = new List<string>(), LokasiAngkut = new List<string>(),
JenisSampah = new List<JenisSampah> { jenisSampah }, JenisSampah = new List<JenisSampah> { jenisSampah },
IsUploaded = true, IsUploaded = true,
@ -182,22 +176,49 @@ namespace eSPJ.Services
}); });
} }
} }
else if (existingRecord?.Timbangan?.Any() == true)
// Save foto petugas
foreach (var file in request.FotoPetugas)
{ {
var fileName = $"petugas_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; tpsData.Timbangan = existingRecord.Timbangan;
var filePath = Path.Combine(uploadPath, fileName); }
using (var stream = new FileStream(filePath, FileMode.Create)) else
{
return new DetailPenjemputanResponse
{ {
await file.CopyToAsync(stream); Success = false,
} Message = "Foto timbangan harus diupload"
tpsData.FotoPetugas.Add($"{uploadBaseUrl}/{fileName}"); };
} }
var allData = await GetAllTpsDataAsync(); if (request.FotoPetugas != null && request.FotoPetugas.Any())
allData.Add(tpsData); {
await SaveTpsDataAsync(allData); tpsData.FotoPetugas = new List<string>();
foreach (var file in request.FotoPetugas)
{
var fileName = $"petugas_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var filePath = Path.Combine(uploadPath, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
tpsData.FotoPetugas.Add($"{uploadBaseUrl}/{fileName}");
}
tpsData.FotoPetugasUploaded = tpsData.FotoPetugas.Count > 0;
}
else if (existingRecord?.FotoPetugas?.Any() == true)
{
tpsData.FotoPetugas = new List<string>(existingRecord.FotoPetugas);
tpsData.FotoPetugasUploaded = existingRecord.FotoPetugasUploaded || tpsData.FotoPetugas.Count > 0;
}
else
{
return new DetailPenjemputanResponse
{
Success = false,
Message = "Foto petugas harus diupload"
};
}
await _store.SaveSubmittedAsync(tpsData);
return new DetailPenjemputanResponse return new DetailPenjemputanResponse
{ {
@ -217,128 +238,102 @@ namespace eSPJ.Services
} }
} }
private string GetDraftFilePath(string prefix, string sessionKey) private Task<RecordSaveResponse> SaveRecordAsync(RecordSaveRequest request)
{ {
var dir = Path.Combine(_env.ContentRootPath, "Data", "drafts"); return SaveRecordInternalAsync(request);
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<DraftSaveResponse> SaveDraftAsync(string prefix, DraftSaveRequest request) private async Task<RecordSaveResponse> SaveRecordInternalAsync(RecordSaveRequest request)
{
return SaveDraftInternalAsync(prefix, request);
}
private async Task<DraftSaveResponse> SaveDraftInternalAsync(string prefix, DraftSaveRequest request)
{ {
try try
{ {
var filePath = GetDraftFilePath(prefix, request.SessionKey); await _store.SaveRecordAsync(request);
var draft = new DraftPenjemputanNonTps
return new RecordSaveResponse { Success = true, Message = "Data tersimpan." };
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving penjemputan record");
return new RecordSaveResponse { Success = false, Message = $"Gagal menyimpan data: {ex.Message}" };
}
}
public async Task<RecordSaveResponse> SaveRecordNonTpsAsync(RecordSaveRequest request)
{
return await SaveRecordAsync(request);
}
public async Task<RecordSaveResponse> SaveRecordTpsAsync(RecordSaveRequest request)
{
return await SaveRecordAsync(request);
}
public async Task<List<TpsData>> GetSubmittedByNomorSpjAsync(string nomorSpj)
{
var normalizedNomorSpj = (nomorSpj ?? string.Empty).Trim();
var allData = await _store.GetSubmittedAsync();
return allData
.Where(item => string.Equals((item.NomorSpj ?? string.Empty).Trim(), normalizedNomorSpj, StringComparison.OrdinalIgnoreCase))
.OrderBy(item => item.SubmittedAt ?? DateTime.MinValue)
.ToList();
}
public async Task<TpsData?> GetSubmittedDetailAsync(string nomorSpj, string? spjDetailId = null, string? lokasiAngkutId = null, string? namaTps = null)
{
return await _store.GetSubmittedDetailAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps);
}
public async Task<TpsData?> GetRecordDetailAsync(string nomorSpj, string? spjDetailId = null, string? lokasiAngkutId = null, string? namaTps = null)
{
var normalizedNomorSpj = (nomorSpj ?? string.Empty).Trim();
var normalizedSpjDetailId = (spjDetailId ?? string.Empty).Trim();
var normalizedLokasiAngkutId = (lokasiAngkutId ?? string.Empty).Trim();
var normalizedNamaTps = (namaTps ?? string.Empty).Trim();
var allData = await _store.GetByNomorSpjAsync(normalizedNomorSpj);
return allData
.OrderByDescending(item => item.UpdatedAt)
.FirstOrDefault(item =>
(string.IsNullOrWhiteSpace(normalizedSpjDetailId) || string.Equals((item.SpjDetailId ?? string.Empty).Trim(), normalizedSpjDetailId, StringComparison.OrdinalIgnoreCase)) &&
(string.IsNullOrWhiteSpace(normalizedLokasiAngkutId) || string.Equals((item.LokasiAngkutId ?? string.Empty).Trim(), normalizedLokasiAngkutId, StringComparison.OrdinalIgnoreCase)) &&
(string.IsNullOrWhiteSpace(normalizedNamaTps) || string.Equals((item.Name ?? string.Empty).Trim(), normalizedNamaTps, StringComparison.OrdinalIgnoreCase)));
}
private static TpsData CloneRecord(TpsData source)
{
return new TpsData
{
NomorSpj = source.NomorSpj,
LokasiAngkutId = source.LokasiAngkutId,
SpjDetailId = source.SpjDetailId,
Name = source.Name,
Index = source.Index,
Latitude = source.Latitude,
Longitude = source.Longitude,
AlamatJalan = source.AlamatJalan,
WaktuKedatangan = source.WaktuKedatangan,
FotoKedatangan = new List<string>(source.FotoKedatangan ?? new List<string>()),
FotoKedatanganUploaded = source.FotoKedatanganUploaded,
Timbangan = (source.Timbangan ?? new List<TimbanganItem>()).Select(item => new TimbanganItem
{ {
SessionKey = request.SessionKey, FotoFileName = item.FotoFileName,
LokasiAngkutId = request.LokasiAngkutId, Berat = new List<decimal>(item.Berat ?? new List<decimal>()),
SpjDetailId = request.SpjDetailId, LokasiAngkut = new List<string>(item.LokasiAngkut ?? new List<string>()),
Latitude = request.Latitude, JenisSampah = new List<JenisSampah>(item.JenisSampah ?? new List<JenisSampah>()),
Longitude = request.Longitude, IsUploaded = item.IsUploaded,
AlamatJalan = request.AlamatJalan, WaktuUpload = item.WaktuUpload
WaktuKedatangan = request.WaktuKedatangan, }).ToList(),
FotoKedatanganFileNames = request.FotoKedatanganFileNames, TotalOrganik = source.TotalOrganik,
FotoKedatanganUploaded = request.FotoKedatanganUploaded, TotalAnorganik = source.TotalAnorganik,
Timbangan = request.Timbangan, TotalResidu = source.TotalResidu,
TotalOrganik = request.TotalOrganik, TotalTimbangan = source.TotalTimbangan,
TotalAnorganik = request.TotalAnorganik, FotoPetugas = new List<string>(source.FotoPetugas ?? new List<string>()),
TotalResidu = request.TotalResidu, FotoPetugasUploaded = source.FotoPetugasUploaded,
TotalTimbangan = request.TotalTimbangan, NamaPetugas = source.NamaPetugas,
FotoPetugasFileNames = request.FotoPetugasFileNames, IsSubmit = source.IsSubmit,
FotoPetugasUploaded = request.FotoPetugasUploaded, UpdatedAt = source.UpdatedAt,
NamaPetugas = request.NamaPetugas, SubmittedAt = source.SubmittedAt,
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<DraftSaveResponse> SaveDraftNonTpsAsync(DraftSaveRequest request)
{
return await SaveDraftAsync("non-tps", request);
}
public async Task<DraftSaveResponse> SaveDraftTpsAsync(DraftSaveRequest request)
{
return await SaveDraftAsync("tps", request);
}
public async Task<DraftLoadResponse> LoadDraftNonTpsAsync(string sessionKey)
{
return await LoadDraftAsync("non-tps", sessionKey);
}
public async Task<DraftLoadResponse> LoadDraftTpsAsync(string sessionKey)
{
return await LoadDraftAsync("tps", sessionKey);
}
private async Task<DraftLoadResponse> 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<DraftPenjemputanNonTps>(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<bool> DeleteDraftNonTpsAsync(string sessionKey)
{
return await DeleteDraftAsync("non-tps", sessionKey);
}
public async Task<bool> DeleteDraftTpsAsync(string sessionKey)
{
return await DeleteDraftAsync("tps", sessionKey);
}
private async Task<bool> 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<OcrTimbanganResponse> ProcessOcrTimbanganAsync(IFormFile foto) public async Task<OcrTimbanganResponse> ProcessOcrTimbanganAsync(IFormFile foto)

View File

@ -0,0 +1,340 @@
using System.Text.Json;
using eSPJ.Models;
namespace eSPJ.Services
{
public class FileDetailPenjemputanStore : IDetailPenjemputanStore
{
private readonly string _storeFilePath;
private readonly ILogger<FileDetailPenjemputanStore> _logger;
private static readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1);
public FileDetailPenjemputanStore(
IWebHostEnvironment env,
ILogger<FileDetailPenjemputanStore> logger)
{
_logger = logger;
_storeFilePath = Path.Combine(env.ContentRootPath, "Data", "detail-penjemputan.json");
}
public async Task<List<TpsData>> GetAllAsync()
{
await _fileLock.WaitAsync();
try
{
return await ReadAllFromDiskAsync();
}
finally
{
_fileLock.Release();
}
}
private async Task<List<TpsData>> ReadAllFromDiskAsync()
{
try
{
if (!File.Exists(_storeFilePath))
{
return new List<TpsData>();
}
var json = await File.ReadAllTextAsync(_storeFilePath);
if (string.IsNullOrWhiteSpace(json))
{
return new List<TpsData>();
}
var data = JsonSerializer.Deserialize<List<TpsData>>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return data?.Where(item => item != null).ToList() ?? new List<TpsData>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error reading submitted penjemputan data");
return new List<TpsData>();
}
}
public async Task<List<TpsData>> GetByNomorSpjAsync(string nomorSpj)
{
var normalizedNomorSpj = (nomorSpj ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(normalizedNomorSpj))
{
return new List<TpsData>();
}
var allData = await GetAllAsync();
return allData
.Where(item => string.Equals((item.NomorSpj ?? string.Empty).Trim(), normalizedNomorSpj, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(item => item.UpdatedAt)
.GroupBy(GetRecordIdentity, StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.OrderBy(item => item.Name)
.ToList();
}
public async Task<List<TpsData>> GetSubmittedAsync()
{
var allData = await GetAllAsync();
return allData
.Where(item => item.IsSubmit)
.OrderByDescending(item => item.UpdatedAt)
.GroupBy(GetRecordIdentity, StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.ToList();
}
private static string GetRecordIdentity(TpsData item)
{
return string.Join("::", new[]
{
item.NomorSpj?.Trim() ?? string.Empty,
item.SpjDetailId?.Trim() ?? string.Empty,
item.LokasiAngkutId?.Trim() ?? string.Empty,
item.Name?.Trim() ?? string.Empty,
});
}
public async Task<TpsData?> GetSubmittedDetailAsync(string nomorSpj, string? spjDetailId = null, string? lokasiAngkutId = null, string? namaTps = null)
{
var normalizedNomorSpj = (nomorSpj ?? string.Empty).Trim();
var normalizedSpjDetailId = (spjDetailId ?? string.Empty).Trim();
var normalizedLokasiAngkutId = (lokasiAngkutId ?? string.Empty).Trim();
var normalizedNamaTps = (namaTps ?? string.Empty).Trim();
var allData = await GetSubmittedAsync();
return allData.FirstOrDefault(item =>
string.Equals((item.NomorSpj ?? string.Empty).Trim(), normalizedNomorSpj, StringComparison.OrdinalIgnoreCase) &&
(string.IsNullOrWhiteSpace(normalizedSpjDetailId) || string.Equals((item.SpjDetailId ?? string.Empty).Trim(), normalizedSpjDetailId, StringComparison.OrdinalIgnoreCase)) &&
(string.IsNullOrWhiteSpace(normalizedLokasiAngkutId) || string.Equals((item.LokasiAngkutId ?? string.Empty).Trim(), normalizedLokasiAngkutId, StringComparison.OrdinalIgnoreCase)) &&
(string.IsNullOrWhiteSpace(normalizedNamaTps) || string.Equals((item.Name ?? string.Empty).Trim(), normalizedNamaTps, StringComparison.OrdinalIgnoreCase)));
}
public async Task SaveSubmittedAsync(TpsData data)
{
await _fileLock.WaitAsync();
try
{
var allData = await ReadAllFromDiskAsync();
data.IsSubmit = true;
data.UpdatedAt = DateTime.Now;
if (!data.SubmittedAt.HasValue)
{
data.SubmittedAt = DateTime.Now;
}
var existingIndex = FindRecordIndex(allData, data.NomorSpj, data.SpjDetailId, data.LokasiAngkutId, data.Name);
if (existingIndex >= 0)
{
allData[existingIndex] = data;
}
else
{
allData.Add(data);
}
await WriteAllToDiskAsync(allData);
}
finally
{
_fileLock.Release();
}
}
public async Task SaveRecordAsync(RecordSaveRequest request)
{
await _fileLock.WaitAsync();
try
{
var allData = await ReadAllFromDiskAsync();
var mapped = MapRequestToRecord(request);
mapped.UpdatedAt = DateTime.Now;
var existingIndex = FindRecordIndex(allData, mapped.NomorSpj, mapped.SpjDetailId, mapped.LokasiAngkutId, mapped.Name);
if (existingIndex >= 0)
{
var existing = allData[existingIndex];
mapped.NomorSpj = string.IsNullOrWhiteSpace(mapped.NomorSpj) ? existing.NomorSpj : mapped.NomorSpj;
mapped.LokasiAngkutId = string.IsNullOrWhiteSpace(mapped.LokasiAngkutId) ? existing.LokasiAngkutId : mapped.LokasiAngkutId;
mapped.SpjDetailId = string.IsNullOrWhiteSpace(mapped.SpjDetailId) ? existing.SpjDetailId : mapped.SpjDetailId;
mapped.Name = string.IsNullOrWhiteSpace(mapped.Name) ? existing.Name : mapped.Name;
mapped.Latitude = string.IsNullOrWhiteSpace(mapped.Latitude) ? existing.Latitude : mapped.Latitude;
mapped.Longitude = string.IsNullOrWhiteSpace(mapped.Longitude) ? existing.Longitude : mapped.Longitude;
mapped.AlamatJalan = string.IsNullOrWhiteSpace(mapped.AlamatJalan) ? existing.AlamatJalan : mapped.AlamatJalan;
mapped.WaktuKedatangan = string.IsNullOrWhiteSpace(mapped.WaktuKedatangan) ? existing.WaktuKedatangan : mapped.WaktuKedatangan;
mapped.FotoKedatangan = mapped.FotoKedatangan.Count > 0 ? mapped.FotoKedatangan : (existing.FotoKedatangan ?? new List<string>());
mapped.FotoKedatanganUploaded = mapped.FotoKedatanganUploaded || existing.FotoKedatanganUploaded;
mapped.Timbangan = MergeTimbangan(existing.Timbangan, mapped.Timbangan);
mapped.TotalOrganik = mapped.TotalOrganik != 0 ? mapped.TotalOrganik : existing.TotalOrganik;
mapped.TotalAnorganik = mapped.TotalAnorganik != 0 ? mapped.TotalAnorganik : existing.TotalAnorganik;
mapped.TotalResidu = mapped.TotalResidu != 0 ? mapped.TotalResidu : existing.TotalResidu;
mapped.TotalTimbangan = mapped.TotalTimbangan != 0 ? mapped.TotalTimbangan : existing.TotalTimbangan;
mapped.FotoPetugas = mapped.FotoPetugas.Count > 0 ? mapped.FotoPetugas : (existing.FotoPetugas ?? new List<string>());
mapped.FotoPetugasUploaded = mapped.FotoPetugasUploaded || existing.FotoPetugasUploaded;
mapped.NamaPetugas = string.IsNullOrWhiteSpace(mapped.NamaPetugas) ? existing.NamaPetugas : mapped.NamaPetugas;
mapped.IsSubmit = existing.IsSubmit || mapped.IsSubmit;
mapped.SubmittedAt = existing.SubmittedAt;
allData[existingIndex] = mapped;
}
else
{
allData.Add(mapped);
}
await WriteAllToDiskAsync(allData);
}
finally
{
_fileLock.Release();
}
}
private static List<TimbanganItem> MergeTimbangan(List<TimbanganItem>? existingItems, List<TimbanganItem>? incomingItems)
{
var existing = existingItems ?? new List<TimbanganItem>();
var incoming = incomingItems ?? new List<TimbanganItem>();
if (incoming.Count == 0)
{
return existing;
}
var maxCount = Math.Max(existing.Count, incoming.Count);
var merged = new List<TimbanganItem>(maxCount);
for (var index = 0; index < maxCount; index++)
{
var existingItem = index < existing.Count ? existing[index] : null;
var incomingItem = index < incoming.Count ? incoming[index] : null;
if (existingItem == null && incomingItem == null)
{
continue;
}
if (existingItem == null)
{
merged.Add(CloneTimbanganItem(incomingItem!));
continue;
}
if (incomingItem == null)
{
merged.Add(CloneTimbanganItem(existingItem));
continue;
}
merged.Add(new TimbanganItem
{
FotoFileName = string.IsNullOrWhiteSpace(incomingItem.FotoFileName)
? existingItem.FotoFileName
: incomingItem.FotoFileName,
Berat = incomingItem.Berat != null && incomingItem.Berat.Count > 0
? new List<decimal>(incomingItem.Berat)
: new List<decimal>(existingItem.Berat ?? new List<decimal>()),
LokasiAngkut = incomingItem.LokasiAngkut != null && incomingItem.LokasiAngkut.Count > 0
? new List<string>(incomingItem.LokasiAngkut)
: new List<string>(existingItem.LokasiAngkut ?? new List<string>()),
JenisSampah = incomingItem.JenisSampah != null && incomingItem.JenisSampah.Count > 0
? new List<JenisSampah>(incomingItem.JenisSampah)
: new List<JenisSampah>(existingItem.JenisSampah ?? new List<JenisSampah>()),
IsUploaded = existingItem.IsUploaded || incomingItem.IsUploaded,
WaktuUpload = incomingItem.WaktuUpload ?? existingItem.WaktuUpload,
});
}
return merged;
}
private static TimbanganItem CloneTimbanganItem(TimbanganItem source)
{
return new TimbanganItem
{
FotoFileName = source.FotoFileName,
Berat = new List<decimal>(source.Berat ?? new List<decimal>()),
LokasiAngkut = new List<string>(source.LokasiAngkut ?? new List<string>()),
JenisSampah = new List<JenisSampah>(source.JenisSampah ?? new List<JenisSampah>()),
IsUploaded = source.IsUploaded,
WaktuUpload = source.WaktuUpload,
};
}
private async Task WriteAllToDiskAsync(List<TpsData> data)
{
var directory = Path.GetDirectoryName(_storeFilePath);
if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
data = data
.Where(item => item != null)
.OrderByDescending(item => item.UpdatedAt)
.GroupBy(GetRecordIdentity, StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.OrderBy(item => item.Name)
.ToList();
var options = new JsonSerializerOptions { WriteIndented = true };
var json = JsonSerializer.Serialize(data, options);
var tempPath = _storeFilePath + ".tmp";
await File.WriteAllTextAsync(tempPath, json);
File.Move(tempPath, _storeFilePath, overwrite: true);
}
private static int FindRecordIndex(List<TpsData> allData, string? nomorSpj, string? spjDetailId, string? lokasiAngkutId, string? namaTps)
{
return allData.FindIndex(item =>
item != null &&
!string.IsNullOrWhiteSpace(nomorSpj) &&
string.Equals(item.NomorSpj, nomorSpj, StringComparison.OrdinalIgnoreCase) &&
string.Equals(item.SpjDetailId, spjDetailId ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
string.Equals(item.LokasiAngkutId, lokasiAngkutId ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
string.Equals(item.Name, namaTps ?? string.Empty, StringComparison.OrdinalIgnoreCase));
}
private static TpsData MapRequestToRecord(RecordSaveRequest request)
{
return new TpsData
{
NomorSpj = request.NomorSpj,
LokasiAngkutId = request.LokasiAngkutId,
SpjDetailId = request.SpjDetailId,
Name = request.NamaTps,
Latitude = request.Latitude,
Longitude = request.Longitude,
AlamatJalan = request.AlamatJalan,
WaktuKedatangan = request.WaktuKedatangan,
FotoKedatangan = request.FotoKedatanganFileNames ?? new List<string>(),
FotoKedatanganUploaded = request.FotoKedatanganUploaded,
Timbangan = (request.Timbangan ?? new List<RecordTimbanganItem>())
.Where(item => item != null)
.Select(item => new TimbanganItem
{
FotoFileName = item.FotoFileName,
Berat = new List<decimal> { item.Berat },
LokasiAngkut = new List<string>(),
JenisSampah = Enum.TryParse<JenisSampah>(item.JenisSampah, out var parsedJenis)
? new List<JenisSampah> { parsedJenis }
: new List<JenisSampah> { JenisSampah.Residu },
IsUploaded = item.Uploaded,
WaktuUpload = DateTime.Now
}).ToList(),
TotalOrganik = request.TotalOrganik,
TotalAnorganik = request.TotalAnorganik,
TotalResidu = request.TotalResidu,
TotalTimbangan = request.TotalTimbangan,
FotoPetugas = request.FotoPetugasFileNames ?? new List<string>(),
FotoPetugasUploaded = request.FotoPetugasUploaded,
NamaPetugas = request.NamaPetugas,
IsSubmit = request.IsSubmit,
UpdatedAt = DateTime.Now,
SubmittedAt = request.IsSubmit ? DateTime.Now : null,
};
}
}
}

View File

@ -0,0 +1,14 @@
using eSPJ.Models;
namespace eSPJ.Services
{
public interface IDetailPenjemputanStore
{
Task<List<TpsData>> GetAllAsync();
Task<List<TpsData>> GetByNomorSpjAsync(string nomorSpj);
Task<List<TpsData>> GetSubmittedAsync();
Task<TpsData?> GetSubmittedDetailAsync(string nomorSpj, string? spjDetailId = null, string? lokasiAngkutId = null, string? namaTps = null);
Task SaveSubmittedAsync(TpsData data);
Task SaveRecordAsync(RecordSaveRequest request);
}
}

View File

@ -10,7 +10,7 @@
<script> <script>
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
window.addEventListener("load", () => { window.addEventListener("load", () => {
navigator.serviceWorker.register("@Url.Content("~/driver/serviceworker.js")", { scope: "/upst" }); navigator.serviceWorker.register("@Url.Content("~/driver/serviceworker.js")", { scope: "/upst", updateViaCache: "none" });
}); });
} }
</script> </script>

View File

@ -146,7 +146,6 @@
--blur-md: 12px; --blur-md: 12px;
--blur-lg: 16px; --blur-lg: 16px;
--blur-xl: 24px; --blur-xl: 24px;
--blur-3xl: 64px;
--default-transition-duration: 150ms; --default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-sans); --default-font-family: var(--font-sans);
@ -359,9 +358,6 @@
.-top-4 { .-top-4 {
top: calc(var(--spacing) * -4); top: calc(var(--spacing) * -4);
} }
.-top-24 {
top: calc(var(--spacing) * -24);
}
.top-0 { .top-0 {
top: calc(var(--spacing) * 0); top: calc(var(--spacing) * 0);
} }
@ -404,9 +400,6 @@
.-right-6 { .-right-6 {
right: calc(var(--spacing) * -6); right: calc(var(--spacing) * -6);
} }
.-right-24 {
right: calc(var(--spacing) * -24);
}
.right-0 { .right-0 {
right: calc(var(--spacing) * 0); right: calc(var(--spacing) * 0);
} }
@ -434,9 +427,6 @@
.right-full { .right-full {
right: 100%; right: 100%;
} }
.-bottom-0 {
bottom: calc(var(--spacing) * -0);
}
.-bottom-0\.5 { .-bottom-0\.5 {
bottom: calc(var(--spacing) * -0.5); bottom: calc(var(--spacing) * -0.5);
} }
@ -446,9 +436,6 @@
.-bottom-6 { .-bottom-6 {
bottom: calc(var(--spacing) * -6); bottom: calc(var(--spacing) * -6);
} }
.-bottom-32 {
bottom: calc(var(--spacing) * -32);
}
.bottom-0 { .bottom-0 {
bottom: calc(var(--spacing) * 0); bottom: calc(var(--spacing) * 0);
} }
@ -476,15 +463,9 @@
.bottom-100 { .bottom-100 {
bottom: calc(var(--spacing) * 100); bottom: calc(var(--spacing) * 100);
} }
.-left-32 {
left: calc(var(--spacing) * -32);
}
.left-0 { .left-0 {
left: calc(var(--spacing) * 0); left: calc(var(--spacing) * 0);
} }
.left-1 {
left: calc(var(--spacing) * 1);
}
.left-1\/2 { .left-1\/2 {
left: calc(1/2 * 100%); left: calc(1/2 * 100%);
} }
@ -542,6 +523,9 @@
.z-\[100\] { .z-\[100\] {
z-index: 100; z-index: 100;
} }
.z-\[9999\] {
z-index: 9999;
}
.order-0 { .order-0 {
order: 0; order: 0;
} }
@ -800,9 +784,6 @@
.-mr-16 { .-mr-16 {
margin-right: calc(var(--spacing) * -16); margin-right: calc(var(--spacing) * -16);
} }
.mr-1 {
margin-right: calc(var(--spacing) * 1);
}
.mr-1\.5 { .mr-1\.5 {
margin-right: calc(var(--spacing) * 1.5); margin-right: calc(var(--spacing) * 1.5);
} }
@ -893,18 +874,12 @@
.aspect-square { .aspect-square {
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
} }
.h-0 {
height: calc(var(--spacing) * 0);
}
.h-0\.5 { .h-0\.5 {
height: calc(var(--spacing) * 0.5); height: calc(var(--spacing) * 0.5);
} }
.h-1 { .h-1 {
height: calc(var(--spacing) * 1); height: calc(var(--spacing) * 1);
} }
.h-1\.5 {
height: calc(var(--spacing) * 1.5);
}
.h-2 { .h-2 {
height: calc(var(--spacing) * 2); height: calc(var(--spacing) * 2);
} }
@ -977,15 +952,9 @@
.h-64 { .h-64 {
height: calc(var(--spacing) * 64); height: calc(var(--spacing) * 64);
} }
.h-72 {
height: calc(var(--spacing) * 72);
}
.h-75 { .h-75 {
height: calc(var(--spacing) * 75); height: calc(var(--spacing) * 75);
} }
.h-96 {
height: calc(var(--spacing) * 96);
}
.h-100 { .h-100 {
height: calc(var(--spacing) * 100); height: calc(var(--spacing) * 100);
} }
@ -1016,9 +985,6 @@
.w-1 { .w-1 {
width: calc(var(--spacing) * 1); width: calc(var(--spacing) * 1);
} }
.w-1\.5 {
width: calc(var(--spacing) * 1.5);
}
.w-1\/3 { .w-1\/3 {
width: calc(1/3 * 100%); width: calc(1/3 * 100%);
} }
@ -1094,15 +1060,9 @@
.w-64 { .w-64 {
width: calc(var(--spacing) * 64); width: calc(var(--spacing) * 64);
} }
.w-72 {
width: calc(var(--spacing) * 72);
}
.w-75 { .w-75 {
width: calc(var(--spacing) * 75); width: calc(var(--spacing) * 75);
} }
.w-96 {
width: calc(var(--spacing) * 96);
}
.w-100 { .w-100 {
width: calc(var(--spacing) * 100); width: calc(var(--spacing) * 100);
} }
@ -1121,9 +1081,6 @@
.w-max { .w-max {
width: max-content; width: max-content;
} }
.max-w-\[260px\] {
max-width: 260px;
}
.max-w-full { .max-w-full {
max-width: 100%; max-width: 100%;
} }
@ -1181,10 +1138,6 @@
.border-collapse { .border-collapse {
border-collapse: 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 { .-translate-x-1\/2 {
--tw-translate-x: calc(calc(1/2 * 100%) * -1); --tw-translate-x: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
@ -1197,10 +1150,6 @@
--tw-translate-x: calc(var(--spacing) * 16); --tw-translate-x: calc(var(--spacing) * 16);
translate: var(--tw-translate-x) var(--tw-translate-y); 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 { .-translate-y-1\/2 {
--tw-translate-y: calc(calc(1/2 * 100%) * -1); --tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y); translate: var(--tw-translate-x) var(--tw-translate-y);
@ -1327,13 +1276,6 @@
.gap-6 { .gap-6 {
gap: calc(var(--spacing) * 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 { .space-y-0\.5 {
:where(& > :not(:last-child)) { :where(& > :not(:last-child)) {
--tw-space-y-reverse: 0; --tw-space-y-reverse: 0;
@ -1710,9 +1652,6 @@
.border-t-transparent { .border-t-transparent {
border-top-color: transparent; border-top-color: transparent;
} }
.border-t-white {
border-top-color: var(--color-white);
}
.bg-amber-400 { .bg-amber-400 {
background-color: var(--color-amber-400); background-color: var(--color-amber-400);
} }
@ -1770,9 +1709,6 @@
.bg-blue-600 { .bg-blue-600 {
background-color: var(--color-blue-600); background-color: var(--color-blue-600);
} }
.bg-cyan-400 {
background-color: var(--color-cyan-400);
}
.bg-cyan-400\/10 { .bg-cyan-400\/10 {
background-color: color-mix(in srgb, oklch(78.9% 0.154 211.53) 10%, transparent); background-color: color-mix(in srgb, oklch(78.9% 0.154 211.53) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@ -1836,9 +1772,6 @@
.bg-indigo-300 { .bg-indigo-300 {
background-color: var(--color-indigo-300); background-color: var(--color-indigo-300);
} }
.bg-lime-500 {
background-color: var(--color-lime-500);
}
.bg-lime-500\/15 { .bg-lime-500\/15 {
background-color: color-mix(in srgb, oklch(76.8% 0.233 130.85) 15%, transparent); background-color: color-mix(in srgb, oklch(76.8% 0.233 130.85) 15%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@ -1860,9 +1793,6 @@
.bg-orange-500 { .bg-orange-500 {
background-color: var(--color-orange-500); background-color: var(--color-orange-500);
} }
.bg-orange-700 {
background-color: var(--color-orange-700);
}
.bg-orange-700\/30 { .bg-orange-700\/30 {
background-color: color-mix(in srgb, oklch(55.3% 0.195 38.402) 30%, transparent); background-color: color-mix(in srgb, oklch(55.3% 0.195 38.402) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@ -1905,9 +1835,6 @@
.bg-slate-900 { .bg-slate-900 {
background-color: var(--color-slate-900); background-color: var(--color-slate-900);
} }
.bg-slate-950 {
background-color: var(--color-slate-950);
}
.bg-slate-950\/60 { .bg-slate-950\/60 {
background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 60%, transparent); background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 60%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@ -1938,18 +1865,18 @@
background-color: color-mix(in oklab, var(--color-white) 20%, transparent); background-color: color-mix(in oklab, var(--color-white) 20%, transparent);
} }
} }
.bg-white\/30 {
background-color: color-mix(in srgb, #fff 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 30%, transparent);
}
}
.bg-white\/70 { .bg-white\/70 {
background-color: color-mix(in srgb, #fff 70%, transparent); background-color: color-mix(in srgb, #fff 70%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 70%, transparent); 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 { .bg-yellow-50 {
background-color: var(--color-yellow-50); background-color: var(--color-yellow-50);
} }
@ -2298,6 +2225,9 @@
.py-6 { .py-6 {
padding-block: calc(var(--spacing) * 6); padding-block: calc(var(--spacing) * 6);
} }
.py-7 {
padding-block: calc(var(--spacing) * 7);
}
.py-12 { .py-12 {
padding-block: calc(var(--spacing) * 12); padding-block: calc(var(--spacing) * 12);
} }
@ -2543,10 +2473,6 @@
--tw-tracking: 0.24em; --tw-tracking: 0.24em;
letter-spacing: 0.24em; letter-spacing: 0.24em;
} }
.tracking-\[0\.28em\] {
--tw-tracking: 0.28em;
letter-spacing: 0.28em;
}
.tracking-tight { .tracking-tight {
--tw-tracking: var(--tracking-tight); --tw-tracking: var(--tracking-tight);
letter-spacing: var(--tracking-tight); letter-spacing: var(--tracking-tight);
@ -2732,30 +2658,12 @@
.text-white { .text-white {
color: var(--color-white); color: var(--color-white);
} }
.text-white\/30 {
color: color-mix(in srgb, #fff 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-white) 30%, transparent);
}
}
.text-white\/40 {
color: color-mix(in srgb, #fff 40%, transparent);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-white) 40%, transparent);
}
}
.text-white\/70 { .text-white\/70 {
color: color-mix(in srgb, #fff 70%, transparent); color: color-mix(in srgb, #fff 70%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-white) 70%, transparent); 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 { .text-white\/90 {
color: color-mix(in srgb, #fff 90%, transparent); color: color-mix(in srgb, #fff 90%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@ -2816,9 +2724,6 @@
.opacity-60 { .opacity-60 {
opacity: 60%; opacity: 60%;
} }
.opacity-70 {
opacity: 70%;
}
.opacity-75 { .opacity-75 {
opacity: 75%; opacity: 75%;
} }
@ -2879,27 +2784,12 @@
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.shadow-black {
--tw-shadow-color: #000;
@supports (color: color-mix(in lab, red, red)) {
--tw-shadow-color: color-mix(in oklab, var(--color-black) var(--tw-shadow-alpha), transparent);
}
}
.shadow-black\/20 {
--tw-shadow-color: color-mix(in srgb, #000 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-black) 20%, transparent) var(--tw-shadow-alpha), transparent);
}
}
.shadow-gray-200 { .shadow-gray-200 {
--tw-shadow-color: oklch(92.8% 0.006 264.531); --tw-shadow-color: oklch(92.8% 0.006 264.531);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
--tw-shadow-color: color-mix(in oklab, var(--color-gray-200) var(--tw-shadow-alpha), transparent); --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 { .ring-black\/5 {
--tw-ring-color: color-mix(in srgb, #000 5%, transparent); --tw-ring-color: color-mix(in srgb, #000 5%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@ -2938,10 +2828,6 @@
--tw-blur: blur(8px); --tw-blur: blur(8px);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
} }
.blur-3xl {
--tw-blur: blur(var(--blur-3xl));
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.drop-shadow { .drop-shadow {
--tw-drop-shadow-size: drop-shadow(0 1px 2px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.1))) drop-shadow(0 1px 1px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.06))); --tw-drop-shadow-size: drop-shadow(0 1px 2px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.1))) drop-shadow(0 1px 1px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.06)));
--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow( 0 1px 1px rgb(0 0 0 / 0.06)); --tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow( 0 1px 1px rgb(0 0 0 / 0.06));
@ -3592,12 +3478,6 @@
scale: var(--tw-scale-x) var(--tw-scale-y); scale: var(--tw-scale-x) var(--tw-scale-y);
} }
} }
.active\:shadow-md {
&:active {
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
.disabled\:cursor-not-allowed { .disabled\:cursor-not-allowed {
&:disabled { &:disabled {
cursor: not-allowed; cursor: not-allowed;

View File

@ -8,22 +8,50 @@ document.addEventListener('DOMContentLoaded', async function() {
let activeTpsIndex = 0; let activeTpsIndex = 0;
let tpsData = []; let tpsData = [];
let nomorSpj = 'SPJ/07-2025/PKM/000476'; let nomorSpj = 'SPJ/07-2025/PKM/000476';
let draftRequestKey = ''; const RECORD_DETAIL_ENDPOINT = '/upst/detail-penjemputan/api/records/detail';
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, '');
}
let autoSaveTimer = null; let autoSaveTimer = null;
let autoSaveStatusEl = null; let autoSaveStatusEl = null;
let loadingOverlayEl = null;
let isAutoSaving = false;
let pendingAutoSave = false;
let lastAutoSaveSignature = "";
function getLoadingOverlay() {
if (loadingOverlayEl && document.body.contains(loadingOverlayEl)) {
return loadingOverlayEl;
}
loadingOverlayEl = document.createElement('div');
loadingOverlayEl.id = 'detail-loading-overlay';
loadingOverlayEl.className = 'fixed inset-0 z-[9999] hidden bg-white/95 backdrop-blur-sm flex items-center justify-center px-6';
loadingOverlayEl.innerHTML = `
<div class="w-full max-w-sm rounded-3xl border border-gray-200 bg-white shadow-2xl px-6 py-7 text-center">
<div class="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-upst"></div>
<h2 class="text-base font-black text-gray-800">Sedang memuat data</h2>
<p class="mt-2 text-sm text-gray-500">Mohon tunggu sebentar, data penjemputan sedang dipulihkan.</p>
</div>
`;
document.body.appendChild(loadingOverlayEl);
return loadingOverlayEl;
}
function showLoadingOverlay() {
const overlay = getLoadingOverlay();
overlay.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function hideLoadingOverlay() {
const overlay = getLoadingOverlay();
overlay.classList.add('hidden');
document.body.style.overflow = '';
}
function scheduleAutoSave() { function scheduleAutoSave() {
clearTimeout(autoSaveTimer); clearTimeout(autoSaveTimer);
showAutoSaveStatus('menyimpan...'); showAutoSaveStatus('menyimpan...');
autoSaveTimer = setTimeout(autoSaveDraft, 1000); autoSaveTimer = setTimeout(autoSaveRecord, 500);
} }
function showAutoSaveStatus(msg, isOk = false) { function showAutoSaveStatus(msg, isOk = false) {
@ -39,150 +67,176 @@ document.addEventListener('DOMContentLoaded', async function() {
if (isOk) setTimeout(() => { autoSaveStatusEl.style.opacity = '0'; }, 2500); if (isOk) setTimeout(() => { autoSaveStatusEl.style.opacity = '0'; }, 2500);
} }
async function autoSaveDraft() { function buildAutoSavePayload(tps) {
return {
nomorSpj: nomorSpj || '',
namaTps: tps.name || DEFAULT_TPS_NAME,
lokasiAngkutId: tps.lokasiAngkutId || '',
spjDetailId: tps.spjDetailId || '',
latitude: tps.latitude || '',
longitude: tps.longitude || '',
alamatJalan: tps.alamatJalan || '',
waktuKedatangan: tps.waktuKedatangan || '',
fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [],
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false,
timbangan: (tps.timbangan || []).map(t => ({
berat: (t.berat && t.berat.length > 0) ? t.berat[0] : 0,
jenisSampah: (t.jenisSampah && t.jenisSampah.length > 0) ? t.jenisSampah[0] : DEFAULT_JENIS,
fotoFileName: t.fotoFileName || '',
uploaded: t.uploaded || false,
ocrInfo: t.ocrInfo || ''
})),
totalOrganik: tps.totalOrganik || 0,
totalAnorganik: tps.totalAnorganik || 0,
totalResidu: tps.totalResidu || 0,
totalTimbangan: tps.totalTimbangan || 0,
fotoPetugasFileNames: tps.fotoPetugasFileNames || [],
fotoPetugasUploaded: tps.fotoPetugasUploaded || false,
namaPetugas: tps.namaPetugas || ''
};
}
async function autoSaveRecord() {
const tps = tpsData[activeTpsIndex];
if (!tps) return;
if (tps.submitted) return;
if (isAutoSaving) {
pendingAutoSave = true;
return;
}
const payload = buildAutoSavePayload(tps);
const payloadSignature = JSON.stringify(payload);
if (payloadSignature === lastAutoSaveSignature) {
showAutoSaveStatus('✓ Data tersimpan', true);
return;
}
try {
isAutoSaving = true;
const res = await fetch('/upst/detail-penjemputan/save-record-non-tps', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payloadSignature
});
const data = await res.json();
if (data.success) {
lastAutoSaveSignature = payloadSignature;
}
showAutoSaveStatus(data.success ? '✓ Data tersimpan' : '✗ Gagal simpan', data.success);
} catch {
showAutoSaveStatus('✗ Gagal simpan data');
} finally {
isAutoSaving = false;
if (pendingAutoSave) {
pendingAutoSave = false;
scheduleAutoSave();
}
}
}
function normalizeStringList(value) {
if (!Array.isArray(value)) {
return [];
}
return value.filter((item) => typeof item === 'string' && item.trim());
}
function normalizeJenisSampahValue(value) {
if (Array.isArray(value)) {
return normalizeJenisSampahValue(value[0]);
}
if (typeof value === 'number') {
return JENIS_SAMPAH[value] || DEFAULT_JENIS;
}
if (typeof value === 'string' && value.trim()) {
const trimmed = value.trim();
const asNumber = Number(trimmed);
if (!Number.isNaN(asNumber) && String(asNumber) === trimmed) {
return JENIS_SAMPAH[asNumber] || DEFAULT_JENIS;
}
const matched = JENIS_SAMPAH.find(
(item) => item.toLowerCase() === trimmed.toLowerCase(),
);
return matched || DEFAULT_JENIS;
}
return DEFAULT_JENIS;
}
function applyServerRecordToTps(record) {
if (!record) return;
const tps = tpsData[activeTpsIndex]; const tps = tpsData[activeTpsIndex];
if (!tps) return; if (!tps) return;
const fotoKedatangan = normalizeStringList(
record.fotoKedatanganFileNames || record.fotoKedatangan || record.FotoKedatangan,
);
const fotoPetugas = normalizeStringList(
record.fotoPetugasFileNames || record.fotoPetugas || record.FotoPetugas,
);
const payload = { tps.name = record.namaTps || record.name || record.Name || tps.name || DEFAULT_TPS_NAME;
draftKey: draftRequestKey, tps.lokasiAngkutId = record.lokasiAngkutId || record.LokasiAngkutID || tps.lokasiAngkutId;
lokasiAngkutId: tps.lokasiAngkutId || '', tps.spjDetailId = record.spjDetailId || record.SpjDetailID || tps.spjDetailId;
spjDetailId: tps.spjDetailId || '', tps.latitude = record.latitude || record.Latitude || tps.latitude;
latitude: tps.latitude || '', tps.longitude = record.longitude || record.Longitude || tps.longitude;
longitude: tps.longitude || '', tps.alamatJalan = record.alamatJalan || record.AlamatJalan || tps.alamatJalan;
alamatJalan: tps.alamatJalan || '', tps.waktuKedatangan = record.waktuKedatangan || record.WaktuKedatangan || tps.waktuKedatangan;
waktuKedatangan: tps.waktuKedatangan || '', tps.fotoKedatangan = [];
fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [], tps.fotoKedatanganFileNames = fotoKedatangan;
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false, tps.fotoKedatanganUploaded = Boolean(record.fotoKedatanganUploaded ?? record.FotoKedatanganUploaded) || fotoKedatangan.length > 0;
timbangan: (tps.timbangan || []).map(t => ({ tps.fotoPetugas = [];
berat: (t.berat && t.berat.length > 0) ? t.berat[0] : 0, tps.fotoPetugasFileNames = fotoPetugas;
jenisSampah: (t.jenisSampah && t.jenisSampah.length > 0) ? t.jenisSampah[0] : 'Residu', tps.fotoPetugasUploaded = Boolean(record.fotoPetugasUploaded ?? record.FotoPetugasUploaded) || fotoPetugas.length > 0;
fotoFileName: t.fotoFileName || '', tps.namaPetugas = record.namaPetugas || record.NamaPetugas || tps.namaPetugas;
uploaded: t.uploaded || false, tps.totalOrganik = Number(record.totalOrganik ?? record.TotalOrganik ?? tps.totalOrganik) || 0;
ocrInfo: t.ocrInfo || '' tps.totalAnorganik = Number(record.totalAnorganik ?? record.TotalAnorganik ?? tps.totalAnorganik) || 0;
})), tps.totalResidu = Number(record.totalResidu ?? record.TotalResidu ?? tps.totalResidu) || 0;
totalOrganik: tps.totalOrganik || 0, tps.totalTimbangan = Number(record.totalTimbangan ?? record.TotalTimbangan ?? tps.totalTimbangan) || 0;
totalAnorganik: tps.totalAnorganik || 0, tps.submitted = Boolean(record.isSubmit ?? record.IsSubmit ?? record.submitted ?? record.Submitted ?? tps.submitted);
totalResidu: tps.totalResidu || 0,
totalTimbangan: tps.totalTimbangan || 0,
fotoPetugasFileNames: tps.fotoPetugasFileNames || [],
fotoPetugasUploaded: tps.fotoPetugasUploaded || false,
namaPetugas: tps.namaPetugas || ''
};
try { const timbangan = record.timbangan || record.Timbangan || [];
const res = await fetch('/upst/detail-penjemputan/save-draft-non-tps', { if (Array.isArray(timbangan) && timbangan.length > 0) {
method: 'POST', tps.timbangan = timbangan.map(item => ({
headers: { 'Content-Type': 'application/json' }, file: null,
body: JSON.stringify(payload) fotoFileName: item.fotoFileName || item.FotoFileName || '',
}); berat: [Number(item.berat ?? (Array.isArray(item.Berat) ? item.Berat[0] : item.Berat) ?? 0) || 0],
const data = await res.json(); jenisSampah: [normalizeJenisSampahValue(item.jenisSampah ?? item.JenisSampah)],
showAutoSaveStatus(data.success ? '✓ Draft tersimpan' : '✗ Gagal simpan', data.success); lokasiAngkut: [],
} catch { uploaded: Boolean(item.uploaded ?? item.isUploaded ?? item.IsUploaded ?? item.fotoFileName ?? item.FotoFileName),
showAutoSaveStatus('✗ Gagal simpan draft'); ocrInfo: item.ocrInfo || item.OcrInfo || (item.fotoFileName || item.FotoFileName ? 'Foto dari server.' : 'OCR: diproses.')
}));
} else {
tps.timbangan = [];
} }
} }
async function loadDraftFromServer() { async function loadRecordForCurrentSpj() {
if (!draftRequestKey) return;
try {
const res = await fetch(`/upst/detail-penjemputan/load-draft-non-tps?draftKey=${encodeURIComponent(draftRequestKey)}`);
if (!res.ok) return;
const data = await res.json();
if (!data.success || !data.hasDraft || !data.draft) return;
const d = data.draft;
const tps = tpsData[activeTpsIndex];
if (!tps) return;
if (d.lokasiAngkutId) tps.lokasiAngkutId = d.lokasiAngkutId;
if (d.spjDetailId) tps.spjDetailId = d.spjDetailId;
if (d.latitude) tps.latitude = d.latitude;
if (d.longitude) tps.longitude = d.longitude;
if (d.alamatJalan) tps.alamatJalan = d.alamatJalan;
if (d.waktuKedatangan) tps.waktuKedatangan = d.waktuKedatangan;
tps.fotoKedatanganFileNames = d.fotoKedatanganFileNames || [];
tps.fotoKedatanganUploaded = d.fotoKedatanganUploaded || false;
tps.fotoPetugasFileNames = d.fotoPetugasFileNames || [];
tps.fotoPetugasUploaded = d.fotoPetugasUploaded || false;
tps.namaPetugas = d.namaPetugas || '';
tps.totalOrganik = d.totalOrganik || 0;
tps.totalAnorganik = d.totalAnorganik || 0;
tps.totalResidu = d.totalResidu || 0;
tps.totalTimbangan = d.totalTimbangan || 0;
if (d.timbangan && d.timbangan.length > 0) {
tps.timbangan = d.timbangan.map(t => ({
file: null,
fotoFileName: t.fotoFileName || '',
berat: [t.berat || 0],
jenisSampah: [t.jenisSampah || 'Residu'],
lokasiAngkut: [],
uploaded: t.uploaded || false,
ocrInfo: t.ocrInfo || 'OCR: diproses.'
}));
}
patchFormFromDraft();
showAutoSaveStatus('✓ Draft dipulihkan', true);
} catch (err) {
console.warn('Gagal memuat draft dari server:', err);
}
}
function patchFormFromDraft() {
const tps = tpsData[activeTpsIndex]; const tps = tpsData[activeTpsIndex];
const form = tpsContentContainer.querySelector('form'); if (!tps || !nomorSpj) return;
if (!form) return;
const hiddenLat = form.querySelector('.tps-latitude'); const params = new URLSearchParams({ nomorSpj });
const hiddenLng = form.querySelector('.tps-longitude'); if (tps.spjDetailId) params.set('spjDetailId', tps.spjDetailId);
const hiddenAlamat = form.querySelector('.tps-alamat-jalan'); if (tps.lokasiAngkutId) params.set('lokasiAngkutId', tps.lokasiAngkutId);
if (hiddenLat) hiddenLat.value = tps.latitude; if (tps.name) params.set('namaTps', tps.name);
if (hiddenLng) hiddenLng.value = tps.longitude;
if (hiddenAlamat) hiddenAlamat.value = tps.alamatJalan;
const latDisplay = form.querySelector('.tps-display-latitude'); try {
const lngDisplay = form.querySelector('.tps-display-longitude'); const res = await fetch(`${RECORD_DETAIL_ENDPOINT}?${params.toString()}`, { cache: 'no-store' });
const waktuDisplay = form.querySelector('.tps-waktu-kedatangan'); if (!res.ok) return;
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'); const data = await res.json();
if (namaPetugasInput && tps.namaPetugas) namaPetugasInput.value = tps.namaPetugas; if (!data.success || !data.hasData || !data.item) return;
refreshKedatanganUploadState(form); applyServerRecordToTps(data.item);
refreshPetugasUploadState(form); } catch (error) {
console.warn('Gagal memuat data non-TPS:', error);
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 OCR_AREAS = [ const OCR_AREAS = [
{ {
@ -213,12 +267,11 @@ document.addEventListener('DOMContentLoaded', async function() {
const JENIS_SAMPAH = ["Organik", "Anorganik", "Residu"]; const JENIS_SAMPAH = ["Organik", "Anorganik", "Residu"];
const DEFAULT_JENIS = "Residu"; const DEFAULT_JENIS = "Residu";
const DETAIL_DATA_URL = "/driver/json/detail-penjemputan-non-tps.json"; const DETAIL_DATA_URL = "/driver/json/detail-penjemputan-non-tps.json";
const DEFAULT_TPS_NAME = "Lokasi Pengangkutan 1"; const DEFAULT_TPS_NAME = "TPS 1";
function isBrowserFile(file) { function isBrowserFile(file) {
return file instanceof File; return file instanceof File;
} }
function resolveStoredPhoto(file) { function resolveStoredPhoto(file) {
return isBrowserFile(file) ? file : null; return isBrowserFile(file) ? file : null;
} }
@ -283,7 +336,6 @@ document.addEventListener('DOMContentLoaded', async function() {
tpsData[0].name = namaTps; tpsData[0].name = namaTps;
tpsData[0].lokasiAngkutId = detail.lokasiAngkutId || detail.LokasiAngkutID || tpsData[0].lokasiAngkutId; tpsData[0].lokasiAngkutId = detail.lokasiAngkutId || detail.LokasiAngkutID || tpsData[0].lokasiAngkutId;
tpsData[0].spjDetailId = detail.spjDetailId || detail.SpjDetailID || tpsData[0].spjDetailId; tpsData[0].spjDetailId = detail.spjDetailId || detail.SpjDetailID || tpsData[0].spjDetailId;
draftRequestKey = buildDraftRequestKey(tpsData[0]);
} }
nomorSpj = detail.nomorSpj || nomorSpj; nomorSpj = detail.nomorSpj || nomorSpj;
@ -324,6 +376,23 @@ document.addEventListener('DOMContentLoaded', async function() {
function renderTpsForm() { function renderTpsForm() {
const tps = tpsData[activeTpsIndex]; const tps = tpsData[activeTpsIndex];
const submitState = getSubmitState(tps); const submitState = getSubmitState(tps);
const actionMarkup = tps.submitted
? `
<div class="flex items-center justify-center gap-2 rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 flex-shrink-0 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="font-medium text-green-700">
Data <strong class="font-bold text-green-800">${tps.name || DEFAULT_TPS_NAME} </strong> sudah disubmit
</span>
</div>`
: `
<div class="flex gap-3">
<a href="/upst/detail-penjemputan/batal" class="w-1/3 text-center bg-red-500 text-white py-3 rounded-xl font-bold text-sm">Batal</a>
<button type="submit" ${submitState.canSubmit ? "" : "disabled"} class="w-2/3 py-3 rounded-xl font-bold text-sm transition ${submitState.canSubmit ? "bg-upst text-white hover:brightness-110" : "bg-gray-300 text-gray-500 cursor-not-allowed"}">Submit</button>
</div>
${submitState.canSubmit ? '' : `<p class="submit-state-message text-[11px] text-center text-red-500 font-medium">${submitState.message}</p>`}
`;
tpsContentContainer.innerHTML = ` tpsContentContainer.innerHTML = `
<form class="space-y-5 pb-8" data-tps-index="${tps.index}"> <form class="space-y-5 pb-8" data-tps-index="${tps.index}">
@ -406,11 +475,7 @@ document.addEventListener('DOMContentLoaded', async function() {
</div> </div>
</section> </section>
<div class="flex gap-3"> ${actionMarkup}
<a href="/upst/detail-penjemputan/batal" class="w-1/3 text-center bg-red-500 text-white py-3 rounded-xl font-bold text-sm">Batal</a>
<button type="submit" ${submitState.canSubmit ? "" : "disabled"} class="w-2/3 py-3 rounded-xl font-bold text-sm transition ${submitState.canSubmit ? "bg-upst text-white hover:brightness-110" : "bg-gray-300 text-gray-500 cursor-not-allowed"}">Submit</button>
</div>
${submitState.canSubmit ? '' : `<p class="submit-state-message text-[11px] text-center text-red-500 font-medium">${submitState.message}</p>`}
<p id="auto-save-status" class="text-[11px] text-amber-500 text-center font-medium" style="opacity:0;transition:opacity 0.4s"></p> <p id="auto-save-status" class="text-[11px] text-amber-500 text-center font-medium" style="opacity:0;transition:opacity 0.4s"></p>
</form> </form>
`; `;
@ -479,6 +544,8 @@ document.addEventListener('DOMContentLoaded', async function() {
const form = tpsContentContainer.querySelector("form"); const form = tpsContentContainer.querySelector("form");
const tps = tpsData[activeTpsIndex]; const tps = tpsData[activeTpsIndex];
if (tps.submitted) return;
const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan"); const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan");
const fotoPetugasInput = form.querySelector(".tps-foto-petugas"); const fotoPetugasInput = form.querySelector(".tps-foto-petugas");
const namaPetugasInput = form.querySelector(".tps-nama-petugas"); const namaPetugasInput = form.querySelector(".tps-nama-petugas");
@ -490,7 +557,6 @@ document.addEventListener('DOMContentLoaded', async function() {
updateWaktuKedatangan(); updateWaktuKedatangan();
updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan')); updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan'));
refreshKedatanganUploadState(form); refreshKedatanganUploadState(form);
scheduleAutoSave();
}); });
fotoPetugasInput.addEventListener('change', function() { fotoPetugasInput.addEventListener('change', function() {
@ -498,12 +564,12 @@ document.addEventListener('DOMContentLoaded', async function() {
tps.fotoPetugasUploaded = false; tps.fotoPetugasUploaded = false;
updateMultiPreview(this, form.querySelector('.tps-preview-petugas')); updateMultiPreview(this, form.querySelector('.tps-preview-petugas'));
refreshPetugasUploadState(form); refreshPetugasUploadState(form);
scheduleAutoSave();
}); });
namaPetugasInput.addEventListener('input', function() { namaPetugasInput.addEventListener('input', function() {
tps.namaPetugas = this.value; tps.namaPetugas = this.value;
refreshPetugasUploadState(form); refreshPetugasUploadState(form);
scheduleAutoSave();
}); });
namaPetugasInput.addEventListener('blur', function() { namaPetugasInput.addEventListener('blur', function() {
@ -565,7 +631,6 @@ document.addEventListener('DOMContentLoaded', async function() {
if (displayWaktu) displayWaktu.value = formatted; if (displayWaktu) displayWaktu.value = formatted;
getLocationUpdate(); getLocationUpdate();
scheduleAutoSave();
} }
function reverseGeocode(lat, lng) { function reverseGeocode(lat, lng) {
@ -587,7 +652,6 @@ document.addEventListener('DOMContentLoaded', async function() {
if (latInput) latInput.value = lat; if (latInput) latInput.value = lat;
if (lngInput) lngInput.value = lng; if (lngInput) lngInput.value = lng;
} }
scheduleAutoSave();
}) })
.catch(() => { .catch(() => {
tps.latitude = lat; tps.latitude = lat;
@ -601,7 +665,6 @@ document.addEventListener('DOMContentLoaded', async function() {
if (latInput) latInput.value = lat; if (latInput) latInput.value = lat;
if (lngInput) lngInput.value = lng; if (lngInput) lngInput.value = lng;
} }
scheduleAutoSave();
}); });
} }
@ -968,7 +1031,6 @@ document.addEventListener('DOMContentLoaded', async function() {
formatWeightDisplay(totalAnorganik); formatWeightDisplay(totalAnorganik);
if (grandTotalResiduDisplay) if (grandTotalResiduDisplay)
grandTotalResiduDisplay.textContent = formatWeightDisplay(totalResidu); grandTotalResiduDisplay.textContent = formatWeightDisplay(totalResidu);
scheduleAutoSave();
} }
function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) { function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) {
@ -1066,6 +1128,10 @@ document.addEventListener('DOMContentLoaded', async function() {
} }
function getSubmitState(tps) { function getSubmitState(tps) {
if (tps?.submitted) {
return { canSubmit: false, message: '' };
}
if (!tps.fotoKedatanganUploaded) { if (!tps.fotoKedatanganUploaded) {
if (!tps.fotoKedatangan.length) if (!tps.fotoKedatangan.length)
return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan.' }; return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan.' };
@ -1105,10 +1171,10 @@ document.addEventListener('DOMContentLoaded', async function() {
function refreshSubmitButtonState(form) { function refreshSubmitButtonState(form) {
const submitButton = form.querySelector('button[type="submit"]'); const submitButton = form.querySelector('button[type="submit"]');
if (!submitButton) return; const tps = tpsData[activeTpsIndex];
if (tps?.submitted || !submitButton) return;
const helperText = form.querySelector(".submit-state-message"); const helperText = form.querySelector(".submit-state-message");
const tps = tpsData[activeTpsIndex];
const submitState = getSubmitState(tps); const submitState = getSubmitState(tps);
submitButton.disabled = !submitState.canSubmit; submitButton.disabled = !submitState.canSubmit;
@ -1275,7 +1341,6 @@ document.addEventListener('DOMContentLoaded', async function() {
if (itemIndex >= 0 && tps.timbangan[itemIndex]) { if (itemIndex >= 0 && tps.timbangan[itemIndex]) {
tps.timbangan[itemIndex].uploaded = false; tps.timbangan[itemIndex].uploaded = false;
refreshTimbanganUploadState(item); refreshTimbanganUploadState(item);
scheduleAutoSave();
} }
} }
}); });
@ -1290,6 +1355,7 @@ document.addEventListener('DOMContentLoaded', async function() {
refreshTimbanganUploadState(item); refreshTimbanganUploadState(item);
const form = tpsContentContainer.querySelector("form"); const form = tpsContentContainer.querySelector("form");
if (form) refreshSubmitButtonState(form); if (form) refreshSubmitButtonState(form);
scheduleAutoSave();
}); });
weightInputDisplay.addEventListener('blur', function() { weightInputDisplay.addEventListener('blur', function() {
@ -1314,7 +1380,6 @@ document.addEventListener('DOMContentLoaded', async function() {
syncTimbanganToTpsData(); syncTimbanganToTpsData();
const form = tpsContentContainer.querySelector('form'); const form = tpsContentContainer.querySelector('form');
if (form) refreshSubmitButtonState(form); if (form) refreshSubmitButtonState(form);
scheduleAutoSave();
}); });
removeBtn.addEventListener('click', function() { removeBtn.addEventListener('click', function() {
@ -1328,7 +1393,6 @@ document.addEventListener('DOMContentLoaded', async function() {
updateTpsTotalTimbangan(); updateTpsTotalTimbangan();
syncTimbanganToTpsData(); syncTimbanganToTpsData();
if (form) refreshSubmitButtonState(form); if (form) refreshSubmitButtonState(form);
scheduleAutoSave();
}); });
repeater.appendChild(item); repeater.appendChild(item);
@ -1367,8 +1431,10 @@ document.addEventListener('DOMContentLoaded', async function() {
function buildSubmitFormData(tps) { function buildSubmitFormData(tps) {
const formData = new FormData(); const formData = new FormData();
formData.append("LokasiAngkutID", tps.lokasiAngkutId || ""); formData.append("NomorSpj", nomorSpj || "");
formData.append("SpjDetailID", tps.spjDetailId || ""); formData.append("TpsName", tps.name || DEFAULT_TPS_NAME);
formData.append("LokasiAngkutId", tps.lokasiAngkutId || "");
formData.append("SpjDetailId", tps.spjDetailId || "");
formData.append("Latitude", tps.latitude); formData.append("Latitude", tps.latitude);
formData.append("Longitude", tps.longitude); formData.append("Longitude", tps.longitude);
formData.append("AlamatJalan", tps.alamatJalan); formData.append("AlamatJalan", tps.alamatJalan);
@ -1440,7 +1506,8 @@ document.addEventListener('DOMContentLoaded', async function() {
const formData = new FormData(); const formData = new FormData();
formData.append('FotoTimbangan', tps.timbangan[itemIndex].file); formData.append('FotoTimbangan', tps.timbangan[itemIndex].file);
formData.append('DraftKey', draftRequestKey); formData.append('NomorSpj', nomorSpj || '');
formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME);
formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('SpjDetailId', tps.spjDetailId || '');
formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
formData.append('ItemIndex', itemIndex); formData.append('ItemIndex', itemIndex);
@ -1485,7 +1552,8 @@ document.addEventListener('DOMContentLoaded', async function() {
const formData = new FormData(); const formData = new FormData();
tps.fotoKedatangan.forEach(f => formData.append('FotoKedatangan', f)); tps.fotoKedatangan.forEach(f => formData.append('FotoKedatangan', f));
formData.append('DraftKey', draftRequestKey); formData.append('NomorSpj', nomorSpj || '');
formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME);
formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('SpjDetailId', tps.spjDetailId || '');
formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
formData.append('WaktuKedatangan', tps.waktuKedatangan || ''); formData.append('WaktuKedatangan', tps.waktuKedatangan || '');
@ -1533,7 +1601,8 @@ document.addEventListener('DOMContentLoaded', async function() {
const formData = new FormData(); const formData = new FormData();
tps.fotoPetugas.forEach(f => formData.append('FotoPetugas', f)); tps.fotoPetugas.forEach(f => formData.append('FotoPetugas', f));
formData.append('DraftKey', draftRequestKey); formData.append('NomorSpj', nomorSpj || '');
formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME);
formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('SpjDetailId', tps.spjDetailId || '');
formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
formData.append('NamaPetugas', tps.namaPetugas); formData.append('NamaPetugas', tps.namaPetugas);
@ -1573,17 +1642,22 @@ document.addEventListener('DOMContentLoaded', async function() {
const formData = buildSubmitFormData(tps); const formData = buildSubmitFormData(tps);
try { try {
const res = await fetch('/upst/detail-penjemputan', { method: 'POST', body: formData }); const res = await fetch('/upst/detail-penjemputan', {
if (res.ok || res.redirected) { method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
body: formData
});
const result = await res.json().catch(() => null);
if (res.ok && result?.success) {
tps.submitted = true; tps.submitted = true;
if (draftRequestKey) { showToast(result.message || 'Data berhasil disimpan!', 'success');
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); setTimeout(() => { window.location.href = '/upst/detail-penjemputan/detail-selesai-tanpa-tps'; }, 1500);
} else { } else {
const txt = await res.text(); showToast(result?.message || 'Gagal submit data.', 'error');
showToast('Gagal submit: ' + (txt || res.statusText), 'error');
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Submit'; } if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Submit'; }
} }
} catch { } catch {
@ -1617,10 +1691,15 @@ document.addEventListener('DOMContentLoaded', async function() {
nomorSpj = nomorSpjEl.textContent.trim(); nomorSpj = nomorSpjEl.textContent.trim();
} }
initializeLocation(); showLoadingOverlay();
await loadDetailData(); try {
await loadDraftFromServer(); initializeLocation();
renderTpsForm(); await loadDetailData();
await loadRecordForCurrentSpj();
renderTpsForm();
} finally {
hideLoadingOverlay();
}
function renderServerImagePreview(fileUrls, container) { function renderServerImagePreview(fileUrls, container) {
container.innerHTML = ''; container.innerHTML = '';

View File

@ -12,9 +12,8 @@ const DetailPenjemputan = (function () {
}; };
const ENDPOINTS = { const ENDPOINTS = {
saveDraft: '/upst/detail-penjemputan/save-draft', saveRecord: '/upst/detail-penjemputan/save-record',
loadDraft: '/upst/detail-penjemputan/load-draft', recordsList: '/upst/detail-penjemputan/api/records',
deleteDraft: '/upst/detail-penjemputan/delete-draft',
uploadKedatangan: '/upst/detail-penjemputan/upload-foto-kedatangan', uploadKedatangan: '/upst/detail-penjemputan/upload-foto-kedatangan',
uploadTimbangan: '/upst/detail-penjemputan/upload-foto-timbangan', uploadTimbangan: '/upst/detail-penjemputan/upload-foto-timbangan',
uploadPetugas: '/upst/detail-penjemputan/upload-foto-petugas', uploadPetugas: '/upst/detail-penjemputan/upload-foto-petugas',
@ -31,37 +30,48 @@ const DetailPenjemputan = (function () {
}; };
const STORAGE_KEY = "detailPenjemputanTpsState"; const STORAGE_KEY = "detailPenjemputanTpsState";
function sanitizeStorageSegment(value) {
function saveState() { return String(value || "default")
try { .trim()
const stateCopy = JSON.parse( .replace(/[^a-zA-Z0-9_-]/g, "-")
JSON.stringify(state, (key, value) => { .replace(/-+/g, "-")
if (value instanceof File) { .replace(/^-|-$/g, "") || "default";
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 loadState() { function getStorageKey(nomorSpj = state.nomorSpj) {
try { return `${STORAGE_KEY}:${sanitizeStorageSegment(nomorSpj)}`;
const saved = localStorage.getItem(STORAGE_KEY); }
if (saved) {
const parsed = JSON.parse(saved); function saveState() {
state = { ...state, ...parsed }; return;
} }
} catch (e) {
console.warn("Failed to load state from localStorage:", e); function loadState(nomorSpj = state.nomorSpj) {
} return;
} }
function clearState() { function clearState() {
localStorage.removeItem(STORAGE_KEY); return;
}
function getTpsIdentity(value) {
if (!value) return "";
const spjDetailId = value.spjDetailId || value.SpjDetailID || "";
const lokasiAngkutId = value.lokasiAngkutId || value.LokasiAngkutID || "";
const name = value.name || value.Name || "";
return [spjDetailId, lokasiAngkutId, name].filter(Boolean).join("::");
}
function getPersistedUiState() {
return null;
}
function applyPersistedTpsData(persistedUiState) {
return;
}
function applyPersistedUiState(persistedUiState) {
return;
} }
function isBrowserFile(value) { function isBrowserFile(value) {
@ -215,25 +225,24 @@ const DetailPenjemputan = (function () {
}; };
} }
function findMatchingApiTps(apiList, currentTps, index) { function findMatchingApiTps(apiList, currentTps) {
return (
apiList.find( return apiList.find(
(item) => (item) =>
(currentTps.spjDetailId && (currentTps.spjDetailId &&
(item.spjDetailId === currentTps.spjDetailId || (item.spjDetailId === currentTps.spjDetailId ||
item.SpjDetailID === currentTps.spjDetailId)) || item.SpjDetailID === currentTps.spjDetailId)) ||
(currentTps.lokasiAngkutId && (currentTps.lokasiAngkutId &&
(item.lokasiAngkutId === currentTps.lokasiAngkutId || (item.lokasiAngkutId === currentTps.lokasiAngkutId ||
item.LokasiAngkutID === currentTps.lokasiAngkutId)) || item.LokasiAngkutID === currentTps.lokasiAngkutId)) ||
(item.name || item.Name) === currentTps.name, (item.name || item.Name) === currentTps.name,
) || apiList[index] ) || null;
);
} }
function applyApiDraftData(draftData) { function applyApiRecordData(recordData, { skipRender = false } = {}) {
const apiList = Array.isArray(draftData) const apiList = Array.isArray(recordData)
? draftData ? recordData
: draftData?.tpsData || draftData?.draftPenjemputan || []; : recordData?.tpsData || [];
if (!Array.isArray(apiList) || apiList.length === 0) { if (!Array.isArray(apiList) || apiList.length === 0) {
return; return;
@ -244,7 +253,7 @@ const DetailPenjemputan = (function () {
} }
state.tpsData = state.tpsData.map((currentTps, index) => { state.tpsData = state.tpsData.map((currentTps, index) => {
const apiTps = findMatchingApiTps(apiList, currentTps, index); const apiTps = findMatchingApiTps(apiList, currentTps);
if (!apiTps) { if (!apiTps) {
return currentTps; return currentTps;
} }
@ -262,6 +271,7 @@ const DetailPenjemputan = (function () {
return { return {
...currentTps, ...currentTps,
nomorSpj: apiTps.nomorSpj || apiTps.NomorSpj || currentTps.nomorSpj,
name: apiTps.name || apiTps.Name || currentTps.name, name: apiTps.name || apiTps.Name || currentTps.name,
lokasiAngkutId: lokasiAngkutId:
apiTps.lokasiAngkutId || apiTps.lokasiAngkutId ||
@ -317,8 +327,10 @@ const DetailPenjemputan = (function () {
namaPetugas: namaPetugas:
apiTps.namaPetugas || apiTps.NamaPetugas || currentTps.namaPetugas, apiTps.namaPetugas || apiTps.NamaPetugas || currentTps.namaPetugas,
submitted: Boolean( submitted: Boolean(
apiTps.submitted ?? apiTps.Submitted ?? currentTps.submitted, apiTps.isSubmit ?? apiTps.IsSubmit ?? apiTps.submitted ?? apiTps.Submitted ?? currentTps.submitted,
), ),
submittedAt:
apiTps.submittedAt || apiTps.SubmittedAt || currentTps.submittedAt,
}; };
}); });
@ -329,17 +341,63 @@ const DetailPenjemputan = (function () {
); );
elements.tpsTabsContainer.style.display = "block"; elements.tpsTabsContainer.style.display = "block";
if (state.tpsData.length === 1) { if (!skipRender) {
renderSingleForm(); if (state.tpsData.length === 1) {
} else { renderSingleForm();
renderTabs(); } else {
renderTpsForm(); renderTabs();
renderTpsForm();
}
} }
updateAllTotals(); updateAllTotals();
saveState(); saveState();
} }
async function loadRecordsForCurrentSpj() {
if (!state.nomorSpj) return;
try {
const fetchRecords = async () => {
const url = new URL(ENDPOINTS.recordsList, window.location.origin);
url.searchParams.set('nomorSpj', state.nomorSpj);
url.searchParams.set('_ts', Date.now().toString());
const res = await fetch(url.toString(), {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
},
});
if (!res.ok) {
return null;
}
return res.json();
};
let data = await fetchRecords();
if (
data?.success &&
Array.isArray(data.items) &&
data.items.length === 0
) {
await new Promise((resolve) => setTimeout(resolve, 250));
data = await fetchRecords();
}
if (!data?.success || !Array.isArray(data.items) || data.items.length === 0) {
return;
}
applyApiRecordData(data.items, { skipRender: true });
} catch (error) {
console.warn('Gagal memuat data TPS:', error);
}
}
const elements = { const elements = {
grandTotalDisplay: null, grandTotalDisplay: null,
tpsSelectionContainer: null, tpsSelectionContainer: null,
@ -355,10 +413,51 @@ const DetailPenjemputan = (function () {
let autoSaveTimer = null; let autoSaveTimer = null;
let autoSaveStatusEl = null; let autoSaveStatusEl = null;
let loadingOverlayEl = null;
let isAutoSaving = false;
let pendingAutoSaveIndex = null;
let lastAutoSaveSignature = "";
async function init(tpsList) { function getLoadingOverlay() {
initElements(); if (loadingOverlayEl && document.body.contains(loadingOverlayEl)) {
await initializeLocation(tpsList); return loadingOverlayEl;
}
loadingOverlayEl = document.createElement('div');
loadingOverlayEl.id = 'detail-loading-overlay';
loadingOverlayEl.className = 'fixed inset-0 z-[9999] hidden bg-white/95 backdrop-blur-sm flex items-center justify-center px-6';
loadingOverlayEl.innerHTML = `
<div class="w-full max-w-sm rounded-3xl border border-gray-200 bg-white shadow-2xl px-6 py-7 text-center">
<div class="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-upst"></div>
<h2 class="text-base font-black text-gray-800">Sedang memuat data</h2>
<p class="mt-2 text-sm text-gray-500">Mohon tunggu sebentar, data penjemputan sedang dipulihkan.</p>
</div>
`;
document.body.appendChild(loadingOverlayEl);
return loadingOverlayEl;
}
function showLoadingOverlay() {
const overlay = getLoadingOverlay();
overlay.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function hideLoadingOverlay() {
const overlay = getLoadingOverlay();
overlay.classList.add('hidden');
document.body.style.overflow = '';
}
async function init(tpsList, nomorSpj = state.nomorSpj) {
state.nomorSpj = nomorSpj || state.nomorSpj;
showLoadingOverlay();
try {
initElements();
await initializeLocation(tpsList);
} finally {
hideLoadingOverlay();
}
} }
function initElements() { function initElements() {
@ -386,13 +485,6 @@ const DetailPenjemputan = (function () {
} }
} }
function buildDraftKey(tps) {
const spjDetailId = (tps?.spjDetailId || '').trim();
const lokasiAngkutId = (tps?.lokasiAngkutId || '').trim();
if (!spjDetailId && !lokasiAngkutId) return '';
return `tps-${spjDetailId || 'no-spj'}-${lokasiAngkutId || 'no-lokasi'}`.replace(/[^a-zA-Z0-9_-]/g, '');
}
function getActiveTps() { function getActiveTps() {
return state.tpsData[state.activeTpsIndex] || null; return state.tpsData[state.activeTpsIndex] || null;
} }
@ -415,98 +507,83 @@ const DetailPenjemputan = (function () {
if (isOk) setTimeout(() => { statusEl.style.opacity = '0'; }, 2500); if (isOk) setTimeout(() => { statusEl.style.opacity = '0'; }, 2500);
} }
function scheduleAutoSave() { function scheduleAutoSave(tpsIndex = state.activeTpsIndex) {
clearTimeout(autoSaveTimer); clearTimeout(autoSaveTimer);
showAutoSaveStatus('menyimpan...'); showAutoSaveStatus('menyimpan...');
autoSaveTimer = setTimeout(autoSaveDraft, 1000); autoSaveTimer = setTimeout(() => autoSaveRecord(tpsIndex), 1000);
} }
async function autoSaveDraft() { function buildAutoSavePayload(tps) {
const tps = getActiveTps(); return {
if (!tps || !tps.draftKey) return; nomorSpj: state.nomorSpj || '',
namaTps: tps.name || '',
lokasiAngkutId: tps.lokasiAngkutId || '',
spjDetailId: tps.spjDetailId || '',
latitude: tps.latitude || '',
longitude: tps.longitude || '',
alamatJalan: tps.alamatJalan || '',
waktuKedatangan: tps.waktuKedatangan || '',
fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [],
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false,
timbangan: (tps.timbangan || []).map(item => ({
berat: item.weight || 0,
jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS,
fotoFileName: item.fotoFileName || '',
uploaded: item.uploaded || false,
ocrInfo: item.ocrInfo || ''
})),
totalOrganik: tps.totalOrganik || 0,
totalAnorganik: tps.totalAnorganik || 0,
totalResidu: tps.totalResidu || 0,
totalTimbangan: tps.totalTimbangan || 0,
fotoPetugasFileNames: tps.fotoPetugasFileNames || [],
fotoPetugasUploaded: tps.fotoPetugasUploaded || false,
namaPetugas: tps.namaPetugas || ''
};
}
const payload = { async function autoSaveRecord(tpsIndex = state.activeTpsIndex) {
draftKey: tps.draftKey, const tps = state.tpsData[tpsIndex] || null;
lokasiAngkutId: tps.lokasiAngkutId || '', if (!tps) return;
spjDetailId: tps.spjDetailId || '', if (tps.submitted) return;
latitude: tps.latitude || '', if (isAutoSaving) {
longitude: tps.longitude || '', pendingAutoSaveIndex = tpsIndex;
alamatJalan: tps.alamatJalan || '', return;
waktuKedatangan: tps.waktuKedatangan || '', }
fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [],
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false, const payload = buildAutoSavePayload(tps);
timbangan: (tps.timbangan || []).map(item => ({ const payloadSignature = JSON.stringify(payload);
berat: item.weight || 0, if (payloadSignature === lastAutoSaveSignature) {
jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS, showAutoSaveStatus('✓ Data tersimpan', true);
fotoFileName: item.fotoFileName || '', return;
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 { try {
const res = await fetch(ENDPOINTS.saveDraft, { isAutoSaving = true;
const res = await fetch(ENDPOINTS.saveRecord, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload) body: payloadSignature
}); });
const data = await res.json(); const data = await res.json();
showAutoSaveStatus(data.success ? '✓ Draft tersimpan' : '✗ Gagal simpan', data.success); if (data.success) {
lastAutoSaveSignature = payloadSignature;
}
showAutoSaveStatus(data.success ? '✓ Data tersimpan' : '✗ Gagal simpan', data.success);
} catch (_) { } catch (_) {
showAutoSaveStatus('✗ Gagal simpan draft'); showAutoSaveStatus('✗ Gagal simpan data');
} finally {
isAutoSaving = false;
if (pendingAutoSaveIndex !== null) {
const nextIndex = pendingAutoSaveIndex;
pendingAutoSaveIndex = null;
scheduleAutoSave(nextIndex);
}
} }
} }
async function loadDraftForTps(tps) {
if (!tps || !tps.draftKey) return;
try {
const res = await fetch(`${ENDPOINTS.loadDraft}?draftKey=${encodeURIComponent(tps.draftKey)}`);
if (!res.ok) return;
const data = await res.json();
if (!data.success || !data.hasDraft || !data.draft) return;
const draft = data.draft;
tps.lokasiAngkutId = draft.lokasiAngkutId || tps.lokasiAngkutId;
tps.spjDetailId = draft.spjDetailId || tps.spjDetailId;
tps.latitude = draft.latitude || '';
tps.longitude = draft.longitude || '';
tps.alamatJalan = draft.alamatJalan || '';
tps.waktuKedatangan = draft.waktuKedatangan || '';
tps.fotoKedatanganFileNames = draft.fotoKedatanganFileNames || [];
tps.fotoKedatanganUploaded = draft.fotoKedatanganUploaded || false;
tps.fotoPetugasFileNames = draft.fotoPetugasFileNames || [];
tps.fotoPetugasUploaded = draft.fotoPetugasUploaded || false;
tps.namaPetugas = draft.namaPetugas || '';
tps.totalOrganik = draft.totalOrganik || 0;
tps.totalAnorganik = draft.totalAnorganik || 0;
tps.totalResidu = draft.totalResidu || 0;
tps.totalTimbangan = draft.totalTimbangan || 0;
tps.timbangan = (draft.timbangan || []).map(item => ({
file: null,
fotoFileName: item.fotoFileName || '',
weight: item.berat || 0,
jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS,
uploaded: item.uploaded || false,
ocrInfo: item.ocrInfo || 'OCR: belum diproses.'
}));
} catch (error) {
console.warn('Gagal memuat draft TPS:', error);
}
}
async function loadDraftsForAllTps() {
await Promise.all(state.tpsData.map(loadDraftForTps));
}
async function initializeLocation(tpsList) { async function initializeLocation(tpsList) {
const persistedUiState = getPersistedUiState();
state.availableTpsList = tpsList || []; state.availableTpsList = tpsList || [];
if (elements.tpsSelectionContainer) { if (elements.tpsSelectionContainer) {
elements.tpsSelectionContainer.style.display = 'none'; elements.tpsSelectionContainer.style.display = 'none';
@ -515,7 +592,7 @@ const DetailPenjemputan = (function () {
if (state.availableTpsList.length === 0) { if (state.availableTpsList.length === 0) {
state.selectedTpsList = ['1 Lokasi TPS']; state.selectedTpsList = ['1 Lokasi TPS'];
initializeTpsData(state.selectedTpsList); initializeTpsData(state.selectedTpsList);
await loadDraftsForAllTps(); await loadRecordsForCurrentSpj();
elements.tpsTabsContainer.style.display = 'block'; elements.tpsTabsContainer.style.display = 'block';
renderSingleForm(); renderSingleForm();
return; return;
@ -523,7 +600,9 @@ const DetailPenjemputan = (function () {
state.selectedTpsList = [...state.availableTpsList]; state.selectedTpsList = [...state.availableTpsList];
initializeTpsData(state.selectedTpsList); initializeTpsData(state.selectedTpsList);
await loadDraftsForAllTps(); await loadRecordsForCurrentSpj();
applyPersistedTpsData(persistedUiState);
applyPersistedUiState(persistedUiState);
elements.tpsTabsContainer.style.display = 'block'; elements.tpsTabsContainer.style.display = 'block';
if (state.selectedTpsList.length === 1) { if (state.selectedTpsList.length === 1) {
@ -540,7 +619,6 @@ const DetailPenjemputan = (function () {
index: index, index: index,
lokasiAngkutId: typeof tpsItem === 'string' ? '' : (tpsItem?.lokasiAngkutId || tpsItem?.LokasiAngkutID || ''), lokasiAngkutId: typeof tpsItem === 'string' ? '' : (tpsItem?.lokasiAngkutId || tpsItem?.LokasiAngkutID || ''),
spjDetailId: typeof tpsItem === 'string' ? '' : (tpsItem?.spjDetailId || tpsItem?.SpjDetailID || ''), spjDetailId: typeof tpsItem === 'string' ? '' : (tpsItem?.spjDetailId || tpsItem?.SpjDetailID || ''),
draftKey: '',
latitude: '', latitude: '',
longitude: '', longitude: '',
alamatJalan: '', alamatJalan: '',
@ -558,7 +636,7 @@ const DetailPenjemputan = (function () {
fotoPetugasUploaded: false, fotoPetugasUploaded: false,
namaPetugas: '', namaPetugas: '',
submitted: false submitted: false
})).map(tps => ({ ...tps, draftKey: buildDraftKey(tps) })); }));
state.hasRequestedLocation = new Array(tpsNames.length).fill(false); state.hasRequestedLocation = new Array(tpsNames.length).fill(false);
} }
@ -596,7 +674,7 @@ const DetailPenjemputan = (function () {
} }
initializeTpsData(state.selectedTpsList); initializeTpsData(state.selectedTpsList);
await loadDraftsForAllTps(); await loadRecordsForCurrentSpj();
elements.tpsSelectionContainer.style.display = 'none'; elements.tpsSelectionContainer.style.display = 'none';
elements.tpsTabsContainer.style.display = 'block'; elements.tpsTabsContainer.style.display = 'block';
@ -654,9 +732,10 @@ const DetailPenjemputan = (function () {
renderTabs(); renderTabs();
renderTpsForm(); renderTpsForm();
if (!state.hasRequestedLocation[index]) { const switchedTps = state.tpsData[index];
if (!state.hasRequestedLocation[index] && !switchedTps?.submitted) {
state.hasRequestedLocation[index] = true; state.hasRequestedLocation[index] = true;
getLocationUpdate(); getLocationUpdate(index);
} }
updateAllTotals(); updateAllTotals();
@ -667,6 +746,22 @@ const DetailPenjemputan = (function () {
const showTpsName = const showTpsName =
state.selectedTpsList.length > 1 || state.availableTpsList.length > 0; state.selectedTpsList.length > 1 || state.availableTpsList.length > 0;
const submitState = getSubmitState(tps); const submitState = getSubmitState(tps);
const actionMarkup = tps.submitted
? `<div class="flex items-center justify-center gap-2 rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 flex-shrink-0 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="font-medium text-green-700">
Data <strong class="font-bold text-green-800">${showTpsName ? tps.name + ' ' : ''}</strong>sudah disubmit
</span>
</div>`
: `
<div class="flex gap-3">
<a href="/upst/detail-penjemputan/batal" class="w-1/3 text-center bg-red-500 text-white py-3 rounded-xl font-bold text-sm">Batal</a>
<button type="submit" ${submitState.canSubmit ? "" : "disabled"} class="w-2/3 py-3 rounded-xl font-bold text-sm transition ${submitState.canSubmit ? "bg-upst text-white hover:brightness-110" : "bg-gray-300 text-gray-500 cursor-not-allowed"}">Submit${showTpsName ? " " + tps.name : ""}</button>
</div>
${submitState.canSubmit ? '' : `<p class="submit-state-message text-[11px] text-center text-red-500 font-medium">${submitState.message}</p>`}
`;
elements.tpsContentContainer.innerHTML = ` elements.tpsContentContainer.innerHTML = `
<form class="space-y-5 pb-8" data-tps-index="${tps.index}"> <form class="space-y-5 pb-8" data-tps-index="${tps.index}">
@ -684,11 +779,7 @@ const DetailPenjemputan = (function () {
${renderSection2Timbangan(tps, showTpsName)} ${renderSection2Timbangan(tps, showTpsName)}
${renderSection3Petugas(tps)} ${renderSection3Petugas(tps)}
<div class="flex gap-3"> ${actionMarkup}
<a href="/upst/detail-penjemputan/batal" class="w-1/3 text-center bg-red-500 text-white py-3 rounded-xl font-bold text-sm">Batal</a>
<button type="submit" ${submitState.canSubmit ? "" : "disabled"} class="w-2/3 py-3 rounded-xl font-bold text-sm transition ${submitState.canSubmit ? "bg-upst text-white hover:brightness-110" : "bg-gray-300 text-gray-500 cursor-not-allowed"}">Submit${showTpsName ? " " + tps.name : ""}</button>
</div>
${submitState.canSubmit ? '' : `<p class="submit-state-message text-[11px] text-center text-red-500 font-medium">${submitState.message}</p>`}
<p id="auto-save-status" class="text-[11px] text-amber-500 text-center font-medium" style="opacity:0;transition:opacity 0.4s"></p> <p id="auto-save-status" class="text-[11px] text-amber-500 text-center font-medium" style="opacity:0;transition:opacity 0.4s"></p>
</form> </form>
`; `;
@ -782,6 +873,7 @@ const DetailPenjemputan = (function () {
function attachTpsFormListeners() { function attachTpsFormListeners() {
const form = elements.tpsContentContainer.querySelector("form"); const form = elements.tpsContentContainer.querySelector("form");
const tps = state.tpsData[state.activeTpsIndex]; const tps = state.tpsData[state.activeTpsIndex];
if (tps.submitted) return;
const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan"); const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan");
const fotoPetugasInput = form.querySelector(".tps-foto-petugas"); const fotoPetugasInput = form.querySelector(".tps-foto-petugas");
@ -791,10 +883,9 @@ const DetailPenjemputan = (function () {
fotoKedatanganInput.addEventListener('change', function() { fotoKedatanganInput.addEventListener('change', function() {
tps.fotoKedatangan = Array.from(this.files); tps.fotoKedatangan = Array.from(this.files);
tps.fotoKedatanganUploaded = false; tps.fotoKedatanganUploaded = false;
updateWaktuKedatangan(); updateWaktuKedatangan(tps.index);
updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan')); updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan'));
refreshKedatanganUploadState(form); refreshKedatanganUploadState(form);
scheduleAutoSave();
}); });
fotoPetugasInput.addEventListener('change', function() { fotoPetugasInput.addEventListener('change', function() {
@ -802,24 +893,23 @@ const DetailPenjemputan = (function () {
tps.fotoPetugasUploaded = false; tps.fotoPetugasUploaded = false;
updateMultiPreview(this, form.querySelector('.tps-preview-petugas')); updateMultiPreview(this, form.querySelector('.tps-preview-petugas'));
refreshPetugasUploadState(form); refreshPetugasUploadState(form);
scheduleAutoSave();
}); });
namaPetugasInput.addEventListener('input', function() { namaPetugasInput.addEventListener('input', function() {
tps.namaPetugas = this.value; tps.namaPetugas = this.value;
refreshPetugasUploadState(form); refreshPetugasUploadState(form);
scheduleAutoSave(tps.index);
}); });
namaPetugasInput.addEventListener('blur', function() { namaPetugasInput.addEventListener('blur', function() {
tps.namaPetugas = this.value; tps.namaPetugas = this.value;
scheduleAutoSave(); scheduleAutoSave(tps.index);
}); });
btnAddTimbangan.addEventListener('click', function() { btnAddTimbangan.addEventListener('click', function() {
createTimbanganItem(form.querySelector('.tps-timbangan-repeater')); createTimbanganItem(form.querySelector('.tps-timbangan-repeater'));
syncTimbanganToTpsData(); syncTimbanganToTpsData();
refreshSubmitButtonState(form); refreshSubmitButtonState(form);
scheduleAutoSave();
}); });
const btnUploadKedatangan = form.querySelector( const btnUploadKedatangan = form.querySelector(
@ -887,7 +977,7 @@ const DetailPenjemputan = (function () {
item.className = item.className =
"rounded-xl border border-gray-200 overflow-hidden bg-black"; "rounded-xl border border-gray-200 overflow-hidden bg-black";
const imageUrl = URL.createObjectURL(file); const imageUrl = getStoredPhotoUrl(file);
item.innerHTML = ` item.innerHTML = `
<div class="h-44 bg-black/80"> <div class="h-44 bg-black/80">
<img src="${imageUrl}" alt="Preview ${index + 1}" class="w-full h-full object-contain preview-multi-image" /> <img src="${imageUrl}" alt="Preview ${index + 1}" class="w-full h-full object-contain preview-multi-image" />
@ -904,8 +994,9 @@ const DetailPenjemputan = (function () {
}); });
} }
function updateWaktuKedatangan() { function updateWaktuKedatangan(tpsIndex = state.activeTpsIndex) {
const tps = state.tpsData[state.activeTpsIndex]; const tps = state.tpsData[tpsIndex];
if (!tps) return;
const now = new Date(); const now = new Date();
const formatted = now.toLocaleString("id-ID", { const formatted = now.toLocaleString("id-ID", {
day: "2-digit", day: "2-digit",
@ -917,22 +1008,24 @@ const DetailPenjemputan = (function () {
}); });
tps.waktuKedatangan = formatted; tps.waktuKedatangan = formatted;
const form = elements.tpsContentContainer.querySelector("form"); if (tpsIndex === state.activeTpsIndex) {
const displayWaktu = form.querySelector(".tps-waktu-kedatangan"); const form = elements.tpsContentContainer.querySelector("form");
if (displayWaktu) displayWaktu.value = formatted; const displayWaktu = form?.querySelector(".tps-waktu-kedatangan");
if (displayWaktu) displayWaktu.value = formatted;
}
getLocationUpdate(); getLocationUpdate(tpsIndex);
saveState(); saveState();
} }
function getLocationUpdate() { function getLocationUpdate(tpsIndex = state.activeTpsIndex) {
if (!("geolocation" in navigator)) return; if (!("geolocation" in navigator)) return;
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
function (position) { function (position) {
const lat = position.coords.latitude.toFixed(6); const lat = position.coords.latitude.toFixed(6);
const lng = position.coords.longitude.toFixed(6); const lng = position.coords.longitude.toFixed(6);
reverseGeocode(lat, lng); reverseGeocode(lat, lng, tpsIndex);
}, },
function () { function () {
console.log("Lokasi tidak diizinkan"); console.log("Lokasi tidak diizinkan");
@ -940,28 +1033,30 @@ const DetailPenjemputan = (function () {
); );
} }
function reverseGeocode(lat, lng) { function reverseGeocode(lat, lng, tpsIndex = state.activeTpsIndex) {
const tps = state.tpsData[state.activeTpsIndex]; const tps = state.tpsData[tpsIndex];
if (!tps) return;
fetch( fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`, `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`,
) )
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
const address = data.display_name || `${lat}, ${lng}`; const address = data.display_name || `${lat}, ${lng}`;
updateTpsLocation(lat, lng, address); updateTpsLocation(lat, lng, address, tpsIndex);
}) })
.catch(() => { .catch(() => {
updateTpsLocation(lat, lng, `${lat}, ${lng}`); updateTpsLocation(lat, lng, `${lat}, ${lng}`, tpsIndex);
}); });
} }
function updateTpsLocation(lat, lng, address) { function updateTpsLocation(lat, lng, address, tpsIndex = state.activeTpsIndex) {
const tps = state.tpsData[state.activeTpsIndex]; const tps = state.tpsData[tpsIndex];
if (!tps) return;
tps.latitude = lat; tps.latitude = lat;
tps.longitude = lng; tps.longitude = lng;
tps.alamatJalan = address; tps.alamatJalan = address;
const form = elements.tpsContentContainer.querySelector('form'); const form = tpsIndex === state.activeTpsIndex ? elements.tpsContentContainer.querySelector('form') : null;
if (form) { if (form) {
const latInput = form.querySelector('.tps-display-latitude'); const latInput = form.querySelector('.tps-display-latitude');
const lngInput = form.querySelector('.tps-display-longitude'); const lngInput = form.querySelector('.tps-display-longitude');
@ -969,7 +1064,6 @@ const DetailPenjemputan = (function () {
if (lngInput) lngInput.value = lng; if (lngInput) lngInput.value = lng;
} }
scheduleAutoSave();
} }
function updateMultiPreview(input, previewContainer) { function updateMultiPreview(input, previewContainer) {
@ -1010,12 +1104,13 @@ const DetailPenjemputan = (function () {
fileUrls.forEach((url, index) => { fileUrls.forEach((url, index) => {
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black'; item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black';
const isUrl = typeof url === 'string' && (url.startsWith('/') || url.startsWith('http')); const imageUrl = getStoredPhotoUrl(url);
const isUrl = Boolean(imageUrl) && (imageUrl.startsWith('/') || imageUrl.startsWith('http'));
if (isUrl) { if (isUrl) {
item.innerHTML = ` item.innerHTML = `
<div class="h-44 bg-black/80"> <div class="h-44 bg-black/80">
<img src="${url}" alt="Foto ${index + 1}" class="w-full h-full object-contain" loading="lazy" /> <img src="${imageUrl}" alt="Foto ${index + 1}" class="w-full h-full object-contain" loading="lazy" />
</div> </div>
`; `;
} else { } else {
@ -1036,8 +1131,8 @@ const DetailPenjemputan = (function () {
const weight = existingData ? (existingData.weight || 0) : 0; const weight = existingData ? (existingData.weight || 0) : 0;
const jenisSampah = existingData ? (existingData.jenisSampah || CONFIG.DEFAULT_JENIS) : CONFIG.DEFAULT_JENIS; const jenisSampah = existingData ? (existingData.jenisSampah || CONFIG.DEFAULT_JENIS) : CONFIG.DEFAULT_JENIS;
const hasFileBlob = Boolean(existingData?.file); const hasFileBlob = isBrowserFile(existingData?.file);
const hasFile = Boolean(existingData?.file || existingData?.fotoFileName); const hasFile = Boolean(hasStoredPhoto(existingData?.file) || existingData?.fotoFileName);
const isUploaded = Boolean(existingData?.uploaded); const isUploaded = Boolean(existingData?.uploaded);
const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'); const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.');
@ -1047,7 +1142,7 @@ const DetailPenjemputan = (function () {
<button type="button" class="btn-remove-timbangan text-[11px] font-bold text-red-500">Hapus</button> <button type="button" class="btn-remove-timbangan text-[11px] font-bold text-red-500">Hapus</button>
</div> </div>
<input type="file" name="FotoTimbangan" accept="image/*" class="input-foto-timbangan block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" /> <input type="file" name="FotoTimbangan" accept="image/*" class="input-foto-timbangan block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" />
<div class="${(hasFileBlob || (hasFile && existingData?.fotoFileName?.startsWith('/'))) ? '' : 'hidden'} input-preview-wrap relative rounded-xl overflow-hidden border border-gray-200 bg-black"> <div class="${(hasFileBlob || (hasFile && getStoredPhotoUrl(existingData?.file || existingData?.fotoFileName))) ? '' : 'hidden'} input-preview-wrap relative rounded-xl overflow-hidden border border-gray-200 bg-black">
<img class="input-preview-image w-full h-44 object-contain" alt="Preview foto timbangan" /> <img class="input-preview-image w-full h-44 object-contain" alt="Preview foto timbangan" />
</div> </div>
<p class="text-[11px] text-gray-500 input-ocr-info">${ocrInfoText}</p> <p class="text-[11px] text-gray-500 input-ocr-info">${ocrInfoText}</p>
@ -1078,11 +1173,12 @@ const DetailPenjemputan = (function () {
const jenisSampahSelect = item.querySelector(".input-jenis-sampah"); const jenisSampahSelect = item.querySelector(".input-jenis-sampah");
const removeBtn = item.querySelector(".btn-remove-timbangan"); const removeBtn = item.querySelector(".btn-remove-timbangan");
if (existingData && existingData.file) { if (existingData && hasStoredPhoto(existingData.file)) {
const localUrl = URL.createObjectURL(existingData.file); const localUrl = getStoredPhotoUrl(existingData.file);
previewImage.src = localUrl; previewImage.src = localUrl;
previewWrap.classList.remove('hidden'); previewWrap.classList.remove('hidden');
previewImage.onload = function() { previewImage.onload = function() {
if (!isBrowserFile(resolveStoredPhoto(existingData.file))) return;
URL.revokeObjectURL(localUrl); URL.revokeObjectURL(localUrl);
}; };
} else if (existingData && existingData.fotoFileName && existingData.fotoFileName.startsWith('/')) { } else if (existingData && existingData.fotoFileName && existingData.fotoFileName.startsWith('/')) {
@ -1120,7 +1216,6 @@ const DetailPenjemputan = (function () {
tps.timbangan[itemIndex].fotoFileName = ''; tps.timbangan[itemIndex].fotoFileName = '';
refreshTimbanganUploadState(item); refreshTimbanganUploadState(item);
} }
scheduleAutoSave();
} }
}); });
@ -1134,7 +1229,7 @@ const DetailPenjemputan = (function () {
refreshTimbanganUploadState(item); refreshTimbanganUploadState(item);
const form = elements.tpsContentContainer.querySelector('form'); const form = elements.tpsContentContainer.querySelector('form');
if (form) refreshSubmitButtonState(form); if (form) refreshSubmitButtonState(form);
scheduleAutoSave(); scheduleAutoSave(state.activeTpsIndex);
}); });
weightInputDisplay.addEventListener("blur", function () { weightInputDisplay.addEventListener("blur", function () {
@ -1158,7 +1253,6 @@ const DetailPenjemputan = (function () {
syncTimbanganToTpsData(); syncTimbanganToTpsData();
const form = elements.tpsContentContainer.querySelector('form'); const form = elements.tpsContentContainer.querySelector('form');
if (form) refreshSubmitButtonState(form); if (form) refreshSubmitButtonState(form);
scheduleAutoSave();
}); });
removeBtn.addEventListener('click', function() { removeBtn.addEventListener('click', function() {
@ -1176,7 +1270,6 @@ const DetailPenjemputan = (function () {
updateTpsTotalTimbangan(); updateTpsTotalTimbangan();
syncTimbanganToTpsData(); syncTimbanganToTpsData();
if (form) refreshSubmitButtonState(form); if (form) refreshSubmitButtonState(form);
scheduleAutoSave();
}); });
repeater.appendChild(item); repeater.appendChild(item);
@ -1277,6 +1370,10 @@ const DetailPenjemputan = (function () {
} }
function getSubmitState(tps) { function getSubmitState(tps) {
if (tps?.submitted) {
return { canSubmit: false, message: '' };
}
if (!tps.fotoKedatanganUploaded) { if (!tps.fotoKedatanganUploaded) {
if (!tps.fotoKedatangan.length) { if (!tps.fotoKedatangan.length) {
return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan terlebih dahulu.' }; return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan terlebih dahulu.' };
@ -1318,10 +1415,10 @@ const DetailPenjemputan = (function () {
function refreshSubmitButtonState(form) { function refreshSubmitButtonState(form) {
const submitButton = form.querySelector('button[type="submit"]'); const submitButton = form.querySelector('button[type="submit"]');
if (!submitButton) return; const tps = state.tpsData[state.activeTpsIndex];
if (tps?.submitted || !submitButton) return;
let messageEl = form.querySelector(".submit-state-message"); let messageEl = form.querySelector(".submit-state-message");
const tps = state.tpsData[state.activeTpsIndex];
const submitState = getSubmitState(tps); const submitState = getSubmitState(tps);
submitButton.disabled = !submitState.canSubmit; submitButton.disabled = !submitState.canSubmit;
@ -1825,7 +1922,8 @@ const DetailPenjemputan = (function () {
const formData = new FormData(); const formData = new FormData();
formData.append('FotoTimbangan', timbanganItem.file); formData.append('FotoTimbangan', timbanganItem.file);
formData.append('DraftKey', tps.draftKey || ''); formData.append('NomorSpj', state.nomorSpj || '');
formData.append('NamaTps', tps.name || 'TPS');
formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('SpjDetailId', tps.spjDetailId || '');
formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
formData.append('ItemIndex', itemIndex); formData.append('ItemIndex', itemIndex);
@ -1863,7 +1961,7 @@ const DetailPenjemputan = (function () {
syncTimbanganToTpsData(); syncTimbanganToTpsData();
const form = elements.tpsContentContainer.querySelector('form'); const form = elements.tpsContentContainer.querySelector('form');
if (form) refreshSubmitButtonState(form); if (form) refreshSubmitButtonState(form);
scheduleAutoSave(); scheduleAutoSave(tps.index);
} }
async function uploadFotoKedatangan() { async function uploadFotoKedatangan() {
@ -1882,7 +1980,8 @@ const DetailPenjemputan = (function () {
const formData = new FormData(); const formData = new FormData();
tps.fotoKedatangan.forEach(file => formData.append('FotoKedatangan', file)); tps.fotoKedatangan.forEach(file => formData.append('FotoKedatangan', file));
formData.append('DraftKey', tps.draftKey || ''); formData.append('NomorSpj', state.nomorSpj || '');
formData.append('NamaTps', tps.name || 'TPS');
formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('SpjDetailId', tps.spjDetailId || '');
formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
formData.append('WaktuKedatangan', tps.waktuKedatangan || ''); formData.append('WaktuKedatangan', tps.waktuKedatangan || '');
@ -1902,7 +2001,7 @@ const DetailPenjemputan = (function () {
if (fotoInput) fotoInput.value = ''; if (fotoInput) fotoInput.value = '';
refreshKedatanganUploadState(form); refreshKedatanganUploadState(form);
} }
scheduleAutoSave(); scheduleAutoSave(tps.index);
} else { } else {
showToast(data.message || 'Gagal upload foto kedatangan.', 'error'); showToast(data.message || 'Gagal upload foto kedatangan.', 'error');
if (btn) { if (btn) {
@ -1939,7 +2038,8 @@ const DetailPenjemputan = (function () {
const formData = new FormData(); const formData = new FormData();
tps.fotoPetugas.forEach(file => formData.append('FotoPetugas', file)); tps.fotoPetugas.forEach(file => formData.append('FotoPetugas', file));
formData.append('DraftKey', tps.draftKey || ''); formData.append('NomorSpj', state.nomorSpj || '');
formData.append('NamaTps', tps.name || 'TPS');
formData.append('SpjDetailId', tps.spjDetailId || ''); formData.append('SpjDetailId', tps.spjDetailId || '');
formData.append('LokasiAngkutId', tps.lokasiAngkutId || ''); formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
formData.append('NamaPetugas', tps.namaPetugas || ''); formData.append('NamaPetugas', tps.namaPetugas || '');
@ -1956,7 +2056,7 @@ const DetailPenjemputan = (function () {
if (fotoInput) fotoInput.value = ''; if (fotoInput) fotoInput.value = '';
refreshPetugasUploadState(form); refreshPetugasUploadState(form);
} }
scheduleAutoSave(); scheduleAutoSave(tps.index);
} else { } else {
showToast(data.message || 'Gagal upload foto petugas.', 'error'); showToast(data.message || 'Gagal upload foto petugas.', 'error');
if (btn) { if (btn) {
@ -1975,8 +2075,9 @@ const DetailPenjemputan = (function () {
function buildSubmitFormData(tps) { function buildSubmitFormData(tps) {
const formData = new FormData(); const formData = new FormData();
formData.append("LokasiAngkutID", tps.lokasiAngkutId || ""); formData.append("NomorSpj", state.nomorSpj || "");
formData.append("SpjDetailID", tps.spjDetailId || ""); formData.append("LokasiAngkutId", tps.lokasiAngkutId || "");
formData.append("SpjDetailId", tps.spjDetailId || "");
formData.append("TpsName", tps.name); formData.append("TpsName", tps.name);
formData.append("Latitude", tps.latitude); formData.append("Latitude", tps.latitude);
formData.append("Longitude", tps.longitude); formData.append("Longitude", tps.longitude);
@ -2053,16 +2154,18 @@ const DetailPenjemputan = (function () {
try { try {
const response = await fetch(ENDPOINTS.submit, { const response = await fetch(ENDPOINTS.submit, {
method: 'POST', method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
body: formData body: formData
}); });
if (response.ok || response.redirected) { const result = await response.json().catch(() => null);
markTpsSubmitted(tps);
if (tps.draftKey) {
await fetch(`${ENDPOINTS.deleteDraft}?draftKey=${encodeURIComponent(tps.draftKey)}`, { method: 'DELETE' });
}
showToast(`Data ${tps.name} berhasil disimpan!`, 'success'); if (response.ok && result?.success) {
markTpsSubmitted(tps);
showToast(result.message || `Data ${tps.name} berhasil disimpan!`, 'success');
const allSubmitted = state.tpsData.every(item => item.submitted); const allSubmitted = state.tpsData.every(item => item.submitted);
if (allSubmitted) { if (allSubmitted) {
@ -2074,8 +2177,7 @@ const DetailPenjemputan = (function () {
renderTpsForm(); renderTpsForm();
} }
} else { } else {
const errorText = await response.text(); showToast(result?.message || 'Gagal menyimpan data. Silakan coba lagi.', 'error');
showToast(errorText || 'Gagal menyimpan data. Silakan coba lagi.', 'error');
if (submitBtn) { if (submitBtn) {
submitBtn.disabled = false; submitBtn.disabled = false;
submitBtn.textContent = `Submit${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}`; submitBtn.textContent = `Submit${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}`;
@ -2139,7 +2241,7 @@ const DetailPenjemputan = (function () {
return { return {
init: init, init: init,
hydrateFromApi: applyApiDraftData, hydrateFromApi: applyApiRecordData,
setNomorSpj: function (nomorSpj) { setNomorSpj: function (nomorSpj) {
state.nomorSpj = nomorSpj; state.nomorSpj = nomorSpj;
saveState(); saveState();
@ -2148,8 +2250,17 @@ const DetailPenjemputan = (function () {
})(); })();
document.addEventListener('DOMContentLoaded', async function() { document.addEventListener('DOMContentLoaded', async function() {
const nomorSpjEl = document.querySelector('.text-gray-600.font-mono');
const nomorSpj = nomorSpjEl ? nomorSpjEl.textContent.trim() : 'SPJ/07-2025/PKM/000476';
try { try {
const response = await fetch('/driver/json/tps-list.json'); const response = await fetch(`/driver/json/tps-list.json?_ts=${Date.now()}`, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache'
}
});
const data = await response.json(); const data = await response.json();
const tpsList = data.tpsList.map(tps => ({ const tpsList = data.tpsList.map(tps => ({
name: tps.name, name: tps.name,
@ -2157,16 +2268,13 @@ document.addEventListener('DOMContentLoaded', async function() {
spjDetailId: tps.spjDetailId, spjDetailId: tps.spjDetailId,
id: tps.id id: tps.id
})); }));
await DetailPenjemputan.init(tpsList); await DetailPenjemputan.init(tpsList, nomorSpj);
} catch (error) { } catch (error) {
console.error('Error loading TPS list:', error); console.error('Error loading TPS list:', error);
} }
const platNomorEl = document.getElementById('plat-nomor'); const platNomorEl = document.getElementById('plat-nomor');
if (platNomorEl) { if (platNomorEl && nomorSpjEl) {
const nomorSpjEl = document.querySelector('.text-gray-600.font-mono'); DetailPenjemputan.setNomorSpj(nomorSpj);
if (nomorSpjEl) {
DetailPenjemputan.setNomorSpj(nomorSpjEl.textContent.trim());
}
} }
}); });

View File

@ -2,11 +2,11 @@
"detailPenjemputan": { "detailPenjemputan": {
"namaTps": "TPS A", "namaTps": "TPS A",
"namaPerusahaan": "CV Tri Berkah Sejahtera", "namaPerusahaan": "CV Tri Berkah Sejahtera",
"nomorSpj": "SPJ/07-2025/PKM/000476", "nomorSpj": "SPJ/07-2025/PKM/000500",
"platNomor": "B 9632 TOR", "platNomor": "B 9632 TOR",
"nomorPintu": "JRC 005", "nomorPintu": "JRC 005",
"alamat": "Kp. Pertanian II Rt.004 Rw.001 Kel. Klender Kec, Duren Sawit, Kota Adm. Jakarta Timur 13470", "alamat": "Kp. Pertanian II Rt.004 Rw.001 Kel. Klender Kec, Duren Sawit, Kota Adm. Jakarta Timur 13470",
"lokasiAngkutId": "", "lokasiAngkutId": "LOK001",
"spjDetailId": "" "spjDetailId": "SPJ001"
} }
} }

View File

@ -1,8 +1,8 @@
{ {
"id": "/upst", "id": "/upst",
"name": "eSPJ - Surat Perjalanan Dinas", "name": "PKM UPST - SPJ Driver UPST",
"short_name": "eSPJ", "short_name": "PKM UPST",
"description": "Aplikasi pengelolaan Surat Perjalanan Dinas yang modern dan efisien", "description": "Aplikasi pengelolaan SPJ untuk driver UPST",
"start_url": "/upst", "start_url": "/upst",
"display": "standalone", "display": "standalone",
"background_color": "#ffffff", "background_color": "#ffffff",
@ -27,17 +27,17 @@
], ],
"shortcuts": [ "shortcuts": [
{ {
"name": "Home UPST", "name": "Beranda",
"short_name": "Home", "short_name": "Beranda",
"description": "Buka dashboard utama UPST", "description": "Buka dashboard utama UPST",
"url": "/upst", "url": "/upst",
"icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }] "icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }]
}, },
{ {
"name": "Halaman Kosong", "name": "Riwayat Perjalanan",
"short_name": "Kosong", "short_name": "Riwayat",
"description": "Buka halaman alternatif UPST", "description": "Buka halaman riwayat perjalanan UPST",
"url": "/upst/kosong", "url": "/upst/riwayat",
"icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }] "icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }]
} }
] ]

View File

@ -1,4 +1,4 @@
const CACHE_NAME = "espj-upst-pwa-v1"; const CACHE_NAME = "espj-upst-pwa-v2";
const OFFLINE_URL = "/driver/offline.html"; const OFFLINE_URL = "/driver/offline.html";
const PRECACHE_URLS = [ const PRECACHE_URLS = [
"/upst", "/upst",
@ -12,7 +12,27 @@ const PRECACHE_URLS = [
self.addEventListener("install", (event) => { self.addEventListener("install", (event) => {
event.waitUntil( event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS)) caches.open(CACHE_NAME).then(async (cache) => {
const results = await Promise.allSettled(
PRECACHE_URLS.map(async (url) => {
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) {
throw new Error(`Gagal precache: ${url} (${response.status})`);
}
await cache.put(url, response.clone());
})
);
const offlineCached = results.some(
(result, index) =>
PRECACHE_URLS[index] === OFFLINE_URL && result.status === "fulfilled"
);
if (!offlineCached) {
throw new Error(`Offline page gagal diprecache: ${OFFLINE_URL}`);
}
})
); );
self.skipWaiting(); self.skipWaiting();
}); });
@ -44,6 +64,16 @@ function isAuthPath(url) {
return AUTH_PATHS.some((path) => url.pathname.startsWith(path)); return AUTH_PATHS.some((path) => url.pathname.startsWith(path));
} }
const BYPASS_CACHE_PATHS = [
"/upst/detail-penjemputan/api/",
"/uploads/penjemputan/",
"/driver/json/"
];
function shouldBypassCache(url) {
return BYPASS_CACHE_PATHS.some((path) => url.pathname.startsWith(path));
}
self.addEventListener("fetch", (event) => { self.addEventListener("fetch", (event) => {
const { request } = event; const { request } = event;
const url = new URL(request.url); const url = new URL(request.url);
@ -60,6 +90,19 @@ self.addEventListener("fetch", (event) => {
return; return;
} }
if (shouldBypassCache(url)) {
event.respondWith(
fetch(new Request(request, { cache: "no-store" })).catch(async () => {
if (request.mode === "navigate") {
return (await caches.match(OFFLINE_URL)) || Response.error();
}
return Response.error();
})
);
return;
}
if (request.mode === "navigate") { if (request.mode === "navigate") {
event.respondWith( event.respondWith(
fetch(request) fetch(request)
@ -72,7 +115,7 @@ self.addEventListener("fetch", (event) => {
}) })
.catch(async () => { .catch(async () => {
const cachedPage = await caches.match(request); const cachedPage = await caches.match(request);
return cachedPage || caches.match(OFFLINE_URL); return cachedPage || (await caches.match(OFFLINE_URL)) || Response.error();
}) })
); );
return; return;