update: fixing detail penjemputan dan offline pages
parent
0390d630b3
commit
61094188f6
|
|
@ -31,27 +31,168 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
_env = env;
|
||||
}
|
||||
|
||||
private static string ResolveDraftKey(string? draftKey, string? sessionKey, string? spjDetailId = null, string? lokasiAngkutId = null)
|
||||
private static string SanitizePathSegment(string? value, string fallback = "umum")
|
||||
{
|
||||
var rawKey = !string.IsNullOrWhiteSpace(draftKey)
|
||||
? draftKey
|
||||
: !string.IsNullOrWhiteSpace(sessionKey)
|
||||
? sessionKey
|
||||
: $"non-tps-{spjDetailId}-{lokasiAngkutId}";
|
||||
var safe = string.Concat((value ?? string.Empty).Trim().Select(c =>
|
||||
char.IsLetterOrDigit(c) || c == '-' || c == '_'
|
||||
? c
|
||||
: '-'));
|
||||
|
||||
return string.Concat((rawKey ?? string.Empty).Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_'));
|
||||
safe = Regex.Replace(safe, "-+", "-").Trim('-');
|
||||
return string.IsNullOrWhiteSpace(safe) ? fallback : safe;
|
||||
}
|
||||
|
||||
private string GetUploadDirectory(string dateFolder)
|
||||
private string GetUploadDirectory(string dateFolder, string? nomorSpj = null, string? namaTps = null)
|
||||
{
|
||||
var uploadDir = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", dateFolder);
|
||||
var uploadDir = Path.Combine(
|
||||
_env.ContentRootPath,
|
||||
"uploads",
|
||||
"penjemputan",
|
||||
dateFolder,
|
||||
SanitizePathSegment(nomorSpj, "spj-umum"),
|
||||
SanitizePathSegment(namaTps, "tps-1"));
|
||||
Directory.CreateDirectory(uploadDir);
|
||||
return uploadDir;
|
||||
}
|
||||
|
||||
private static string BuildUploadUrl(string dateFolder, string fileName)
|
||||
private static string BuildUploadUrl(string dateFolder, string fileName, string? nomorSpj = null, string? namaTps = null)
|
||||
{
|
||||
return $"/uploads/penjemputan/{dateFolder}/{fileName}";
|
||||
var spjFolder = SanitizePathSegment(nomorSpj, "spj-umum");
|
||||
var tpsFolder = SanitizePathSegment(namaTps, "tps-1");
|
||||
return $"/uploads/penjemputan/{dateFolder}/{spjFolder}/{tpsFolder}/{fileName}";
|
||||
}
|
||||
|
||||
private async Task<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("")]
|
||||
|
|
@ -89,14 +230,96 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesaiTanpaTps.cshtml");
|
||||
}
|
||||
|
||||
[HttpGet("api/submitted")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public async Task<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("")]
|
||||
[ValidateAntiForgeryToken]
|
||||
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
|
||||
{
|
||||
var result = await _detailService.SubmitPenjemputanAsync(request);
|
||||
|
||||
if (isAjaxRequest)
|
||||
{
|
||||
return result.Success
|
||||
? Ok(result)
|
||||
: BadRequest(result);
|
||||
}
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
TempData["Success"] = result.Message;
|
||||
|
|
@ -111,94 +334,54 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error submitting penjemputan data");
|
||||
|
||||
if (isAjaxRequest)
|
||||
{
|
||||
return StatusCode(500, new DetailPenjemputanResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Terjadi kesalahan saat menyimpan data."
|
||||
});
|
||||
}
|
||||
|
||||
TempData["Error"] = "Terjadi kesalahan saat menyimpan data.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("save-draft-non-tps")]
|
||||
[HttpPost("save-record-non-tps")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> SaveDraftNonTps([FromBody] DraftSaveRequest request)
|
||||
public async Task<IActionResult> SaveRecordNonTps()
|
||||
{
|
||||
var request = await ResolveRecordSaveRequestAsync();
|
||||
|
||||
if (request == null)
|
||||
return BadRequest(new DraftSaveResponse { Success = false, Message = "Request tidak valid." });
|
||||
return BadRequest(new RecordSaveResponse { Success = false, Message = "Request tidak valid." });
|
||||
|
||||
request.DraftKey = ResolveDraftKey(request.DraftKey, request.SessionKey, request.SpjDetailId, request.LokasiAngkutId);
|
||||
request.SessionKey = request.DraftKey;
|
||||
if (string.IsNullOrWhiteSpace(request.DraftKey))
|
||||
return BadRequest(new DraftSaveResponse { Success = false, Message = "Draft key tidak valid." });
|
||||
|
||||
var result = await _detailService.SaveDraftNonTpsAsync(request);
|
||||
var result = await _detailService.SaveRecordNonTpsAsync(request);
|
||||
return result.Success ? Ok(result) : StatusCode(500, result);
|
||||
}
|
||||
|
||||
[HttpGet("load-draft-non-tps")]
|
||||
[HttpPost("save-record")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> LoadDraftNonTps([FromQuery] string? draftKey = null, [FromQuery] string? sessionKey = null)
|
||||
public async Task<IActionResult> SaveRecord()
|
||||
{
|
||||
var key = ResolveDraftKey(draftKey, sessionKey);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return Ok(new DraftLoadResponse { Success = true, HasDraft = false, Message = "Draft key kosong." });
|
||||
var result = await _detailService.LoadDraftNonTpsAsync(key);
|
||||
return Ok(result);
|
||||
}
|
||||
var request = await ResolveRecordSaveRequestAsync();
|
||||
|
||||
[HttpDelete("delete-draft-non-tps")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<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)
|
||||
return BadRequest(new DraftSaveResponse { Success = false, Message = "Request tidak valid." });
|
||||
return BadRequest(new RecordSaveResponse { Success = false, Message = "Request tidak valid." });
|
||||
|
||||
request.DraftKey = ResolveDraftKey(request.DraftKey, request.SessionKey, request.SpjDetailId, request.LokasiAngkutId);
|
||||
request.SessionKey = request.DraftKey;
|
||||
if (string.IsNullOrWhiteSpace(request.DraftKey))
|
||||
return BadRequest(new DraftSaveResponse { Success = false, Message = "Draft key tidak valid." });
|
||||
|
||||
var result = await _detailService.SaveDraftTpsAsync(request);
|
||||
var result = await _detailService.SaveRecordTpsAsync(request);
|
||||
return result.Success ? Ok(result) : StatusCode(500, result);
|
||||
}
|
||||
|
||||
[HttpGet("load-draft")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<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")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UploadFotoKedatanganNonTps(
|
||||
[FromForm] List<IFormFile>? FotoKedatangan,
|
||||
[FromForm] string? DraftKey,
|
||||
[FromForm] string? SessionKey,
|
||||
[FromForm] string? NomorSpj,
|
||||
[FromForm] string? NamaTps,
|
||||
[FromForm] string? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] string? WaktuKedatangan,
|
||||
|
|
@ -210,7 +393,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
|
||||
|
||||
var fileNames = new List<string>();
|
||||
foreach (var file in FotoKedatangan)
|
||||
|
|
@ -223,42 +406,27 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
await file.CopyToAsync(stream);
|
||||
fileNames.Add(name);
|
||||
}
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList();
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList();
|
||||
|
||||
var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKey))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
draft.FotoKedatanganFileNames = fileUrls;
|
||||
draft.FotoKedatanganUploaded = true;
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
if (!string.IsNullOrWhiteSpace(WaktuKedatangan)) draft.WaktuKedatangan = WaktuKedatangan;
|
||||
if (!string.IsNullOrWhiteSpace(Latitude)) draft.Latitude = Latitude;
|
||||
if (!string.IsNullOrWhiteSpace(Longitude)) draft.Longitude = Longitude;
|
||||
if (!string.IsNullOrWhiteSpace(AlamatJalan)) draft.AlamatJalan = AlamatJalan;
|
||||
await _detailService.SaveDraftNonTpsAsync(new DraftSaveRequest
|
||||
var saveResult = await SaveUploadedRecordAsync(
|
||||
isTps: false,
|
||||
nomorSpj: NomorSpj,
|
||||
namaTps: NamaTps,
|
||||
spjDetailId: SpjDetailId,
|
||||
lokasiAngkutId: LokasiAngkutId,
|
||||
applyChanges: request =>
|
||||
{
|
||||
DraftKey = resolvedDraftKey,
|
||||
SessionKey = resolvedDraftKey,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = fileUrls,
|
||||
FotoKedatanganUploaded = true,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = draft.FotoPetugasFileNames,
|
||||
FotoPetugasUploaded = draft.FotoPetugasUploaded,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
request.WaktuKedatangan = string.IsNullOrWhiteSpace(WaktuKedatangan) ? request.WaktuKedatangan : WaktuKedatangan;
|
||||
request.Latitude = string.IsNullOrWhiteSpace(Latitude) ? request.Latitude : Latitude;
|
||||
request.Longitude = string.IsNullOrWhiteSpace(Longitude) ? request.Longitude : Longitude;
|
||||
request.AlamatJalan = string.IsNullOrWhiteSpace(AlamatJalan) ? request.AlamatJalan : AlamatJalan;
|
||||
request.FotoKedatanganFileNames = fileUrls;
|
||||
request.FotoKedatanganUploaded = true;
|
||||
});
|
||||
|
||||
if (!saveResult.Success)
|
||||
{
|
||||
return StatusCode(500, new { success = false, message = saveResult.Message });
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." });
|
||||
|
|
@ -268,8 +436,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UploadFotoTimbanganNonTps(
|
||||
[FromForm] IFormFile? FotoTimbangan,
|
||||
[FromForm] string? DraftKey,
|
||||
[FromForm] string? SessionKey,
|
||||
[FromForm] string? NomorSpj,
|
||||
[FromForm] string? NamaTps,
|
||||
[FromForm] string? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] int ItemIndex,
|
||||
|
|
@ -280,7 +448,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
|
||||
|
||||
var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant();
|
||||
var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant();
|
||||
|
|
@ -289,45 +457,33 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
var filePath = Path.Combine(uploadDir, name);
|
||||
await using var stream = new FileStream(filePath, FileMode.Create);
|
||||
await FotoTimbangan.CopyToAsync(stream);
|
||||
var fileUrl = BuildUploadUrl(dateFolder, name);
|
||||
var fileUrl = BuildUploadUrl(dateFolder, name, NomorSpj, NamaTps);
|
||||
|
||||
var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKey))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
while (draft.Timbangan.Count <= ItemIndex)
|
||||
draft.Timbangan.Add(new DraftTimbanganItem());
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
draft.Timbangan[ItemIndex] = new DraftTimbanganItem
|
||||
var saveResult = await SaveUploadedRecordAsync(
|
||||
isTps: false,
|
||||
nomorSpj: NomorSpj,
|
||||
namaTps: NamaTps,
|
||||
spjDetailId: SpjDetailId,
|
||||
lokasiAngkutId: LokasiAngkutId,
|
||||
applyChanges: request =>
|
||||
{
|
||||
FotoFileName = fileUrl,
|
||||
JenisSampah = JenisSampah ?? "Residu",
|
||||
Berat = Berat,
|
||||
Uploaded = true
|
||||
};
|
||||
await _detailService.SaveDraftNonTpsAsync(new DraftSaveRequest
|
||||
{
|
||||
DraftKey = resolvedDraftKey,
|
||||
SessionKey = resolvedDraftKey,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = draft.FotoKedatanganFileNames,
|
||||
FotoKedatanganUploaded = draft.FotoKedatanganUploaded,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = draft.FotoPetugasFileNames,
|
||||
FotoPetugasUploaded = draft.FotoPetugasUploaded,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
while (request.Timbangan.Count <= ItemIndex)
|
||||
{
|
||||
request.Timbangan.Add(new RecordTimbanganItem());
|
||||
}
|
||||
|
||||
request.Timbangan[ItemIndex] = new RecordTimbanganItem
|
||||
{
|
||||
FotoFileName = fileUrl,
|
||||
JenisSampah = JenisSampah ?? "Residu",
|
||||
Berat = Berat,
|
||||
Uploaded = true
|
||||
};
|
||||
});
|
||||
|
||||
if (!saveResult.Success)
|
||||
{
|
||||
return StatusCode(500, new { success = false, message = saveResult.Message });
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." });
|
||||
|
|
@ -337,8 +493,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UploadFotoPetugasNonTps(
|
||||
[FromForm] List<IFormFile>? FotoPetugas,
|
||||
[FromForm] string? DraftKey,
|
||||
[FromForm] string? SessionKey,
|
||||
[FromForm] string? NomorSpj,
|
||||
[FromForm] string? NamaTps,
|
||||
[FromForm] string? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] string? NamaPetugas)
|
||||
|
|
@ -347,7 +503,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
|
||||
|
||||
var fileNames = new List<string>();
|
||||
foreach (var file in FotoPetugas)
|
||||
|
|
@ -360,39 +516,24 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
await file.CopyToAsync(stream);
|
||||
fileNames.Add(name);
|
||||
}
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList();
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList();
|
||||
|
||||
var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKey))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
draft.FotoPetugasFileNames = fileUrls;
|
||||
draft.FotoPetugasUploaded = true;
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
if (!string.IsNullOrWhiteSpace(NamaPetugas)) draft.NamaPetugas = NamaPetugas;
|
||||
await _detailService.SaveDraftNonTpsAsync(new DraftSaveRequest
|
||||
var saveResult = await SaveUploadedRecordAsync(
|
||||
isTps: false,
|
||||
nomorSpj: NomorSpj,
|
||||
namaTps: NamaTps,
|
||||
spjDetailId: SpjDetailId,
|
||||
lokasiAngkutId: LokasiAngkutId,
|
||||
applyChanges: request =>
|
||||
{
|
||||
DraftKey = resolvedDraftKey,
|
||||
SessionKey = resolvedDraftKey,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = draft.FotoKedatanganFileNames,
|
||||
FotoKedatanganUploaded = draft.FotoKedatanganUploaded,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = fileUrls,
|
||||
FotoPetugasUploaded = true,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
request.FotoPetugasFileNames = fileUrls;
|
||||
request.FotoPetugasUploaded = true;
|
||||
request.NamaPetugas = string.IsNullOrWhiteSpace(NamaPetugas) ? request.NamaPetugas : NamaPetugas;
|
||||
});
|
||||
|
||||
if (!saveResult.Success)
|
||||
{
|
||||
return StatusCode(500, new { success = false, message = saveResult.Message });
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." });
|
||||
|
|
@ -402,8 +543,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UploadFotoKedatangan(
|
||||
[FromForm] List<IFormFile>? FotoKedatangan,
|
||||
[FromForm] string? DraftKey,
|
||||
[FromForm] string? SessionKey,
|
||||
[FromForm] string? NomorSpj,
|
||||
[FromForm] string? NamaTps,
|
||||
[FromForm] string? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] string? WaktuKedatangan,
|
||||
|
|
@ -415,7 +556,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
|
||||
|
||||
var fileNames = new List<string>();
|
||||
foreach (var file in FotoKedatangan)
|
||||
|
|
@ -428,42 +569,27 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
await file.CopyToAsync(stream);
|
||||
fileNames.Add(name);
|
||||
}
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList();
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList();
|
||||
|
||||
var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
draft.FotoKedatanganFileNames = fileUrls;
|
||||
draft.FotoKedatanganUploaded = true;
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
if (!string.IsNullOrWhiteSpace(WaktuKedatangan)) draft.WaktuKedatangan = WaktuKedatangan;
|
||||
if (!string.IsNullOrWhiteSpace(Latitude)) draft.Latitude = Latitude;
|
||||
if (!string.IsNullOrWhiteSpace(Longitude)) draft.Longitude = Longitude;
|
||||
if (!string.IsNullOrWhiteSpace(AlamatJalan)) draft.AlamatJalan = AlamatJalan;
|
||||
await _detailService.SaveDraftTpsAsync(new DraftSaveRequest
|
||||
var saveResult = await SaveUploadedRecordAsync(
|
||||
isTps: true,
|
||||
nomorSpj: NomorSpj,
|
||||
namaTps: NamaTps,
|
||||
spjDetailId: SpjDetailId,
|
||||
lokasiAngkutId: LokasiAngkutId,
|
||||
applyChanges: request =>
|
||||
{
|
||||
DraftKey = resolvedDraftKeyTps,
|
||||
SessionKey = resolvedDraftKeyTps,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = fileUrls,
|
||||
FotoKedatanganUploaded = true,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = draft.FotoPetugasFileNames,
|
||||
FotoPetugasUploaded = draft.FotoPetugasUploaded,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
request.WaktuKedatangan = string.IsNullOrWhiteSpace(WaktuKedatangan) ? request.WaktuKedatangan : WaktuKedatangan;
|
||||
request.Latitude = string.IsNullOrWhiteSpace(Latitude) ? request.Latitude : Latitude;
|
||||
request.Longitude = string.IsNullOrWhiteSpace(Longitude) ? request.Longitude : Longitude;
|
||||
request.AlamatJalan = string.IsNullOrWhiteSpace(AlamatJalan) ? request.AlamatJalan : AlamatJalan;
|
||||
request.FotoKedatanganFileNames = fileUrls;
|
||||
request.FotoKedatanganUploaded = true;
|
||||
});
|
||||
|
||||
if (!saveResult.Success)
|
||||
{
|
||||
return StatusCode(500, new { success = false, message = saveResult.Message });
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." });
|
||||
|
|
@ -473,8 +599,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UploadFotoTimbangan(
|
||||
[FromForm] IFormFile? FotoTimbangan,
|
||||
[FromForm] string? DraftKey,
|
||||
[FromForm] string? SessionKey,
|
||||
[FromForm] string? NomorSpj,
|
||||
[FromForm] string? NamaTps,
|
||||
[FromForm] string? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] int ItemIndex,
|
||||
|
|
@ -485,7 +611,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
|
||||
|
||||
var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant();
|
||||
var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant();
|
||||
|
|
@ -494,45 +620,33 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
var filePath = Path.Combine(uploadDir, name);
|
||||
await using var stream = new FileStream(filePath, FileMode.Create);
|
||||
await FotoTimbangan.CopyToAsync(stream);
|
||||
var fileUrl = BuildUploadUrl(dateFolder, name);
|
||||
var fileUrl = BuildUploadUrl(dateFolder, name, NomorSpj, NamaTps);
|
||||
|
||||
var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
while (draft.Timbangan.Count <= ItemIndex)
|
||||
draft.Timbangan.Add(new DraftTimbanganItem());
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
draft.Timbangan[ItemIndex] = new DraftTimbanganItem
|
||||
var saveResult = await SaveUploadedRecordAsync(
|
||||
isTps: true,
|
||||
nomorSpj: NomorSpj,
|
||||
namaTps: NamaTps,
|
||||
spjDetailId: SpjDetailId,
|
||||
lokasiAngkutId: LokasiAngkutId,
|
||||
applyChanges: request =>
|
||||
{
|
||||
FotoFileName = fileUrl,
|
||||
JenisSampah = JenisSampah ?? "Residu",
|
||||
Berat = Berat,
|
||||
Uploaded = true
|
||||
};
|
||||
await _detailService.SaveDraftTpsAsync(new DraftSaveRequest
|
||||
{
|
||||
DraftKey = resolvedDraftKeyTps,
|
||||
SessionKey = resolvedDraftKeyTps,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = draft.FotoKedatanganFileNames,
|
||||
FotoKedatanganUploaded = draft.FotoKedatanganUploaded,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = draft.FotoPetugasFileNames,
|
||||
FotoPetugasUploaded = draft.FotoPetugasUploaded,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
while (request.Timbangan.Count <= ItemIndex)
|
||||
{
|
||||
request.Timbangan.Add(new RecordTimbanganItem());
|
||||
}
|
||||
|
||||
request.Timbangan[ItemIndex] = new RecordTimbanganItem
|
||||
{
|
||||
FotoFileName = fileUrl,
|
||||
JenisSampah = JenisSampah ?? "Residu",
|
||||
Berat = Berat,
|
||||
Uploaded = true
|
||||
};
|
||||
});
|
||||
|
||||
if (!saveResult.Success)
|
||||
{
|
||||
return StatusCode(500, new { success = false, message = saveResult.Message });
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." });
|
||||
|
|
@ -542,8 +656,8 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UploadFotoPetugas(
|
||||
[FromForm] List<IFormFile>? FotoPetugas,
|
||||
[FromForm] string? DraftKey,
|
||||
[FromForm] string? SessionKey,
|
||||
[FromForm] string? NomorSpj,
|
||||
[FromForm] string? NamaTps,
|
||||
[FromForm] string? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] string? NamaPetugas)
|
||||
|
|
@ -552,7 +666,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
var uploadDir = GetUploadDirectory(dateFolder, NomorSpj, NamaTps);
|
||||
|
||||
var fileNames = new List<string>();
|
||||
foreach (var file in FotoPetugas)
|
||||
|
|
@ -565,39 +679,24 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
await file.CopyToAsync(stream);
|
||||
fileNames.Add(name);
|
||||
}
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList();
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n, NomorSpj, NamaTps)).ToList();
|
||||
|
||||
var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
draft.FotoPetugasFileNames = fileUrls;
|
||||
draft.FotoPetugasUploaded = true;
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
if (!string.IsNullOrWhiteSpace(NamaPetugas)) draft.NamaPetugas = NamaPetugas;
|
||||
await _detailService.SaveDraftTpsAsync(new DraftSaveRequest
|
||||
var saveResult = await SaveUploadedRecordAsync(
|
||||
isTps: true,
|
||||
nomorSpj: NomorSpj,
|
||||
namaTps: NamaTps,
|
||||
spjDetailId: SpjDetailId,
|
||||
lokasiAngkutId: LokasiAngkutId,
|
||||
applyChanges: request =>
|
||||
{
|
||||
DraftKey = resolvedDraftKeyTps,
|
||||
SessionKey = resolvedDraftKeyTps,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = draft.FotoKedatanganFileNames,
|
||||
FotoKedatanganUploaded = draft.FotoKedatanganUploaded,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = fileUrls,
|
||||
FotoPetugasUploaded = true,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
request.FotoPetugasFileNames = fileUrls;
|
||||
request.FotoPetugasUploaded = true;
|
||||
request.NamaPetugas = string.IsNullOrWhiteSpace(NamaPetugas) ? request.NamaPetugas : NamaPetugas;
|
||||
});
|
||||
|
||||
if (!saveResult.Success)
|
||||
{
|
||||
return StatusCode(500, new { success = false, message = saveResult.Message });
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." });
|
||||
|
|
@ -660,31 +759,9 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
- Jawab hanya angka dengan format 2 digit desimal pakai titik (contoh: 54.45).
|
||||
- Jika tidak terbaca jawab: UNREADABLE
|
||||
- Fokus pada angka layar LED merah yang menyala.
|
||||
|
||||
Saya berikan 3 contoh foto timbangan yang benar:
|
||||
Foto 1 = 75.23
|
||||
Foto 2 = 79.86
|
||||
Foto 3 = 54.45
|
||||
|
||||
Sekarang baca angka pada foto terakhir."
|
||||
},
|
||||
|
||||
new
|
||||
{
|
||||
type = "image_url",
|
||||
image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888384/Notes_-_2026-02-11_08.52.31_wonhbm.jpg" }
|
||||
},
|
||||
|
||||
new
|
||||
{
|
||||
type = "image_url",
|
||||
image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888429/Notes_-_2026-02-11_08.52.34_xairzy.jpg" }
|
||||
},
|
||||
|
||||
new
|
||||
{
|
||||
type = "image_url",
|
||||
image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888473/ChatGPT_Image_Feb_11_2026_03_00_33_PM_ujhdlw.png" }
|
||||
- Abaikan refleksi atau pantulan cahaya yang mungkin muncul di layar.
|
||||
- Abaikan timestamp seperti tanggal, jam, atau informasi lain yang biasanya muncul di layar timbangan.
|
||||
"
|
||||
},
|
||||
|
||||
new
|
||||
|
|
@ -702,7 +779,7 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
var request = new HttpRequestMessage(HttpMethod.Post, "https://openrouter.ai/api/v1/chat/completions");
|
||||
request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {apiKey}");
|
||||
request.Headers.TryAddWithoutValidation("Accept", "application/json");
|
||||
request.Headers.TryAddWithoutValidation("HTTP-Referer", "https://yourdomain.com");
|
||||
request.Headers.TryAddWithoutValidation("HTTP-Referer", "https://pesapakawan.dinaslhdki.id");
|
||||
request.Headers.TryAddWithoutValidation("X-Title", "eSPJ OCR Timbangan");
|
||||
|
||||
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
|
|
|||
|
|
@ -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 string NomorSpj { get; set; } = string.Empty;
|
||||
public string LokasiAngkutId { get; set; } = string.Empty;
|
||||
public string SpjDetailId { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public int Index { get; set; }
|
||||
public string Latitude { get; set; } = string.Empty;
|
||||
|
|
@ -35,11 +38,16 @@ namespace eSPJ.Models
|
|||
public List<string> FotoPetugas { get; set; } = new();
|
||||
public bool FotoPetugasUploaded { get; set; }
|
||||
public string NamaPetugas { get; set; } = string.Empty;
|
||||
public bool Submitted { get; set; }
|
||||
public bool IsSubmit { get; set; }
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.Now;
|
||||
public DateTime? SubmittedAt { get; set; }
|
||||
}
|
||||
|
||||
public class DetailPenjemputanRequest
|
||||
{
|
||||
public string NomorSpj { get; set; } = string.Empty;
|
||||
public string LokasiAngkutId { get; set; } = string.Empty;
|
||||
public string SpjDetailId { get; set; } = string.Empty;
|
||||
public string TpsName { get; set; } = string.Empty;
|
||||
public string Latitude { get; set; } = string.Empty;
|
||||
public string Longitude { get; set; } = string.Empty;
|
||||
|
|
@ -77,7 +85,7 @@ namespace eSPJ.Models
|
|||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class DraftTimbanganItem
|
||||
public class RecordTimbanganItem
|
||||
{
|
||||
public decimal Berat { get; set; }
|
||||
public string JenisSampah { get; set; } = "Residu";
|
||||
|
|
@ -86,33 +94,10 @@ namespace eSPJ.Models
|
|||
public string OcrInfo { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class DraftPenjemputanNonTps
|
||||
public class RecordSaveRequest
|
||||
{
|
||||
public string SessionKey { get; set; } = string.Empty;
|
||||
public string LokasiAngkutId { get; set; } = string.Empty;
|
||||
public string SpjDetailId { get; set; } = string.Empty;
|
||||
public string Latitude { get; set; } = string.Empty;
|
||||
public string Longitude { get; set; } = string.Empty;
|
||||
public string AlamatJalan { get; set; } = string.Empty;
|
||||
public string WaktuKedatangan { get; set; } = string.Empty;
|
||||
public List<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 NomorSpj { get; set; } = string.Empty;
|
||||
public string NamaTps { get; set; } = string.Empty;
|
||||
public string LokasiAngkutId { get; set; } = string.Empty;
|
||||
public string SpjDetailId { get; set; } = string.Empty;
|
||||
public string Latitude { get; set; } = string.Empty;
|
||||
|
|
@ -121,7 +106,7 @@ namespace eSPJ.Models
|
|||
public string WaktuKedatangan { get; set; } = string.Empty;
|
||||
public bool FotoKedatanganUploaded { get; set; }
|
||||
public List<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 TotalAnorganik { get; set; }
|
||||
public decimal TotalResidu { get; set; }
|
||||
|
|
@ -129,21 +114,12 @@ namespace eSPJ.Models
|
|||
public bool FotoPetugasUploaded { get; set; }
|
||||
public List<string> FotoPetugasFileNames { get; set; } = new();
|
||||
public string NamaPetugas { get; set; } = string.Empty;
|
||||
public bool IsSubmit { get; set; }
|
||||
}
|
||||
|
||||
public class DraftSaveResponse
|
||||
public class RecordSaveResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string? DraftKey { get; set; }
|
||||
public string? SessionKey { get; set; }
|
||||
}
|
||||
|
||||
public class DraftLoadResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public bool HasDraft { get; set; }
|
||||
public DraftPenjemputanNonTps? Draft { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
Program.cs
10
Program.cs
|
|
@ -1,3 +1,4 @@
|
|||
using Microsoft.Extensions.FileProviders;
|
||||
using eSPJ.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
|
@ -7,6 +8,7 @@ builder.Services.AddControllersWithViews();
|
|||
builder.Services.AddHttpClient();
|
||||
|
||||
// Register custom services
|
||||
builder.Services.AddScoped<IDetailPenjemputanStore, FileDetailPenjemputanStore>();
|
||||
builder.Services.AddScoped<DetailPenjemputanService>();
|
||||
builder.Services.AddScoped<HistoryService>();
|
||||
|
||||
|
|
@ -21,6 +23,11 @@ if (!app.Environment.IsDevelopment())
|
|||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "uploads")),
|
||||
RequestPath = "/uploads"
|
||||
});
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
if (context.Request.Path.Equals("/driver/serviceworker.js", StringComparison.OrdinalIgnoreCase))
|
||||
|
|
@ -28,6 +35,9 @@ app.Use(async (context, next) =>
|
|||
context.Response.OnStarting(() =>
|
||||
{
|
||||
context.Response.Headers["Service-Worker-Allowed"] = "/upst";
|
||||
context.Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0";
|
||||
context.Response.Headers["Pragma"] = "no-cache";
|
||||
context.Response.Headers["Expires"] = "0";
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +1,57 @@
|
|||
using System.Text.Json;
|
||||
using eSPJ.Models;
|
||||
|
||||
namespace eSPJ.Services
|
||||
{
|
||||
public class DetailPenjemputanService
|
||||
{
|
||||
private readonly string _dataFilePath;
|
||||
private readonly IDetailPenjemputanStore _store;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
private readonly ILogger<DetailPenjemputanService> _logger;
|
||||
|
||||
public DetailPenjemputanService(
|
||||
IWebHostEnvironment env,
|
||||
IDetailPenjemputanStore store,
|
||||
ILogger<DetailPenjemputanService> logger)
|
||||
{
|
||||
_env = env;
|
||||
_store = store;
|
||||
_logger = logger;
|
||||
_dataFilePath = Path.Combine(_env.ContentRootPath, "Data", "detail-penjemputan.json");
|
||||
}
|
||||
|
||||
private static string SanitizePathSegment(string? value, string fallback = "umum")
|
||||
{
|
||||
var safe = string.Concat((value ?? string.Empty).Trim().Select(c =>
|
||||
char.IsLetterOrDigit(c) || c == '-' || c == '_'
|
||||
? c
|
||||
: '-'));
|
||||
|
||||
while (safe.Contains("--"))
|
||||
{
|
||||
safe = safe.Replace("--", "-");
|
||||
}
|
||||
|
||||
safe = safe.Trim('-');
|
||||
return string.IsNullOrWhiteSpace(safe) ? fallback : safe;
|
||||
}
|
||||
|
||||
public async Task<List<TpsData>> GetAllTpsDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_dataFilePath))
|
||||
{
|
||||
return new List<TpsData>();
|
||||
}
|
||||
return await _store.GetSubmittedAsync();
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(_dataFilePath);
|
||||
var data = JsonSerializer.Deserialize<List<TpsData>>(json);
|
||||
return data ?? new List<TpsData>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reading TPS data from JSON");
|
||||
return new List<TpsData>();
|
||||
}
|
||||
public async Task<List<TpsData>> GetRecordsByNomorSpjAsync(string nomorSpj)
|
||||
{
|
||||
return await _store.GetByNomorSpjAsync(nomorSpj);
|
||||
}
|
||||
|
||||
public async Task<bool> SaveTpsDataAsync(List<TpsData> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_dataFilePath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
foreach (var item in data)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
await _store.SaveSubmittedAsync(item);
|
||||
}
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(data, options);
|
||||
await File.WriteAllTextAsync(_dataFilePath, json);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -68,7 +65,6 @@ namespace eSPJ.Services
|
|||
{
|
||||
try
|
||||
{
|
||||
// Validate request
|
||||
if (string.IsNullOrEmpty(request.TpsName))
|
||||
{
|
||||
return new DetailPenjemputanResponse
|
||||
|
|
@ -78,33 +74,6 @@ namespace eSPJ.Services
|
|||
};
|
||||
}
|
||||
|
||||
if (request.FotoKedatangan == null || !request.FotoKedatangan.Any())
|
||||
{
|
||||
return new DetailPenjemputanResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Foto kedatangan harus diupload"
|
||||
};
|
||||
}
|
||||
|
||||
if (request.FotoTimbangan == null || !request.FotoTimbangan.Any())
|
||||
{
|
||||
return new DetailPenjemputanResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Foto timbangan harus diupload"
|
||||
};
|
||||
}
|
||||
|
||||
if (request.FotoPetugas == null || !request.FotoPetugas.Any())
|
||||
{
|
||||
return new DetailPenjemputanResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Foto petugas harus diupload"
|
||||
};
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.NamaPetugas))
|
||||
{
|
||||
return new DetailPenjemputanResponse
|
||||
|
|
@ -114,47 +83,72 @@ namespace eSPJ.Services
|
|||
};
|
||||
}
|
||||
|
||||
var existingRecord = await GetRecordDetailAsync(request.NomorSpj, request.SpjDetailId, request.LokasiAngkutId, request.TpsName);
|
||||
|
||||
var now = DateTime.Now;
|
||||
var datePart = now.ToString("yyyy-MM-dd");
|
||||
var uploadPath = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", datePart);
|
||||
var uploadBaseUrl = $"/uploads/penjemputan/{datePart}";
|
||||
var spjFolder = SanitizePathSegment(request.NomorSpj, "spj-umum");
|
||||
var tpsFolder = SanitizePathSegment(request.TpsName, "tps-1");
|
||||
var uploadPath = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", datePart, spjFolder, tpsFolder);
|
||||
var uploadBaseUrl = $"/uploads/penjemputan/{datePart}/{spjFolder}/{tpsFolder}";
|
||||
if (!Directory.Exists(uploadPath))
|
||||
{
|
||||
Directory.CreateDirectory(uploadPath);
|
||||
}
|
||||
|
||||
var tpsData = new TpsData
|
||||
{
|
||||
Name = request.TpsName,
|
||||
Latitude = request.Latitude,
|
||||
Longitude = request.Longitude,
|
||||
AlamatJalan = request.AlamatJalan,
|
||||
WaktuKedatangan = request.WaktuKedatangan,
|
||||
TotalTimbangan = request.TotalTimbangan,
|
||||
TotalOrganik = request.TotalOrganik,
|
||||
TotalAnorganik = request.TotalAnorganik,
|
||||
TotalResidu = request.TotalResidu,
|
||||
NamaPetugas = request.NamaPetugas,
|
||||
Submitted = true,
|
||||
FotoKedatanganUploaded = true,
|
||||
FotoPetugasUploaded = true
|
||||
};
|
||||
var tpsData = existingRecord != null
|
||||
? CloneRecord(existingRecord)
|
||||
: new TpsData();
|
||||
|
||||
// Save foto kedatangan
|
||||
foreach (var file in request.FotoKedatangan)
|
||||
tpsData.NomorSpj = request.NomorSpj;
|
||||
tpsData.LokasiAngkutId = request.LokasiAngkutId;
|
||||
tpsData.SpjDetailId = request.SpjDetailId;
|
||||
tpsData.Name = request.TpsName;
|
||||
tpsData.Latitude = request.Latitude;
|
||||
tpsData.Longitude = request.Longitude;
|
||||
tpsData.AlamatJalan = request.AlamatJalan;
|
||||
tpsData.WaktuKedatangan = request.WaktuKedatangan;
|
||||
tpsData.TotalTimbangan = request.TotalTimbangan;
|
||||
tpsData.TotalOrganik = request.TotalOrganik;
|
||||
tpsData.TotalAnorganik = request.TotalAnorganik;
|
||||
tpsData.TotalResidu = request.TotalResidu;
|
||||
tpsData.NamaPetugas = request.NamaPetugas;
|
||||
tpsData.IsSubmit = true;
|
||||
tpsData.SubmittedAt ??= DateTime.Now;
|
||||
tpsData.UpdatedAt = DateTime.Now;
|
||||
|
||||
if (request.FotoKedatangan != null && request.FotoKedatangan.Any())
|
||||
{
|
||||
var fileName = $"kedatangan_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
|
||||
var filePath = Path.Combine(uploadPath, fileName);
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
tpsData.FotoKedatangan = new List<string>();
|
||||
foreach (var file in request.FotoKedatangan)
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
var fileName = $"kedatangan_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
|
||||
var filePath = Path.Combine(uploadPath, fileName);
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
tpsData.FotoKedatangan.Add($"{uploadBaseUrl}/{fileName}");
|
||||
}
|
||||
tpsData.FotoKedatangan.Add($"{uploadBaseUrl}/{fileName}");
|
||||
tpsData.FotoKedatanganUploaded = tpsData.FotoKedatangan.Count > 0;
|
||||
}
|
||||
else if (existingRecord?.FotoKedatangan?.Any() == true)
|
||||
{
|
||||
tpsData.FotoKedatangan = new List<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.BeratTimbangan != null && request.JenisSampahList != null)
|
||||
if (request.FotoTimbangan != null && request.FotoTimbangan.Any() && request.BeratTimbangan != null && request.JenisSampahList != null)
|
||||
{
|
||||
tpsData.Timbangan = new List<TimbanganItem>();
|
||||
for (int i = 0; i < request.FotoTimbangan.Count; i++)
|
||||
{
|
||||
var file = request.FotoTimbangan[i];
|
||||
|
|
@ -174,7 +168,7 @@ namespace eSPJ.Services
|
|||
tpsData.Timbangan.Add(new TimbanganItem
|
||||
{
|
||||
FotoFileName = $"{uploadBaseUrl}/{fileName}",
|
||||
Berat = new List<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>(),
|
||||
JenisSampah = new List<JenisSampah> { jenisSampah },
|
||||
IsUploaded = true,
|
||||
|
|
@ -182,22 +176,49 @@ namespace eSPJ.Services
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Save foto petugas
|
||||
foreach (var file in request.FotoPetugas)
|
||||
else if (existingRecord?.Timbangan?.Any() == true)
|
||||
{
|
||||
var fileName = $"petugas_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
|
||||
var filePath = Path.Combine(uploadPath, fileName);
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
tpsData.Timbangan = existingRecord.Timbangan;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new DetailPenjemputanResponse
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
tpsData.FotoPetugas.Add($"{uploadBaseUrl}/{fileName}");
|
||||
Success = false,
|
||||
Message = "Foto timbangan harus diupload"
|
||||
};
|
||||
}
|
||||
|
||||
var allData = await GetAllTpsDataAsync();
|
||||
allData.Add(tpsData);
|
||||
await SaveTpsDataAsync(allData);
|
||||
if (request.FotoPetugas != null && request.FotoPetugas.Any())
|
||||
{
|
||||
tpsData.FotoPetugas = new List<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
|
||||
{
|
||||
|
|
@ -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");
|
||||
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||||
var safe = string.Concat(sessionKey.Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_'));
|
||||
if (string.IsNullOrEmpty(safe)) safe = "default";
|
||||
return Path.Combine(dir, $"draft-{prefix}-{safe}.json");
|
||||
return SaveRecordInternalAsync(request);
|
||||
}
|
||||
|
||||
private Task<DraftSaveResponse> SaveDraftAsync(string prefix, DraftSaveRequest request)
|
||||
{
|
||||
return SaveDraftInternalAsync(prefix, request);
|
||||
}
|
||||
|
||||
private async Task<DraftSaveResponse> SaveDraftInternalAsync(string prefix, DraftSaveRequest request)
|
||||
private async Task<RecordSaveResponse> SaveRecordInternalAsync(RecordSaveRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = GetDraftFilePath(prefix, request.SessionKey);
|
||||
var draft = new DraftPenjemputanNonTps
|
||||
await _store.SaveRecordAsync(request);
|
||||
|
||||
return new RecordSaveResponse { Success = true, Message = "Data tersimpan." };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error saving penjemputan record");
|
||||
return new RecordSaveResponse { Success = false, Message = $"Gagal menyimpan data: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<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,
|
||||
LokasiAngkutId = request.LokasiAngkutId,
|
||||
SpjDetailId = request.SpjDetailId,
|
||||
Latitude = request.Latitude,
|
||||
Longitude = request.Longitude,
|
||||
AlamatJalan = request.AlamatJalan,
|
||||
WaktuKedatangan = request.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = request.FotoKedatanganFileNames,
|
||||
FotoKedatanganUploaded = request.FotoKedatanganUploaded,
|
||||
Timbangan = request.Timbangan,
|
||||
TotalOrganik = request.TotalOrganik,
|
||||
TotalAnorganik = request.TotalAnorganik,
|
||||
TotalResidu = request.TotalResidu,
|
||||
TotalTimbangan = request.TotalTimbangan,
|
||||
FotoPetugasFileNames = request.FotoPetugasFileNames,
|
||||
FotoPetugasUploaded = request.FotoPetugasUploaded,
|
||||
NamaPetugas = request.NamaPetugas,
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||
var json = JsonSerializer.Serialize(draft, options);
|
||||
await File.WriteAllTextAsync(filePath, json);
|
||||
|
||||
return new DraftSaveResponse { Success = true, Message = "Draft tersimpan.", DraftKey = request.DraftKey, SessionKey = request.SessionKey };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error saving {Prefix} draft", prefix);
|
||||
return new DraftSaveResponse { Success = false, Message = $"Gagal menyimpan draft: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<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;
|
||||
}
|
||||
FotoFileName = item.FotoFileName,
|
||||
Berat = new List<decimal>(item.Berat ?? new List<decimal>()),
|
||||
LokasiAngkut = new List<string>(item.LokasiAngkut ?? new List<string>()),
|
||||
JenisSampah = new List<JenisSampah>(item.JenisSampah ?? new List<JenisSampah>()),
|
||||
IsUploaded = item.IsUploaded,
|
||||
WaktuUpload = item.WaktuUpload
|
||||
}).ToList(),
|
||||
TotalOrganik = source.TotalOrganik,
|
||||
TotalAnorganik = source.TotalAnorganik,
|
||||
TotalResidu = source.TotalResidu,
|
||||
TotalTimbangan = source.TotalTimbangan,
|
||||
FotoPetugas = new List<string>(source.FotoPetugas ?? new List<string>()),
|
||||
FotoPetugasUploaded = source.FotoPetugasUploaded,
|
||||
NamaPetugas = source.NamaPetugas,
|
||||
IsSubmit = source.IsSubmit,
|
||||
UpdatedAt = source.UpdatedAt,
|
||||
SubmittedAt = source.SubmittedAt,
|
||||
};
|
||||
}
|
||||
|
||||
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>
|
||||
if ("serviceWorker" in navigator) {
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -146,7 +146,6 @@
|
|||
--blur-md: 12px;
|
||||
--blur-lg: 16px;
|
||||
--blur-xl: 24px;
|
||||
--blur-3xl: 64px;
|
||||
--default-transition-duration: 150ms;
|
||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--default-font-family: var(--font-sans);
|
||||
|
|
@ -359,9 +358,6 @@
|
|||
.-top-4 {
|
||||
top: calc(var(--spacing) * -4);
|
||||
}
|
||||
.-top-24 {
|
||||
top: calc(var(--spacing) * -24);
|
||||
}
|
||||
.top-0 {
|
||||
top: calc(var(--spacing) * 0);
|
||||
}
|
||||
|
|
@ -404,9 +400,6 @@
|
|||
.-right-6 {
|
||||
right: calc(var(--spacing) * -6);
|
||||
}
|
||||
.-right-24 {
|
||||
right: calc(var(--spacing) * -24);
|
||||
}
|
||||
.right-0 {
|
||||
right: calc(var(--spacing) * 0);
|
||||
}
|
||||
|
|
@ -434,9 +427,6 @@
|
|||
.right-full {
|
||||
right: 100%;
|
||||
}
|
||||
.-bottom-0 {
|
||||
bottom: calc(var(--spacing) * -0);
|
||||
}
|
||||
.-bottom-0\.5 {
|
||||
bottom: calc(var(--spacing) * -0.5);
|
||||
}
|
||||
|
|
@ -446,9 +436,6 @@
|
|||
.-bottom-6 {
|
||||
bottom: calc(var(--spacing) * -6);
|
||||
}
|
||||
.-bottom-32 {
|
||||
bottom: calc(var(--spacing) * -32);
|
||||
}
|
||||
.bottom-0 {
|
||||
bottom: calc(var(--spacing) * 0);
|
||||
}
|
||||
|
|
@ -476,15 +463,9 @@
|
|||
.bottom-100 {
|
||||
bottom: calc(var(--spacing) * 100);
|
||||
}
|
||||
.-left-32 {
|
||||
left: calc(var(--spacing) * -32);
|
||||
}
|
||||
.left-0 {
|
||||
left: calc(var(--spacing) * 0);
|
||||
}
|
||||
.left-1 {
|
||||
left: calc(var(--spacing) * 1);
|
||||
}
|
||||
.left-1\/2 {
|
||||
left: calc(1/2 * 100%);
|
||||
}
|
||||
|
|
@ -542,6 +523,9 @@
|
|||
.z-\[100\] {
|
||||
z-index: 100;
|
||||
}
|
||||
.z-\[9999\] {
|
||||
z-index: 9999;
|
||||
}
|
||||
.order-0 {
|
||||
order: 0;
|
||||
}
|
||||
|
|
@ -800,9 +784,6 @@
|
|||
.-mr-16 {
|
||||
margin-right: calc(var(--spacing) * -16);
|
||||
}
|
||||
.mr-1 {
|
||||
margin-right: calc(var(--spacing) * 1);
|
||||
}
|
||||
.mr-1\.5 {
|
||||
margin-right: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
|
|
@ -893,18 +874,12 @@
|
|||
.aspect-square {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
.h-0 {
|
||||
height: calc(var(--spacing) * 0);
|
||||
}
|
||||
.h-0\.5 {
|
||||
height: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
.h-1 {
|
||||
height: calc(var(--spacing) * 1);
|
||||
}
|
||||
.h-1\.5 {
|
||||
height: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.h-2 {
|
||||
height: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
|
@ -977,15 +952,9 @@
|
|||
.h-64 {
|
||||
height: calc(var(--spacing) * 64);
|
||||
}
|
||||
.h-72 {
|
||||
height: calc(var(--spacing) * 72);
|
||||
}
|
||||
.h-75 {
|
||||
height: calc(var(--spacing) * 75);
|
||||
}
|
||||
.h-96 {
|
||||
height: calc(var(--spacing) * 96);
|
||||
}
|
||||
.h-100 {
|
||||
height: calc(var(--spacing) * 100);
|
||||
}
|
||||
|
|
@ -1016,9 +985,6 @@
|
|||
.w-1 {
|
||||
width: calc(var(--spacing) * 1);
|
||||
}
|
||||
.w-1\.5 {
|
||||
width: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.w-1\/3 {
|
||||
width: calc(1/3 * 100%);
|
||||
}
|
||||
|
|
@ -1094,15 +1060,9 @@
|
|||
.w-64 {
|
||||
width: calc(var(--spacing) * 64);
|
||||
}
|
||||
.w-72 {
|
||||
width: calc(var(--spacing) * 72);
|
||||
}
|
||||
.w-75 {
|
||||
width: calc(var(--spacing) * 75);
|
||||
}
|
||||
.w-96 {
|
||||
width: calc(var(--spacing) * 96);
|
||||
}
|
||||
.w-100 {
|
||||
width: calc(var(--spacing) * 100);
|
||||
}
|
||||
|
|
@ -1121,9 +1081,6 @@
|
|||
.w-max {
|
||||
width: max-content;
|
||||
}
|
||||
.max-w-\[260px\] {
|
||||
max-width: 260px;
|
||||
}
|
||||
.max-w-full {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
@ -1181,10 +1138,6 @@
|
|||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.-translate-x-1 {
|
||||
--tw-translate-x: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-x-1\/2 {
|
||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
|
|
@ -1197,10 +1150,6 @@
|
|||
--tw-translate-x: calc(var(--spacing) * 16);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1 {
|
||||
--tw-translate-y: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1\/2 {
|
||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
|
|
@ -1327,13 +1276,6 @@
|
|||
.gap-6 {
|
||||
gap: calc(var(--spacing) * 6);
|
||||
}
|
||||
.space-y-0 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));
|
||||
margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.space-y-0\.5 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
|
|
@ -1710,9 +1652,6 @@
|
|||
.border-t-transparent {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
.border-t-white {
|
||||
border-top-color: var(--color-white);
|
||||
}
|
||||
.bg-amber-400 {
|
||||
background-color: var(--color-amber-400);
|
||||
}
|
||||
|
|
@ -1770,9 +1709,6 @@
|
|||
.bg-blue-600 {
|
||||
background-color: var(--color-blue-600);
|
||||
}
|
||||
.bg-cyan-400 {
|
||||
background-color: var(--color-cyan-400);
|
||||
}
|
||||
.bg-cyan-400\/10 {
|
||||
background-color: color-mix(in srgb, oklch(78.9% 0.154 211.53) 10%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -1836,9 +1772,6 @@
|
|||
.bg-indigo-300 {
|
||||
background-color: var(--color-indigo-300);
|
||||
}
|
||||
.bg-lime-500 {
|
||||
background-color: var(--color-lime-500);
|
||||
}
|
||||
.bg-lime-500\/15 {
|
||||
background-color: color-mix(in srgb, oklch(76.8% 0.233 130.85) 15%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -1860,9 +1793,6 @@
|
|||
.bg-orange-500 {
|
||||
background-color: var(--color-orange-500);
|
||||
}
|
||||
.bg-orange-700 {
|
||||
background-color: var(--color-orange-700);
|
||||
}
|
||||
.bg-orange-700\/30 {
|
||||
background-color: color-mix(in srgb, oklch(55.3% 0.195 38.402) 30%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -1905,9 +1835,6 @@
|
|||
.bg-slate-900 {
|
||||
background-color: var(--color-slate-900);
|
||||
}
|
||||
.bg-slate-950 {
|
||||
background-color: var(--color-slate-950);
|
||||
}
|
||||
.bg-slate-950\/60 {
|
||||
background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 60%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -1938,18 +1865,18 @@
|
|||
background-color: color-mix(in oklab, var(--color-white) 20%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-white\/30 {
|
||||
background-color: color-mix(in srgb, #fff 30%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-white) 30%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-white\/70 {
|
||||
background-color: color-mix(in srgb, #fff 70%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-white) 70%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-white\/95 {
|
||||
background-color: color-mix(in srgb, #fff 95%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-white) 95%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-yellow-50 {
|
||||
background-color: var(--color-yellow-50);
|
||||
}
|
||||
|
|
@ -2298,6 +2225,9 @@
|
|||
.py-6 {
|
||||
padding-block: calc(var(--spacing) * 6);
|
||||
}
|
||||
.py-7 {
|
||||
padding-block: calc(var(--spacing) * 7);
|
||||
}
|
||||
.py-12 {
|
||||
padding-block: calc(var(--spacing) * 12);
|
||||
}
|
||||
|
|
@ -2543,10 +2473,6 @@
|
|||
--tw-tracking: 0.24em;
|
||||
letter-spacing: 0.24em;
|
||||
}
|
||||
.tracking-\[0\.28em\] {
|
||||
--tw-tracking: 0.28em;
|
||||
letter-spacing: 0.28em;
|
||||
}
|
||||
.tracking-tight {
|
||||
--tw-tracking: var(--tracking-tight);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
|
|
@ -2732,30 +2658,12 @@
|
|||
.text-white {
|
||||
color: var(--color-white);
|
||||
}
|
||||
.text-white\/30 {
|
||||
color: color-mix(in srgb, #fff 30%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-white) 30%, transparent);
|
||||
}
|
||||
}
|
||||
.text-white\/40 {
|
||||
color: color-mix(in srgb, #fff 40%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-white) 40%, transparent);
|
||||
}
|
||||
}
|
||||
.text-white\/70 {
|
||||
color: color-mix(in srgb, #fff 70%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-white) 70%, transparent);
|
||||
}
|
||||
}
|
||||
.text-white\/80 {
|
||||
color: color-mix(in srgb, #fff 80%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-white) 80%, transparent);
|
||||
}
|
||||
}
|
||||
.text-white\/90 {
|
||||
color: color-mix(in srgb, #fff 90%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -2816,9 +2724,6 @@
|
|||
.opacity-60 {
|
||||
opacity: 60%;
|
||||
}
|
||||
.opacity-70 {
|
||||
opacity: 70%;
|
||||
}
|
||||
.opacity-75 {
|
||||
opacity: 75%;
|
||||
}
|
||||
|
|
@ -2879,27 +2784,12 @@
|
|||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.shadow-black {
|
||||
--tw-shadow-color: #000;
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
--tw-shadow-color: color-mix(in oklab, var(--color-black) var(--tw-shadow-alpha), transparent);
|
||||
}
|
||||
}
|
||||
.shadow-black\/20 {
|
||||
--tw-shadow-color: color-mix(in srgb, #000 20%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
--tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-black) 20%, transparent) var(--tw-shadow-alpha), transparent);
|
||||
}
|
||||
}
|
||||
.shadow-gray-200 {
|
||||
--tw-shadow-color: oklch(92.8% 0.006 264.531);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
--tw-shadow-color: color-mix(in oklab, var(--color-gray-200) var(--tw-shadow-alpha), transparent);
|
||||
}
|
||||
}
|
||||
.ring-black {
|
||||
--tw-ring-color: var(--color-black);
|
||||
}
|
||||
.ring-black\/5 {
|
||||
--tw-ring-color: color-mix(in srgb, #000 5%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -2938,10 +2828,6 @@
|
|||
--tw-blur: blur(8px);
|
||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||
}
|
||||
.blur-3xl {
|
||||
--tw-blur: blur(var(--blur-3xl));
|
||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||
}
|
||||
.drop-shadow {
|
||||
--tw-drop-shadow-size: drop-shadow(0 1px 2px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.1))) drop-shadow(0 1px 1px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.06)));
|
||||
--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow( 0 1px 1px rgb(0 0 0 / 0.06));
|
||||
|
|
@ -3592,12 +3478,6 @@
|
|||
scale: var(--tw-scale-x) var(--tw-scale-y);
|
||||
}
|
||||
}
|
||||
.active\:shadow-md {
|
||||
&:active {
|
||||
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
.disabled\:cursor-not-allowed {
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
|
|
|
|||
|
|
@ -8,22 +8,50 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
let activeTpsIndex = 0;
|
||||
let tpsData = [];
|
||||
let nomorSpj = 'SPJ/07-2025/PKM/000476';
|
||||
let draftRequestKey = '';
|
||||
|
||||
function buildDraftRequestKey(tps) {
|
||||
const spjDetailId = (tps?.spjDetailId || '').trim();
|
||||
const lokasiAngkutId = (tps?.lokasiAngkutId || '').trim();
|
||||
if (!spjDetailId && !lokasiAngkutId) return '';
|
||||
return `non-tps-${spjDetailId || 'no-spj'}-${lokasiAngkutId || 'no-lokasi'}`.replace(/[^a-zA-Z0-9_-]/g, '');
|
||||
}
|
||||
const RECORD_DETAIL_ENDPOINT = '/upst/detail-penjemputan/api/records/detail';
|
||||
|
||||
let autoSaveTimer = null;
|
||||
let autoSaveStatusEl = null;
|
||||
let loadingOverlayEl = null;
|
||||
let isAutoSaving = false;
|
||||
let pendingAutoSave = false;
|
||||
let lastAutoSaveSignature = "";
|
||||
|
||||
function getLoadingOverlay() {
|
||||
if (loadingOverlayEl && document.body.contains(loadingOverlayEl)) {
|
||||
return loadingOverlayEl;
|
||||
}
|
||||
|
||||
loadingOverlayEl = document.createElement('div');
|
||||
loadingOverlayEl.id = 'detail-loading-overlay';
|
||||
loadingOverlayEl.className = 'fixed inset-0 z-[9999] hidden bg-white/95 backdrop-blur-sm flex items-center justify-center px-6';
|
||||
loadingOverlayEl.innerHTML = `
|
||||
<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() {
|
||||
clearTimeout(autoSaveTimer);
|
||||
showAutoSaveStatus('menyimpan...');
|
||||
autoSaveTimer = setTimeout(autoSaveDraft, 1000);
|
||||
autoSaveTimer = setTimeout(autoSaveRecord, 500);
|
||||
}
|
||||
|
||||
function showAutoSaveStatus(msg, isOk = false) {
|
||||
|
|
@ -39,150 +67,176 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
if (isOk) setTimeout(() => { autoSaveStatusEl.style.opacity = '0'; }, 2500);
|
||||
}
|
||||
|
||||
async function autoSaveDraft() {
|
||||
function buildAutoSavePayload(tps) {
|
||||
return {
|
||||
nomorSpj: nomorSpj || '',
|
||||
namaTps: tps.name || DEFAULT_TPS_NAME,
|
||||
lokasiAngkutId: tps.lokasiAngkutId || '',
|
||||
spjDetailId: tps.spjDetailId || '',
|
||||
latitude: tps.latitude || '',
|
||||
longitude: tps.longitude || '',
|
||||
alamatJalan: tps.alamatJalan || '',
|
||||
waktuKedatangan: tps.waktuKedatangan || '',
|
||||
fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [],
|
||||
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false,
|
||||
timbangan: (tps.timbangan || []).map(t => ({
|
||||
berat: (t.berat && t.berat.length > 0) ? t.berat[0] : 0,
|
||||
jenisSampah: (t.jenisSampah && t.jenisSampah.length > 0) ? t.jenisSampah[0] : DEFAULT_JENIS,
|
||||
fotoFileName: t.fotoFileName || '',
|
||||
uploaded: t.uploaded || false,
|
||||
ocrInfo: t.ocrInfo || ''
|
||||
})),
|
||||
totalOrganik: tps.totalOrganik || 0,
|
||||
totalAnorganik: tps.totalAnorganik || 0,
|
||||
totalResidu: tps.totalResidu || 0,
|
||||
totalTimbangan: tps.totalTimbangan || 0,
|
||||
fotoPetugasFileNames: tps.fotoPetugasFileNames || [],
|
||||
fotoPetugasUploaded: tps.fotoPetugasUploaded || false,
|
||||
namaPetugas: tps.namaPetugas || ''
|
||||
};
|
||||
}
|
||||
|
||||
async function autoSaveRecord() {
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
if (!tps) return;
|
||||
if (tps.submitted) return;
|
||||
if (isAutoSaving) {
|
||||
pendingAutoSave = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildAutoSavePayload(tps);
|
||||
const payloadSignature = JSON.stringify(payload);
|
||||
if (payloadSignature === lastAutoSaveSignature) {
|
||||
showAutoSaveStatus('✓ Data tersimpan', true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isAutoSaving = true;
|
||||
const res = await fetch('/upst/detail-penjemputan/save-record-non-tps', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: payloadSignature
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
lastAutoSaveSignature = payloadSignature;
|
||||
}
|
||||
showAutoSaveStatus(data.success ? '✓ Data tersimpan' : '✗ Gagal simpan', data.success);
|
||||
} catch {
|
||||
showAutoSaveStatus('✗ Gagal simpan data');
|
||||
} finally {
|
||||
isAutoSaving = false;
|
||||
if (pendingAutoSave) {
|
||||
pendingAutoSave = false;
|
||||
scheduleAutoSave();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStringList(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value.filter((item) => typeof item === 'string' && item.trim());
|
||||
}
|
||||
|
||||
function normalizeJenisSampahValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return normalizeJenisSampahValue(value[0]);
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return JENIS_SAMPAH[value] || DEFAULT_JENIS;
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const trimmed = value.trim();
|
||||
const asNumber = Number(trimmed);
|
||||
if (!Number.isNaN(asNumber) && String(asNumber) === trimmed) {
|
||||
return JENIS_SAMPAH[asNumber] || DEFAULT_JENIS;
|
||||
}
|
||||
|
||||
const matched = JENIS_SAMPAH.find(
|
||||
(item) => item.toLowerCase() === trimmed.toLowerCase(),
|
||||
);
|
||||
return matched || DEFAULT_JENIS;
|
||||
}
|
||||
|
||||
return DEFAULT_JENIS;
|
||||
}
|
||||
|
||||
function applyServerRecordToTps(record) {
|
||||
if (!record) return;
|
||||
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
if (!tps) return;
|
||||
const fotoKedatangan = normalizeStringList(
|
||||
record.fotoKedatanganFileNames || record.fotoKedatangan || record.FotoKedatangan,
|
||||
);
|
||||
const fotoPetugas = normalizeStringList(
|
||||
record.fotoPetugasFileNames || record.fotoPetugas || record.FotoPetugas,
|
||||
);
|
||||
|
||||
const payload = {
|
||||
draftKey: draftRequestKey,
|
||||
lokasiAngkutId: tps.lokasiAngkutId || '',
|
||||
spjDetailId: tps.spjDetailId || '',
|
||||
latitude: tps.latitude || '',
|
||||
longitude: tps.longitude || '',
|
||||
alamatJalan: tps.alamatJalan || '',
|
||||
waktuKedatangan: tps.waktuKedatangan || '',
|
||||
fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [],
|
||||
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false,
|
||||
timbangan: (tps.timbangan || []).map(t => ({
|
||||
berat: (t.berat && t.berat.length > 0) ? t.berat[0] : 0,
|
||||
jenisSampah: (t.jenisSampah && t.jenisSampah.length > 0) ? t.jenisSampah[0] : 'Residu',
|
||||
fotoFileName: t.fotoFileName || '',
|
||||
uploaded: t.uploaded || false,
|
||||
ocrInfo: t.ocrInfo || ''
|
||||
})),
|
||||
totalOrganik: tps.totalOrganik || 0,
|
||||
totalAnorganik: tps.totalAnorganik || 0,
|
||||
totalResidu: tps.totalResidu || 0,
|
||||
totalTimbangan: tps.totalTimbangan || 0,
|
||||
fotoPetugasFileNames: tps.fotoPetugasFileNames || [],
|
||||
fotoPetugasUploaded: tps.fotoPetugasUploaded || false,
|
||||
namaPetugas: tps.namaPetugas || ''
|
||||
};
|
||||
tps.name = record.namaTps || record.name || record.Name || tps.name || DEFAULT_TPS_NAME;
|
||||
tps.lokasiAngkutId = record.lokasiAngkutId || record.LokasiAngkutID || tps.lokasiAngkutId;
|
||||
tps.spjDetailId = record.spjDetailId || record.SpjDetailID || tps.spjDetailId;
|
||||
tps.latitude = record.latitude || record.Latitude || tps.latitude;
|
||||
tps.longitude = record.longitude || record.Longitude || tps.longitude;
|
||||
tps.alamatJalan = record.alamatJalan || record.AlamatJalan || tps.alamatJalan;
|
||||
tps.waktuKedatangan = record.waktuKedatangan || record.WaktuKedatangan || tps.waktuKedatangan;
|
||||
tps.fotoKedatangan = [];
|
||||
tps.fotoKedatanganFileNames = fotoKedatangan;
|
||||
tps.fotoKedatanganUploaded = Boolean(record.fotoKedatanganUploaded ?? record.FotoKedatanganUploaded) || fotoKedatangan.length > 0;
|
||||
tps.fotoPetugas = [];
|
||||
tps.fotoPetugasFileNames = fotoPetugas;
|
||||
tps.fotoPetugasUploaded = Boolean(record.fotoPetugasUploaded ?? record.FotoPetugasUploaded) || fotoPetugas.length > 0;
|
||||
tps.namaPetugas = record.namaPetugas || record.NamaPetugas || tps.namaPetugas;
|
||||
tps.totalOrganik = Number(record.totalOrganik ?? record.TotalOrganik ?? tps.totalOrganik) || 0;
|
||||
tps.totalAnorganik = Number(record.totalAnorganik ?? record.TotalAnorganik ?? tps.totalAnorganik) || 0;
|
||||
tps.totalResidu = Number(record.totalResidu ?? record.TotalResidu ?? tps.totalResidu) || 0;
|
||||
tps.totalTimbangan = Number(record.totalTimbangan ?? record.TotalTimbangan ?? tps.totalTimbangan) || 0;
|
||||
tps.submitted = Boolean(record.isSubmit ?? record.IsSubmit ?? record.submitted ?? record.Submitted ?? tps.submitted);
|
||||
|
||||
try {
|
||||
const res = await fetch('/upst/detail-penjemputan/save-draft-non-tps', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
showAutoSaveStatus(data.success ? '✓ Draft tersimpan' : '✗ Gagal simpan', data.success);
|
||||
} catch {
|
||||
showAutoSaveStatus('✗ Gagal simpan draft');
|
||||
const timbangan = record.timbangan || record.Timbangan || [];
|
||||
if (Array.isArray(timbangan) && timbangan.length > 0) {
|
||||
tps.timbangan = timbangan.map(item => ({
|
||||
file: null,
|
||||
fotoFileName: item.fotoFileName || item.FotoFileName || '',
|
||||
berat: [Number(item.berat ?? (Array.isArray(item.Berat) ? item.Berat[0] : item.Berat) ?? 0) || 0],
|
||||
jenisSampah: [normalizeJenisSampahValue(item.jenisSampah ?? item.JenisSampah)],
|
||||
lokasiAngkut: [],
|
||||
uploaded: Boolean(item.uploaded ?? item.isUploaded ?? item.IsUploaded ?? item.fotoFileName ?? item.FotoFileName),
|
||||
ocrInfo: item.ocrInfo || item.OcrInfo || (item.fotoFileName || item.FotoFileName ? 'Foto dari server.' : 'OCR: diproses.')
|
||||
}));
|
||||
} else {
|
||||
tps.timbangan = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDraftFromServer() {
|
||||
if (!draftRequestKey) return;
|
||||
try {
|
||||
const res = await fetch(`/upst/detail-penjemputan/load-draft-non-tps?draftKey=${encodeURIComponent(draftRequestKey)}`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (!data.success || !data.hasDraft || !data.draft) return;
|
||||
|
||||
const d = data.draft;
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
if (!tps) return;
|
||||
|
||||
if (d.lokasiAngkutId) tps.lokasiAngkutId = d.lokasiAngkutId;
|
||||
if (d.spjDetailId) tps.spjDetailId = d.spjDetailId;
|
||||
if (d.latitude) tps.latitude = d.latitude;
|
||||
if (d.longitude) tps.longitude = d.longitude;
|
||||
if (d.alamatJalan) tps.alamatJalan = d.alamatJalan;
|
||||
if (d.waktuKedatangan) tps.waktuKedatangan = d.waktuKedatangan;
|
||||
tps.fotoKedatanganFileNames = d.fotoKedatanganFileNames || [];
|
||||
tps.fotoKedatanganUploaded = d.fotoKedatanganUploaded || false;
|
||||
tps.fotoPetugasFileNames = d.fotoPetugasFileNames || [];
|
||||
tps.fotoPetugasUploaded = d.fotoPetugasUploaded || false;
|
||||
tps.namaPetugas = d.namaPetugas || '';
|
||||
tps.totalOrganik = d.totalOrganik || 0;
|
||||
tps.totalAnorganik = d.totalAnorganik || 0;
|
||||
tps.totalResidu = d.totalResidu || 0;
|
||||
tps.totalTimbangan = d.totalTimbangan || 0;
|
||||
|
||||
if (d.timbangan && d.timbangan.length > 0) {
|
||||
tps.timbangan = d.timbangan.map(t => ({
|
||||
file: null,
|
||||
fotoFileName: t.fotoFileName || '',
|
||||
berat: [t.berat || 0],
|
||||
jenisSampah: [t.jenisSampah || 'Residu'],
|
||||
lokasiAngkut: [],
|
||||
uploaded: t.uploaded || false,
|
||||
ocrInfo: t.ocrInfo || 'OCR: diproses.'
|
||||
}));
|
||||
}
|
||||
|
||||
patchFormFromDraft();
|
||||
showAutoSaveStatus('✓ Draft dipulihkan', true);
|
||||
} catch (err) {
|
||||
console.warn('Gagal memuat draft dari server:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function patchFormFromDraft() {
|
||||
async function loadRecordForCurrentSpj() {
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
const form = tpsContentContainer.querySelector('form');
|
||||
if (!form) return;
|
||||
if (!tps || !nomorSpj) return;
|
||||
|
||||
const hiddenLat = form.querySelector('.tps-latitude');
|
||||
const hiddenLng = form.querySelector('.tps-longitude');
|
||||
const hiddenAlamat = form.querySelector('.tps-alamat-jalan');
|
||||
if (hiddenLat) hiddenLat.value = tps.latitude;
|
||||
if (hiddenLng) hiddenLng.value = tps.longitude;
|
||||
if (hiddenAlamat) hiddenAlamat.value = tps.alamatJalan;
|
||||
const params = new URLSearchParams({ nomorSpj });
|
||||
if (tps.spjDetailId) params.set('spjDetailId', tps.spjDetailId);
|
||||
if (tps.lokasiAngkutId) params.set('lokasiAngkutId', tps.lokasiAngkutId);
|
||||
if (tps.name) params.set('namaTps', tps.name);
|
||||
|
||||
const latDisplay = form.querySelector('.tps-display-latitude');
|
||||
const lngDisplay = form.querySelector('.tps-display-longitude');
|
||||
const waktuDisplay = form.querySelector('.tps-waktu-kedatangan');
|
||||
if (latDisplay && tps.latitude) latDisplay.value = tps.latitude;
|
||||
if (lngDisplay && tps.longitude) lngDisplay.value = tps.longitude;
|
||||
if (waktuDisplay && tps.waktuKedatangan) waktuDisplay.value = tps.waktuKedatangan;
|
||||
try {
|
||||
const res = await fetch(`${RECORD_DETAIL_ENDPOINT}?${params.toString()}`, { cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
|
||||
const namaPetugasInput = form.querySelector('.tps-nama-petugas');
|
||||
if (namaPetugasInput && tps.namaPetugas) namaPetugasInput.value = tps.namaPetugas;
|
||||
const data = await res.json();
|
||||
if (!data.success || !data.hasData || !data.item) return;
|
||||
|
||||
refreshKedatanganUploadState(form);
|
||||
refreshPetugasUploadState(form);
|
||||
|
||||
if (tps.timbangan.length > 0) {
|
||||
const repeater = form.querySelector('.tps-timbangan-repeater');
|
||||
if (repeater) {
|
||||
repeater.innerHTML = '';
|
||||
tps.timbangan.forEach(timb => createTimbanganItem(repeater, timb));
|
||||
}
|
||||
applyServerRecordToTps(data.item);
|
||||
} catch (error) {
|
||||
console.warn('Gagal memuat data non-TPS:', error);
|
||||
}
|
||||
|
||||
const previewKedatangan = form.querySelector('.tps-preview-kedatangan');
|
||||
if (previewKedatangan) {
|
||||
if (tps.fotoKedatangan.length > 0) {
|
||||
renderStoredPhotos(tps.fotoKedatangan, previewKedatangan);
|
||||
} else if (tps.fotoKedatanganUploaded && tps.fotoKedatanganFileNames.length > 0) {
|
||||
renderServerImagePreview(tps.fotoKedatanganFileNames, previewKedatangan);
|
||||
}
|
||||
}
|
||||
|
||||
const previewPetugas = form.querySelector('.tps-preview-petugas');
|
||||
if (previewPetugas) {
|
||||
if (tps.fotoPetugas.length > 0) {
|
||||
renderStoredPhotos(tps.fotoPetugas, previewPetugas);
|
||||
} else if (tps.fotoPetugasUploaded && tps.fotoPetugasFileNames.length > 0) {
|
||||
renderServerImagePreview(tps.fotoPetugasFileNames, previewPetugas);
|
||||
}
|
||||
}
|
||||
|
||||
refreshSubmitButtonState(form);
|
||||
updateTpsTotalTimbangan();
|
||||
}
|
||||
}
|
||||
|
||||
const OCR_AREAS = [
|
||||
{
|
||||
|
|
@ -213,12 +267,11 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
const JENIS_SAMPAH = ["Organik", "Anorganik", "Residu"];
|
||||
const DEFAULT_JENIS = "Residu";
|
||||
const DETAIL_DATA_URL = "/driver/json/detail-penjemputan-non-tps.json";
|
||||
const DEFAULT_TPS_NAME = "Lokasi Pengangkutan 1";
|
||||
const DEFAULT_TPS_NAME = "TPS 1";
|
||||
|
||||
function isBrowserFile(file) {
|
||||
return file instanceof File;
|
||||
}
|
||||
|
||||
function resolveStoredPhoto(file) {
|
||||
return isBrowserFile(file) ? file : null;
|
||||
}
|
||||
|
|
@ -283,7 +336,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
tpsData[0].name = namaTps;
|
||||
tpsData[0].lokasiAngkutId = detail.lokasiAngkutId || detail.LokasiAngkutID || tpsData[0].lokasiAngkutId;
|
||||
tpsData[0].spjDetailId = detail.spjDetailId || detail.SpjDetailID || tpsData[0].spjDetailId;
|
||||
draftRequestKey = buildDraftRequestKey(tpsData[0]);
|
||||
}
|
||||
|
||||
nomorSpj = detail.nomorSpj || nomorSpj;
|
||||
|
|
@ -324,6 +376,23 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
function renderTpsForm() {
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
const submitState = getSubmitState(tps);
|
||||
const actionMarkup = tps.submitted
|
||||
? `
|
||||
<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 = `
|
||||
<form class="space-y-5 pb-8" data-tps-index="${tps.index}">
|
||||
|
|
@ -406,11 +475,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<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>`}
|
||||
${actionMarkup}
|
||||
<p id="auto-save-status" class="text-[11px] text-amber-500 text-center font-medium" style="opacity:0;transition:opacity 0.4s"></p>
|
||||
</form>
|
||||
`;
|
||||
|
|
@ -479,6 +544,8 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
const form = tpsContentContainer.querySelector("form");
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
|
||||
if (tps.submitted) return;
|
||||
|
||||
const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan");
|
||||
const fotoPetugasInput = form.querySelector(".tps-foto-petugas");
|
||||
const namaPetugasInput = form.querySelector(".tps-nama-petugas");
|
||||
|
|
@ -490,7 +557,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
updateWaktuKedatangan();
|
||||
updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan'));
|
||||
refreshKedatanganUploadState(form);
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
fotoPetugasInput.addEventListener('change', function() {
|
||||
|
|
@ -498,12 +564,12 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
tps.fotoPetugasUploaded = false;
|
||||
updateMultiPreview(this, form.querySelector('.tps-preview-petugas'));
|
||||
refreshPetugasUploadState(form);
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
namaPetugasInput.addEventListener('input', function() {
|
||||
tps.namaPetugas = this.value;
|
||||
refreshPetugasUploadState(form);
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
namaPetugasInput.addEventListener('blur', function() {
|
||||
|
|
@ -565,7 +631,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
if (displayWaktu) displayWaktu.value = formatted;
|
||||
|
||||
getLocationUpdate();
|
||||
scheduleAutoSave();
|
||||
}
|
||||
|
||||
function reverseGeocode(lat, lng) {
|
||||
|
|
@ -587,7 +652,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
if (latInput) latInput.value = lat;
|
||||
if (lngInput) lngInput.value = lng;
|
||||
}
|
||||
scheduleAutoSave();
|
||||
})
|
||||
.catch(() => {
|
||||
tps.latitude = lat;
|
||||
|
|
@ -601,7 +665,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
if (latInput) latInput.value = lat;
|
||||
if (lngInput) lngInput.value = lng;
|
||||
}
|
||||
scheduleAutoSave();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -968,7 +1031,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
formatWeightDisplay(totalAnorganik);
|
||||
if (grandTotalResiduDisplay)
|
||||
grandTotalResiduDisplay.textContent = formatWeightDisplay(totalResidu);
|
||||
scheduleAutoSave();
|
||||
}
|
||||
|
||||
function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) {
|
||||
|
|
@ -1066,6 +1128,10 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
}
|
||||
|
||||
function getSubmitState(tps) {
|
||||
if (tps?.submitted) {
|
||||
return { canSubmit: false, message: '' };
|
||||
}
|
||||
|
||||
if (!tps.fotoKedatanganUploaded) {
|
||||
if (!tps.fotoKedatangan.length)
|
||||
return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan.' };
|
||||
|
|
@ -1105,10 +1171,10 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
|
||||
function refreshSubmitButtonState(form) {
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
if (!submitButton) return;
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
if (tps?.submitted || !submitButton) return;
|
||||
|
||||
const helperText = form.querySelector(".submit-state-message");
|
||||
const tps = tpsData[activeTpsIndex];
|
||||
const submitState = getSubmitState(tps);
|
||||
|
||||
submitButton.disabled = !submitState.canSubmit;
|
||||
|
|
@ -1275,7 +1341,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
if (itemIndex >= 0 && tps.timbangan[itemIndex]) {
|
||||
tps.timbangan[itemIndex].uploaded = false;
|
||||
refreshTimbanganUploadState(item);
|
||||
scheduleAutoSave();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1290,6 +1355,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
refreshTimbanganUploadState(item);
|
||||
const form = tpsContentContainer.querySelector("form");
|
||||
if (form) refreshSubmitButtonState(form);
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
weightInputDisplay.addEventListener('blur', function() {
|
||||
|
|
@ -1314,7 +1380,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
syncTimbanganToTpsData();
|
||||
const form = tpsContentContainer.querySelector('form');
|
||||
if (form) refreshSubmitButtonState(form);
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
removeBtn.addEventListener('click', function() {
|
||||
|
|
@ -1328,7 +1393,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
updateTpsTotalTimbangan();
|
||||
syncTimbanganToTpsData();
|
||||
if (form) refreshSubmitButtonState(form);
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
repeater.appendChild(item);
|
||||
|
|
@ -1367,8 +1431,10 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
|
||||
function buildSubmitFormData(tps) {
|
||||
const formData = new FormData();
|
||||
formData.append("LokasiAngkutID", tps.lokasiAngkutId || "");
|
||||
formData.append("SpjDetailID", tps.spjDetailId || "");
|
||||
formData.append("NomorSpj", nomorSpj || "");
|
||||
formData.append("TpsName", tps.name || DEFAULT_TPS_NAME);
|
||||
formData.append("LokasiAngkutId", tps.lokasiAngkutId || "");
|
||||
formData.append("SpjDetailId", tps.spjDetailId || "");
|
||||
formData.append("Latitude", tps.latitude);
|
||||
formData.append("Longitude", tps.longitude);
|
||||
formData.append("AlamatJalan", tps.alamatJalan);
|
||||
|
|
@ -1440,7 +1506,8 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
|
||||
const formData = new FormData();
|
||||
formData.append('FotoTimbangan', tps.timbangan[itemIndex].file);
|
||||
formData.append('DraftKey', draftRequestKey);
|
||||
formData.append('NomorSpj', nomorSpj || '');
|
||||
formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME);
|
||||
formData.append('SpjDetailId', tps.spjDetailId || '');
|
||||
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
|
||||
formData.append('ItemIndex', itemIndex);
|
||||
|
|
@ -1485,7 +1552,8 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
|
||||
const formData = new FormData();
|
||||
tps.fotoKedatangan.forEach(f => formData.append('FotoKedatangan', f));
|
||||
formData.append('DraftKey', draftRequestKey);
|
||||
formData.append('NomorSpj', nomorSpj || '');
|
||||
formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME);
|
||||
formData.append('SpjDetailId', tps.spjDetailId || '');
|
||||
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
|
||||
formData.append('WaktuKedatangan', tps.waktuKedatangan || '');
|
||||
|
|
@ -1533,7 +1601,8 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
|
||||
const formData = new FormData();
|
||||
tps.fotoPetugas.forEach(f => formData.append('FotoPetugas', f));
|
||||
formData.append('DraftKey', draftRequestKey);
|
||||
formData.append('NomorSpj', nomorSpj || '');
|
||||
formData.append('NamaTps', tps.name || DEFAULT_TPS_NAME);
|
||||
formData.append('SpjDetailId', tps.spjDetailId || '');
|
||||
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
|
||||
formData.append('NamaPetugas', tps.namaPetugas);
|
||||
|
|
@ -1573,17 +1642,22 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
const formData = buildSubmitFormData(tps);
|
||||
|
||||
try {
|
||||
const res = await fetch('/upst/detail-penjemputan', { method: 'POST', body: formData });
|
||||
if (res.ok || res.redirected) {
|
||||
const res = await fetch('/upst/detail-penjemputan', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
const result = await res.json().catch(() => null);
|
||||
|
||||
if (res.ok && result?.success) {
|
||||
tps.submitted = true;
|
||||
if (draftRequestKey) {
|
||||
await fetch(`/upst/detail-penjemputan/delete-draft-non-tps?draftKey=${encodeURIComponent(draftRequestKey)}`, { method: 'DELETE' });
|
||||
}
|
||||
showToast('Data berhasil disimpan!', 'success');
|
||||
showToast(result.message || 'Data berhasil disimpan!', 'success');
|
||||
setTimeout(() => { window.location.href = '/upst/detail-penjemputan/detail-selesai-tanpa-tps'; }, 1500);
|
||||
} else {
|
||||
const txt = await res.text();
|
||||
showToast('Gagal submit: ' + (txt || res.statusText), 'error');
|
||||
showToast(result?.message || 'Gagal submit data.', 'error');
|
||||
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Submit'; }
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -1617,10 +1691,15 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
nomorSpj = nomorSpjEl.textContent.trim();
|
||||
}
|
||||
|
||||
initializeLocation();
|
||||
await loadDetailData();
|
||||
await loadDraftFromServer();
|
||||
renderTpsForm();
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
initializeLocation();
|
||||
await loadDetailData();
|
||||
await loadRecordForCurrentSpj();
|
||||
renderTpsForm();
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
|
||||
function renderServerImagePreview(fileUrls, container) {
|
||||
container.innerHTML = '';
|
||||
|
|
|
|||
|
|
@ -12,9 +12,8 @@ const DetailPenjemputan = (function () {
|
|||
};
|
||||
|
||||
const ENDPOINTS = {
|
||||
saveDraft: '/upst/detail-penjemputan/save-draft',
|
||||
loadDraft: '/upst/detail-penjemputan/load-draft',
|
||||
deleteDraft: '/upst/detail-penjemputan/delete-draft',
|
||||
saveRecord: '/upst/detail-penjemputan/save-record',
|
||||
recordsList: '/upst/detail-penjemputan/api/records',
|
||||
uploadKedatangan: '/upst/detail-penjemputan/upload-foto-kedatangan',
|
||||
uploadTimbangan: '/upst/detail-penjemputan/upload-foto-timbangan',
|
||||
uploadPetugas: '/upst/detail-penjemputan/upload-foto-petugas',
|
||||
|
|
@ -31,37 +30,48 @@ const DetailPenjemputan = (function () {
|
|||
};
|
||||
|
||||
const STORAGE_KEY = "detailPenjemputanTpsState";
|
||||
|
||||
function saveState() {
|
||||
try {
|
||||
const stateCopy = JSON.parse(
|
||||
JSON.stringify(state, (key, value) => {
|
||||
if (value instanceof File) {
|
||||
return { name: value.name, size: value.size, type: value.type };
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(stateCopy));
|
||||
} catch (e) {
|
||||
console.warn("Failed to save state to localStorage:", e);
|
||||
}
|
||||
function sanitizeStorageSegment(value) {
|
||||
return String(value || "default")
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9_-]/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "") || "default";
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
state = { ...state, ...parsed };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to load state from localStorage:", e);
|
||||
}
|
||||
function getStorageKey(nomorSpj = state.nomorSpj) {
|
||||
return `${STORAGE_KEY}:${sanitizeStorageSegment(nomorSpj)}`;
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
return;
|
||||
}
|
||||
|
||||
function loadState(nomorSpj = state.nomorSpj) {
|
||||
return;
|
||||
}
|
||||
|
||||
function clearState() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
function getTpsIdentity(value) {
|
||||
if (!value) return "";
|
||||
const spjDetailId = value.spjDetailId || value.SpjDetailID || "";
|
||||
const lokasiAngkutId = value.lokasiAngkutId || value.LokasiAngkutID || "";
|
||||
const name = value.name || value.Name || "";
|
||||
return [spjDetailId, lokasiAngkutId, name].filter(Boolean).join("::");
|
||||
}
|
||||
|
||||
function getPersistedUiState() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function applyPersistedTpsData(persistedUiState) {
|
||||
return;
|
||||
}
|
||||
|
||||
function applyPersistedUiState(persistedUiState) {
|
||||
return;
|
||||
}
|
||||
|
||||
function isBrowserFile(value) {
|
||||
|
|
@ -215,25 +225,24 @@ const DetailPenjemputan = (function () {
|
|||
};
|
||||
}
|
||||
|
||||
function findMatchingApiTps(apiList, currentTps, index) {
|
||||
return (
|
||||
apiList.find(
|
||||
(item) =>
|
||||
(currentTps.spjDetailId &&
|
||||
(item.spjDetailId === currentTps.spjDetailId ||
|
||||
item.SpjDetailID === currentTps.spjDetailId)) ||
|
||||
(currentTps.lokasiAngkutId &&
|
||||
(item.lokasiAngkutId === currentTps.lokasiAngkutId ||
|
||||
item.LokasiAngkutID === currentTps.lokasiAngkutId)) ||
|
||||
(item.name || item.Name) === currentTps.name,
|
||||
) || apiList[index]
|
||||
);
|
||||
function findMatchingApiTps(apiList, currentTps) {
|
||||
|
||||
return apiList.find(
|
||||
(item) =>
|
||||
(currentTps.spjDetailId &&
|
||||
(item.spjDetailId === currentTps.spjDetailId ||
|
||||
item.SpjDetailID === currentTps.spjDetailId)) ||
|
||||
(currentTps.lokasiAngkutId &&
|
||||
(item.lokasiAngkutId === currentTps.lokasiAngkutId ||
|
||||
item.LokasiAngkutID === currentTps.lokasiAngkutId)) ||
|
||||
(item.name || item.Name) === currentTps.name,
|
||||
) || null;
|
||||
}
|
||||
|
||||
function applyApiDraftData(draftData) {
|
||||
const apiList = Array.isArray(draftData)
|
||||
? draftData
|
||||
: draftData?.tpsData || draftData?.draftPenjemputan || [];
|
||||
function applyApiRecordData(recordData, { skipRender = false } = {}) {
|
||||
const apiList = Array.isArray(recordData)
|
||||
? recordData
|
||||
: recordData?.tpsData || [];
|
||||
|
||||
if (!Array.isArray(apiList) || apiList.length === 0) {
|
||||
return;
|
||||
|
|
@ -244,7 +253,7 @@ const DetailPenjemputan = (function () {
|
|||
}
|
||||
|
||||
state.tpsData = state.tpsData.map((currentTps, index) => {
|
||||
const apiTps = findMatchingApiTps(apiList, currentTps, index);
|
||||
const apiTps = findMatchingApiTps(apiList, currentTps);
|
||||
if (!apiTps) {
|
||||
return currentTps;
|
||||
}
|
||||
|
|
@ -262,6 +271,7 @@ const DetailPenjemputan = (function () {
|
|||
|
||||
return {
|
||||
...currentTps,
|
||||
nomorSpj: apiTps.nomorSpj || apiTps.NomorSpj || currentTps.nomorSpj,
|
||||
name: apiTps.name || apiTps.Name || currentTps.name,
|
||||
lokasiAngkutId:
|
||||
apiTps.lokasiAngkutId ||
|
||||
|
|
@ -317,8 +327,10 @@ const DetailPenjemputan = (function () {
|
|||
namaPetugas:
|
||||
apiTps.namaPetugas || apiTps.NamaPetugas || currentTps.namaPetugas,
|
||||
submitted: Boolean(
|
||||
apiTps.submitted ?? apiTps.Submitted ?? currentTps.submitted,
|
||||
apiTps.isSubmit ?? apiTps.IsSubmit ?? apiTps.submitted ?? apiTps.Submitted ?? currentTps.submitted,
|
||||
),
|
||||
submittedAt:
|
||||
apiTps.submittedAt || apiTps.SubmittedAt || currentTps.submittedAt,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -329,17 +341,63 @@ const DetailPenjemputan = (function () {
|
|||
);
|
||||
elements.tpsTabsContainer.style.display = "block";
|
||||
|
||||
if (state.tpsData.length === 1) {
|
||||
renderSingleForm();
|
||||
} else {
|
||||
renderTabs();
|
||||
renderTpsForm();
|
||||
if (!skipRender) {
|
||||
if (state.tpsData.length === 1) {
|
||||
renderSingleForm();
|
||||
} else {
|
||||
renderTabs();
|
||||
renderTpsForm();
|
||||
}
|
||||
}
|
||||
|
||||
updateAllTotals();
|
||||
saveState();
|
||||
}
|
||||
|
||||
async function loadRecordsForCurrentSpj() {
|
||||
if (!state.nomorSpj) return;
|
||||
|
||||
try {
|
||||
const fetchRecords = async () => {
|
||||
const url = new URL(ENDPOINTS.recordsList, window.location.origin);
|
||||
url.searchParams.set('nomorSpj', state.nomorSpj);
|
||||
url.searchParams.set('_ts', Date.now().toString());
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return res.json();
|
||||
};
|
||||
|
||||
let data = await fetchRecords();
|
||||
if (
|
||||
data?.success &&
|
||||
Array.isArray(data.items) &&
|
||||
data.items.length === 0
|
||||
) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
data = await fetchRecords();
|
||||
}
|
||||
|
||||
if (!data?.success || !Array.isArray(data.items) || data.items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyApiRecordData(data.items, { skipRender: true });
|
||||
} catch (error) {
|
||||
console.warn('Gagal memuat data TPS:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const elements = {
|
||||
grandTotalDisplay: null,
|
||||
tpsSelectionContainer: null,
|
||||
|
|
@ -355,10 +413,51 @@ const DetailPenjemputan = (function () {
|
|||
|
||||
let autoSaveTimer = null;
|
||||
let autoSaveStatusEl = null;
|
||||
let loadingOverlayEl = null;
|
||||
let isAutoSaving = false;
|
||||
let pendingAutoSaveIndex = null;
|
||||
let lastAutoSaveSignature = "";
|
||||
|
||||
async function init(tpsList) {
|
||||
initElements();
|
||||
await initializeLocation(tpsList);
|
||||
function getLoadingOverlay() {
|
||||
if (loadingOverlayEl && document.body.contains(loadingOverlayEl)) {
|
||||
return loadingOverlayEl;
|
||||
}
|
||||
|
||||
loadingOverlayEl = document.createElement('div');
|
||||
loadingOverlayEl.id = 'detail-loading-overlay';
|
||||
loadingOverlayEl.className = 'fixed inset-0 z-[9999] hidden bg-white/95 backdrop-blur-sm flex items-center justify-center px-6';
|
||||
loadingOverlayEl.innerHTML = `
|
||||
<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() {
|
||||
|
|
@ -386,13 +485,6 @@ const DetailPenjemputan = (function () {
|
|||
}
|
||||
}
|
||||
|
||||
function buildDraftKey(tps) {
|
||||
const spjDetailId = (tps?.spjDetailId || '').trim();
|
||||
const lokasiAngkutId = (tps?.lokasiAngkutId || '').trim();
|
||||
if (!spjDetailId && !lokasiAngkutId) return '';
|
||||
return `tps-${spjDetailId || 'no-spj'}-${lokasiAngkutId || 'no-lokasi'}`.replace(/[^a-zA-Z0-9_-]/g, '');
|
||||
}
|
||||
|
||||
function getActiveTps() {
|
||||
return state.tpsData[state.activeTpsIndex] || null;
|
||||
}
|
||||
|
|
@ -415,98 +507,83 @@ const DetailPenjemputan = (function () {
|
|||
if (isOk) setTimeout(() => { statusEl.style.opacity = '0'; }, 2500);
|
||||
}
|
||||
|
||||
function scheduleAutoSave() {
|
||||
function scheduleAutoSave(tpsIndex = state.activeTpsIndex) {
|
||||
clearTimeout(autoSaveTimer);
|
||||
showAutoSaveStatus('menyimpan...');
|
||||
autoSaveTimer = setTimeout(autoSaveDraft, 1000);
|
||||
autoSaveTimer = setTimeout(() => autoSaveRecord(tpsIndex), 1000);
|
||||
}
|
||||
|
||||
async function autoSaveDraft() {
|
||||
const tps = getActiveTps();
|
||||
if (!tps || !tps.draftKey) return;
|
||||
function buildAutoSavePayload(tps) {
|
||||
return {
|
||||
nomorSpj: state.nomorSpj || '',
|
||||
namaTps: tps.name || '',
|
||||
lokasiAngkutId: tps.lokasiAngkutId || '',
|
||||
spjDetailId: tps.spjDetailId || '',
|
||||
latitude: tps.latitude || '',
|
||||
longitude: tps.longitude || '',
|
||||
alamatJalan: tps.alamatJalan || '',
|
||||
waktuKedatangan: tps.waktuKedatangan || '',
|
||||
fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [],
|
||||
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false,
|
||||
timbangan: (tps.timbangan || []).map(item => ({
|
||||
berat: item.weight || 0,
|
||||
jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS,
|
||||
fotoFileName: item.fotoFileName || '',
|
||||
uploaded: item.uploaded || false,
|
||||
ocrInfo: item.ocrInfo || ''
|
||||
})),
|
||||
totalOrganik: tps.totalOrganik || 0,
|
||||
totalAnorganik: tps.totalAnorganik || 0,
|
||||
totalResidu: tps.totalResidu || 0,
|
||||
totalTimbangan: tps.totalTimbangan || 0,
|
||||
fotoPetugasFileNames: tps.fotoPetugasFileNames || [],
|
||||
fotoPetugasUploaded: tps.fotoPetugasUploaded || false,
|
||||
namaPetugas: tps.namaPetugas || ''
|
||||
};
|
||||
}
|
||||
|
||||
const payload = {
|
||||
draftKey: tps.draftKey,
|
||||
lokasiAngkutId: tps.lokasiAngkutId || '',
|
||||
spjDetailId: tps.spjDetailId || '',
|
||||
latitude: tps.latitude || '',
|
||||
longitude: tps.longitude || '',
|
||||
alamatJalan: tps.alamatJalan || '',
|
||||
waktuKedatangan: tps.waktuKedatangan || '',
|
||||
fotoKedatanganFileNames: tps.fotoKedatanganFileNames || [],
|
||||
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false,
|
||||
timbangan: (tps.timbangan || []).map(item => ({
|
||||
berat: item.weight || 0,
|
||||
jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS,
|
||||
fotoFileName: item.fotoFileName || '',
|
||||
uploaded: item.uploaded || false,
|
||||
ocrInfo: item.ocrInfo || ''
|
||||
})),
|
||||
totalOrganik: tps.totalOrganik || 0,
|
||||
totalAnorganik: tps.totalAnorganik || 0,
|
||||
totalResidu: tps.totalResidu || 0,
|
||||
totalTimbangan: tps.totalTimbangan || 0,
|
||||
fotoPetugasFileNames: tps.fotoPetugasFileNames || [],
|
||||
fotoPetugasUploaded: tps.fotoPetugasUploaded || false,
|
||||
namaPetugas: tps.namaPetugas || ''
|
||||
};
|
||||
async function autoSaveRecord(tpsIndex = state.activeTpsIndex) {
|
||||
const tps = state.tpsData[tpsIndex] || null;
|
||||
if (!tps) return;
|
||||
if (tps.submitted) return;
|
||||
if (isAutoSaving) {
|
||||
pendingAutoSaveIndex = tpsIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildAutoSavePayload(tps);
|
||||
const payloadSignature = JSON.stringify(payload);
|
||||
if (payloadSignature === lastAutoSaveSignature) {
|
||||
showAutoSaveStatus('✓ Data tersimpan', true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(ENDPOINTS.saveDraft, {
|
||||
isAutoSaving = true;
|
||||
const res = await fetch(ENDPOINTS.saveRecord, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
body: payloadSignature
|
||||
});
|
||||
const data = await res.json();
|
||||
showAutoSaveStatus(data.success ? '✓ Draft tersimpan' : '✗ Gagal simpan', data.success);
|
||||
if (data.success) {
|
||||
lastAutoSaveSignature = payloadSignature;
|
||||
}
|
||||
showAutoSaveStatus(data.success ? '✓ Data tersimpan' : '✗ Gagal simpan', data.success);
|
||||
} catch (_) {
|
||||
showAutoSaveStatus('✗ Gagal simpan draft');
|
||||
showAutoSaveStatus('✗ Gagal simpan data');
|
||||
} finally {
|
||||
isAutoSaving = false;
|
||||
if (pendingAutoSaveIndex !== null) {
|
||||
const nextIndex = pendingAutoSaveIndex;
|
||||
pendingAutoSaveIndex = null;
|
||||
scheduleAutoSave(nextIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDraftForTps(tps) {
|
||||
if (!tps || !tps.draftKey) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${ENDPOINTS.loadDraft}?draftKey=${encodeURIComponent(tps.draftKey)}`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (!data.success || !data.hasDraft || !data.draft) return;
|
||||
|
||||
const draft = data.draft;
|
||||
tps.lokasiAngkutId = draft.lokasiAngkutId || tps.lokasiAngkutId;
|
||||
tps.spjDetailId = draft.spjDetailId || tps.spjDetailId;
|
||||
tps.latitude = draft.latitude || '';
|
||||
tps.longitude = draft.longitude || '';
|
||||
tps.alamatJalan = draft.alamatJalan || '';
|
||||
tps.waktuKedatangan = draft.waktuKedatangan || '';
|
||||
tps.fotoKedatanganFileNames = draft.fotoKedatanganFileNames || [];
|
||||
tps.fotoKedatanganUploaded = draft.fotoKedatanganUploaded || false;
|
||||
tps.fotoPetugasFileNames = draft.fotoPetugasFileNames || [];
|
||||
tps.fotoPetugasUploaded = draft.fotoPetugasUploaded || false;
|
||||
tps.namaPetugas = draft.namaPetugas || '';
|
||||
tps.totalOrganik = draft.totalOrganik || 0;
|
||||
tps.totalAnorganik = draft.totalAnorganik || 0;
|
||||
tps.totalResidu = draft.totalResidu || 0;
|
||||
tps.totalTimbangan = draft.totalTimbangan || 0;
|
||||
tps.timbangan = (draft.timbangan || []).map(item => ({
|
||||
file: null,
|
||||
fotoFileName: item.fotoFileName || '',
|
||||
weight: item.berat || 0,
|
||||
jenisSampah: item.jenisSampah || CONFIG.DEFAULT_JENIS,
|
||||
uploaded: item.uploaded || false,
|
||||
ocrInfo: item.ocrInfo || 'OCR: belum diproses.'
|
||||
}));
|
||||
} catch (error) {
|
||||
console.warn('Gagal memuat draft TPS:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDraftsForAllTps() {
|
||||
await Promise.all(state.tpsData.map(loadDraftForTps));
|
||||
}
|
||||
|
||||
async function initializeLocation(tpsList) {
|
||||
const persistedUiState = getPersistedUiState();
|
||||
state.availableTpsList = tpsList || [];
|
||||
if (elements.tpsSelectionContainer) {
|
||||
elements.tpsSelectionContainer.style.display = 'none';
|
||||
|
|
@ -515,7 +592,7 @@ const DetailPenjemputan = (function () {
|
|||
if (state.availableTpsList.length === 0) {
|
||||
state.selectedTpsList = ['1 Lokasi TPS'];
|
||||
initializeTpsData(state.selectedTpsList);
|
||||
await loadDraftsForAllTps();
|
||||
await loadRecordsForCurrentSpj();
|
||||
elements.tpsTabsContainer.style.display = 'block';
|
||||
renderSingleForm();
|
||||
return;
|
||||
|
|
@ -523,7 +600,9 @@ const DetailPenjemputan = (function () {
|
|||
|
||||
state.selectedTpsList = [...state.availableTpsList];
|
||||
initializeTpsData(state.selectedTpsList);
|
||||
await loadDraftsForAllTps();
|
||||
await loadRecordsForCurrentSpj();
|
||||
applyPersistedTpsData(persistedUiState);
|
||||
applyPersistedUiState(persistedUiState);
|
||||
elements.tpsTabsContainer.style.display = 'block';
|
||||
|
||||
if (state.selectedTpsList.length === 1) {
|
||||
|
|
@ -540,7 +619,6 @@ const DetailPenjemputan = (function () {
|
|||
index: index,
|
||||
lokasiAngkutId: typeof tpsItem === 'string' ? '' : (tpsItem?.lokasiAngkutId || tpsItem?.LokasiAngkutID || ''),
|
||||
spjDetailId: typeof tpsItem === 'string' ? '' : (tpsItem?.spjDetailId || tpsItem?.SpjDetailID || ''),
|
||||
draftKey: '',
|
||||
latitude: '',
|
||||
longitude: '',
|
||||
alamatJalan: '',
|
||||
|
|
@ -558,7 +636,7 @@ const DetailPenjemputan = (function () {
|
|||
fotoPetugasUploaded: false,
|
||||
namaPetugas: '',
|
||||
submitted: false
|
||||
})).map(tps => ({ ...tps, draftKey: buildDraftKey(tps) }));
|
||||
}));
|
||||
state.hasRequestedLocation = new Array(tpsNames.length).fill(false);
|
||||
}
|
||||
|
||||
|
|
@ -596,7 +674,7 @@ const DetailPenjemputan = (function () {
|
|||
}
|
||||
|
||||
initializeTpsData(state.selectedTpsList);
|
||||
await loadDraftsForAllTps();
|
||||
await loadRecordsForCurrentSpj();
|
||||
elements.tpsSelectionContainer.style.display = 'none';
|
||||
elements.tpsTabsContainer.style.display = 'block';
|
||||
|
||||
|
|
@ -654,9 +732,10 @@ const DetailPenjemputan = (function () {
|
|||
renderTabs();
|
||||
renderTpsForm();
|
||||
|
||||
if (!state.hasRequestedLocation[index]) {
|
||||
const switchedTps = state.tpsData[index];
|
||||
if (!state.hasRequestedLocation[index] && !switchedTps?.submitted) {
|
||||
state.hasRequestedLocation[index] = true;
|
||||
getLocationUpdate();
|
||||
getLocationUpdate(index);
|
||||
}
|
||||
|
||||
updateAllTotals();
|
||||
|
|
@ -667,6 +746,22 @@ const DetailPenjemputan = (function () {
|
|||
const showTpsName =
|
||||
state.selectedTpsList.length > 1 || state.availableTpsList.length > 0;
|
||||
const submitState = getSubmitState(tps);
|
||||
const actionMarkup = tps.submitted
|
||||
? `<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 = `
|
||||
<form class="space-y-5 pb-8" data-tps-index="${tps.index}">
|
||||
|
|
@ -684,11 +779,7 @@ const DetailPenjemputan = (function () {
|
|||
${renderSection2Timbangan(tps, showTpsName)}
|
||||
${renderSection3Petugas(tps)}
|
||||
|
||||
<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>`}
|
||||
${actionMarkup}
|
||||
<p id="auto-save-status" class="text-[11px] text-amber-500 text-center font-medium" style="opacity:0;transition:opacity 0.4s"></p>
|
||||
</form>
|
||||
`;
|
||||
|
|
@ -782,6 +873,7 @@ const DetailPenjemputan = (function () {
|
|||
function attachTpsFormListeners() {
|
||||
const form = elements.tpsContentContainer.querySelector("form");
|
||||
const tps = state.tpsData[state.activeTpsIndex];
|
||||
if (tps.submitted) return;
|
||||
|
||||
const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan");
|
||||
const fotoPetugasInput = form.querySelector(".tps-foto-petugas");
|
||||
|
|
@ -791,10 +883,9 @@ const DetailPenjemputan = (function () {
|
|||
fotoKedatanganInput.addEventListener('change', function() {
|
||||
tps.fotoKedatangan = Array.from(this.files);
|
||||
tps.fotoKedatanganUploaded = false;
|
||||
updateWaktuKedatangan();
|
||||
updateWaktuKedatangan(tps.index);
|
||||
updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan'));
|
||||
refreshKedatanganUploadState(form);
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
fotoPetugasInput.addEventListener('change', function() {
|
||||
|
|
@ -802,24 +893,23 @@ const DetailPenjemputan = (function () {
|
|||
tps.fotoPetugasUploaded = false;
|
||||
updateMultiPreview(this, form.querySelector('.tps-preview-petugas'));
|
||||
refreshPetugasUploadState(form);
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
namaPetugasInput.addEventListener('input', function() {
|
||||
tps.namaPetugas = this.value;
|
||||
refreshPetugasUploadState(form);
|
||||
scheduleAutoSave(tps.index);
|
||||
});
|
||||
|
||||
namaPetugasInput.addEventListener('blur', function() {
|
||||
tps.namaPetugas = this.value;
|
||||
scheduleAutoSave();
|
||||
scheduleAutoSave(tps.index);
|
||||
});
|
||||
|
||||
btnAddTimbangan.addEventListener('click', function() {
|
||||
createTimbanganItem(form.querySelector('.tps-timbangan-repeater'));
|
||||
syncTimbanganToTpsData();
|
||||
refreshSubmitButtonState(form);
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
const btnUploadKedatangan = form.querySelector(
|
||||
|
|
@ -887,7 +977,7 @@ const DetailPenjemputan = (function () {
|
|||
item.className =
|
||||
"rounded-xl border border-gray-200 overflow-hidden bg-black";
|
||||
|
||||
const imageUrl = URL.createObjectURL(file);
|
||||
const imageUrl = getStoredPhotoUrl(file);
|
||||
item.innerHTML = `
|
||||
<div class="h-44 bg-black/80">
|
||||
<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() {
|
||||
const tps = state.tpsData[state.activeTpsIndex];
|
||||
function updateWaktuKedatangan(tpsIndex = state.activeTpsIndex) {
|
||||
const tps = state.tpsData[tpsIndex];
|
||||
if (!tps) return;
|
||||
const now = new Date();
|
||||
const formatted = now.toLocaleString("id-ID", {
|
||||
day: "2-digit",
|
||||
|
|
@ -917,22 +1008,24 @@ const DetailPenjemputan = (function () {
|
|||
});
|
||||
tps.waktuKedatangan = formatted;
|
||||
|
||||
const form = elements.tpsContentContainer.querySelector("form");
|
||||
const displayWaktu = form.querySelector(".tps-waktu-kedatangan");
|
||||
if (displayWaktu) displayWaktu.value = formatted;
|
||||
if (tpsIndex === state.activeTpsIndex) {
|
||||
const form = elements.tpsContentContainer.querySelector("form");
|
||||
const displayWaktu = form?.querySelector(".tps-waktu-kedatangan");
|
||||
if (displayWaktu) displayWaktu.value = formatted;
|
||||
}
|
||||
|
||||
getLocationUpdate();
|
||||
getLocationUpdate(tpsIndex);
|
||||
saveState();
|
||||
}
|
||||
|
||||
function getLocationUpdate() {
|
||||
function getLocationUpdate(tpsIndex = state.activeTpsIndex) {
|
||||
if (!("geolocation" in navigator)) return;
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function (position) {
|
||||
const lat = position.coords.latitude.toFixed(6);
|
||||
const lng = position.coords.longitude.toFixed(6);
|
||||
reverseGeocode(lat, lng);
|
||||
reverseGeocode(lat, lng, tpsIndex);
|
||||
},
|
||||
function () {
|
||||
console.log("Lokasi tidak diizinkan");
|
||||
|
|
@ -940,28 +1033,30 @@ const DetailPenjemputan = (function () {
|
|||
);
|
||||
}
|
||||
|
||||
function reverseGeocode(lat, lng) {
|
||||
const tps = state.tpsData[state.activeTpsIndex];
|
||||
function reverseGeocode(lat, lng, tpsIndex = state.activeTpsIndex) {
|
||||
const tps = state.tpsData[tpsIndex];
|
||||
if (!tps) return;
|
||||
fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`,
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const address = data.display_name || `${lat}, ${lng}`;
|
||||
updateTpsLocation(lat, lng, address);
|
||||
updateTpsLocation(lat, lng, address, tpsIndex);
|
||||
})
|
||||
.catch(() => {
|
||||
updateTpsLocation(lat, lng, `${lat}, ${lng}`);
|
||||
updateTpsLocation(lat, lng, `${lat}, ${lng}`, tpsIndex);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTpsLocation(lat, lng, address) {
|
||||
const tps = state.tpsData[state.activeTpsIndex];
|
||||
function updateTpsLocation(lat, lng, address, tpsIndex = state.activeTpsIndex) {
|
||||
const tps = state.tpsData[tpsIndex];
|
||||
if (!tps) return;
|
||||
tps.latitude = lat;
|
||||
tps.longitude = lng;
|
||||
tps.alamatJalan = address;
|
||||
|
||||
const form = elements.tpsContentContainer.querySelector('form');
|
||||
const form = tpsIndex === state.activeTpsIndex ? elements.tpsContentContainer.querySelector('form') : null;
|
||||
if (form) {
|
||||
const latInput = form.querySelector('.tps-display-latitude');
|
||||
const lngInput = form.querySelector('.tps-display-longitude');
|
||||
|
|
@ -969,7 +1064,6 @@ const DetailPenjemputan = (function () {
|
|||
if (lngInput) lngInput.value = lng;
|
||||
}
|
||||
|
||||
scheduleAutoSave();
|
||||
}
|
||||
|
||||
function updateMultiPreview(input, previewContainer) {
|
||||
|
|
@ -1010,12 +1104,13 @@ const DetailPenjemputan = (function () {
|
|||
fileUrls.forEach((url, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black';
|
||||
const isUrl = typeof url === 'string' && (url.startsWith('/') || url.startsWith('http'));
|
||||
const imageUrl = getStoredPhotoUrl(url);
|
||||
const isUrl = Boolean(imageUrl) && (imageUrl.startsWith('/') || imageUrl.startsWith('http'));
|
||||
|
||||
if (isUrl) {
|
||||
item.innerHTML = `
|
||||
<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>
|
||||
`;
|
||||
} else {
|
||||
|
|
@ -1036,8 +1131,8 @@ const DetailPenjemputan = (function () {
|
|||
|
||||
const weight = existingData ? (existingData.weight || 0) : 0;
|
||||
const jenisSampah = existingData ? (existingData.jenisSampah || CONFIG.DEFAULT_JENIS) : CONFIG.DEFAULT_JENIS;
|
||||
const hasFileBlob = Boolean(existingData?.file);
|
||||
const hasFile = Boolean(existingData?.file || existingData?.fotoFileName);
|
||||
const hasFileBlob = isBrowserFile(existingData?.file);
|
||||
const hasFile = Boolean(hasStoredPhoto(existingData?.file) || existingData?.fotoFileName);
|
||||
const isUploaded = Boolean(existingData?.uploaded);
|
||||
const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.');
|
||||
|
||||
|
|
@ -1047,7 +1142,7 @@ const DetailPenjemputan = (function () {
|
|||
<button type="button" class="btn-remove-timbangan text-[11px] font-bold text-red-500">Hapus</button>
|
||||
</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" />
|
||||
<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" />
|
||||
</div>
|
||||
<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 removeBtn = item.querySelector(".btn-remove-timbangan");
|
||||
|
||||
if (existingData && existingData.file) {
|
||||
const localUrl = URL.createObjectURL(existingData.file);
|
||||
if (existingData && hasStoredPhoto(existingData.file)) {
|
||||
const localUrl = getStoredPhotoUrl(existingData.file);
|
||||
previewImage.src = localUrl;
|
||||
previewWrap.classList.remove('hidden');
|
||||
previewImage.onload = function() {
|
||||
if (!isBrowserFile(resolveStoredPhoto(existingData.file))) return;
|
||||
URL.revokeObjectURL(localUrl);
|
||||
};
|
||||
} else if (existingData && existingData.fotoFileName && existingData.fotoFileName.startsWith('/')) {
|
||||
|
|
@ -1120,7 +1216,6 @@ const DetailPenjemputan = (function () {
|
|||
tps.timbangan[itemIndex].fotoFileName = '';
|
||||
refreshTimbanganUploadState(item);
|
||||
}
|
||||
scheduleAutoSave();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1134,7 +1229,7 @@ const DetailPenjemputan = (function () {
|
|||
refreshTimbanganUploadState(item);
|
||||
const form = elements.tpsContentContainer.querySelector('form');
|
||||
if (form) refreshSubmitButtonState(form);
|
||||
scheduleAutoSave();
|
||||
scheduleAutoSave(state.activeTpsIndex);
|
||||
});
|
||||
|
||||
weightInputDisplay.addEventListener("blur", function () {
|
||||
|
|
@ -1158,7 +1253,6 @@ const DetailPenjemputan = (function () {
|
|||
syncTimbanganToTpsData();
|
||||
const form = elements.tpsContentContainer.querySelector('form');
|
||||
if (form) refreshSubmitButtonState(form);
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
removeBtn.addEventListener('click', function() {
|
||||
|
|
@ -1176,7 +1270,6 @@ const DetailPenjemputan = (function () {
|
|||
updateTpsTotalTimbangan();
|
||||
syncTimbanganToTpsData();
|
||||
if (form) refreshSubmitButtonState(form);
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
repeater.appendChild(item);
|
||||
|
|
@ -1277,6 +1370,10 @@ const DetailPenjemputan = (function () {
|
|||
}
|
||||
|
||||
function getSubmitState(tps) {
|
||||
if (tps?.submitted) {
|
||||
return { canSubmit: false, message: '' };
|
||||
}
|
||||
|
||||
if (!tps.fotoKedatanganUploaded) {
|
||||
if (!tps.fotoKedatangan.length) {
|
||||
return { canSubmit: false, message: 'Silakan pilih dan upload foto kedatangan terlebih dahulu.' };
|
||||
|
|
@ -1318,10 +1415,10 @@ const DetailPenjemputan = (function () {
|
|||
|
||||
function refreshSubmitButtonState(form) {
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
if (!submitButton) return;
|
||||
const tps = state.tpsData[state.activeTpsIndex];
|
||||
if (tps?.submitted || !submitButton) return;
|
||||
|
||||
let messageEl = form.querySelector(".submit-state-message");
|
||||
const tps = state.tpsData[state.activeTpsIndex];
|
||||
const submitState = getSubmitState(tps);
|
||||
|
||||
submitButton.disabled = !submitState.canSubmit;
|
||||
|
|
@ -1825,7 +1922,8 @@ const DetailPenjemputan = (function () {
|
|||
|
||||
const formData = new FormData();
|
||||
formData.append('FotoTimbangan', timbanganItem.file);
|
||||
formData.append('DraftKey', tps.draftKey || '');
|
||||
formData.append('NomorSpj', state.nomorSpj || '');
|
||||
formData.append('NamaTps', tps.name || 'TPS');
|
||||
formData.append('SpjDetailId', tps.spjDetailId || '');
|
||||
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
|
||||
formData.append('ItemIndex', itemIndex);
|
||||
|
|
@ -1863,7 +1961,7 @@ const DetailPenjemputan = (function () {
|
|||
syncTimbanganToTpsData();
|
||||
const form = elements.tpsContentContainer.querySelector('form');
|
||||
if (form) refreshSubmitButtonState(form);
|
||||
scheduleAutoSave();
|
||||
scheduleAutoSave(tps.index);
|
||||
}
|
||||
|
||||
async function uploadFotoKedatangan() {
|
||||
|
|
@ -1882,7 +1980,8 @@ const DetailPenjemputan = (function () {
|
|||
|
||||
const formData = new FormData();
|
||||
tps.fotoKedatangan.forEach(file => formData.append('FotoKedatangan', file));
|
||||
formData.append('DraftKey', tps.draftKey || '');
|
||||
formData.append('NomorSpj', state.nomorSpj || '');
|
||||
formData.append('NamaTps', tps.name || 'TPS');
|
||||
formData.append('SpjDetailId', tps.spjDetailId || '');
|
||||
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
|
||||
formData.append('WaktuKedatangan', tps.waktuKedatangan || '');
|
||||
|
|
@ -1902,7 +2001,7 @@ const DetailPenjemputan = (function () {
|
|||
if (fotoInput) fotoInput.value = '';
|
||||
refreshKedatanganUploadState(form);
|
||||
}
|
||||
scheduleAutoSave();
|
||||
scheduleAutoSave(tps.index);
|
||||
} else {
|
||||
showToast(data.message || 'Gagal upload foto kedatangan.', 'error');
|
||||
if (btn) {
|
||||
|
|
@ -1939,7 +2038,8 @@ const DetailPenjemputan = (function () {
|
|||
|
||||
const formData = new FormData();
|
||||
tps.fotoPetugas.forEach(file => formData.append('FotoPetugas', file));
|
||||
formData.append('DraftKey', tps.draftKey || '');
|
||||
formData.append('NomorSpj', state.nomorSpj || '');
|
||||
formData.append('NamaTps', tps.name || 'TPS');
|
||||
formData.append('SpjDetailId', tps.spjDetailId || '');
|
||||
formData.append('LokasiAngkutId', tps.lokasiAngkutId || '');
|
||||
formData.append('NamaPetugas', tps.namaPetugas || '');
|
||||
|
|
@ -1956,7 +2056,7 @@ const DetailPenjemputan = (function () {
|
|||
if (fotoInput) fotoInput.value = '';
|
||||
refreshPetugasUploadState(form);
|
||||
}
|
||||
scheduleAutoSave();
|
||||
scheduleAutoSave(tps.index);
|
||||
} else {
|
||||
showToast(data.message || 'Gagal upload foto petugas.', 'error');
|
||||
if (btn) {
|
||||
|
|
@ -1975,8 +2075,9 @@ const DetailPenjemputan = (function () {
|
|||
|
||||
function buildSubmitFormData(tps) {
|
||||
const formData = new FormData();
|
||||
formData.append("LokasiAngkutID", tps.lokasiAngkutId || "");
|
||||
formData.append("SpjDetailID", tps.spjDetailId || "");
|
||||
formData.append("NomorSpj", state.nomorSpj || "");
|
||||
formData.append("LokasiAngkutId", tps.lokasiAngkutId || "");
|
||||
formData.append("SpjDetailId", tps.spjDetailId || "");
|
||||
formData.append("TpsName", tps.name);
|
||||
formData.append("Latitude", tps.latitude);
|
||||
formData.append("Longitude", tps.longitude);
|
||||
|
|
@ -2053,16 +2154,18 @@ const DetailPenjemputan = (function () {
|
|||
try {
|
||||
const response = await fetch(ENDPOINTS.submit, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok || response.redirected) {
|
||||
markTpsSubmitted(tps);
|
||||
if (tps.draftKey) {
|
||||
await fetch(`${ENDPOINTS.deleteDraft}?draftKey=${encodeURIComponent(tps.draftKey)}`, { method: 'DELETE' });
|
||||
}
|
||||
const result = await response.json().catch(() => null);
|
||||
|
||||
showToast(`Data ${tps.name} berhasil disimpan!`, 'success');
|
||||
if (response.ok && result?.success) {
|
||||
markTpsSubmitted(tps);
|
||||
showToast(result.message || `Data ${tps.name} berhasil disimpan!`, 'success');
|
||||
|
||||
const allSubmitted = state.tpsData.every(item => item.submitted);
|
||||
if (allSubmitted) {
|
||||
|
|
@ -2074,8 +2177,7 @@ const DetailPenjemputan = (function () {
|
|||
renderTpsForm();
|
||||
}
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
showToast(errorText || 'Gagal menyimpan data. Silakan coba lagi.', 'error');
|
||||
showToast(result?.message || 'Gagal menyimpan data. Silakan coba lagi.', 'error');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = `Submit${state.selectedTpsList.length > 1 ? ' ' + tps.name : ''}`;
|
||||
|
|
@ -2139,7 +2241,7 @@ const DetailPenjemputan = (function () {
|
|||
|
||||
return {
|
||||
init: init,
|
||||
hydrateFromApi: applyApiDraftData,
|
||||
hydrateFromApi: applyApiRecordData,
|
||||
setNomorSpj: function (nomorSpj) {
|
||||
state.nomorSpj = nomorSpj;
|
||||
saveState();
|
||||
|
|
@ -2148,8 +2250,17 @@ const DetailPenjemputan = (function () {
|
|||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
const nomorSpjEl = document.querySelector('.text-gray-600.font-mono');
|
||||
const nomorSpj = nomorSpjEl ? nomorSpjEl.textContent.trim() : 'SPJ/07-2025/PKM/000476';
|
||||
|
||||
try {
|
||||
const response = await fetch('/driver/json/tps-list.json');
|
||||
const response = await fetch(`/driver/json/tps-list.json?_ts=${Date.now()}`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
Pragma: 'no-cache'
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
const tpsList = data.tpsList.map(tps => ({
|
||||
name: tps.name,
|
||||
|
|
@ -2157,16 +2268,13 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||
spjDetailId: tps.spjDetailId,
|
||||
id: tps.id
|
||||
}));
|
||||
await DetailPenjemputan.init(tpsList);
|
||||
await DetailPenjemputan.init(tpsList, nomorSpj);
|
||||
} catch (error) {
|
||||
console.error('Error loading TPS list:', error);
|
||||
}
|
||||
|
||||
const platNomorEl = document.getElementById('plat-nomor');
|
||||
if (platNomorEl) {
|
||||
const nomorSpjEl = document.querySelector('.text-gray-600.font-mono');
|
||||
if (nomorSpjEl) {
|
||||
DetailPenjemputan.setNomorSpj(nomorSpjEl.textContent.trim());
|
||||
}
|
||||
const platNomorEl = document.getElementById('plat-nomor');
|
||||
if (platNomorEl && nomorSpjEl) {
|
||||
DetailPenjemputan.setNomorSpj(nomorSpj);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
"detailPenjemputan": {
|
||||
"namaTps": "TPS A",
|
||||
"namaPerusahaan": "CV Tri Berkah Sejahtera",
|
||||
"nomorSpj": "SPJ/07-2025/PKM/000476",
|
||||
"nomorSpj": "SPJ/07-2025/PKM/000500",
|
||||
"platNomor": "B 9632 TOR",
|
||||
"nomorPintu": "JRC 005",
|
||||
"alamat": "Kp. Pertanian II Rt.004 Rw.001 Kel. Klender Kec, Duren Sawit, Kota Adm. Jakarta Timur 13470",
|
||||
"lokasiAngkutId": "",
|
||||
"spjDetailId": ""
|
||||
"lokasiAngkutId": "LOK001",
|
||||
"spjDetailId": "SPJ001"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "/upst",
|
||||
"name": "eSPJ - Surat Perjalanan Dinas",
|
||||
"short_name": "eSPJ",
|
||||
"description": "Aplikasi pengelolaan Surat Perjalanan Dinas yang modern dan efisien",
|
||||
"name": "PKM UPST - SPJ Driver UPST",
|
||||
"short_name": "PKM UPST",
|
||||
"description": "Aplikasi pengelolaan SPJ untuk driver UPST",
|
||||
"start_url": "/upst",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
|
|
@ -27,17 +27,17 @@
|
|||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Home UPST",
|
||||
"short_name": "Home",
|
||||
"name": "Beranda",
|
||||
"short_name": "Beranda",
|
||||
"description": "Buka dashboard utama UPST",
|
||||
"url": "/upst",
|
||||
"icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }]
|
||||
},
|
||||
{
|
||||
"name": "Halaman Kosong",
|
||||
"short_name": "Kosong",
|
||||
"description": "Buka halaman alternatif UPST",
|
||||
"url": "/upst/kosong",
|
||||
"name": "Riwayat Perjalanan",
|
||||
"short_name": "Riwayat",
|
||||
"description": "Buka halaman riwayat perjalanan UPST",
|
||||
"url": "/upst/riwayat",
|
||||
"icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const CACHE_NAME = "espj-upst-pwa-v1";
|
||||
const CACHE_NAME = "espj-upst-pwa-v2";
|
||||
const OFFLINE_URL = "/driver/offline.html";
|
||||
const PRECACHE_URLS = [
|
||||
"/upst",
|
||||
|
|
@ -12,7 +12,27 @@ const PRECACHE_URLS = [
|
|||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
|
||||
caches.open(CACHE_NAME).then(async (cache) => {
|
||||
const results = await Promise.allSettled(
|
||||
PRECACHE_URLS.map(async (url) => {
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gagal precache: ${url} (${response.status})`);
|
||||
}
|
||||
|
||||
await cache.put(url, response.clone());
|
||||
})
|
||||
);
|
||||
|
||||
const offlineCached = results.some(
|
||||
(result, index) =>
|
||||
PRECACHE_URLS[index] === OFFLINE_URL && result.status === "fulfilled"
|
||||
);
|
||||
|
||||
if (!offlineCached) {
|
||||
throw new Error(`Offline page gagal diprecache: ${OFFLINE_URL}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
|
@ -44,6 +64,16 @@ function isAuthPath(url) {
|
|||
return AUTH_PATHS.some((path) => url.pathname.startsWith(path));
|
||||
}
|
||||
|
||||
const BYPASS_CACHE_PATHS = [
|
||||
"/upst/detail-penjemputan/api/",
|
||||
"/uploads/penjemputan/",
|
||||
"/driver/json/"
|
||||
];
|
||||
|
||||
function shouldBypassCache(url) {
|
||||
return BYPASS_CACHE_PATHS.some((path) => url.pathname.startsWith(path));
|
||||
}
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
|
@ -60,6 +90,19 @@ self.addEventListener("fetch", (event) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (shouldBypassCache(url)) {
|
||||
event.respondWith(
|
||||
fetch(new Request(request, { cache: "no-store" })).catch(async () => {
|
||||
if (request.mode === "navigate") {
|
||||
return (await caches.match(OFFLINE_URL)) || Response.error();
|
||||
}
|
||||
|
||||
return Response.error();
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.mode === "navigate") {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
|
|
@ -72,7 +115,7 @@ self.addEventListener("fetch", (event) => {
|
|||
})
|
||||
.catch(async () => {
|
||||
const cachedPage = await caches.match(request);
|
||||
return cachedPage || caches.match(OFFLINE_URL);
|
||||
return cachedPage || (await caches.match(OFFLINE_URL)) || Response.error();
|
||||
})
|
||||
);
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in New Issue