update: pwa dll
parent
fb75e82964
commit
a21c903ea7
|
|
@ -15,17 +15,43 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
private readonly IConfiguration _configuration;
|
||||
private readonly DetailPenjemputanService _detailService;
|
||||
private readonly ILogger<DetailPenjemputanController> _logger;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
|
||||
public DetailPenjemputanController(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IConfiguration configuration,
|
||||
DetailPenjemputanService detailService,
|
||||
ILogger<DetailPenjemputanController> logger)
|
||||
ILogger<DetailPenjemputanController> logger,
|
||||
IWebHostEnvironment env)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_configuration = configuration;
|
||||
_detailService = detailService;
|
||||
_logger = logger;
|
||||
_env = env;
|
||||
}
|
||||
|
||||
private static string ResolveDraftKey(string? draftKey, string? sessionKey, string? spjDetailId = null, string? lokasiAngkutId = null)
|
||||
{
|
||||
var rawKey = !string.IsNullOrWhiteSpace(draftKey)
|
||||
? draftKey
|
||||
: !string.IsNullOrWhiteSpace(sessionKey)
|
||||
? sessionKey
|
||||
: $"non-tps-{spjDetailId}-{lokasiAngkutId}";
|
||||
|
||||
return string.Concat((rawKey ?? string.Empty).Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_'));
|
||||
}
|
||||
|
||||
private string GetUploadDirectory(string dateFolder)
|
||||
{
|
||||
var uploadDir = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", dateFolder);
|
||||
Directory.CreateDirectory(uploadDir);
|
||||
return uploadDir;
|
||||
}
|
||||
|
||||
private static string BuildUploadUrl(string dateFolder, string fileName)
|
||||
{
|
||||
return $"/uploads/penjemputan/{dateFolder}/{fileName}";
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
|
|
@ -90,6 +116,494 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
}
|
||||
}
|
||||
|
||||
[HttpPost("save-draft-non-tps")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> SaveDraftNonTps([FromBody] DraftSaveRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
return BadRequest(new DraftSaveResponse { Success = false, Message = "Request tidak valid." });
|
||||
|
||||
request.DraftKey = ResolveDraftKey(request.DraftKey, request.SessionKey, request.SpjDetailId, request.LokasiAngkutId);
|
||||
request.SessionKey = request.DraftKey;
|
||||
if (string.IsNullOrWhiteSpace(request.DraftKey))
|
||||
return BadRequest(new DraftSaveResponse { Success = false, Message = "Draft key tidak valid." });
|
||||
|
||||
var result = await _detailService.SaveDraftNonTpsAsync(request);
|
||||
return result.Success ? Ok(result) : StatusCode(500, result);
|
||||
}
|
||||
|
||||
[HttpGet("load-draft-non-tps")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> LoadDraftNonTps([FromQuery] string? draftKey = null, [FromQuery] string? sessionKey = null)
|
||||
{
|
||||
var key = ResolveDraftKey(draftKey, sessionKey);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return Ok(new DraftLoadResponse { Success = true, HasDraft = false, Message = "Draft key kosong." });
|
||||
var result = await _detailService.LoadDraftNonTpsAsync(key);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpDelete("delete-draft-non-tps")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<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." });
|
||||
|
||||
request.DraftKey = ResolveDraftKey(request.DraftKey, request.SessionKey, request.SpjDetailId, request.LokasiAngkutId);
|
||||
request.SessionKey = request.DraftKey;
|
||||
if (string.IsNullOrWhiteSpace(request.DraftKey))
|
||||
return BadRequest(new DraftSaveResponse { Success = false, Message = "Draft key tidak valid." });
|
||||
|
||||
var result = await _detailService.SaveDraftTpsAsync(request);
|
||||
return result.Success ? Ok(result) : StatusCode(500, result);
|
||||
}
|
||||
|
||||
[HttpGet("load-draft")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<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? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] string? WaktuKedatangan,
|
||||
[FromForm] string? Latitude,
|
||||
[FromForm] string? Longitude,
|
||||
[FromForm] string? AlamatJalan)
|
||||
{
|
||||
if (FotoKedatangan == null || FotoKedatangan.Count == 0)
|
||||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
|
||||
var fileNames = new List<string>();
|
||||
foreach (var file in FotoKedatangan)
|
||||
{
|
||||
if (file.Length == 0) continue;
|
||||
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
var name = $"kedatangan_{Guid.NewGuid()}{ext}";
|
||||
var path = Path.Combine(uploadDir, name);
|
||||
await using var stream = new FileStream(path, FileMode.Create);
|
||||
await file.CopyToAsync(stream);
|
||||
fileNames.Add(name);
|
||||
}
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList();
|
||||
|
||||
var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKey))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
draft.FotoKedatanganFileNames = fileUrls;
|
||||
draft.FotoKedatanganUploaded = true;
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
if (!string.IsNullOrWhiteSpace(WaktuKedatangan)) draft.WaktuKedatangan = WaktuKedatangan;
|
||||
if (!string.IsNullOrWhiteSpace(Latitude)) draft.Latitude = Latitude;
|
||||
if (!string.IsNullOrWhiteSpace(Longitude)) draft.Longitude = Longitude;
|
||||
if (!string.IsNullOrWhiteSpace(AlamatJalan)) draft.AlamatJalan = AlamatJalan;
|
||||
await _detailService.SaveDraftNonTpsAsync(new DraftSaveRequest
|
||||
{
|
||||
DraftKey = resolvedDraftKey,
|
||||
SessionKey = resolvedDraftKey,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = fileUrls,
|
||||
FotoKedatanganUploaded = true,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = draft.FotoPetugasFileNames,
|
||||
FotoPetugasUploaded = draft.FotoPetugasUploaded,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." });
|
||||
}
|
||||
|
||||
[HttpPost("upload-foto-timbangan-non-tps")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UploadFotoTimbanganNonTps(
|
||||
[FromForm] IFormFile? FotoTimbangan,
|
||||
[FromForm] string? DraftKey,
|
||||
[FromForm] string? SessionKey,
|
||||
[FromForm] string? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] int ItemIndex,
|
||||
[FromForm] string? JenisSampah,
|
||||
[FromForm] decimal Berat)
|
||||
{
|
||||
if (FotoTimbangan == null || FotoTimbangan.Length == 0)
|
||||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
|
||||
var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant();
|
||||
var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant();
|
||||
var beratStr = Berat.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture).Replace('.', '_');
|
||||
var name = $"timbangan{ItemIndex + 1}-{jenisSafe}-{beratStr}{ext}";
|
||||
var filePath = Path.Combine(uploadDir, name);
|
||||
await using var stream = new FileStream(filePath, FileMode.Create);
|
||||
await FotoTimbangan.CopyToAsync(stream);
|
||||
var fileUrl = BuildUploadUrl(dateFolder, name);
|
||||
|
||||
var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKey))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
while (draft.Timbangan.Count <= ItemIndex)
|
||||
draft.Timbangan.Add(new DraftTimbanganItem());
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
draft.Timbangan[ItemIndex] = new DraftTimbanganItem
|
||||
{
|
||||
FotoFileName = fileUrl,
|
||||
JenisSampah = JenisSampah ?? "Residu",
|
||||
Berat = Berat,
|
||||
Uploaded = true
|
||||
};
|
||||
await _detailService.SaveDraftNonTpsAsync(new DraftSaveRequest
|
||||
{
|
||||
DraftKey = resolvedDraftKey,
|
||||
SessionKey = resolvedDraftKey,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = draft.FotoKedatanganFileNames,
|
||||
FotoKedatanganUploaded = draft.FotoKedatanganUploaded,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = draft.FotoPetugasFileNames,
|
||||
FotoPetugasUploaded = draft.FotoPetugasUploaded,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." });
|
||||
}
|
||||
|
||||
[HttpPost("upload-foto-petugas-non-tps")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UploadFotoPetugasNonTps(
|
||||
[FromForm] List<IFormFile>? FotoPetugas,
|
||||
[FromForm] string? DraftKey,
|
||||
[FromForm] string? SessionKey,
|
||||
[FromForm] string? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] string? NamaPetugas)
|
||||
{
|
||||
if (FotoPetugas == null || FotoPetugas.Count == 0)
|
||||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
|
||||
var fileNames = new List<string>();
|
||||
foreach (var file in FotoPetugas)
|
||||
{
|
||||
if (file.Length == 0) continue;
|
||||
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
var name = $"petugas_{Guid.NewGuid()}{ext}";
|
||||
var path = Path.Combine(uploadDir, name);
|
||||
await using var stream = new FileStream(path, FileMode.Create);
|
||||
await file.CopyToAsync(stream);
|
||||
fileNames.Add(name);
|
||||
}
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList();
|
||||
|
||||
var resolvedDraftKey = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKey))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftNonTpsAsync(resolvedDraftKey);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKey, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
draft.FotoPetugasFileNames = fileUrls;
|
||||
draft.FotoPetugasUploaded = true;
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
if (!string.IsNullOrWhiteSpace(NamaPetugas)) draft.NamaPetugas = NamaPetugas;
|
||||
await _detailService.SaveDraftNonTpsAsync(new DraftSaveRequest
|
||||
{
|
||||
DraftKey = resolvedDraftKey,
|
||||
SessionKey = resolvedDraftKey,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = draft.FotoKedatanganFileNames,
|
||||
FotoKedatanganUploaded = draft.FotoKedatanganUploaded,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = fileUrls,
|
||||
FotoPetugasUploaded = true,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." });
|
||||
}
|
||||
|
||||
[HttpPost("upload-foto-kedatangan")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UploadFotoKedatangan(
|
||||
[FromForm] List<IFormFile>? FotoKedatangan,
|
||||
[FromForm] string? DraftKey,
|
||||
[FromForm] string? SessionKey,
|
||||
[FromForm] string? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] string? WaktuKedatangan,
|
||||
[FromForm] string? Latitude,
|
||||
[FromForm] string? Longitude,
|
||||
[FromForm] string? AlamatJalan)
|
||||
{
|
||||
if (FotoKedatangan == null || FotoKedatangan.Count == 0)
|
||||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
|
||||
var fileNames = new List<string>();
|
||||
foreach (var file in FotoKedatangan)
|
||||
{
|
||||
if (file.Length == 0) continue;
|
||||
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
var name = $"kedatangan_{Guid.NewGuid()}{ext}";
|
||||
var path = Path.Combine(uploadDir, name);
|
||||
await using var stream = new FileStream(path, FileMode.Create);
|
||||
await file.CopyToAsync(stream);
|
||||
fileNames.Add(name);
|
||||
}
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList();
|
||||
|
||||
var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
draft.FotoKedatanganFileNames = fileUrls;
|
||||
draft.FotoKedatanganUploaded = true;
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
if (!string.IsNullOrWhiteSpace(WaktuKedatangan)) draft.WaktuKedatangan = WaktuKedatangan;
|
||||
if (!string.IsNullOrWhiteSpace(Latitude)) draft.Latitude = Latitude;
|
||||
if (!string.IsNullOrWhiteSpace(Longitude)) draft.Longitude = Longitude;
|
||||
if (!string.IsNullOrWhiteSpace(AlamatJalan)) draft.AlamatJalan = AlamatJalan;
|
||||
await _detailService.SaveDraftTpsAsync(new DraftSaveRequest
|
||||
{
|
||||
DraftKey = resolvedDraftKeyTps,
|
||||
SessionKey = resolvedDraftKeyTps,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = fileUrls,
|
||||
FotoKedatanganUploaded = true,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = draft.FotoPetugasFileNames,
|
||||
FotoPetugasUploaded = draft.FotoPetugasUploaded,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." });
|
||||
}
|
||||
|
||||
[HttpPost("upload-foto-timbangan")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UploadFotoTimbangan(
|
||||
[FromForm] IFormFile? FotoTimbangan,
|
||||
[FromForm] string? DraftKey,
|
||||
[FromForm] string? SessionKey,
|
||||
[FromForm] string? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] int ItemIndex,
|
||||
[FromForm] string? JenisSampah,
|
||||
[FromForm] decimal Berat)
|
||||
{
|
||||
if (FotoTimbangan == null || FotoTimbangan.Length == 0)
|
||||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
|
||||
var ext = Path.GetExtension(FotoTimbangan.FileName).ToLowerInvariant();
|
||||
var jenisSafe = (JenisSampah ?? "residu").ToLowerInvariant();
|
||||
var beratStr = Berat.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture).Replace('.', '_');
|
||||
var name = $"timbangan{ItemIndex + 1}-{jenisSafe}-{beratStr}{ext}";
|
||||
var filePath = Path.Combine(uploadDir, name);
|
||||
await using var stream = new FileStream(filePath, FileMode.Create);
|
||||
await FotoTimbangan.CopyToAsync(stream);
|
||||
var fileUrl = BuildUploadUrl(dateFolder, name);
|
||||
|
||||
var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
while (draft.Timbangan.Count <= ItemIndex)
|
||||
draft.Timbangan.Add(new DraftTimbanganItem());
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
draft.Timbangan[ItemIndex] = new DraftTimbanganItem
|
||||
{
|
||||
FotoFileName = fileUrl,
|
||||
JenisSampah = JenisSampah ?? "Residu",
|
||||
Berat = Berat,
|
||||
Uploaded = true
|
||||
};
|
||||
await _detailService.SaveDraftTpsAsync(new DraftSaveRequest
|
||||
{
|
||||
DraftKey = resolvedDraftKeyTps,
|
||||
SessionKey = resolvedDraftKeyTps,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = draft.FotoKedatanganFileNames,
|
||||
FotoKedatanganUploaded = draft.FotoKedatanganUploaded,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = draft.FotoPetugasFileNames,
|
||||
FotoPetugasUploaded = draft.FotoPetugasUploaded,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." });
|
||||
}
|
||||
|
||||
[HttpPost("upload-foto-petugas")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UploadFotoPetugas(
|
||||
[FromForm] List<IFormFile>? FotoPetugas,
|
||||
[FromForm] string? DraftKey,
|
||||
[FromForm] string? SessionKey,
|
||||
[FromForm] string? SpjDetailId,
|
||||
[FromForm] string? LokasiAngkutId,
|
||||
[FromForm] string? NamaPetugas)
|
||||
{
|
||||
if (FotoPetugas == null || FotoPetugas.Count == 0)
|
||||
return BadRequest(new { success = false, message = "Tidak ada foto." });
|
||||
|
||||
var dateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadDir = GetUploadDirectory(dateFolder);
|
||||
|
||||
var fileNames = new List<string>();
|
||||
foreach (var file in FotoPetugas)
|
||||
{
|
||||
if (file.Length == 0) continue;
|
||||
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
var name = $"petugas_{Guid.NewGuid()}{ext}";
|
||||
var path = Path.Combine(uploadDir, name);
|
||||
await using var stream = new FileStream(path, FileMode.Create);
|
||||
await file.CopyToAsync(stream);
|
||||
fileNames.Add(name);
|
||||
}
|
||||
var fileUrls = fileNames.Select(n => BuildUploadUrl(dateFolder, n)).ToList();
|
||||
|
||||
var resolvedDraftKeyTps = ResolveDraftKey(DraftKey, SessionKey, SpjDetailId, LokasiAngkutId);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedDraftKeyTps))
|
||||
{
|
||||
var loadResult = await _detailService.LoadDraftTpsAsync(resolvedDraftKeyTps);
|
||||
var draft = loadResult.Draft ?? new DraftPenjemputanNonTps { SessionKey = resolvedDraftKeyTps, SpjDetailId = SpjDetailId ?? string.Empty, LokasiAngkutId = LokasiAngkutId ?? string.Empty };
|
||||
draft.FotoPetugasFileNames = fileUrls;
|
||||
draft.FotoPetugasUploaded = true;
|
||||
if (!string.IsNullOrWhiteSpace(SpjDetailId)) draft.SpjDetailId = SpjDetailId;
|
||||
if (!string.IsNullOrWhiteSpace(LokasiAngkutId)) draft.LokasiAngkutId = LokasiAngkutId;
|
||||
if (!string.IsNullOrWhiteSpace(NamaPetugas)) draft.NamaPetugas = NamaPetugas;
|
||||
await _detailService.SaveDraftTpsAsync(new DraftSaveRequest
|
||||
{
|
||||
DraftKey = resolvedDraftKeyTps,
|
||||
SessionKey = resolvedDraftKeyTps,
|
||||
LokasiAngkutId = draft.LokasiAngkutId,
|
||||
SpjDetailId = draft.SpjDetailId,
|
||||
Latitude = draft.Latitude,
|
||||
Longitude = draft.Longitude,
|
||||
AlamatJalan = draft.AlamatJalan,
|
||||
WaktuKedatangan = draft.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = draft.FotoKedatanganFileNames,
|
||||
FotoKedatanganUploaded = draft.FotoKedatanganUploaded,
|
||||
Timbangan = draft.Timbangan,
|
||||
TotalOrganik = draft.TotalOrganik,
|
||||
TotalAnorganik = draft.TotalAnorganik,
|
||||
TotalResidu = draft.TotalResidu,
|
||||
TotalTimbangan = draft.TotalTimbangan,
|
||||
FotoPetugasFileNames = fileUrls,
|
||||
FotoPetugasUploaded = true,
|
||||
NamaPetugas = draft.NamaPetugas
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." });
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("ocr-timbangan")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> OcrTimbangan(IFormFile? Foto)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using eSPJ.Services;
|
||||
|
||||
namespace eSPJ.Controllers.SpjDriverUpstController
|
||||
{
|
||||
[Route("upst/history")]
|
||||
public class HistoryController : Controller
|
||||
{
|
||||
private readonly HistoryService _historyService;
|
||||
|
||||
public HistoryController(HistoryService historyService)
|
||||
{
|
||||
_historyService = historyService;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public IActionResult Index()
|
||||
|
|
@ -12,6 +19,26 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
|||
return View("~/Views/Admin/Transport/SpjDriverUpst/History/Index.cshtml");
|
||||
}
|
||||
|
||||
[HttpGet("api")]
|
||||
public async Task<IActionResult> GetHistory([FromQuery] string? fromDate = null, [FromQuery] string? toDate = null, [FromQuery] int page = 1, [FromQuery] int pageSize = 5)
|
||||
{
|
||||
DateOnly? parsedFromDate = null;
|
||||
DateOnly? parsedToDate = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(fromDate) && DateOnly.TryParse(fromDate, out var from))
|
||||
{
|
||||
parsedFromDate = from;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(toDate) && DateOnly.TryParse(toDate, out var to))
|
||||
{
|
||||
parsedToDate = to;
|
||||
}
|
||||
|
||||
var result = await _historyService.GetUpstHistoryAsync(parsedFromDate, parsedToDate, page, pageSize <= 0 ? 5 : pageSize);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("details/{id}")]
|
||||
public IActionResult Details(int id)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1 +1,38 @@
|
|||
[]
|
||||
[
|
||||
{
|
||||
"Name": "TPS A",
|
||||
"Index": 0,
|
||||
"Latitude": "-6.481670",
|
||||
"Longitude": "106.854000",
|
||||
"AlamatJalan": "Cibinong, Bogor, West Java, Java, 16911, Indonesia",
|
||||
"WaktuKedatangan": "16/03/2026, 09.37.44",
|
||||
"FotoKedatangan": [
|
||||
"kedatangan_ff132727-8942-402e-a612-ac3436e905b9.png"
|
||||
],
|
||||
"FotoKedatanganUploaded": true,
|
||||
"Timbangan": [
|
||||
{
|
||||
"FotoFileName": "timbangan_0de03ef1-dbdd-4643-a0e6-ac7670b12679.png",
|
||||
"Berat": [
|
||||
75.23
|
||||
],
|
||||
"LokasiAngkut": [],
|
||||
"JenisSampah": [
|
||||
2
|
||||
],
|
||||
"IsUploaded": true,
|
||||
"WaktuUpload": "2026-03-16T09:38:37.1840709+07:00"
|
||||
}
|
||||
],
|
||||
"TotalOrganik": 0,
|
||||
"TotalAnorganik": 0,
|
||||
"TotalResidu": 75.23,
|
||||
"TotalTimbangan": 75.23,
|
||||
"FotoPetugas": [
|
||||
"petugas_dffeaabf-4f30-41dd-9281-e2c33164e2c6.jpg"
|
||||
],
|
||||
"FotoPetugasUploaded": true,
|
||||
"NamaPetugas": "usmannn",
|
||||
"Submitted": true
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"noSpj": "SPJ/07-2025/PKM/000478",
|
||||
"plat": "B 5678 ABC",
|
||||
"kode": "JRC 007",
|
||||
"tujuan": "Bantar Gebang",
|
||||
"status": "In Progress",
|
||||
"tanggalWaktu": "2025-07-28T16:45:00"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"noSpj": "SPJ/07-2025/PKM/000476",
|
||||
"plat": "B 9632 TOR",
|
||||
"kode": "JRC 005",
|
||||
"tujuan": "RDF Rorotan",
|
||||
"status": "Completed",
|
||||
"tanggalWaktu": "2025-07-27T14:30:00"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"noSpj": "SPJ/07-2025/PKM/000477",
|
||||
"plat": "B 1234 XYZ",
|
||||
"kode": "JRC 006",
|
||||
"tujuan": "RDF Pesanggarahan",
|
||||
"status": "Completed",
|
||||
"tanggalWaktu": "2025-07-26T09:15:00"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"noSpj": "SPJ/07-2025/PKM/000479",
|
||||
"plat": "B 9876 DEF",
|
||||
"kode": "JRC 008",
|
||||
"tujuan": "RDF Sunter",
|
||||
"status": "Completed",
|
||||
"tanggalWaktu": "2025-07-25T11:20:00"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"noSpj": "SPJ/07-2025/PKM/000480",
|
||||
"plat": "B 4321 GHI",
|
||||
"kode": "JRC 009",
|
||||
"tujuan": "Bantar Gebang",
|
||||
"status": "Completed",
|
||||
"tanggalWaktu": "2025-07-24T08:45:00"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"noSpj": "SPJ/07-2025/PKM/000481",
|
||||
"plat": "B 7654 JKL",
|
||||
"kode": "JRC 010",
|
||||
"tujuan": "RDF Marunda",
|
||||
"status": "Completed",
|
||||
"tanggalWaktu": "2025-07-23T10:10:00"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"noSpj": "SPJ/07-2025/PKM/000482",
|
||||
"plat": "B 2468 MNO",
|
||||
"kode": "JRC 011",
|
||||
"tujuan": "Bantar Gebang",
|
||||
"status": "Completed",
|
||||
"tanggalWaktu": "2025-07-22T07:25:00"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"noSpj": "SPJ/07-2025/PKM/000483",
|
||||
"plat": "B 1357 PQR",
|
||||
"kode": "JRC 012",
|
||||
"tujuan": "RDF Rorotan",
|
||||
"status": "In Progress",
|
||||
"tanggalWaktu": "2025-07-21T13:05:00"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"noSpj": "SPJ/07-2025/PKM/000484",
|
||||
"plat": "B 8080 STU",
|
||||
"kode": "JRC 013",
|
||||
"tujuan": "RDF Pesanggarahan",
|
||||
"status": "Completed",
|
||||
"tanggalWaktu": "2025-07-20T15:40:00"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"noSpj": "SPJ/07-2025/PKM/000485",
|
||||
"plat": "B 9090 VWX",
|
||||
"kode": "JRC 014",
|
||||
"tujuan": "RDF Sunter",
|
||||
"status": "Completed",
|
||||
"tanggalWaktu": "2025-07-19T12:00:00"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"noSpj": "SPJ/07-2025/PKM/000486",
|
||||
"plat": "B 2222 YZA",
|
||||
"kode": "JRC 015",
|
||||
"tujuan": "Bantar Gebang",
|
||||
"status": "Completed",
|
||||
"tanggalWaktu": "2025-07-18T06:55:00"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"noSpj": "SPJ/07-2025/PKM/000487",
|
||||
"plat": "B 3333 BCD",
|
||||
"kode": "JRC 016",
|
||||
"tujuan": "RDF Marunda",
|
||||
"status": "Completed",
|
||||
"tanggalWaktu": "2025-07-18T17:30:00"
|
||||
}
|
||||
]
|
||||
|
|
@ -76,4 +76,74 @@ namespace eSPJ.Models
|
|||
public string? Raw { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class DraftTimbanganItem
|
||||
{
|
||||
public decimal Berat { get; set; }
|
||||
public string JenisSampah { get; set; } = "Residu";
|
||||
public string FotoFileName { get; set; } = string.Empty;
|
||||
public bool Uploaded { get; set; }
|
||||
public string OcrInfo { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class DraftPenjemputanNonTps
|
||||
{
|
||||
public string SessionKey { get; set; } = string.Empty;
|
||||
public string LokasiAngkutId { get; set; } = string.Empty;
|
||||
public string SpjDetailId { get; set; } = string.Empty;
|
||||
public string Latitude { get; set; } = string.Empty;
|
||||
public string Longitude { get; set; } = string.Empty;
|
||||
public string AlamatJalan { get; set; } = string.Empty;
|
||||
public string WaktuKedatangan { get; set; } = string.Empty;
|
||||
public List<string> FotoKedatanganFileNames { get; set; } = new();
|
||||
public bool FotoKedatanganUploaded { get; set; }
|
||||
public List<DraftTimbanganItem> Timbangan { get; set; } = new();
|
||||
public decimal TotalOrganik { get; set; }
|
||||
public decimal TotalAnorganik { get; set; }
|
||||
public decimal TotalResidu { get; set; }
|
||||
public decimal TotalTimbangan { get; set; }
|
||||
public List<string> FotoPetugasFileNames { get; set; } = new();
|
||||
public bool FotoPetugasUploaded { get; set; }
|
||||
public string NamaPetugas { get; set; } = string.Empty;
|
||||
public bool Submitted { get; set; }
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.Now;
|
||||
}
|
||||
|
||||
public class DraftSaveRequest
|
||||
{
|
||||
public string DraftKey { get; set; } = string.Empty;
|
||||
public string SessionKey { get; set; } = string.Empty;
|
||||
public string LokasiAngkutId { get; set; } = string.Empty;
|
||||
public string SpjDetailId { get; set; } = string.Empty;
|
||||
public string Latitude { get; set; } = string.Empty;
|
||||
public string Longitude { get; set; } = string.Empty;
|
||||
public string AlamatJalan { get; set; } = string.Empty;
|
||||
public string WaktuKedatangan { get; set; } = string.Empty;
|
||||
public bool FotoKedatanganUploaded { get; set; }
|
||||
public List<string> FotoKedatanganFileNames { get; set; } = new();
|
||||
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 bool FotoPetugasUploaded { get; set; }
|
||||
public List<string> FotoPetugasFileNames { get; set; } = new();
|
||||
public string NamaPetugas { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class DraftSaveResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string? DraftKey { get; set; }
|
||||
public string? SessionKey { get; set; }
|
||||
}
|
||||
|
||||
public class DraftLoadResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public bool HasDraft { get; set; }
|
||||
public DraftPenjemputanNonTps? Draft { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
namespace eSPJ.Models
|
||||
{
|
||||
public class HistoryItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string NoSpj { get; set; } = string.Empty;
|
||||
public string Plat { get; set; } = string.Empty;
|
||||
public string Kode { get; set; } = string.Empty;
|
||||
public string Tujuan { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public DateTime TanggalWaktu { get; set; }
|
||||
}
|
||||
|
||||
public class HistoryListResponse
|
||||
{
|
||||
public List<HistoryItem> Items { get; set; } = new();
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalItems { get; set; }
|
||||
public int TotalPages { get; set; }
|
||||
public string? FromDate { get; set; }
|
||||
public string? ToDate { get; set; }
|
||||
}
|
||||
}
|
||||
15
Program.cs
15
Program.cs
|
|
@ -8,6 +8,7 @@ builder.Services.AddHttpClient();
|
|||
|
||||
// Register custom services
|
||||
builder.Services.AddScoped<DetailPenjemputanService>();
|
||||
builder.Services.AddScoped<HistoryService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
|
@ -20,8 +21,20 @@ if (!app.Environment.IsDevelopment())
|
|||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseRouting();
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
if (context.Request.Path.Equals("/driver/serviceworker.js", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
context.Response.OnStarting(() =>
|
||||
{
|
||||
context.Response.Headers["Service-Worker-Allowed"] = "/upst";
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
app.UseRouting();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapStaticAssets();
|
||||
|
|
|
|||
|
|
@ -114,9 +114,7 @@ namespace eSPJ.Services
|
|||
};
|
||||
}
|
||||
|
||||
var uploadDateFolder = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var uploadPath = Path.Combine(_env.WebRootPath, "uploads", "penjemputan", uploadDateFolder);
|
||||
var uploadBaseUrl = $"/uploads/penjemputan/{uploadDateFolder}";
|
||||
var uploadPath = Path.Combine(_env.ContentRootPath, "uploads", "penjemputan", DateTime.Now.ToString("yyyy-MM-dd"));
|
||||
if (!Directory.Exists(uploadPath))
|
||||
{
|
||||
Directory.CreateDirectory(uploadPath);
|
||||
|
|
@ -216,6 +214,130 @@ namespace eSPJ.Services
|
|||
}
|
||||
}
|
||||
|
||||
private string GetDraftFilePath(string prefix, string sessionKey)
|
||||
{
|
||||
var dir = Path.Combine(_env.ContentRootPath, "Data", "drafts");
|
||||
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||||
var safe = string.Concat(sessionKey.Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_'));
|
||||
if (string.IsNullOrEmpty(safe)) safe = "default";
|
||||
return Path.Combine(dir, $"draft-{prefix}-{safe}.json");
|
||||
}
|
||||
|
||||
private Task<DraftSaveResponse> SaveDraftAsync(string prefix, DraftSaveRequest request)
|
||||
{
|
||||
return SaveDraftInternalAsync(prefix, request);
|
||||
}
|
||||
|
||||
private async Task<DraftSaveResponse> SaveDraftInternalAsync(string prefix, DraftSaveRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = GetDraftFilePath(prefix, request.SessionKey);
|
||||
var draft = new DraftPenjemputanNonTps
|
||||
{
|
||||
SessionKey = request.SessionKey,
|
||||
LokasiAngkutId = request.LokasiAngkutId,
|
||||
SpjDetailId = request.SpjDetailId,
|
||||
Latitude = request.Latitude,
|
||||
Longitude = request.Longitude,
|
||||
AlamatJalan = request.AlamatJalan,
|
||||
WaktuKedatangan = request.WaktuKedatangan,
|
||||
FotoKedatanganFileNames = request.FotoKedatanganFileNames,
|
||||
FotoKedatanganUploaded = request.FotoKedatanganUploaded,
|
||||
Timbangan = request.Timbangan,
|
||||
TotalOrganik = request.TotalOrganik,
|
||||
TotalAnorganik = request.TotalAnorganik,
|
||||
TotalResidu = request.TotalResidu,
|
||||
TotalTimbangan = request.TotalTimbangan,
|
||||
FotoPetugasFileNames = request.FotoPetugasFileNames,
|
||||
FotoPetugasUploaded = request.FotoPetugasUploaded,
|
||||
NamaPetugas = request.NamaPetugas,
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||
var json = JsonSerializer.Serialize(draft, options);
|
||||
await File.WriteAllTextAsync(filePath, json);
|
||||
|
||||
return new DraftSaveResponse { Success = true, Message = "Draft tersimpan.", DraftKey = request.DraftKey, SessionKey = request.SessionKey };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error saving {Prefix} draft", prefix);
|
||||
return new DraftSaveResponse { Success = false, Message = $"Gagal menyimpan draft: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DraftSaveResponse> SaveDraftNonTpsAsync(DraftSaveRequest request)
|
||||
{
|
||||
return await SaveDraftAsync("non-tps", request);
|
||||
}
|
||||
|
||||
public async Task<DraftSaveResponse> SaveDraftTpsAsync(DraftSaveRequest request)
|
||||
{
|
||||
return await SaveDraftAsync("tps", request);
|
||||
}
|
||||
|
||||
public async Task<DraftLoadResponse> LoadDraftNonTpsAsync(string sessionKey)
|
||||
{
|
||||
return await LoadDraftAsync("non-tps", sessionKey);
|
||||
}
|
||||
|
||||
public async Task<DraftLoadResponse> LoadDraftTpsAsync(string sessionKey)
|
||||
{
|
||||
return await LoadDraftAsync("tps", sessionKey);
|
||||
}
|
||||
|
||||
private async Task<DraftLoadResponse> LoadDraftAsync(string prefix, string sessionKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = GetDraftFilePath(prefix, sessionKey);
|
||||
if (!File.Exists(filePath))
|
||||
return new DraftLoadResponse { Success = true, HasDraft = false, Message = "Tidak ada draft." };
|
||||
|
||||
var json = await File.ReadAllTextAsync(filePath);
|
||||
var draft = JsonSerializer.Deserialize<DraftPenjemputanNonTps>(json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
if (draft == null)
|
||||
return new DraftLoadResponse { Success = true, HasDraft = false, Message = "Draft kosong." };
|
||||
|
||||
return new DraftLoadResponse { Success = true, HasDraft = true, Draft = draft };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading {Prefix} draft", prefix);
|
||||
return new DraftLoadResponse { Success = false, HasDraft = false, Message = $"Gagal memuat draft: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteDraftNonTpsAsync(string sessionKey)
|
||||
{
|
||||
return await DeleteDraftAsync("non-tps", sessionKey);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteDraftTpsAsync(string sessionKey)
|
||||
{
|
||||
return await DeleteDraftAsync("tps", sessionKey);
|
||||
}
|
||||
|
||||
private async Task<bool> DeleteDraftAsync(string prefix, string sessionKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = GetDraftFilePath(prefix, sessionKey);
|
||||
if (File.Exists(filePath)) File.Delete(filePath);
|
||||
await Task.CompletedTask;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting {Prefix} draft", prefix);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OcrTimbanganResponse> ProcessOcrTimbanganAsync(IFormFile foto)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
using System.Text.Json;
|
||||
using eSPJ.Models;
|
||||
|
||||
namespace eSPJ.Services
|
||||
{
|
||||
public class HistoryService
|
||||
{
|
||||
private readonly string _historyUpstFilePath;
|
||||
private readonly ILogger<HistoryService> _logger;
|
||||
|
||||
public HistoryService(IWebHostEnvironment env, ILogger<HistoryService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_historyUpstFilePath = Path.Combine(env.ContentRootPath, "Data", "history-upst.json");
|
||||
}
|
||||
|
||||
public async Task<HistoryListResponse> GetUpstHistoryAsync(DateOnly? fromDate, DateOnly? toDate, int page = 1, int pageSize = 5)
|
||||
{
|
||||
page = page < 1 ? 1 : page;
|
||||
pageSize = pageSize <= 0 ? 5 : pageSize;
|
||||
|
||||
var items = await ReadUpstHistoryAsync();
|
||||
var query = items.AsEnumerable();
|
||||
|
||||
if (fromDate.HasValue)
|
||||
{
|
||||
query = query.Where(item => DateOnly.FromDateTime(item.TanggalWaktu) >= fromDate.Value);
|
||||
}
|
||||
|
||||
if (toDate.HasValue)
|
||||
{
|
||||
query = query.Where(item => DateOnly.FromDateTime(item.TanggalWaktu) <= toDate.Value);
|
||||
}
|
||||
|
||||
var ordered = query
|
||||
.OrderByDescending(item => item.TanggalWaktu)
|
||||
.ToList();
|
||||
|
||||
var totalItems = ordered.Count;
|
||||
var totalPages = totalItems == 0 ? 1 : (int)Math.Ceiling(totalItems / (double)pageSize);
|
||||
var safePage = Math.Min(page, totalPages);
|
||||
var pagedItems = ordered
|
||||
.Skip((safePage - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
|
||||
return new HistoryListResponse
|
||||
{
|
||||
Items = pagedItems,
|
||||
Page = safePage,
|
||||
PageSize = pageSize,
|
||||
TotalItems = totalItems,
|
||||
TotalPages = totalPages,
|
||||
FromDate = fromDate?.ToString("yyyy-MM-dd"),
|
||||
ToDate = toDate?.ToString("yyyy-MM-dd")
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<HistoryItem>> ReadUpstHistoryAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_historyUpstFilePath))
|
||||
{
|
||||
return new List<HistoryItem>();
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(_historyUpstFilePath);
|
||||
var items = JsonSerializer.Deserialize<List<HistoryItem>>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
return items ?? new List<HistoryItem>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reading UPST history JSON");
|
||||
return new List<HistoryItem>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
ViewData["Title"] = "History - DLH";
|
||||
}
|
||||
|
||||
<div class="w-full lg:max-w-sm mx-auto bg-gray-50 min-h-screen">
|
||||
<div class="w-full lg:max-w-sm mx-auto bg-gray-50 min-h-screen" id="history-page" data-history-api="@Url.Action("GetHistory", "History")" data-history-detail-base="@Url.Action("Details", "History", new { id = "__id__" })">
|
||||
<div class="bg-upst text-white px-6 pt-10 pb-16 rounded-b-[40px] shadow-lg relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -mr-16 -mt-16"></div>
|
||||
<div class="flex items-center justify-between relative z-10">
|
||||
|
|
@ -18,134 +18,58 @@
|
|||
<div class="w-10"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@{
|
||||
var spjList = new[]
|
||||
{
|
||||
new {
|
||||
Id = 1,
|
||||
NoSpj = "SPJ/07-2025/PKM/000478",
|
||||
Plat = "B 5678 ABC",
|
||||
Kode = "JRC 007",
|
||||
Tujuan = "Bantar Gebang",
|
||||
Status = "In Progress",
|
||||
Tanggal = "28 Jul 2025",
|
||||
Waktu = "16:45"
|
||||
},
|
||||
new {
|
||||
Id = 2,
|
||||
NoSpj = "SPJ/07-2025/PKM/000476",
|
||||
Plat = "B 9632 TOR",
|
||||
Kode = "JRC 005",
|
||||
Tujuan = "RDF Rorotan",
|
||||
Status = "Completed",
|
||||
Tanggal = "27 Jul 2025",
|
||||
Waktu = "14:30"
|
||||
},
|
||||
new {
|
||||
Id = 3,
|
||||
NoSpj = "SPJ/07-2025/PKM/000477",
|
||||
Plat = "B 1234 XYZ",
|
||||
Kode = "JRC 006",
|
||||
Tujuan = "RDF Pesanggarahan",
|
||||
Status = "Completed",
|
||||
Tanggal = "26 Jul 2025",
|
||||
Waktu = "09:15"
|
||||
},
|
||||
new {
|
||||
Id = 4,
|
||||
NoSpj = "SPJ/07-2025/PKM/000479",
|
||||
Plat = "B 9876 DEF",
|
||||
Kode = "JRC 008",
|
||||
Tujuan = "RDF Sunter",
|
||||
Status = "Completed",
|
||||
Tanggal = "25 Jul 2025",
|
||||
Waktu = "11:20"
|
||||
},
|
||||
new {
|
||||
Id = 5,
|
||||
NoSpj = "SPJ/07-2025/PKM/000480",
|
||||
Plat = "B 4321 GHI",
|
||||
Kode = "JRC 009",
|
||||
Tujuan = "Bantar Gebang",
|
||||
Status = "Completed",
|
||||
Tanggal = "24 Jul 2025",
|
||||
Waktu = "08:45"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
<div class="px-5 -mt-8 space-y-5 relative z-20">
|
||||
@foreach (var spj in spjList)
|
||||
{
|
||||
<a href="@Url.Action("Details", "History", new { id = spj.Id })" class="block group">
|
||||
<div class="bg-white rounded-3xl p-5 shadow-sm border border-gray-100 group-hover:shadow-md group-hover:-translate-y-1 transition-all duration-300">
|
||||
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="space-y-0.5">
|
||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Nomor Dokumen</p>
|
||||
<p class="text-sm font-black text-gray-800">@spj.NoSpj</p>
|
||||
</div>
|
||||
|
||||
@if (spj.Status == "Completed")
|
||||
{
|
||||
<span class="bg-green-50 text-green-600 px-3 py-1 rounded-lg text-[10px] font-black uppercase border border-green-100">
|
||||
Selesai
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="bg-blue-50 text-blue-600 px-3 py-1 rounded-lg text-[10px] font-black uppercase border border-blue-100 animate-pulse">
|
||||
Proses
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 bg-gray-50/80 p-3 rounded-2xl border border-dashed border-gray-200">
|
||||
<div class="w-12 h-12 bg-upst rounded-xl flex items-center justify-center shadow-inner">
|
||||
<i class="w-6 h-6 text-white" data-lucide="truck"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-baseline">
|
||||
<h2 class="font-black text-gray-900 leading-tight">@spj.Plat</h2>
|
||||
<span class="text-[11px] font-bold text-upst">@spj.Waktu</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 font-medium">@spj.Kode</span>
|
||||
<span class="text-[10px] text-gray-400 font-semibold">@spj.Tanggal</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-upst/40"></div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-[10px] text-gray-400 font-bold uppercase">Tujuan Akhir</span>
|
||||
<span class="text-sm font-bold text-gray-700">@spj.Tujuan</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center group-hover:bg-upst transition-colors">
|
||||
<i class="w-4 h-4" data-lucide="chevron-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-5 -mt-8 space-y-4 relative z-20 pb-28">
|
||||
<div class="bg-white rounded-3xl p-4 border border-gray-100 shadow-sm space-y-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-gray-400 uppercase tracking-widest">Filter Riwayat</p>
|
||||
<h2 class="text-sm font-black text-gray-900">Range Tanggal Perjalanan</h2>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
<button id="history-reset-filter" type="button" class="text-[11px] font-bold text-upst px-3 py-2 rounded-xl bg-orange-50 border border-orange-100">Reset</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<input id="history-from-date-filter" type="date" class="w-full rounded-2xl border border-gray-200 px-4 py-3 text-sm font-medium text-gray-700 bg-gray-50" />
|
||||
<input id="history-to-date-filter" type="date" class="w-full rounded-2xl border border-gray-200 px-4 py-3 text-sm font-medium text-gray-700 bg-gray-50" />
|
||||
</div>
|
||||
<div class="flex items-center justify-end">
|
||||
<button id="history-apply-filter" type="button" class="rounded-2xl bg-upst text-white px-4 py-3 text-xs font-black uppercase tracking-wide">Terapkan</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="history-loading" class="bg-white rounded-3xl p-5 border border-gray-100 text-center text-sm font-medium text-gray-500 shadow-sm">
|
||||
Memuat riwayat perjalanan...
|
||||
</div>
|
||||
|
||||
<div id="history-empty" class="hidden bg-white rounded-3xl p-6 border border-gray-100 shadow-sm text-center">
|
||||
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i class="w-8 h-8 text-gray-400" data-lucide="clock-3"></i>
|
||||
</div>
|
||||
<h3 class="text-base font-black text-gray-900 mb-1">Belum Ada Riwayat</h3>
|
||||
<p class="text-sm text-gray-500">Coba ubah filter tanggal atau tunggu data perjalanan masuk.</p>
|
||||
</div>
|
||||
|
||||
<div id="history-list" class="space-y-5"></div>
|
||||
|
||||
<div id="history-pagination" class="hidden bg-white rounded-3xl p-4 border border-gray-100 shadow-sm space-y-3">
|
||||
<div class="flex items-center justify-between text-[11px] font-bold text-gray-500 uppercase tracking-wide">
|
||||
<span id="history-page-info">Page 1</span>
|
||||
<span id="history-total-info">0 data</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<button id="history-prev-page" type="button" class="sm:flex-1 rounded-2xl border border-gray-200 px-4 py-3 text-xs font-black text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed">Sebelumnya</button>
|
||||
<div class="max-w-full overflow-x-auto">
|
||||
<div id="history-page-buttons" class="flex items-center gap-2 min-w-max"></div>
|
||||
</div>
|
||||
<button id="history-next-page" type="button" class="sm:flex-1 rounded-2xl border border-gray-200 px-4 py-3 text-xs font-black text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed">Berikutnya</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<partial name="~/Views/Admin/Transport/SpjDriverUpst/Shared/Components/_Navigation.cshtml" />
|
||||
|
||||
<!-- Kalau butuh tampilan kosong (jika tidak ada data) -->
|
||||
|
||||
@* <div class="flex flex-col items-center justify-center py-16 px-4">
|
||||
<div class="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||||
<i class="w-12 h-12 text-gray-400" data-lucide="clock"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Belum Ada Riwayat</h3>
|
||||
<p class="text-gray-500 text-center text-sm">Riwayat perjalanan Anda akan muncul di sini setelah melakukan perjalanan pertama.</p>
|
||||
</div> *@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="@Url.Content("~/driver/js/history-upst.js")" asp-append-version="true"></script>
|
||||
}
|
||||
|
|
@ -33,80 +33,95 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 -mt-16 relative z-20">
|
||||
<div class="bg-upst-light rounded-3xl p-4 border border-gray-100 mb-6">
|
||||
<div class="flex items-center gap-4 group cursor-pointer" id="userLocationBtn">
|
||||
<div class="w-10 h-10 bg-upst rounded-2xl flex items-center justify-center flex-shrink-0 group-active:scale-90 transition-transform">
|
||||
<i class="w-5 h-5 text-white" data-lucide="map-pin"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-[9px] font-black text-gray-400 uppercase tracking-wider leading-none mb-1">Lokasi Saat Ini</p>
|
||||
<p id="userLocation" class="text-xs line-clamp-3 font-bold text-gray-700 italic">Mendeteksi lokasi...</p>
|
||||
</div>
|
||||
<i class="w-4 h-4 text-gray-700" data-lucide="refresh-cw"></i>
|
||||
<div class="px-4 -mt-16 relative z-20 flex flex-col gap-6">
|
||||
|
||||
<div class="bg-upst-light rounded-[28px] p-4 border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center gap-4 group cursor-pointer" id="userLocationBtn">
|
||||
<div class="w-12 h-12 bg-upst rounded-2xl flex items-center justify-center shrink-0 shadow-md group-active:scale-90 transition-transform">
|
||||
<i class="w-6 h-6 text-white" data-lucide="map-pin"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 mb-4 p-1 bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||
<button id="tabSpjButton" type="button" class="px-3 py-2 text-xs font-black rounded-xl bg-upst text-white transition-all">
|
||||
SPJ
|
||||
</button>
|
||||
<button id="tabMapsButton" type="button" class="px-3 py-2 text-xs font-black rounded-xl text-gray-500 bg-gray-100 transition-all">
|
||||
MAPS
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="spjCardPanel" class="bg-upst rounded-[35px] overflow-hidden shadow-2xl relative h-[300px]">
|
||||
<div class="bg-white/10 backdrop-blur-md p-5 flex justify-between items-center border-b border-white/10">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-green-100 uppercase tracking-widest">Nomor SPJ</p>
|
||||
<p class="text-white font-mono font-bold text-sm">SPJ/07-2025/PKM/000476</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="bg-white text-green-600 text-[10px] font-black px-3 py-1 rounded-lg shadow-sm">B 9632 TOR</span>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Lokasi Saat Ini</p>
|
||||
<p id="userLocation" class="text-sm font-bold text-gray-800 leading-tight line-clamp-2">Mendeteksi lokasi...</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6 relative h-[236px]">
|
||||
<div class="flex justify-between items-center h-full">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<p class="text-[10px] text-green-100 font-bold uppercase opacity-80">Tujuan Pembuangan</p>
|
||||
<h2 class="text-2xl font-black text-white tracking-tight leading-tight">JRC Rorotan</h2>
|
||||
<p class="text-xs text-green-100/70 font-medium">(JRC 005)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="qrCodeTrigger" class="w-16 h-16 bg-white rounded-[24px] flex items-center justify-center shadow-2xl border-4 border-green-600/20 active:scale-90 transition-transform">
|
||||
<img src="@Url.Content("~/driver/images/qr.png")" alt="QR" class="w-10 h-10 object-contain">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<img src="@Url.Content("~/driver/tree.svg")" class="absolute -left-4 -bottom-4 w-20 h-20 opacity-10 pointer-events-none" alt="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mapsCardPanel" class="bg-upst rounded-[35px] overflow-hidden shadow-2xl relative h-[300px] flex-col hidden">
|
||||
<div class="bg-white/10 backdrop-blur-md p-5 flex justify-between items-center border-b border-white/10">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-green-100 uppercase tracking-widest">Maps</p>
|
||||
<p class="text-white font-mono font-bold text-sm">Rute Pengangkutan</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="bg-white text-green-600 text-[10px] font-black px-3 py-1 rounded-lg shadow-sm">LIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 relative h-[236px]">
|
||||
<div id="pickupMap" class="h-full w-full z-10 rounded-3xl overflow-hidden border border-white/20"></div>
|
||||
<button id="recenterMapButton" type="button" class="absolute top-9 right-9 z-20 w-11 h-11 bg-white/95 text-upst rounded-2xl shadow-xl border border-white/70 backdrop-blur flex items-center justify-center active:scale-95 transition-transform" title="Tampilkan semua lokasi">
|
||||
<i class="w-5 h-5" data-lucide="locate-fixed"></i>
|
||||
</button>
|
||||
<img src="@Url.Content("~/driver/tree.svg")" class="absolute -left-4 -bottom-4 w-20 h-20 opacity-10 pointer-events-none" alt="">
|
||||
<div class="w-10 h-10 rounded-full bg-white flex items-center justify-center shadow-sm border border-gray-100 shrink-0 group-active:rotate-180 transition-transform duration-500">
|
||||
<i class="w-4 h-4 text-gray-500" data-lucide="refresh-cw"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
|
||||
<div class="bg-white p-1.5 rounded-2xl border border-gray-100 shadow-sm flex">
|
||||
<button id="tabSpjButton" type="button" class="flex-1 py-3 text-xs font-black rounded-xl bg-upst text-white shadow-md transition-all">
|
||||
DETAIL SPJ
|
||||
</button>
|
||||
<button id="tabMapsButton" type="button" class="flex-1 py-3 text-xs font-black rounded-xl text-gray-500 bg-transparent hover:bg-gray-50 transition-all">
|
||||
LIVE MAPS
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
|
||||
<div id="spjCardPanel" class="bg-upst rounded-[32px] overflow-hidden shadow-xl flex flex-col h-[340px]">
|
||||
|
||||
<div class="px-6 py-5 bg-black/10 border-b border-white/5 flex justify-between items-center shrink-0">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-green-100 uppercase tracking-widest mb-1">Nomor SPJ</p>
|
||||
<p class="text-white font-mono font-bold text-sm">SPJ/07-2025/PKM/000476</p>
|
||||
</div>
|
||||
<div class="bg-white px-3 py-1.5 rounded-xl shadow-sm">
|
||||
<span class="text-green-600 text-xs font-black">B 9632 TOR</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 flex-1 flex flex-col justify-center relative overflow-hidden">
|
||||
<img src="@Url.Content("~/driver/tree.svg")" class="absolute -right-6 -bottom-6 w-40 h-40 opacity-10 pointer-events-none" alt="">
|
||||
|
||||
<div class="z-10 flex flex-row justify-between items-center w-full mt-2">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-[10px] text-green-100 font-bold uppercase tracking-wider mb-2">Tujuan Pembuangan</p>
|
||||
<h2 class="text-3xl font-black text-white leading-tight mb-2">JRC Rorotan</h2>
|
||||
<span class="bg-white/10 text-green-50 text-xs font-bold px-3 py-1 rounded-lg border border-white/10 inline-block">
|
||||
JRC 005
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button id="qrCodeTrigger" class="bg-white p-4 rounded-3xl shadow-xl border-4 border-white/20 active:scale-90 transition-transform group hover:border-green-400 shrink-0">
|
||||
<img src="@Url.Content("~/driver/images/qr.png")" alt="QR" class="w-10 h-10 object-contain group-hover:scale-110 transition-transform">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="mapsCardPanel" class="bg-upst rounded-[32px] overflow-hidden shadow-xl hidden flex-col h-[340px]">
|
||||
|
||||
<div class="px-6 py-5 bg-black/10 flex justify-between items-center shrink-0 z-20">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-green-100 uppercase tracking-widest mb-1">Rute Pengangkutan</p>
|
||||
<p class="text-white font-bold text-sm">Live Location</p>
|
||||
</div>
|
||||
<div class="bg-white/10 border border-white/20 px-3 py-1.5 rounded-xl shadow-sm flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-red-400 animate-pulse"></span>
|
||||
<span class="text-white text-[10px] font-black tracking-wider">LIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative flex-1 w-full bg-gray-100">
|
||||
<div id="pickupMap" class="absolute inset-0 w-full h-full z-10"></div>
|
||||
|
||||
<button id="recenterMapButton" type="button" class="absolute bottom-5 right-5 z-20 w-12 h-12 bg-white text-upst rounded-2xl shadow-lg border border-gray-200 flex items-center justify-center active:scale-90 transition-transform">
|
||||
<i class="w-6 h-6" data-lucide="locate-fixed"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="px-6 mt-10">
|
||||
<div class="flex justify-between items-end mb-6">
|
||||
<div>
|
||||
|
|
@ -300,7 +315,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
getLocationUpdate();
|
||||
}
|
||||
|
||||
// Update Lokasi cuy
|
||||
userLocationEl.addEventListener("click", function () {
|
||||
getLocationUpdate();
|
||||
});
|
||||
|
|
@ -358,30 +372,131 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
let mapInstance = null;
|
||||
let mapBounds = null;
|
||||
let mapInitialized = false;
|
||||
|
||||
let userMarker = null;
|
||||
let liveLocationWatchId = null;
|
||||
let lastUserLatLng = null;
|
||||
let lastKnownHeading = 0;
|
||||
|
||||
function fitAllLocations() {
|
||||
if (!mapInstance || !mapBounds) return;
|
||||
|
||||
mapInstance.invalidateSize();
|
||||
|
||||
if (Array.isArray(mapBounds) && mapBounds.length === 1) {
|
||||
mapInstance.setView(mapBounds[0], 16);
|
||||
return;
|
||||
}
|
||||
mapInstance.fitBounds(mapBounds, { padding: [24, 24], maxZoom: 17 });
|
||||
}
|
||||
|
||||
mapInstance.fitBounds(mapBounds, {
|
||||
padding: [24, 24],
|
||||
maxZoom: 17
|
||||
function toRadians(deg) {
|
||||
return deg * Math.PI / 180;
|
||||
}
|
||||
|
||||
function toDegrees(rad) {
|
||||
return rad * 180 / Math.PI;
|
||||
}
|
||||
|
||||
function getBearing(from, to) {
|
||||
const [lat1, lng1] = from;
|
||||
const [lat2, lng2] = to;
|
||||
|
||||
const phi1 = toRadians(lat1);
|
||||
const phi2 = toRadians(lat2);
|
||||
const lambda1 = toRadians(lng1);
|
||||
const lambda2 = toRadians(lng2);
|
||||
|
||||
const y = Math.sin(lambda2 - lambda1) * Math.cos(phi2);
|
||||
const x =
|
||||
Math.cos(phi1) * Math.sin(phi2) -
|
||||
Math.sin(phi1) * Math.cos(phi2) * Math.cos(lambda2 - lambda1);
|
||||
|
||||
return (toDegrees(Math.atan2(y, x)) + 360) % 360;
|
||||
}
|
||||
|
||||
function createTruckDivIcon(rotationDeg) {
|
||||
const carIconHtml = `
|
||||
<div class="w-10 h-10 flex items-center justify-center">
|
||||
<img
|
||||
src="@Url.Content("~/driver/images/truck_icon.png")"
|
||||
alt="Truck"
|
||||
class="w-10 h-10 object-contain drop-shadow-lg"
|
||||
style="transform: rotate(${rotationDeg}deg); transition: transform 0.25s ease;"
|
||||
/>
|
||||
</div>`;
|
||||
|
||||
return L.divIcon({
|
||||
className: 'bg-transparent border-0',
|
||||
html: carIconHtml,
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 20],
|
||||
popupAnchor: [0, -15]
|
||||
});
|
||||
}
|
||||
|
||||
function handleUserPosition(position) {
|
||||
if (!mapInstance) return;
|
||||
|
||||
const lat = position.coords.latitude;
|
||||
const lng = position.coords.longitude;
|
||||
const nextLatLng = [lat, lng];
|
||||
|
||||
let heading = Number.isFinite(position.coords.heading)
|
||||
? position.coords.heading
|
||||
: null;
|
||||
|
||||
if (!Number.isFinite(heading) && lastUserLatLng) {
|
||||
heading = getBearing(lastUserLatLng, nextLatLng);
|
||||
}
|
||||
|
||||
if (Number.isFinite(heading)) {
|
||||
lastKnownHeading = heading;
|
||||
}
|
||||
|
||||
const rotationDeg = (lastKnownHeading + 180) % 360;
|
||||
const truckIcon = createTruckDivIcon(rotationDeg);
|
||||
|
||||
if (userMarker) {
|
||||
userMarker.setLatLng(nextLatLng);
|
||||
userMarker.setIcon(truckIcon);
|
||||
} else {
|
||||
userMarker = L.marker(nextLatLng, { icon: truckIcon, zIndexOffset: 1000 })
|
||||
.addTo(mapInstance)
|
||||
.bindPopup("<div class='text-xs font-bold text-center text-blue-600 mb-1'>Posisi Anda</div><div class='text-[10px] text-gray-500 text-center'>Diperbarui otomatis via GPS</div>");
|
||||
}
|
||||
|
||||
lastUserLatLng = nextLatLng;
|
||||
}
|
||||
|
||||
function updateUserLocationOnMap() {
|
||||
if (!mapInstance || !("geolocation" in navigator)) return;
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
handleUserPosition,
|
||||
function (error) {
|
||||
console.warn("Gagal mengambil GPS untuk live tracking:", error);
|
||||
},
|
||||
{ enableHighAccuracy: true, maximumAge: 0, timeout: 15000 }
|
||||
);
|
||||
}
|
||||
|
||||
function startLiveTracking() {
|
||||
if (!mapInstance || !("geolocation" in navigator) || liveLocationWatchId !== null) return;
|
||||
|
||||
liveLocationWatchId = navigator.geolocation.watchPosition(
|
||||
handleUserPosition,
|
||||
function (error) {
|
||||
console.warn("Gagal mengambil GPS untuk live tracking:", error);
|
||||
},
|
||||
{ enableHighAccuracy: true, maximumAge: 0, timeout: 15000 }
|
||||
);
|
||||
}
|
||||
|
||||
function ensureLeaflet() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.L && typeof window.L.map === "function") {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!document.getElementById("leaflet-css")) {
|
||||
const css = document.createElement("link");
|
||||
css.id = "leaflet-css";
|
||||
|
|
@ -389,14 +504,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
css.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
|
||||
document.head.appendChild(css);
|
||||
}
|
||||
|
||||
const existingScript = document.getElementById("leaflet-js");
|
||||
if (existingScript) {
|
||||
existingScript.addEventListener("load", resolve, { once: true });
|
||||
existingScript.addEventListener("error", reject, { once: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.id = "leaflet-js";
|
||||
script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
|
||||
|
|
@ -418,7 +531,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
function normalizeLatLng(item) {
|
||||
const lat = toNumber(item.latitude);
|
||||
const lng = toNumber(item.longitude);
|
||||
|
||||
if (isValidLatLng(lat, lng)) return { lat, lng };
|
||||
if (isValidLatLng(lng, lat)) return { lat: lng, lng: lat };
|
||||
return null;
|
||||
|
|
@ -446,23 +558,15 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
|
||||
async function getRoadRouteLatLngs(latLngs) {
|
||||
if (!Array.isArray(latLngs) || latLngs.length < 2) return null;
|
||||
|
||||
try {
|
||||
const coordString = latLngs
|
||||
.map(([lat, lng]) => `${lng},${lat}`)
|
||||
.join(";");
|
||||
|
||||
const coordString = latLngs.map(([lat, lng]) => `${lng},${lat}`).join(";");
|
||||
const url = `https://router.project-osrm.org/route/v1/driving/${coordString}?overview=full&geometries=geojson&steps=false&alternatives=false`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return null;
|
||||
|
||||
const data = await res.json();
|
||||
const coords = data?.routes?.[0]?.geometry?.coordinates;
|
||||
if (!Array.isArray(coords) || coords.length < 2) return null;
|
||||
|
||||
return coords
|
||||
.map(pair => [toNumber(pair?.[1]), toNumber(pair?.[0])])
|
||||
.filter(([lat, lng]) => isValidLatLng(lat, lng));
|
||||
return coords.map(pair => [toNumber(pair?.[1]), toNumber(pair?.[0])]).filter(([lat, lng]) => isValidLatLng(lat, lng));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -471,13 +575,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
async function initMap() {
|
||||
try {
|
||||
if (mapInitialized && mapInstance) {
|
||||
setTimeout(() => {
|
||||
fitAllLocations();
|
||||
}, 80);
|
||||
setTimeout(() => fitAllLocations(), 80);
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureLeaflet();
|
||||
|
||||
const res = await fetch("@Url.Content("~/driver/json/pengangkutan.json")");
|
||||
const payload = await res.json();
|
||||
const rows = Array.isArray(payload?.data) ? payload.data : [];
|
||||
|
|
@ -513,13 +616,25 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
const latLngs = points.map(p => [p.lat, p.lng]);
|
||||
|
||||
points.forEach((point, index) => {
|
||||
const popupContent = `
|
||||
<div class="font-sans min-w-[150px]">
|
||||
<b class="block text-sm text-gray-800 mb-1">${index + 1}. ${point.name || "Lokasi"}</b>
|
||||
<span class="text-xs text-gray-500 block mb-3 line-clamp-2">${point.alamat || "-"}</span>
|
||||
<a href="https://www.google.com/maps/dir/?api=1&destination=${point.lat},${point.lng}"
|
||||
target="_blank"
|
||||
class="bg-blue-600 !text-white w-full py-2 rounded-xl text-[11px] font-bold flex items-center justify-center shadow-md hover:bg-blue-700 active:scale-95 transition-all"
|
||||
style="text-decoration:none;">
|
||||
<svg class="w-3 h-3 mr-1.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg> BUKA DI GMAPS
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
L.marker([point.lat, point.lng], { icon: pickupIcon })
|
||||
.addTo(mapInstance)
|
||||
.bindPopup(`<b>${index + 1}. ${point.name || "Lokasi"}</b><br>${point.alamat || "-"}`);
|
||||
.bindPopup(popupContent);
|
||||
});
|
||||
|
||||
let routeLatLngs = latLngs;
|
||||
|
||||
if (latLngs.length > 1) {
|
||||
const routed = await getRoadRouteLatLngs(latLngs);
|
||||
if (routed && routed.length > 1) {
|
||||
|
|
@ -537,9 +652,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
fitAllLocations();
|
||||
mapInitialized = true;
|
||||
|
||||
setTimeout(() => {
|
||||
fitAllLocations();
|
||||
}, 80);
|
||||
updateUserLocationOnMap();
|
||||
startLiveTracking();
|
||||
|
||||
} catch (err) {
|
||||
mapEl.innerHTML = '<div class="h-full flex items-center justify-center text-xs font-semibold text-red-500">Gagal memuat peta</div>';
|
||||
console.error("Map init error:", err);
|
||||
|
|
@ -563,9 +678,17 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
if (mapsCardPanel && !mapsCardPanel.classList.contains("hidden")) {
|
||||
initMap();
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", function () {
|
||||
if (liveLocationWatchId !== null && "geolocation" in navigator) {
|
||||
navigator.geolocation.clearWatch(liveLocationWatchId);
|
||||
liveLocationWatchId = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const btn = document.getElementById("profileMenuButton");
|
||||
|
|
|
|||
|
|
@ -529,7 +529,7 @@
|
|||
// PWA Service Worker Registration
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/driver/sw.js')
|
||||
navigator.serviceWorker.register('/serviceworker.js', { scope: '/' })
|
||||
.then((registration) => {
|
||||
console.log('SW registered: ', registration);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
<div id="pwaInstallPrompt" class="fixed inset-x-4 bottom-28 z-120 hidden">
|
||||
<div class="mx-auto w-full max-w-xs rounded-[28px] bg-upst-light px-4 py-4 text-gray-500 shadow-2xl ring-1 ring-black/5">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-upst text-xl font-black text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tablet-smartphone-icon lucide-tablet-smartphone"><rect width="10" height="14" x="3" y="8" rx="2"/><path d="M5 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2h-2.4"/><path d="M8 18h.01"/></svg>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[10px] font-black uppercase tracking-[0.22em] text-gray-400">Install App</p>
|
||||
<h3 class="mt-1 text-sm font-black tracking-tight">Pasang eSPJ di perangkat</h3>
|
||||
<p id="pwaInstallDescription" class="mt-1 text-[11px] leading-relaxed text-gray-500/80">Akses lebih cepat langsung dari home screen tanpa buka browser dulu.</p>
|
||||
</div>
|
||||
<button id="pwaInstallDismiss" type="button" class="flex h-9 w-9 items-center justify-center rounded-xl bg-white/10 text-lg font-bold text-gray-500/80 transition active:scale-90">×</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex gap-2">
|
||||
<button id="pwaInstallButton" type="button" class="flex-1 rounded-2xl bg-upst text-white px-4 py-3 text-xs font-black uppercase tracking-wide text-upst shadow-sm transition active:scale-95">
|
||||
Install Sekarang
|
||||
</button>
|
||||
<button id="pwaInstallHelpButton" type="button" class="rounded-2xl border border-gray-200 bg-gray-100 px-4 py-3 text-xs font-black uppercase tracking-wide text-gray-500 transition active:scale-95">
|
||||
Bantuan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pwaInstallHelpModal" class="fixed inset-0 z-130 hidden items-end justify-center bg-slate-950/60 p-4 backdrop-blur-sm sm:items-center">
|
||||
<div class="w-full max-w-sm rounded-4xl bg-white p-6 shadow-2xl">
|
||||
<p class="text-[10px] font-black uppercase tracking-[0.24em] text-upst">Cara Install</p>
|
||||
<h3 class="mt-2 text-lg font-black tracking-tight text-slate-900">Pasang eSPJ ke home screen</h3>
|
||||
<p id="pwaInstallHelpText" class="mt-3 text-sm leading-relaxed text-slate-600">Gunakan menu browser lalu pilih install aplikasi.</p>
|
||||
|
||||
<div id="pwaInstallHelpSteps" class="mt-5 grid gap-2 rounded-2xl bg-slate-50 p-4 text-xs leading-relaxed text-slate-600">
|
||||
<p>Chrome / Edge: buka menu browser lalu pilih <span class="font-bold text-slate-800">Install app</span> atau <span class="font-bold text-slate-800">Add to Home screen</span>.</p>
|
||||
<p>Firefox Android: buka menu browser lalu pilih <span class="font-bold text-slate-800">Install</span> atau <span class="font-bold text-slate-800">Add to Home screen</span> jika tersedia.</p>
|
||||
<p>iPhone / iPad: tekan <span class="font-bold text-slate-800">Share</span> lalu pilih <span class="font-bold text-slate-800">Add to Home Screen</span>.</p>
|
||||
<p>Firefox desktop: browser ini belum mendukung install PWA berbasis manifest dari web.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex gap-2">
|
||||
<button id="pwaInstallHelpClose" type="button" class="flex-1 rounded-2xl bg-slate-100 px-4 py-3 text-xs font-black uppercase tracking-wide text-slate-700 transition active:scale-95">Tutup</button>
|
||||
<button id="pwaInstallHelpPrimary" type="button" class="flex-1 rounded-2xl bg-upst px-4 py-3 text-xs font-black uppercase tracking-wide text-gray-500 shadow-sm transition active:scale-95">Install</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
<script src="@Url.Content("~/driver/lib/jquery/dist/jquery.min.js")"></script>
|
||||
<script src="@Url.Content("~/driver/lib/bootstrap/dist/js/bootstrap.bundle.min.js")"></script>
|
||||
<script src="@Url.Content("~/driver/js/site.js")" asp-append-version="true"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
lucide.createIcons();
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker.register("@Url.Content("~/driver/serviceworker.js")", { scope: "/upst" });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const installPrompt = document.getElementById("pwaInstallPrompt");
|
||||
const installButton = document.getElementById("pwaInstallButton");
|
||||
const dismissButton = document.getElementById("pwaInstallDismiss");
|
||||
const helpButton = document.getElementById("pwaInstallHelpButton");
|
||||
const helpModal = document.getElementById("pwaInstallHelpModal");
|
||||
const helpClose = document.getElementById("pwaInstallHelpClose");
|
||||
const helpPrimary = document.getElementById("pwaInstallHelpPrimary");
|
||||
const helpText = document.getElementById("pwaInstallHelpText");
|
||||
const helpSteps = document.getElementById("pwaInstallHelpSteps");
|
||||
const description = document.getElementById("pwaInstallDescription");
|
||||
|
||||
if (!installPrompt || !installButton || !dismissButton || !helpButton || !helpModal || !helpClose || !helpPrimary || !helpText || !helpSteps || !description) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dismissKey = "espj-upst-pwa-install-dismissed-session";
|
||||
let deferredPrompt = null;
|
||||
|
||||
function isStandalone() {
|
||||
return window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true;
|
||||
}
|
||||
|
||||
function isIos() {
|
||||
return /iphone|ipad|ipod/i.test(window.navigator.userAgent);
|
||||
}
|
||||
|
||||
function isAndroid() {
|
||||
return /android/i.test(window.navigator.userAgent);
|
||||
}
|
||||
|
||||
function isFirefox() {
|
||||
return /firefox|fxios/i.test(window.navigator.userAgent);
|
||||
}
|
||||
|
||||
function isDesktopFirefox() {
|
||||
return isFirefox() && !isAndroid() && !isIos();
|
||||
}
|
||||
|
||||
function showInstallPrompt() {
|
||||
if (isStandalone()) return;
|
||||
installPrompt.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function hideInstallPrompt(rememberDismissal) {
|
||||
installPrompt.classList.add("hidden");
|
||||
if (rememberDismissal) {
|
||||
window.sessionStorage.setItem(dismissKey, "true");
|
||||
}
|
||||
}
|
||||
|
||||
function openHelpModal() {
|
||||
helpModal.classList.remove("hidden");
|
||||
helpModal.classList.add("flex");
|
||||
}
|
||||
|
||||
function closeHelpModal() {
|
||||
helpModal.classList.add("hidden");
|
||||
helpModal.classList.remove("flex");
|
||||
}
|
||||
|
||||
function updateManualInstallCopy() {
|
||||
if (isIos()) {
|
||||
description.textContent = "Di iPhone/iPad, install dilakukan lewat menu Share browser, termasuk di Firefox iOS.";
|
||||
helpText.textContent = "Tekan tombol Share di browser, lalu pilih Add to Home Screen untuk memasang eSPJ.";
|
||||
helpSteps.innerHTML = `
|
||||
<p>Safari / Chrome / Edge / Firefox di iPhone & iPad: tekan <span class="font-bold text-slate-800">Share</span>, lalu pilih <span class="font-bold text-slate-800">Add to Home Screen</span>.</p>
|
||||
<p>Kalau opsi belum terlihat, scroll menu Share ke bawah sampai bagian tindakan tambahan.</p>
|
||||
`;
|
||||
helpPrimary.textContent = "Lihat Panduan";
|
||||
installButton.textContent = "Buka Panduan";
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDesktopFirefox()) {
|
||||
description.textContent = "Firefox desktop belum mendukung install PWA berbasis manifest langsung dari web.";
|
||||
helpText.textContent = "eSPJ tetap bisa dipakai normal di Firefox desktop, tapi untuk install sebagai app gunakan Chrome, Edge, atau Safari macOS yang mendukung.";
|
||||
helpSteps.innerHTML = `
|
||||
<p>Firefox desktop: belum ada dukungan install PWA via manifest file.</p>
|
||||
<p>Jika ingin pengalaman app di desktop, buka eSPJ di <span class="font-bold text-slate-800">Chrome</span> atau <span class="font-bold text-slate-800">Edge</span>, lalu pilih <span class="font-bold text-slate-800">Install app</span>.</p>
|
||||
`;
|
||||
helpPrimary.textContent = "Mengerti";
|
||||
installButton.textContent = "Info Firefox";
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFirefox() && isAndroid() && !deferredPrompt) {
|
||||
description.textContent = "Firefox Android biasanya install lewat menu browser, bukan popup native seperti Chrome.";
|
||||
helpText.textContent = "Buka menu tiga titik Firefox lalu pilih Install atau Add to Home screen jika tersedia.";
|
||||
helpSteps.innerHTML = `
|
||||
<p>Firefox Android: buka menu <span class="font-bold text-slate-800">⋮</span> lalu cari <span class="font-bold text-slate-800">Install</span> atau <span class="font-bold text-slate-800">Add to Home screen</span>.</p>
|
||||
<p>Kalau opsi belum muncul, refresh `/upst` sekali setelah service worker aktif lalu cek lagi.</p>
|
||||
`;
|
||||
helpPrimary.textContent = "Lihat Panduan";
|
||||
installButton.textContent = "Cara Install";
|
||||
return;
|
||||
}
|
||||
|
||||
description.textContent = deferredPrompt
|
||||
? "Install app untuk pengalaman lebih cepat dan nyaman dari browser."
|
||||
: "Kalau popup install belum muncul, tekan tombol ini lalu ikuti panduan. Kadang perlu refresh sekali setelah service worker aktif.";
|
||||
helpText.textContent = deferredPrompt
|
||||
? "Tekan Install untuk membuka dialog install native dari browser."
|
||||
: "Gunakan menu browser lalu pilih Install app atau Add to Home screen. Jika ini pertama kali buka `/upst`, refresh sekali lalu coba lagi.";
|
||||
helpSteps.innerHTML = `
|
||||
<p>Chrome / Edge: buka menu browser lalu pilih <span class="font-bold text-slate-800">Install app</span> atau <span class="font-bold text-slate-800">Add to Home screen</span>.</p>
|
||||
<p>Firefox Android: buka menu browser lalu pilih <span class="font-bold text-slate-800">Install</span> atau <span class="font-bold text-slate-800">Add to Home screen</span> jika tersedia.</p>
|
||||
<p>Firefox desktop: belum mendukung install PWA berbasis manifest dari web.</p>
|
||||
`;
|
||||
helpPrimary.textContent = deferredPrompt ? "Install" : "Buka Panduan";
|
||||
installButton.textContent = deferredPrompt ? "Install Sekarang" : "Cara Install";
|
||||
}
|
||||
|
||||
const isDismissedThisSession = window.sessionStorage.getItem(dismissKey) === "true";
|
||||
|
||||
updateManualInstallCopy();
|
||||
|
||||
if (!isStandalone() && !isDismissedThisSession) {
|
||||
showInstallPrompt();
|
||||
}
|
||||
|
||||
window.addEventListener("beforeinstallprompt", function (event) {
|
||||
event.preventDefault();
|
||||
deferredPrompt = event;
|
||||
updateManualInstallCopy();
|
||||
|
||||
if (!isStandalone()) {
|
||||
showInstallPrompt();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("appinstalled", function () {
|
||||
deferredPrompt = null;
|
||||
hideInstallPrompt(false);
|
||||
closeHelpModal();
|
||||
});
|
||||
|
||||
dismissButton.addEventListener("click", function () {
|
||||
hideInstallPrompt(true);
|
||||
});
|
||||
|
||||
helpButton.addEventListener("click", function () {
|
||||
updateManualInstallCopy();
|
||||
openHelpModal();
|
||||
});
|
||||
|
||||
helpClose.addEventListener("click", closeHelpModal);
|
||||
|
||||
helpModal.addEventListener("click", function (event) {
|
||||
if (event.target === helpModal) {
|
||||
closeHelpModal();
|
||||
}
|
||||
});
|
||||
|
||||
async function triggerInstall() {
|
||||
updateManualInstallCopy();
|
||||
|
||||
if (!deferredPrompt) {
|
||||
openHelpModal();
|
||||
return;
|
||||
}
|
||||
|
||||
deferredPrompt.prompt();
|
||||
const choice = await deferredPrompt.userChoice;
|
||||
if (choice.outcome !== "accepted") {
|
||||
showInstallPrompt();
|
||||
}
|
||||
deferredPrompt = null;
|
||||
updateManualInstallCopy();
|
||||
}
|
||||
|
||||
installButton.addEventListener("click", triggerInstall);
|
||||
helpPrimary.addEventListener("click", function () {
|
||||
if (deferredPrompt) {
|
||||
closeHelpModal();
|
||||
triggerInstall();
|
||||
return;
|
||||
}
|
||||
|
||||
closeHelpModal();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
@ -7,8 +7,8 @@
|
|||
<script type="importmap"></script>
|
||||
|
||||
<!-- Theme Colors -->
|
||||
<meta name="theme-color" content="#f97316">
|
||||
<meta name="msapplication-navbutton-color" content="#f97316">
|
||||
<meta name="theme-color" content="#fb923c">
|
||||
<meta name="msapplication-navbutton-color" content="#fb923c">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
|
||||
<!-- Mobile Optimization -->
|
||||
|
|
@ -18,12 +18,12 @@
|
|||
<meta name="apple-mobile-web-app-title" content="SPJ Angkut">
|
||||
|
||||
<!-- iOS Icons -->
|
||||
<link rel="apple-touch-icon" href="~/icons/icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="~/icons/icon-180x180.png">
|
||||
<link rel="apple-touch-icon" href="@Url.Content("~/driver/images/pwa_192.png")">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="@Url.Content("~/driver/images/pwa_192.png")">
|
||||
|
||||
<!-- Windows Tiles -->
|
||||
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png">
|
||||
<meta name="msapplication-TileColor" content="#f97316">
|
||||
<meta name="msapplication-TileImage" content="@Url.Content("~/driver/images/pwa_192.png")">
|
||||
<meta name="msapplication-TileColor" content="#fb923c">
|
||||
|
||||
<link rel="manifest" href="@Url.Content("~/driver/manifest.json")" />
|
||||
@* <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> *@
|
||||
|
|
@ -40,15 +40,9 @@
|
|||
|
||||
@RenderBody()
|
||||
|
||||
<script src="@Url.Content("~/driver/lib/jquery/dist/jquery.min.js")"></script>
|
||||
<script src="@Url.Content("~/driver/lib/bootstrap/dist/js/bootstrap.bundle.min.js")"></script>
|
||||
<script src="@Url.Content("~/driver/js/site.js")">" asp-append-version="true"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
lucide.createIcons();
|
||||
});
|
||||
</script>
|
||||
@await Html.PartialAsync("~/Views/Admin/Transport/SpjDriverUpst/Shared/Components/_PWAinstall.cshtml")
|
||||
@await Html.PartialAsync("~/Views/Admin/Transport/SpjDriverUpst/Shared/Partials/_Scripts.cshtml")
|
||||
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
<dynamic-section name="scripts" />
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -77,11 +77,14 @@
|
|||
--color-slate-50: oklch(98.4% 0.003 247.858);
|
||||
--color-slate-100: oklch(96.8% 0.007 247.896);
|
||||
--color-slate-200: oklch(92.9% 0.013 255.508);
|
||||
--color-slate-300: oklch(86.9% 0.022 252.894);
|
||||
--color-slate-400: oklch(70.4% 0.04 256.788);
|
||||
--color-slate-500: oklch(55.4% 0.046 257.417);
|
||||
--color-slate-600: oklch(44.6% 0.043 257.281);
|
||||
--color-slate-700: oklch(37.2% 0.044 257.287);
|
||||
--color-slate-800: oklch(27.9% 0.041 260.031);
|
||||
--color-slate-900: oklch(20.8% 0.042 265.755);
|
||||
--color-slate-950: oklch(12.9% 0.042 264.695);
|
||||
--color-gray-50: oklch(98.5% 0.002 247.839);
|
||||
--color-gray-100: oklch(96.7% 0.003 264.542);
|
||||
--color-gray-200: oklch(92.8% 0.006 264.531);
|
||||
|
|
@ -97,6 +100,7 @@
|
|||
--spacing: 0.25rem;
|
||||
--container-xs: 20rem;
|
||||
--container-sm: 24rem;
|
||||
--container-md: 28rem;
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
--text-sm: 0.875rem;
|
||||
|
|
@ -109,6 +113,8 @@
|
|||
--text-xl--line-height: calc(1.75 / 1.25);
|
||||
--text-2xl: 1.5rem;
|
||||
--text-2xl--line-height: calc(2 / 1.5);
|
||||
--text-3xl: 1.875rem;
|
||||
--text-3xl--line-height: calc(2.25 / 1.875);
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
|
@ -127,6 +133,7 @@
|
|||
--radius-xl: 0.75rem;
|
||||
--radius-2xl: 1rem;
|
||||
--radius-3xl: 1.5rem;
|
||||
--radius-4xl: 2rem;
|
||||
--drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15);
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
|
|
@ -324,6 +331,9 @@
|
|||
.inset-0 {
|
||||
inset: calc(var(--spacing) * 0);
|
||||
}
|
||||
.inset-x-4 {
|
||||
inset-inline: calc(var(--spacing) * 4);
|
||||
}
|
||||
.start-0 {
|
||||
inset-inline-start: calc(var(--spacing) * 0);
|
||||
}
|
||||
|
|
@ -369,9 +379,6 @@
|
|||
.top-6 {
|
||||
top: calc(var(--spacing) * 6);
|
||||
}
|
||||
.top-9 {
|
||||
top: calc(var(--spacing) * 9);
|
||||
}
|
||||
.top-12 {
|
||||
top: calc(var(--spacing) * 12);
|
||||
}
|
||||
|
|
@ -390,6 +397,9 @@
|
|||
.-right-1 {
|
||||
right: calc(var(--spacing) * -1);
|
||||
}
|
||||
.-right-6 {
|
||||
right: calc(var(--spacing) * -6);
|
||||
}
|
||||
.right-0 {
|
||||
right: calc(var(--spacing) * 0);
|
||||
}
|
||||
|
|
@ -402,29 +412,32 @@
|
|||
.right-4 {
|
||||
right: calc(var(--spacing) * 4);
|
||||
}
|
||||
.right-5 {
|
||||
right: calc(var(--spacing) * 5);
|
||||
}
|
||||
.right-6 {
|
||||
right: calc(var(--spacing) * 6);
|
||||
}
|
||||
.right-8 {
|
||||
right: calc(var(--spacing) * 8);
|
||||
}
|
||||
.right-9 {
|
||||
right: calc(var(--spacing) * 9);
|
||||
}
|
||||
.right-16 {
|
||||
right: calc(var(--spacing) * 16);
|
||||
}
|
||||
.right-full {
|
||||
right: 100%;
|
||||
}
|
||||
.-bottom-0 {
|
||||
bottom: calc(var(--spacing) * -0);
|
||||
}
|
||||
.-bottom-0\.5 {
|
||||
bottom: calc(var(--spacing) * -0.5);
|
||||
}
|
||||
.-bottom-1 {
|
||||
bottom: calc(var(--spacing) * -1);
|
||||
}
|
||||
.-bottom-4 {
|
||||
bottom: calc(var(--spacing) * -4);
|
||||
.-bottom-6 {
|
||||
bottom: calc(var(--spacing) * -6);
|
||||
}
|
||||
.bottom-0 {
|
||||
bottom: calc(var(--spacing) * 0);
|
||||
|
|
@ -435,24 +448,30 @@
|
|||
.bottom-2 {
|
||||
bottom: calc(var(--spacing) * 2);
|
||||
}
|
||||
.bottom-5 {
|
||||
bottom: calc(var(--spacing) * 5);
|
||||
}
|
||||
.bottom-8 {
|
||||
bottom: calc(var(--spacing) * 8);
|
||||
}
|
||||
.bottom-16 {
|
||||
bottom: calc(var(--spacing) * 16);
|
||||
}
|
||||
.bottom-28 {
|
||||
bottom: calc(var(--spacing) * 28);
|
||||
}
|
||||
.bottom-50 {
|
||||
bottom: calc(var(--spacing) * 50);
|
||||
}
|
||||
.bottom-100 {
|
||||
bottom: calc(var(--spacing) * 100);
|
||||
}
|
||||
.-left-4 {
|
||||
left: calc(var(--spacing) * -4);
|
||||
}
|
||||
.left-0 {
|
||||
left: calc(var(--spacing) * 0);
|
||||
}
|
||||
.left-1 {
|
||||
left: calc(var(--spacing) * 1);
|
||||
}
|
||||
.left-1\/2 {
|
||||
left: calc(1/2 * 100%);
|
||||
}
|
||||
|
|
@ -501,6 +520,12 @@
|
|||
.z-99 {
|
||||
z-index: 99;
|
||||
}
|
||||
.z-120 {
|
||||
z-index: 120;
|
||||
}
|
||||
.z-130 {
|
||||
z-index: 130;
|
||||
}
|
||||
.z-\[100\] {
|
||||
z-index: 100;
|
||||
}
|
||||
|
|
@ -762,6 +787,12 @@
|
|||
.-mr-16 {
|
||||
margin-right: calc(var(--spacing) * -16);
|
||||
}
|
||||
.mr-1 {
|
||||
margin-right: calc(var(--spacing) * 1);
|
||||
}
|
||||
.mr-1\.5 {
|
||||
margin-right: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.mr-2 {
|
||||
margin-right: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
|
@ -798,11 +829,11 @@
|
|||
.ml-auto {
|
||||
margin-left: auto;
|
||||
}
|
||||
.line-clamp-3 {
|
||||
.line-clamp-2 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
.\!hidden {
|
||||
display: none !important;
|
||||
|
|
@ -849,6 +880,9 @@
|
|||
.aspect-square {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
.h-0 {
|
||||
height: calc(var(--spacing) * 0);
|
||||
}
|
||||
.h-0\.5 {
|
||||
height: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
|
|
@ -876,6 +910,9 @@
|
|||
.h-8 {
|
||||
height: calc(var(--spacing) * 8);
|
||||
}
|
||||
.h-9 {
|
||||
height: calc(var(--spacing) * 9);
|
||||
}
|
||||
.h-10 {
|
||||
height: calc(var(--spacing) * 10);
|
||||
}
|
||||
|
|
@ -930,14 +967,11 @@
|
|||
.h-100 {
|
||||
height: calc(var(--spacing) * 100);
|
||||
}
|
||||
.h-\[236px\] {
|
||||
height: 236px;
|
||||
}
|
||||
.h-\[250px\] {
|
||||
height: 250px;
|
||||
}
|
||||
.h-\[300px\] {
|
||||
height: 300px;
|
||||
.h-\[340px\] {
|
||||
height: 340px;
|
||||
}
|
||||
.h-auto {
|
||||
height: auto;
|
||||
|
|
@ -1020,6 +1054,9 @@
|
|||
.w-36 {
|
||||
width: calc(var(--spacing) * 36);
|
||||
}
|
||||
.w-40 {
|
||||
width: calc(var(--spacing) * 40);
|
||||
}
|
||||
.w-48 {
|
||||
width: calc(var(--spacing) * 48);
|
||||
}
|
||||
|
|
@ -1050,6 +1087,12 @@
|
|||
.w-max {
|
||||
width: max-content;
|
||||
}
|
||||
.max-w-full {
|
||||
max-width: 100%;
|
||||
}
|
||||
.max-w-md {
|
||||
max-width: var(--container-md);
|
||||
}
|
||||
.max-w-sm {
|
||||
max-width: var(--container-sm);
|
||||
}
|
||||
|
|
@ -1059,6 +1102,12 @@
|
|||
.min-w-0 {
|
||||
min-width: calc(var(--spacing) * 0);
|
||||
}
|
||||
.min-w-\[150px\] {
|
||||
min-width: 150px;
|
||||
}
|
||||
.min-w-max {
|
||||
min-width: max-content;
|
||||
}
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
|
@ -1095,6 +1144,10 @@
|
|||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.-translate-x-1 {
|
||||
--tw-translate-x: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-x-1\/2 {
|
||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
|
|
@ -1107,6 +1160,10 @@
|
|||
--tw-translate-x: calc(var(--spacing) * 16);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1 {
|
||||
--tw-translate-y: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1\/2 {
|
||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
|
|
@ -1209,6 +1266,9 @@
|
|||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.gap-0 {
|
||||
gap: calc(var(--spacing) * 0);
|
||||
}
|
||||
|
|
@ -1227,6 +1287,16 @@
|
|||
.gap-5 {
|
||||
gap: calc(var(--spacing) * 5);
|
||||
}
|
||||
.gap-6 {
|
||||
gap: calc(var(--spacing) * 6);
|
||||
}
|
||||
.space-y-0 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));
|
||||
margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.space-y-0\.5 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
|
|
@ -1276,6 +1346,13 @@
|
|||
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.space-y-8 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-block-start: calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));
|
||||
margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.space-x-2 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-x-reverse: 0;
|
||||
|
|
@ -1353,8 +1430,11 @@
|
|||
.rounded-3xl {
|
||||
border-radius: var(--radius-3xl);
|
||||
}
|
||||
.rounded-\[24px\] {
|
||||
border-radius: 24px;
|
||||
.rounded-4xl {
|
||||
border-radius: var(--radius-4xl);
|
||||
}
|
||||
.rounded-\[28px\] {
|
||||
border-radius: 28px;
|
||||
}
|
||||
.rounded-\[32px\] {
|
||||
border-radius: 32px;
|
||||
|
|
@ -1503,6 +1583,9 @@
|
|||
.border-gray-400 {
|
||||
border-color: var(--color-gray-400);
|
||||
}
|
||||
.border-gray-500 {
|
||||
border-color: var(--color-gray-500);
|
||||
}
|
||||
.border-green-100 {
|
||||
border-color: var(--color-green-100);
|
||||
}
|
||||
|
|
@ -1515,15 +1598,12 @@
|
|||
.border-green-400 {
|
||||
border-color: var(--color-green-400);
|
||||
}
|
||||
.border-green-600\/20 {
|
||||
border-color: color-mix(in srgb, oklch(62.7% 0.194 149.214) 20%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
border-color: color-mix(in oklab, var(--color-green-600) 20%, transparent);
|
||||
}
|
||||
}
|
||||
.border-lime-400 {
|
||||
border-color: var(--color-lime-400);
|
||||
}
|
||||
.border-orange-100 {
|
||||
border-color: var(--color-orange-100);
|
||||
}
|
||||
.border-orange-200 {
|
||||
border-color: var(--color-orange-200);
|
||||
}
|
||||
|
|
@ -1560,6 +1640,12 @@
|
|||
.border-white {
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
.border-white\/5 {
|
||||
border-color: color-mix(in srgb, #fff 5%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
border-color: color-mix(in oklab, var(--color-white) 5%, transparent);
|
||||
}
|
||||
}
|
||||
.border-white\/10 {
|
||||
border-color: color-mix(in srgb, #fff 10%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -1578,12 +1664,6 @@
|
|||
border-color: color-mix(in oklab, var(--color-white) 30%, transparent);
|
||||
}
|
||||
}
|
||||
.border-white\/70 {
|
||||
border-color: color-mix(in srgb, #fff 70%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
border-color: color-mix(in oklab, var(--color-white) 70%, transparent);
|
||||
}
|
||||
}
|
||||
.border-yellow-100 {
|
||||
border-color: var(--color-yellow-100);
|
||||
}
|
||||
|
|
@ -1608,6 +1688,12 @@
|
|||
.bg-black {
|
||||
background-color: var(--color-black);
|
||||
}
|
||||
.bg-black\/10 {
|
||||
background-color: color-mix(in srgb, #000 10%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-black) 10%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-black\/20 {
|
||||
background-color: color-mix(in srgb, #000 20%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -1644,6 +1730,12 @@
|
|||
.bg-blue-500 {
|
||||
background-color: var(--color-blue-500);
|
||||
}
|
||||
.bg-blue-600 {
|
||||
background-color: var(--color-blue-600);
|
||||
}
|
||||
.bg-cyan-400 {
|
||||
background-color: var(--color-cyan-400);
|
||||
}
|
||||
.bg-cyan-400\/10 {
|
||||
background-color: color-mix(in srgb, oklch(78.9% 0.154 211.53) 10%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -1707,6 +1799,9 @@
|
|||
.bg-indigo-300 {
|
||||
background-color: var(--color-indigo-300);
|
||||
}
|
||||
.bg-lime-500 {
|
||||
background-color: var(--color-lime-500);
|
||||
}
|
||||
.bg-lime-500\/15 {
|
||||
background-color: color-mix(in srgb, oklch(76.8% 0.233 130.85) 15%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -1728,6 +1823,9 @@
|
|||
.bg-orange-500 {
|
||||
background-color: var(--color-orange-500);
|
||||
}
|
||||
.bg-orange-700 {
|
||||
background-color: var(--color-orange-700);
|
||||
}
|
||||
.bg-orange-700\/30 {
|
||||
background-color: color-mix(in srgb, oklch(55.3% 0.195 38.402) 30%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -1752,12 +1850,33 @@
|
|||
.bg-red-500 {
|
||||
background-color: var(--color-red-500);
|
||||
}
|
||||
.bg-slate-50 {
|
||||
background-color: var(--color-slate-50);
|
||||
}
|
||||
.bg-slate-100 {
|
||||
background-color: var(--color-slate-100);
|
||||
}
|
||||
.bg-slate-200 {
|
||||
background-color: var(--color-slate-200);
|
||||
}
|
||||
.bg-slate-300 {
|
||||
background-color: var(--color-slate-300);
|
||||
}
|
||||
.bg-slate-400 {
|
||||
background-color: var(--color-slate-400);
|
||||
}
|
||||
.bg-slate-900 {
|
||||
background-color: var(--color-slate-900);
|
||||
}
|
||||
.bg-slate-950 {
|
||||
background-color: var(--color-slate-950);
|
||||
}
|
||||
.bg-slate-950\/60 {
|
||||
background-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 60%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-slate-950) 60%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
|
@ -1776,6 +1895,12 @@
|
|||
background-color: color-mix(in oklab, var(--color-white) 10%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-white\/15 {
|
||||
background-color: color-mix(in srgb, #fff 15%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-white) 15%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-white\/20 {
|
||||
background-color: color-mix(in srgb, #fff 20%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -1788,12 +1913,6 @@
|
|||
background-color: color-mix(in oklab, var(--color-white) 70%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-white\/95 {
|
||||
background-color: color-mix(in srgb, #fff 95%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-white) 95%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-yellow-50 {
|
||||
background-color: var(--color-yellow-50);
|
||||
}
|
||||
|
|
@ -2067,6 +2186,9 @@
|
|||
.p-1 {
|
||||
padding: calc(var(--spacing) * 1);
|
||||
}
|
||||
.p-1\.5 {
|
||||
padding: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.p-2 {
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
|
@ -2118,6 +2240,9 @@
|
|||
.py-1 {
|
||||
padding-block: calc(var(--spacing) * 1);
|
||||
}
|
||||
.py-1\.5 {
|
||||
padding-block: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.py-2 {
|
||||
padding-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
|
@ -2205,6 +2330,12 @@
|
|||
.pt-10 {
|
||||
padding-top: calc(var(--spacing) * 10);
|
||||
}
|
||||
.pt-12 {
|
||||
padding-top: calc(var(--spacing) * 12);
|
||||
}
|
||||
.pr-4 {
|
||||
padding-right: calc(var(--spacing) * 4);
|
||||
}
|
||||
.pb-0 {
|
||||
padding-bottom: calc(var(--spacing) * 0);
|
||||
}
|
||||
|
|
@ -2290,6 +2421,10 @@
|
|||
font-size: var(--text-2xl);
|
||||
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||
}
|
||||
.text-3xl {
|
||||
font-size: var(--text-3xl);
|
||||
line-height: var(--tw-leading, var(--text-3xl--line-height));
|
||||
}
|
||||
.text-base {
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--tw-leading, var(--text-base--line-height));
|
||||
|
|
@ -2363,6 +2498,14 @@
|
|||
--tw-tracking: 0.3em;
|
||||
letter-spacing: 0.3em;
|
||||
}
|
||||
.tracking-\[0\.22em\] {
|
||||
--tw-tracking: 0.22em;
|
||||
letter-spacing: 0.22em;
|
||||
}
|
||||
.tracking-\[0\.24em\] {
|
||||
--tw-tracking: 0.24em;
|
||||
letter-spacing: 0.24em;
|
||||
}
|
||||
.tracking-tight {
|
||||
--tw-tracking: var(--tracking-tight);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
|
|
@ -2395,6 +2538,15 @@
|
|||
.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
.whitespace-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.\!text-white {
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
.text-amber-500 {
|
||||
color: var(--color-amber-500);
|
||||
}
|
||||
.text-amber-600 {
|
||||
color: var(--color-amber-600);
|
||||
}
|
||||
|
|
@ -2425,6 +2577,12 @@
|
|||
.text-gray-500 {
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
.text-gray-500\/80 {
|
||||
color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 80%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-gray-500) 80%, transparent);
|
||||
}
|
||||
}
|
||||
.text-gray-600 {
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
|
|
@ -2437,15 +2595,12 @@
|
|||
.text-gray-900 {
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
.text-green-50 {
|
||||
color: var(--color-green-50);
|
||||
}
|
||||
.text-green-100 {
|
||||
color: var(--color-green-100);
|
||||
}
|
||||
.text-green-100\/70 {
|
||||
color: color-mix(in srgb, oklch(96.2% 0.044 156.743) 70%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-green-100) 70%, transparent);
|
||||
}
|
||||
}
|
||||
.text-green-400 {
|
||||
color: var(--color-green-400);
|
||||
}
|
||||
|
|
@ -2488,6 +2643,9 @@
|
|||
.text-orange-700 {
|
||||
color: var(--color-orange-700);
|
||||
}
|
||||
.text-red-400 {
|
||||
color: var(--color-red-400);
|
||||
}
|
||||
.text-red-500 {
|
||||
color: var(--color-red-500);
|
||||
}
|
||||
|
|
@ -2506,9 +2664,15 @@
|
|||
.text-red-800 {
|
||||
color: var(--color-red-800);
|
||||
}
|
||||
.text-slate-50 {
|
||||
color: var(--color-slate-50);
|
||||
}
|
||||
.text-slate-400 {
|
||||
color: var(--color-slate-400);
|
||||
}
|
||||
.text-slate-500 {
|
||||
color: var(--color-slate-500);
|
||||
}
|
||||
.text-slate-600 {
|
||||
color: var(--color-slate-600);
|
||||
}
|
||||
|
|
@ -2518,6 +2682,9 @@
|
|||
.text-slate-800 {
|
||||
color: var(--color-slate-800);
|
||||
}
|
||||
.text-slate-900 {
|
||||
color: var(--color-slate-900);
|
||||
}
|
||||
.text-transparent {
|
||||
color: transparent;
|
||||
}
|
||||
|
|
@ -2530,6 +2697,12 @@
|
|||
color: color-mix(in oklab, var(--color-white) 70%, transparent);
|
||||
}
|
||||
}
|
||||
.text-white\/80 {
|
||||
color: color-mix(in srgb, #fff 80%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-white) 80%, transparent);
|
||||
}
|
||||
}
|
||||
.text-white\/90 {
|
||||
color: color-mix(in srgb, #fff 90%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
|
|
@ -2638,6 +2811,10 @@
|
|||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring-1 {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring-2 {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
|
|
@ -2652,6 +2829,15 @@
|
|||
--tw-shadow-color: color-mix(in oklab, var(--color-gray-200) var(--tw-shadow-alpha), transparent);
|
||||
}
|
||||
}
|
||||
.ring-black {
|
||||
--tw-ring-color: var(--color-black);
|
||||
}
|
||||
.ring-black\/5 {
|
||||
--tw-ring-color: color-mix(in srgb, #000 5%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
--tw-ring-color: color-mix(in oklab, var(--color-black) 5%, transparent);
|
||||
}
|
||||
}
|
||||
.ring-gray-200 {
|
||||
--tw-ring-color: var(--color-gray-200);
|
||||
}
|
||||
|
|
@ -2704,11 +2890,6 @@
|
|||
.filter {
|
||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||
}
|
||||
.backdrop-blur {
|
||||
--tw-backdrop-blur: blur(8px);
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.backdrop-blur-lg {
|
||||
--tw-backdrop-blur: blur(var(--blur-lg));
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
|
|
@ -2775,6 +2956,10 @@
|
|||
--tw-duration: 300ms;
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
.duration-500 {
|
||||
--tw-duration: 500ms;
|
||||
transition-duration: 500ms;
|
||||
}
|
||||
.ease-in {
|
||||
--tw-ease: var(--ease-in);
|
||||
transition-timing-function: var(--ease-in);
|
||||
|
|
@ -2864,6 +3049,11 @@
|
|||
scale: var(--tw-scale-x) var(--tw-scale-y);
|
||||
}
|
||||
}
|
||||
.group-active\:rotate-180 {
|
||||
&:is(:where(.group):active *) {
|
||||
rotate: 180deg;
|
||||
}
|
||||
}
|
||||
.group-active\:bg-gray-50 {
|
||||
&:is(:where(.group):active *) {
|
||||
background-color: var(--color-gray-50);
|
||||
|
|
@ -2974,6 +3164,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:border-green-400 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
border-color: var(--color-green-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:border-orange-200 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
|
@ -2995,6 +3192,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-blue-700 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-blue-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-gray-50 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
|
@ -3065,6 +3269,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-slate-800 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-slate-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-white\/10 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
|
@ -3276,6 +3487,23 @@
|
|||
outline-style: none;
|
||||
}
|
||||
}
|
||||
.focus-visible\:ring-2 {
|
||||
&:focus-visible {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
.focus-visible\:ring-slate-400 {
|
||||
&:focus-visible {
|
||||
--tw-ring-color: var(--color-slate-400);
|
||||
}
|
||||
}
|
||||
.focus-visible\:outline-none {
|
||||
&:focus-visible {
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
}
|
||||
}
|
||||
.active\:scale-90 {
|
||||
&:active {
|
||||
--tw-scale-x: 90%;
|
||||
|
|
@ -3292,6 +3520,36 @@
|
|||
scale: var(--tw-scale-x) var(--tw-scale-y);
|
||||
}
|
||||
}
|
||||
.disabled\:cursor-not-allowed {
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.disabled\:opacity-50 {
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
}
|
||||
}
|
||||
.sm\:flex-1 {
|
||||
@media (width >= 40rem) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.sm\:grid-cols-2 {
|
||||
@media (width >= 40rem) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.sm\:flex-row {
|
||||
@media (width >= 40rem) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
.sm\:items-center {
|
||||
@media (width >= 40rem) {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.lg\:max-w-sm {
|
||||
@media (width >= 64rem) {
|
||||
max-width: var(--container-sm);
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,291 @@
|
|||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const pageEl = document.getElementById('history-page');
|
||||
if (!pageEl) return;
|
||||
|
||||
const apiUrl = pageEl.dataset.historyApi;
|
||||
const detailBase = pageEl.dataset.historyDetailBase || '/upst/history/details/__id__';
|
||||
|
||||
const loadingEl = document.getElementById('history-loading');
|
||||
const emptyEl = document.getElementById('history-empty');
|
||||
const listEl = document.getElementById('history-list');
|
||||
const paginationEl = document.getElementById('history-pagination');
|
||||
const pageInfoEl = document.getElementById('history-page-info');
|
||||
const totalInfoEl = document.getElementById('history-total-info');
|
||||
const pageButtonsEl = document.getElementById('history-page-buttons');
|
||||
const prevBtn = document.getElementById('history-prev-page');
|
||||
const nextBtn = document.getElementById('history-next-page');
|
||||
const fromDateInput = document.getElementById('history-from-date-filter');
|
||||
const toDateInput = document.getElementById('history-to-date-filter');
|
||||
const applyFilterBtn = document.getElementById('history-apply-filter');
|
||||
const resetFilterBtn = document.getElementById('history-reset-filter');
|
||||
|
||||
const state = {
|
||||
page: 1,
|
||||
pageSize: 5,
|
||||
totalPages: 1,
|
||||
totalItems: 0,
|
||||
fromDate: '',
|
||||
toDate: ''
|
||||
};
|
||||
|
||||
function formatTanggal(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatWaktu(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat('id-ID', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function getStatusMarkup(status) {
|
||||
if ((status || '').toLowerCase() === 'completed') {
|
||||
return '<span class="bg-green-50 text-green-600 px-3 py-1 rounded-lg text-[10px] font-black uppercase border border-green-100">Selesai</span>';
|
||||
}
|
||||
|
||||
return '<span class="bg-blue-50 text-blue-600 px-3 py-1 rounded-lg text-[10px] font-black uppercase border border-blue-100 animate-pulse">Proses</span>';
|
||||
}
|
||||
|
||||
function buildDetailUrl(id) {
|
||||
return detailBase.replace('__id__', String(id));
|
||||
}
|
||||
|
||||
function renderItems(items) {
|
||||
listEl.innerHTML = '';
|
||||
|
||||
items.forEach(function (item) {
|
||||
const card = document.createElement('a');
|
||||
card.href = buildDetailUrl(item.id);
|
||||
card.className = 'block group';
|
||||
card.innerHTML = `
|
||||
<div class="bg-white rounded-3xl p-5 shadow-sm border border-gray-100 group-hover:shadow-md group-hover:-translate-y-1 transition-all duration-300">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="space-y-0.5">
|
||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Nomor Dokumen</p>
|
||||
<p class="text-sm font-black text-gray-800">${item.noSpj}</p>
|
||||
</div>
|
||||
${getStatusMarkup(item.status)}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 bg-gray-50/80 p-3 rounded-2xl border border-dashed border-gray-200">
|
||||
<div class="w-12 h-12 bg-upst rounded-xl flex items-center justify-center shadow-inner">
|
||||
<i class="w-6 h-6 text-white" data-lucide="truck"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-baseline gap-2">
|
||||
<h2 class="font-black text-gray-900 leading-tight">${item.plat}</h2>
|
||||
<span class="text-[11px] font-bold text-upst whitespace-nowrap">${formatWaktu(item.tanggalWaktu)}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<span class="text-xs text-gray-500 font-medium">${item.kode}</span>
|
||||
<span class="text-[10px] text-gray-400 font-semibold whitespace-nowrap">${formatTanggal(item.tanggalWaktu)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div class="w-2 h-2 rounded-full bg-upst/40"></div>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-[10px] text-gray-400 font-bold uppercase">Tujuan Akhir</span>
|
||||
<span class="text-sm font-bold text-gray-700 truncate">${item.tujuan}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center group-hover:bg-upst transition-colors shrink-0">
|
||||
<i class="w-4 h-4" data-lucide="chevron-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
listEl.appendChild(card);
|
||||
});
|
||||
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination() {
|
||||
const hasItems = state.totalItems > 0;
|
||||
paginationEl.classList.toggle('hidden', !hasItems);
|
||||
if (!hasItems) return;
|
||||
|
||||
pageInfoEl.textContent = `Halaman ${state.page} dari ${state.totalPages}`;
|
||||
totalInfoEl.textContent = `${state.totalItems} data`;
|
||||
prevBtn.disabled = state.page <= 1;
|
||||
nextBtn.disabled = state.page >= state.totalPages;
|
||||
|
||||
pageButtonsEl.innerHTML = '';
|
||||
const pages = [];
|
||||
const startPage = Math.max(1, state.page - 2);
|
||||
const endPage = Math.min(state.totalPages, state.page + 2);
|
||||
|
||||
if (startPage > 1) {
|
||||
pages.push(1);
|
||||
if (startPage > 2) pages.push('ellipsis-left');
|
||||
}
|
||||
|
||||
for (let page = startPage; page <= endPage; page++) {
|
||||
pages.push(page);
|
||||
}
|
||||
|
||||
if (endPage < state.totalPages) {
|
||||
if (endPage < state.totalPages - 1) pages.push('ellipsis-right');
|
||||
pages.push(state.totalPages);
|
||||
}
|
||||
|
||||
pages.forEach(function (page) {
|
||||
if (typeof page !== 'number') {
|
||||
const ellipsis = document.createElement('span');
|
||||
ellipsis.className = 'w-10 h-10 flex items-center justify-center text-xs font-black text-gray-400';
|
||||
ellipsis.textContent = '...';
|
||||
pageButtonsEl.appendChild(ellipsis);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = String(page);
|
||||
btn.className = page === state.page
|
||||
? 'w-10 h-10 rounded-2xl bg-upst text-white text-xs font-black'
|
||||
: 'w-10 h-10 rounded-2xl border border-gray-200 text-gray-700 text-xs font-black bg-white';
|
||||
btn.addEventListener('click', function () {
|
||||
if (page === state.page) return;
|
||||
loadHistory(page);
|
||||
});
|
||||
pageButtonsEl.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
loadingEl.classList.toggle('hidden', !isLoading);
|
||||
if (isLoading) {
|
||||
emptyEl.classList.add('hidden');
|
||||
listEl.innerHTML = '';
|
||||
paginationEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function showEmpty() {
|
||||
listEl.innerHTML = '';
|
||||
emptyEl.classList.remove('hidden');
|
||||
paginationEl.classList.add('hidden');
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory(page) {
|
||||
state.page = page || 1;
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: String(state.page),
|
||||
pageSize: String(state.pageSize)
|
||||
});
|
||||
|
||||
if (state.fromDate) {
|
||||
params.set('fromDate', state.fromDate);
|
||||
}
|
||||
|
||||
if (state.toDate) {
|
||||
params.set('toDate', state.toDate);
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiUrl}?${params.toString()}`, { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Gagal memuat history');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
state.page = data.page || 1;
|
||||
state.totalPages = data.totalPages || 1;
|
||||
state.totalItems = data.totalItems || 0;
|
||||
|
||||
loadingEl.classList.add('hidden');
|
||||
if (!data.items || data.items.length === 0) {
|
||||
showEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
emptyEl.classList.add('hidden');
|
||||
renderItems(data.items);
|
||||
renderPagination();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
loadingEl.classList.add('hidden');
|
||||
emptyEl.classList.remove('hidden');
|
||||
emptyEl.innerHTML = `
|
||||
<div class="w-16 h-16 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i class="w-8 h-8 text-red-400" data-lucide="triangle-alert"></i>
|
||||
</div>
|
||||
<h3 class="text-base font-black text-gray-900 mb-1">Gagal Memuat Riwayat</h3>
|
||||
<p class="text-sm text-gray-500">Silakan coba lagi beberapa saat.</p>
|
||||
`;
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyDateRangeFilter() {
|
||||
const nextFromDate = fromDateInput?.value || '';
|
||||
const nextToDate = toDateInput?.value || '';
|
||||
|
||||
if (nextFromDate && nextToDate && nextFromDate > nextToDate) {
|
||||
const temp = nextFromDate;
|
||||
state.fromDate = nextToDate;
|
||||
state.toDate = temp;
|
||||
if (fromDateInput) fromDateInput.value = state.fromDate;
|
||||
if (toDateInput) toDateInput.value = state.toDate;
|
||||
} else {
|
||||
state.fromDate = nextFromDate;
|
||||
state.toDate = nextToDate;
|
||||
}
|
||||
|
||||
loadHistory(1);
|
||||
}
|
||||
|
||||
applyFilterBtn?.addEventListener('click', function () {
|
||||
applyDateRangeFilter();
|
||||
});
|
||||
|
||||
resetFilterBtn?.addEventListener('click', function () {
|
||||
state.fromDate = '';
|
||||
state.toDate = '';
|
||||
if (fromDateInput) fromDateInput.value = '';
|
||||
if (toDateInput) toDateInput.value = '';
|
||||
loadHistory(1);
|
||||
});
|
||||
|
||||
[fromDateInput, toDateInput].forEach(function (input) {
|
||||
input?.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
applyDateRangeFilter();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
prevBtn?.addEventListener('click', function () {
|
||||
if (state.page > 1) {
|
||||
loadHistory(state.page - 1);
|
||||
}
|
||||
});
|
||||
|
||||
nextBtn?.addEventListener('click', function () {
|
||||
if (state.page < state.totalPages) {
|
||||
loadHistory(state.page + 1);
|
||||
}
|
||||
});
|
||||
|
||||
loadHistory(1);
|
||||
});
|
||||
|
|
@ -3,20 +3,20 @@
|
|||
{
|
||||
"name": "CV Tri Mitra Utama - Shell Radio Dalam",
|
||||
"alamat": "Kp. Pertanian II Rt.004 Rw.001 Kel. Klender Kec, Duren Sawit, Kota Adm. Jakarta Timur 13470",
|
||||
"longitude": -6.260066361357777,
|
||||
"latitude": 106.78918653869111
|
||||
"longitude": -6.26012668512782,
|
||||
"latitude": 106.8712511969409
|
||||
},
|
||||
{
|
||||
"name": "Jakarta Islamic Hospital",
|
||||
"alamat": "Gang Masjid Baitusalam, Jl. E No.19, RT.10/RW.11, Cipinang, Kec. Pulo Gadung, Kota Jakarta Timur, Daerah Khusus Ibukota Jakarta 13240",
|
||||
"longitude": -6.168850536704003,
|
||||
"latitude": 106.87091508600079
|
||||
"longitude": -6.2550852145095694,
|
||||
"latitude": 106.86310593093889
|
||||
},
|
||||
{
|
||||
"name": "Puskesmas Kelurahan Klender 1",
|
||||
"alamat": "Jl. Pertanian Tengah No.7, RT.4/RW.2, Klender, Kec. Duren Sawit, Kota Jakarta Timur, Daerah Khusus Ibukota Jakarta 13430",
|
||||
"longitude": -6.2146980384303845,
|
||||
"latitude": 106.89777460376108
|
||||
"longitude": -6.268768227222534,
|
||||
"latitude": 106.87026643078956
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,93 +1,44 @@
|
|||
{
|
||||
"id": "/upst",
|
||||
"name": "eSPJ - Surat Perjalanan Dinas",
|
||||
"short_name": "eSPJ",
|
||||
"description": "Aplikasi pengelolaan Surat Perjalanan Dinas yang modern dan efisien",
|
||||
"start_url": "/",
|
||||
"start_url": "/upst",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#fb923c",
|
||||
"orientation": "portrait-primary",
|
||||
"scope": "/",
|
||||
"scope": "/upst",
|
||||
"lang": "id",
|
||||
"dir": "ltr",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon-72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "icon-96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "icon-128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "icon-144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "icon-152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "icon-192.png",
|
||||
"src": "/driver/images/pwa_192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icon-384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "icon-512.png",
|
||||
"src": "/driver/images/pwa_512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "screenshot-wide.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide"
|
||||
},
|
||||
{
|
||||
"src": "screenshot-narrow.png",
|
||||
"sizes": "360x640",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Login",
|
||||
"short_name": "Login",
|
||||
"description": "Masuk ke aplikasi eSPJ",
|
||||
"url": "/login",
|
||||
"icons": [{ "src": "icon-96.png", "sizes": "96x96" }]
|
||||
"name": "Home UPST",
|
||||
"short_name": "Home",
|
||||
"description": "Buka dashboard utama UPST",
|
||||
"url": "/upst",
|
||||
"icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }]
|
||||
},
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"short_name": "Dashboard",
|
||||
"description": "Buka dashboard utama",
|
||||
"url": "/dashboard",
|
||||
"icons": [{ "src": "icon-96.png", "sizes": "96x96" }]
|
||||
"name": "Halaman Kosong",
|
||||
"short_name": "Kosong",
|
||||
"description": "Buka halaman alternatif UPST",
|
||||
"url": "/upst/kosong",
|
||||
"icons": [{ "src": "/driver/images/pwa_192.png", "sizes": "192x192", "type": "image/png" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Offline - DLH DKI Jakarta</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: fadeIn 0.5s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-background text-foreground flex min-h-screen flex-col items-center justify-center p-6 bg-slate-50">
|
||||
|
||||
<div class="max-w-md w-full space-y-8 text-center animate-in">
|
||||
<div class="relative flex justify-center">
|
||||
<div class="absolute inset-0 m-auto h-24 w-24 animate-ping rounded-full bg-slate-200 opacity-20"></div>
|
||||
|
||||
<div class="relative rounded-full bg-white p-6 shadow-sm border border-slate-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="text-slate-400">
|
||||
<path
|
||||
d="M1 1l22 22M16.72 11.06A10.94 10.94 0 0 1 19 12.55M5 12.55a10.94 10.94 0 0 1 5.17-2.39M10.71 5.05A16 16 0 0 1 22.58 9M1.42 9a15.91 15.91 0 0 1 4.7-2.88M8.53 16.11a6 6 0 0 1 6.95 0M12 20h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-3xl font-bold tracking-tight text-slate-900">Koneksi Terputus</h1>
|
||||
<p class="text-muted-foreground text-slate-500">
|
||||
Sepertinya perangkatmu sedang tidak terhubung ke internet. Silakan cek koneksi Wi-Fi atau data seluler
|
||||
kamu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<button onclick="window.location.reload()"
|
||||
class="inline-flex h-10 items-center justify-center rounded-md bg-slate-900 px-8 text-sm font-medium text-slate-50 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 transition-colors shadow-lg">
|
||||
Coba Lagi
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pt-12 flex flex-col items-center gap-2 opacity-60">
|
||||
<div class="h-1 w-12 bg-slate-300 rounded-full"></div>
|
||||
<p class="text-xs font-semibold tracking-widest uppercase text-slate-400">DLH DKI Jakarta</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
const CACHE_NAME = "espj-upst-pwa-v1";
|
||||
const OFFLINE_URL = "/driver/offline.html";
|
||||
const PRECACHE_URLS = [
|
||||
"/upst",
|
||||
"/upst/kosong",
|
||||
"/driver/manifest.json",
|
||||
"/driver/favicon.ico",
|
||||
"/driver/images/pwa_192.png",
|
||||
"/driver/images/pwa_512.png",
|
||||
OFFLINE_URL
|
||||
];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keys) =>
|
||||
Promise.all(
|
||||
keys
|
||||
.filter((key) => key !== CACHE_NAME)
|
||||
.map((key) => caches.delete(key))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method !== "GET") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.mode === "navigate") {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
const responseClone = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(request, responseClone));
|
||||
return response;
|
||||
})
|
||||
.catch(async () => {
|
||||
const cachedPage = await caches.match(request);
|
||||
return cachedPage || caches.match(OFFLINE_URL);
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.origin !== self.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(request).then((cachedResponse) => {
|
||||
const networkFetch = fetch(request)
|
||||
.then((response) => {
|
||||
if (response && response.ok) {
|
||||
const responseClone = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(request, responseClone));
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => cachedResponse);
|
||||
|
||||
return cachedResponse || networkFetch;
|
||||
})
|
||||
);
|
||||
});
|
||||
Loading…
Reference in New Issue