eSPJ/Controllers/SpjDriverUpstController/DetailController.cs

856 lines
35 KiB
C#

using Microsoft.AspNetCore.Mvc;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using eSPJ.Models;
using eSPJ.Services;
namespace eSPJ.Controllers.SpjDriverUpstController
{
[Route("upst/detail-penjemputan")]
public class DetailPenjemputanController : Controller
{
private readonly IHttpClientFactory _httpClientFactory;
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,
IWebHostEnvironment env)
{
_httpClientFactory = httpClientFactory;
_configuration = configuration;
_detailService = detailService;
_logger = logger;
_env = env;
}
private static string SanitizePathSegment(string? value, string fallback = "umum")
{
var safe = string.Concat((value ?? string.Empty).Trim().Select(c =>
char.IsLetterOrDigit(c) || c == '-' || c == '_'
? c
: '-'));
safe = Regex.Replace(safe, "-+", "-").Trim('-');
return string.IsNullOrWhiteSpace(safe) ? fallback : safe;
}
private string GetUploadDirectory(string dateFolder, string? nomorSpj = null, string? namaTps = null)
{
var uploadDir = Path.Combine(
_env.ContentRootPath,
"uploads",
"penjemputan",
dateFolder,
SanitizePathSegment(nomorSpj, "spj-umum"),
SanitizePathSegment(namaTps, "tps-1"));
Directory.CreateDirectory(uploadDir);
return uploadDir;
}
private static string BuildUploadUrl(string dateFolder, string fileName, string? nomorSpj = null, string? namaTps = null)
{
var spjFolder = SanitizePathSegment(nomorSpj, "spj-umum");
var tpsFolder = SanitizePathSegment(namaTps, "tps-1");
return $"/uploads/penjemputan/{dateFolder}/{spjFolder}/{tpsFolder}/{fileName}";
}
private async Task<TpsData?> FindExistingRecordAsync(string? nomorSpj, string? spjDetailId, string? lokasiAngkutId, string? namaTps)
{
if (string.IsNullOrWhiteSpace(nomorSpj))
{
return null;
}
return await _detailService.GetRecordDetailAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps);
}
private static List<RecordTimbanganItem> MapRecordTimbangan(List<TimbanganItem>? items)
{
return (items ?? new List<TimbanganItem>())
.Select(item => new RecordTimbanganItem
{
Berat = item.Berat?.FirstOrDefault() ?? 0,
JenisSampah = item.JenisSampah != null && item.JenisSampah.Count > 0
? item.JenisSampah[0].ToString()
: "Residu",
FotoFileName = item.FotoFileName ?? string.Empty,
Uploaded = item.IsUploaded,
OcrInfo = item.IsUploaded ? "Foto dari server." : "OCR: belum diproses."
})
.ToList();
}
private static RecordSaveRequest BuildRecordSaveRequest(TpsData? existingRecord, string? nomorSpj, string? namaTps, string? spjDetailId, string? lokasiAngkutId)
{
return new RecordSaveRequest
{
NomorSpj = nomorSpj ?? existingRecord?.NomorSpj ?? string.Empty,
NamaTps = namaTps ?? existingRecord?.Name ?? string.Empty,
SpjDetailId = spjDetailId ?? existingRecord?.SpjDetailId ?? string.Empty,
LokasiAngkutId = lokasiAngkutId ?? existingRecord?.LokasiAngkutId ?? string.Empty,
Latitude = existingRecord?.Latitude ?? string.Empty,
Longitude = existingRecord?.Longitude ?? string.Empty,
AlamatJalan = existingRecord?.AlamatJalan ?? string.Empty,
WaktuKedatangan = existingRecord?.WaktuKedatangan ?? string.Empty,
FotoKedatanganFileNames = existingRecord?.FotoKedatangan != null ? new List<string>(existingRecord.FotoKedatangan) : new List<string>(),
FotoKedatanganUploaded = existingRecord?.FotoKedatanganUploaded ?? false,
Timbangan = MapRecordTimbangan(existingRecord?.Timbangan),
TotalOrganik = existingRecord?.TotalOrganik ?? 0,
TotalAnorganik = existingRecord?.TotalAnorganik ?? 0,
TotalResidu = existingRecord?.TotalResidu ?? 0,
TotalTimbangan = existingRecord?.TotalTimbangan ?? 0,
FotoPetugasFileNames = existingRecord?.FotoPetugas != null ? new List<string>(existingRecord.FotoPetugas) : new List<string>(),
FotoPetugasUploaded = existingRecord?.FotoPetugasUploaded ?? false,
NamaPetugas = existingRecord?.NamaPetugas ?? string.Empty,
IsSubmit = existingRecord?.IsSubmit ?? false,
};
}
private static void RecalculateTotals(RecordSaveRequest request)
{
var timbangan = request.Timbangan ?? new List<RecordTimbanganItem>();
request.TotalOrganik = timbangan
.Where(item => string.Equals(item.JenisSampah, "Organik", StringComparison.OrdinalIgnoreCase))
.Sum(item => item.Berat);
request.TotalAnorganik = timbangan
.Where(item => string.Equals(item.JenisSampah, "Anorganik", StringComparison.OrdinalIgnoreCase))
.Sum(item => item.Berat);
request.TotalResidu = timbangan
.Where(item => string.Equals(item.JenisSampah, "Residu", StringComparison.OrdinalIgnoreCase))
.Sum(item => item.Berat);
request.TotalTimbangan = timbangan.Sum(item => item.Berat);
}
private async Task<RecordSaveResponse> SaveUploadedRecordAsync(
bool isTps,
string? nomorSpj,
string? namaTps,
string? spjDetailId,
string? lokasiAngkutId,
Action<RecordSaveRequest> applyChanges)
{
var existingRecord = await FindExistingRecordAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps);
var request = BuildRecordSaveRequest(existingRecord, nomorSpj, namaTps, spjDetailId, lokasiAngkutId);
applyChanges(request);
RecalculateTotals(request);
return isTps
? await _detailService.SaveRecordTpsAsync(request)
: await _detailService.SaveRecordNonTpsAsync(request);
}
private void ApplyNoCacheHeaders()
{
Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0";
Response.Headers["Pragma"] = "no-cache";
Response.Headers["Expires"] = "0";
}
private async Task<RecordSaveRequest?> ResolveRecordSaveRequestAsync()
{
Request.EnableBuffering();
if (Request.Body.CanSeek)
{
Request.Body.Position = 0;
}
using var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
var rawBody = await reader.ReadToEndAsync();
if (Request.Body.CanSeek)
{
Request.Body.Position = 0;
}
if (string.IsNullOrWhiteSpace(rawBody))
{
return null;
}
try
{
return JsonSerializer.Deserialize<RecordSaveRequest>(rawBody, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch (JsonException ex)
{
_logger.LogWarning(ex, "Gagal parse body save-record. Body: {RawBody}", rawBody);
return null;
}
}
[HttpGet("")]
public IActionResult Index()
{
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Index.cshtml");
}
[HttpGet("tanpa-tps")]
public IActionResult TanpaTps()
{
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/TanpaTps.cshtml");
}
[HttpGet("batal")]
public IActionResult Batal()
{
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Batal.cshtml");
}
[HttpGet("detail-batal")]
public IActionResult DetailBatal()
{
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailBatal.cshtml");
}
[HttpGet("detail-selesai")]
public IActionResult DetailSelesai()
{
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesai.cshtml");
}
[HttpGet("detail-selesai-tanpa-tps")]
public IActionResult DetailSelesaiTanpaTps()
{
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/DetailSelesaiTanpaTps.cshtml");
}
[HttpGet("api/submitted")]
[IgnoreAntiforgeryToken]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> GetSubmittedBySpj([FromQuery] string nomorSpj)
{
ApplyNoCacheHeaders();
if (string.IsNullOrWhiteSpace(nomorSpj))
{
return BadRequest(new { success = false, message = "nomorSpj wajib diisi." });
}
var items = await _detailService.GetSubmittedByNomorSpjAsync(nomorSpj);
return Ok(new { success = true, items });
}
[HttpGet("api/records")]
[IgnoreAntiforgeryToken]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> GetRecordsBySpj([FromQuery] string nomorSpj)
{
ApplyNoCacheHeaders();
if (string.IsNullOrWhiteSpace(nomorSpj))
{
return BadRequest(new { success = false, message = "nomorSpj wajib diisi." });
}
var items = await _detailService.GetRecordsByNomorSpjAsync(nomorSpj);
return Ok(new { success = true, items });
}
[HttpGet("api/submitted/detail")]
[IgnoreAntiforgeryToken]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> GetSubmittedDetail(
[FromQuery] string nomorSpj,
[FromQuery] string? spjDetailId = null,
[FromQuery] string? lokasiAngkutId = null,
[FromQuery] string? namaTps = null)
{
ApplyNoCacheHeaders();
if (string.IsNullOrWhiteSpace(nomorSpj))
{
return BadRequest(new { success = false, message = "nomorSpj wajib diisi." });
}
var item = await _detailService.GetSubmittedDetailAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps);
return Ok(new { success = true, hasData = item != null, item });
}
[HttpGet("api/records/detail")]
[IgnoreAntiforgeryToken]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> GetRecordDetail(
[FromQuery] string nomorSpj,
[FromQuery] string? spjDetailId = null,
[FromQuery] string? lokasiAngkutId = null,
[FromQuery] string? namaTps = null)
{
ApplyNoCacheHeaders();
if (string.IsNullOrWhiteSpace(nomorSpj))
{
return BadRequest(new { success = false, message = "nomorSpj wajib diisi." });
}
var item = await _detailService.GetRecordDetailAsync(nomorSpj, spjDetailId, lokasiAngkutId, namaTps);
return Ok(new { success = true, hasData = item != null, item });
}
[HttpPost("")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Submit([FromForm] DetailPenjemputanRequest request)
{
var isAjaxRequest = string.Equals(Request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.OrdinalIgnoreCase)
|| Request.Headers.Accept.Any(value => value?.Contains("application/json", StringComparison.OrdinalIgnoreCase) == true);
try
{
var result = await _detailService.SubmitPenjemputanAsync(request);
if (isAjaxRequest)
{
return result.Success
? Ok(result)
: BadRequest(result);
}
if (result.Success)
{
TempData["Success"] = result.Message;
}
else
{
TempData["Error"] = result.Message;
}
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error submitting penjemputan data");
if (isAjaxRequest)
{
return StatusCode(500, new DetailPenjemputanResponse
{
Success = false,
Message = "Terjadi kesalahan saat menyimpan data."
});
}
TempData["Error"] = "Terjadi kesalahan saat menyimpan data.";
return RedirectToAction(nameof(Index));
}
}
[HttpPost("save-record-non-tps")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> SaveRecordNonTps()
{
var request = await ResolveRecordSaveRequestAsync();
if (request == null)
return BadRequest(new RecordSaveResponse { Success = false, Message = "Request tidak valid." });
var result = await _detailService.SaveRecordNonTpsAsync(request);
return result.Success ? Ok(result) : StatusCode(500, result);
}
[HttpPost("save-record")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> SaveRecord()
{
var request = await ResolveRecordSaveRequestAsync();
if (request == null)
return BadRequest(new RecordSaveResponse { Success = false, Message = "Request tidak valid." });
var result = await _detailService.SaveRecordTpsAsync(request);
return result.Success ? Ok(result) : StatusCode(500, result);
}
[HttpPost("upload-foto-kedatangan-non-tps")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoKedatanganNonTps(
[FromForm] List<IFormFile>? FotoKedatangan,
[FromForm] string? NomorSpj,
[FromForm] string? NamaTps,
[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, NomorSpj, NamaTps);
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, NomorSpj, NamaTps)).ToList();
var saveResult = await SaveUploadedRecordAsync(
isTps: false,
nomorSpj: NomorSpj,
namaTps: NamaTps,
spjDetailId: SpjDetailId,
lokasiAngkutId: LokasiAngkutId,
applyChanges: request =>
{
request.WaktuKedatangan = string.IsNullOrWhiteSpace(WaktuKedatangan) ? request.WaktuKedatangan : WaktuKedatangan;
request.Latitude = string.IsNullOrWhiteSpace(Latitude) ? request.Latitude : Latitude;
request.Longitude = string.IsNullOrWhiteSpace(Longitude) ? request.Longitude : Longitude;
request.AlamatJalan = string.IsNullOrWhiteSpace(AlamatJalan) ? request.AlamatJalan : AlamatJalan;
request.FotoKedatanganFileNames = fileUrls;
request.FotoKedatanganUploaded = true;
});
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
}
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." });
}
[HttpPost("upload-foto-timbangan-non-tps")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoTimbanganNonTps(
[FromForm] IFormFile? FotoTimbangan,
[FromForm] string? NomorSpj,
[FromForm] string? NamaTps,
[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, NomorSpj, NamaTps);
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, NomorSpj, NamaTps);
var saveResult = await SaveUploadedRecordAsync(
isTps: false,
nomorSpj: NomorSpj,
namaTps: NamaTps,
spjDetailId: SpjDetailId,
lokasiAngkutId: LokasiAngkutId,
applyChanges: request =>
{
while (request.Timbangan.Count <= ItemIndex)
{
request.Timbangan.Add(new RecordTimbanganItem());
}
request.Timbangan[ItemIndex] = new RecordTimbanganItem
{
FotoFileName = fileUrl,
JenisSampah = JenisSampah ?? "Residu",
Berat = Berat,
Uploaded = true
};
});
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
}
return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." });
}
[HttpPost("upload-foto-petugas-non-tps")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoPetugasNonTps(
[FromForm] List<IFormFile>? FotoPetugas,
[FromForm] string? NomorSpj,
[FromForm] string? NamaTps,
[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, NomorSpj, NamaTps);
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, NomorSpj, NamaTps)).ToList();
var saveResult = await SaveUploadedRecordAsync(
isTps: false,
nomorSpj: NomorSpj,
namaTps: NamaTps,
spjDetailId: SpjDetailId,
lokasiAngkutId: LokasiAngkutId,
applyChanges: request =>
{
request.FotoPetugasFileNames = fileUrls;
request.FotoPetugasUploaded = true;
request.NamaPetugas = string.IsNullOrWhiteSpace(NamaPetugas) ? request.NamaPetugas : NamaPetugas;
});
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
}
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." });
}
[HttpPost("upload-foto-kedatangan")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoKedatangan(
[FromForm] List<IFormFile>? FotoKedatangan,
[FromForm] string? NomorSpj,
[FromForm] string? NamaTps,
[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, NomorSpj, NamaTps);
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, NomorSpj, NamaTps)).ToList();
var saveResult = await SaveUploadedRecordAsync(
isTps: true,
nomorSpj: NomorSpj,
namaTps: NamaTps,
spjDetailId: SpjDetailId,
lokasiAngkutId: LokasiAngkutId,
applyChanges: request =>
{
request.WaktuKedatangan = string.IsNullOrWhiteSpace(WaktuKedatangan) ? request.WaktuKedatangan : WaktuKedatangan;
request.Latitude = string.IsNullOrWhiteSpace(Latitude) ? request.Latitude : Latitude;
request.Longitude = string.IsNullOrWhiteSpace(Longitude) ? request.Longitude : Longitude;
request.AlamatJalan = string.IsNullOrWhiteSpace(AlamatJalan) ? request.AlamatJalan : AlamatJalan;
request.FotoKedatanganFileNames = fileUrls;
request.FotoKedatanganUploaded = true;
});
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
}
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto kedatangan berhasil diupload." });
}
[HttpPost("upload-foto-timbangan")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoTimbangan(
[FromForm] IFormFile? FotoTimbangan,
[FromForm] string? NomorSpj,
[FromForm] string? NamaTps,
[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, NomorSpj, NamaTps);
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, NomorSpj, NamaTps);
var saveResult = await SaveUploadedRecordAsync(
isTps: true,
nomorSpj: NomorSpj,
namaTps: NamaTps,
spjDetailId: SpjDetailId,
lokasiAngkutId: LokasiAngkutId,
applyChanges: request =>
{
while (request.Timbangan.Count <= ItemIndex)
{
request.Timbangan.Add(new RecordTimbanganItem());
}
request.Timbangan[ItemIndex] = new RecordTimbanganItem
{
FotoFileName = fileUrl,
JenisSampah = JenisSampah ?? "Residu",
Berat = Berat,
Uploaded = true
};
});
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
}
return Ok(new { success = true, fileName = name, fileUrl, message = $"Foto timbangan #{ItemIndex + 1} berhasil diupload." });
}
[HttpPost("upload-foto-petugas")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> UploadFotoPetugas(
[FromForm] List<IFormFile>? FotoPetugas,
[FromForm] string? NomorSpj,
[FromForm] string? NamaTps,
[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, NomorSpj, NamaTps);
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, NomorSpj, NamaTps)).ToList();
var saveResult = await SaveUploadedRecordAsync(
isTps: true,
nomorSpj: NomorSpj,
namaTps: NamaTps,
spjDetailId: SpjDetailId,
lokasiAngkutId: LokasiAngkutId,
applyChanges: request =>
{
request.FotoPetugasFileNames = fileUrls;
request.FotoPetugasUploaded = true;
request.NamaPetugas = string.IsNullOrWhiteSpace(NamaPetugas) ? request.NamaPetugas : NamaPetugas;
});
if (!saveResult.Success)
{
return StatusCode(500, new { success = false, message = saveResult.Message });
}
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." });
}
[HttpPost("ocr-timbangan")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> OcrTimbangan(IFormFile? Foto)
{
if (Foto == null || Foto.Length == 0)
{
return BadRequest(new { success = false, message = "Foto tidak ditemukan." });
}
if (Foto.Length > 5 * 1024 * 1024)
{
return BadRequest(new { success = false, message = "Ukuran foto terlalu besar. Maksimal 5MB." });
}
var apiKey = _configuration["OpenRouter:OCRkey"];
if (string.IsNullOrWhiteSpace(apiKey))
{
return StatusCode(500, new { success = false, message = "OpenRouter API key belum diset." });
}
byte[] fileBytes;
await using (var ms = new MemoryStream())
{
await Foto.CopyToAsync(ms);
fileBytes = ms.ToArray();
}
var mimeType = string.IsNullOrWhiteSpace(Foto.ContentType) ? "image/jpeg" : Foto.ContentType;
var base64 = Convert.ToBase64String(fileBytes);
var dataUrl = $"data:{mimeType};base64,{base64}";
var payload = new
{
// model = "nvidia/nemotron-nano-12b-v2-vl:free",
model = "google/gemini-2.5-flash-image",
// model = "google/gemini-2.5-flash-lite",
// model = "google/gemini-2.5-flash-lite-preview-09-2025",
temperature = 0,
messages = new object[]
{
new
{
role = "user",
content = new object[]
{
new
{
type = "text",
text = @"
Baca angka berat timbangan digital pada foto.
Rules:
- Abaikan tulisan seperti ZERO, TARE, STABLE, AC, PACK, PCS, KG, ADD, HOLD.
- Jawab hanya angka dengan format 2 digit desimal pakai titik (contoh: 54.45).
- Jika tidak terbaca jawab: UNREADABLE
- Fokus pada angka layar LED merah yang menyala.
- Abaikan refleksi atau pantulan cahaya yang mungkin muncul di layar.
- Abaikan timestamp seperti tanggal, jam, atau informasi lain yang biasanya muncul di layar timbangan.
"
},
new
{
type = "image_url",
image_url = new { url = dataUrl }
}
}
}
}
};
var json = JsonSerializer.Serialize(payload);
var request = new HttpRequestMessage(HttpMethod.Post, "https://openrouter.ai/api/v1/chat/completions");
request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {apiKey}");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("HTTP-Referer", "https://pesapakawan.dinaslhdki.id");
request.Headers.TryAddWithoutValidation("X-Title", "eSPJ OCR Timbangan");
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
var client = _httpClientFactory.CreateClient();
using var response = await client.SendAsync(request);
var responseText = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
return StatusCode((int)response.StatusCode, new
{
success = false,
message = "OpenRouter request gagal.",
detail = responseText
});
}
using var doc = JsonDocument.Parse(responseText);
var content = doc.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString() ?? "";
content = content.Trim();
if (content.Contains("UNREADABLE", StringComparison.OrdinalIgnoreCase))
{
return Ok(new
{
success = false,
message = "Angka tidak terbaca.",
raw = content
});
}
// cari format angka 2 desimal
var match = Regex.Match(content, @"-?\d{1,5}([.,]\d{2})");
if (!match.Success)
{
return Ok(new
{
success = false,
message = "AI tidak menemukan angka valid.",
raw = content
});
}
var normalized = match.Value.Replace(',', '.');
if (!decimal.TryParse(normalized, NumberStyles.Any, CultureInfo.InvariantCulture, out var weight))
{
return Ok(new
{
success = false,
message = "Format angka AI tidak valid.",
raw = content
});
}
return Ok(new
{
success = true,
weight = weight.ToString("0.00", CultureInfo.InvariantCulture),
raw = content
});
}
}
}