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 _logger; private readonly IWebHostEnvironment _env; public DetailPenjemputanController( IHttpClientFactory httpClientFactory, IConfiguration configuration, DetailPenjemputanService detailService, ILogger 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 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 MapRecordTimbangan(List? items) { return (items ?? new List()) .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(existingRecord.FotoKedatangan) : new List(), 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(existingRecord.FotoPetugas) : new List(), 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(); 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 SaveUploadedRecordAsync( bool isTps, string? nomorSpj, string? namaTps, string? spjDetailId, string? lokasiAngkutId, Action 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 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(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 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 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 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 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 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 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 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 UploadFotoKedatanganNonTps( [FromForm] List? 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(); 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 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 UploadFotoPetugasNonTps( [FromForm] List? 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(); 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 UploadFotoKedatangan( [FromForm] List? 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(); 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 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 UploadFotoPetugas( [FromForm] List? 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(); 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 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 }); } } }