using Microsoft.AspNetCore.Mvc; using System.Globalization; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; namespace eSPJ.Controllers.SpjDriverUpstController { [Route("upst/detail-penjemputan")] public class DetailPenjemputanController : Controller { private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; public DetailPenjemputanController(IHttpClientFactory httpClientFactory, IConfiguration configuration) { _httpClientFactory = httpClientFactory; _configuration = configuration; } [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"); } [HttpPost("")] [ValidateAntiForgeryToken] public IActionResult Index( string? Latitude, string? Longitude, string? AlamatJalan, string? GpsTruck, string? WaktuKedatangan, decimal? TotalTimbangan, List? BeratTimbangan, List? FotoKedatangan, List? FotoTimbangan, List? FotoPetugas) { if (FotoKedatangan == null || FotoKedatangan.Count == 0) { TempData["Error"] = "Step 1 wajib upload minimal 1 foto kedatangan."; return RedirectToAction(nameof(Index)); } if (FotoTimbangan == null || FotoTimbangan.Count == 0) { TempData["Error"] = "Step 2 wajib upload minimal 1 foto timbangan."; return RedirectToAction(nameof(Index)); } if (FotoPetugas == null || FotoPetugas.Count == 0) { TempData["Error"] = "Step 3 wajib upload minimal 1 foto petugas."; return RedirectToAction(nameof(Index)); } var totalByDetail = (BeratTimbangan ?? new List()) .Where(x => x > 0) .Sum(); var total = TotalTimbangan.GetValueOrDefault() > 0 ? TotalTimbangan.GetValueOrDefault() : totalByDetail; var totalDisplay = total.ToString("N2", CultureInfo.GetCultureInfo("id-ID")); TempData["Success"] = $"Data tersimpan. Kedatangan: {FotoKedatangan.Count} foto, " + $"Timbangan: {FotoTimbangan.Count} foto, Total: {totalDisplay} kg, " + $"Petugas: {FotoPetugas.Count} foto."; // TODO: simpan ke database _ = Latitude; _ = Longitude; _ = AlamatJalan; _ = GpsTruck; _ = WaktuKedatangan; return RedirectToAction(nameof(Index)); } [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." }); } // limit size biar ga gila (contoh 5MB) 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", 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. Saya berikan 3 contoh foto timbangan yang benar: Foto 1 = 75.23 Foto 2 = 79.86 Foto 3 = 54.45 Sekarang baca angka pada foto terakhir." }, new { type = "image_url", image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888384/Notes_-_2026-02-11_08.52.31_wonhbm.jpg" } }, new { type = "image_url", image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888429/Notes_-_2026-02-11_08.52.34_xairzy.jpg" } }, new { type = "image_url", image_url = new { url = "https://res.cloudinary.com/drejcprhe/image/upload/v1770888473/ChatGPT_Image_Feb_11_2026_03_00_33_PM_ujhdlw.png" } }, 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://yourdomain.com"); 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 }); } [HttpGet("batal")] public IActionResult Batal() { return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Batal.cshtml"); } } }