288 lines
12 KiB
C#
288 lines
12 KiB
C#
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<IActionResult> 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"": ""<nomor struk tanpa prefix, contoh: 8001441>"",
|
|
""nomorPolisi"": ""<nomor polisi kendaraan, contoh: B 9125 PJA>"",
|
|
""penugasan"": ""<area penugasan, contoh: JAKARTA BARAT>"",
|
|
""waktuMasuk"": ""<waktu masuk format YYYY-MM-DD, HH:MM:SS>"",
|
|
""waktuKeluar"": ""<waktu keluar format YYYY-MM-DD, HH:MM:SS>"",
|
|
""beratMasuk"": <berat masuk dalam kg sebagai angka integer tanpa desimal>,
|
|
""beratKeluar"": <berat keluar dalam kg sebagai angka integer tanpa desimal>,
|
|
""beratNett"": <berat netto dalam kg sebagai angka integer tanpa desimal>
|
|
}
|
|
|
|
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<IActionResult> 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;
|
|
}
|
|
}
|
|
}
|