856 lines
35 KiB
C#
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
|
|
});
|
|
}
|
|
|
|
}
|
|
}
|