update: fixing detail penjemputan dan offline pages
parent
0390d630b3
commit
61094188f6
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
Program.cs
10
Program.cs
|
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 = '';
|
||||||
|
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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" }]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue