using Microsoft.AspNetCore.Mvc; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; namespace eSPJ.Controllers.SpjDriverUpstController { [Route("upst/submit")] public class SubmitController : Controller { private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly IWebHostEnvironment _env; public SubmitController( IHttpClientFactory httpClientFactory, IConfiguration configuration, IWebHostEnvironment env) { _httpClientFactory = httpClientFactory; _configuration = configuration; _env = env; } [HttpGet("")] public IActionResult Index() { return View("~/Views/Admin/Transport/SpjDriverUpst/Submit/Index.cshtml"); } [HttpGet("struk")] public IActionResult Struk() { return View("~/Views/Admin/Transport/SpjDriverUpst/Submit/Struk.cshtml"); } [HttpPost("ocr-struk")] [IgnoreAntiforgeryToken] public async Task OcrStruk(IFormFile? Foto) { if (Foto == null || Foto.Length == 0) return BadRequest(new { success = false, message = "Foto tidak ditemukan." }); if (Foto.Length > 10 * 1024 * 1024) return BadRequest(new { success = false, message = "Ukuran foto terlalu besar. Maksimal 10MB." }); 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 = "google/gemini-2.5-flash-image", temperature = 0, messages = new object[] { new { role = "user", content = new object[] { new { type = "text", text = @"Baca data dari foto struk timbang kendaraan ini. Ekstrak semua data yang tersedia ke dalam format JSON berikut: { ""nomorStruk"": """", ""nomorPolisi"": """", ""penugasan"": """", ""waktuMasuk"": """", ""waktuKeluar"": """", ""beratMasuk"": , ""beratKeluar"": , ""beratNett"": } Catatan penting: - nomorStruk: ambil angka utama saja, abaikan prefix bulan seperti '03_' atau '03 ' - waktu: konversi ke format YYYY-MM-DD, HH:MM:SS (contoh: 2025-08-04, 08:13:51) - berat: hanya angka integer tanpa satuan 'kg' - Jika data tidak ada atau tidak terbaca, isi dengan null - Kembalikan HANYA JSON valid tanpa penjelasan atau markdown code block" }, 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 Struk"); 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() ?? ""; var jsonMatch = Regex.Match(content, @"\{[\s\S]*\}"); if (!jsonMatch.Success) return Ok(new { success = false, message = "AI tidak mengembalikan data valid.", raw = content }); try { using var dataDoc = JsonDocument.Parse(jsonMatch.Value); var root = dataDoc.RootElement; return Ok(new { success = true, data = new { nomorStruk = GetStringValue(root, "nomorStruk"), nomorPolisi = GetStringValue(root, "nomorPolisi"), penugasan = GetStringValue(root, "penugasan"), waktuMasuk = GetStringValue(root, "waktuMasuk"), waktuKeluar = GetStringValue(root, "waktuKeluar"), beratMasuk = GetIntValue(root, "beratMasuk"), beratKeluar = GetIntValue(root, "beratKeluar"), beratNett = GetIntValue(root, "beratNett") }, raw = content }); } catch { return Ok(new { success = false, message = "Gagal memproses respons AI.", raw = content }); } } [HttpPost("struk")] public async Task ProcessStruk( string? NomorStruk, string? NomorPolisi, string? Penugasan, string? WaktuMasuk, string? WaktuKeluar, int? BeratMasuk, int? BeratKeluar, int? BeratNett, string? Timbang, IFormFile? FotoStruk) { try { if (string.IsNullOrWhiteSpace(NomorStruk)) { TempData["Error"] = "Nomor struk wajib diisi."; return RedirectToAction("Struk"); } if (!Regex.IsMatch(NomorStruk, @"^\d{3,}$")) { TempData["Error"] = "Format nomor struk tidak valid. Harus berupa angka minimal 3 digit."; return RedirectToAction("Struk"); } if (string.IsNullOrWhiteSpace(NomorPolisi)) { TempData["Error"] = "Nomor polisi wajib diisi."; return RedirectToAction("Struk"); } if (string.IsNullOrWhiteSpace(Penugasan)) { TempData["Error"] = "Penugasan wajib diisi."; return RedirectToAction("Struk"); } if (string.IsNullOrWhiteSpace(WaktuMasuk)) { TempData["Error"] = "Waktu masuk wajib diisi."; return RedirectToAction("Struk"); } if (string.IsNullOrWhiteSpace(WaktuKeluar)) { TempData["Error"] = "Waktu keluar wajib diisi."; return RedirectToAction("Struk"); } if (!BeratMasuk.HasValue || BeratMasuk <= 0) { TempData["Error"] = "Berat masuk wajib diisi."; return RedirectToAction("Struk"); } if (!BeratKeluar.HasValue || BeratKeluar <= 0) { TempData["Error"] = "Berat keluar wajib diisi."; return RedirectToAction("Struk"); } if (!BeratNett.HasValue || BeratNett <= 0) { TempData["Error"] = "Berat nett wajib diisi."; return RedirectToAction("Struk"); } if (FotoStruk == null || FotoStruk.Length == 0) { TempData["Error"] = "Foto struk wajib dilampirkan."; return RedirectToAction("Struk"); } string? fotoStrukUrl = null; if (FotoStruk != null && FotoStruk.Length > 0) { var datePart = DateTime.Now.ToString("yyyy-MM-dd"); var uploadPath = Path.Combine(_env.ContentRootPath, "uploads", "struk", datePart); if (!Directory.Exists(uploadPath)) Directory.CreateDirectory(uploadPath); var ext = Path.GetExtension(FotoStruk.FileName).ToLowerInvariant(); if (string.IsNullOrEmpty(ext)) ext = ".jpg"; var fileName = $"struk_{NomorStruk}_{Guid.NewGuid():N}{ext}"; var filePath = Path.Combine(uploadPath, fileName); await using var stream = new FileStream(filePath, FileMode.Create); await FotoStruk.CopyToAsync(stream); fotoStrukUrl = $"/uploads/struk/{datePart}/{fileName}"; } var timbangLabel = Timbang == "RDF" ? "Timbangan RDF" : "Timbangan TPA"; TempData["Success"] = $"Struk berhasil disubmit! No: {NomorStruk}, {timbangLabel}, Nett: {BeratNett!.Value} kg"; return RedirectToAction("Index", "Home"); } catch (Exception) { TempData["Error"] = "Terjadi kesalahan saat memproses struk. Silakan coba lagi."; return RedirectToAction("Struk"); } } private static string? GetStringValue(JsonElement root, string key) { if (root.TryGetProperty(key, out var prop) && prop.ValueKind == JsonValueKind.String) return prop.GetString(); return null; } private static int? GetIntValue(JsonElement root, string key) { if (root.TryGetProperty(key, out var prop)) { if (prop.ValueKind == JsonValueKind.Number && prop.TryGetInt32(out var val)) return val; if (prop.ValueKind == JsonValueKind.String && int.TryParse(prop.GetString(), out var strVal)) return strVal; } return null; } } }