update: fixing ai foto
parent
9e2f7d3cab
commit
b1b36240b6
|
|
@ -159,6 +159,114 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
||||||
Response.Headers["Expires"] = "0";
|
Response.Headers["Expires"] = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ExtractOcrMessageContent(JsonElement messageContent)
|
||||||
|
{
|
||||||
|
if (messageContent.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return messageContent.GetString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageContent.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
foreach (var item in messageContent.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var text = item.GetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
parts.Add(text);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.ValueKind == JsonValueKind.Object
|
||||||
|
&& item.TryGetProperty("text", out var textProp)
|
||||||
|
&& textProp.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var text = textProp.GetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
parts.Add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(" ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal? TryParseWeightFromOcrContent(string content)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var rawLine in content.Split('\n'))
|
||||||
|
{
|
||||||
|
var line = rawLine.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
line = line.Trim('"', '\'', '`', '.', ';', ':');
|
||||||
|
|
||||||
|
if (Regex.IsMatch(line, @"^-?\d{1,6}[.,]\d{2}$"))
|
||||||
|
{
|
||||||
|
var normalized = line.Replace(',', '.');
|
||||||
|
if (decimal.TryParse(normalized, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Regex.IsMatch(line, @"^\d{4}$")
|
||||||
|
&& int.TryParse(line, NumberStyles.Integer, CultureInfo.InvariantCulture, out var int4Value))
|
||||||
|
{
|
||||||
|
return int4Value / 100m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var looksExplanatory = Regex.IsMatch(content, @"[A-Za-z]")
|
||||||
|
|| content.Contains("contoh", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| content.Contains("misal", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (looksExplanatory)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var decMatch = Regex.Match(content, @"-?\d{1,6}[.,]\d{2}");
|
||||||
|
if (decMatch.Success)
|
||||||
|
{
|
||||||
|
var normalized = decMatch.Value.Replace(',', '.');
|
||||||
|
if (decimal.TryParse(normalized, NumberStyles.Any, CultureInfo.InvariantCulture, out var decValue))
|
||||||
|
{
|
||||||
|
return decValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var int4Match = Regex.Match(content, @"\b\d{4}\b");
|
||||||
|
if (int4Match.Success
|
||||||
|
&& int.TryParse(int4Match.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var fallbackInt4))
|
||||||
|
{
|
||||||
|
return fallbackInt4 / 100m;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void TryDeleteFile(string path)
|
||||||
|
{
|
||||||
|
try { System.IO.File.Delete(path); } catch { /* best-effort cleanup */ }
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<RecordSaveRequest?> ResolveRecordSaveRequestAsync()
|
private async Task<RecordSaveRequest?> ResolveRecordSaveRequestAsync()
|
||||||
{
|
{
|
||||||
Request.EnableBuffering();
|
Request.EnableBuffering();
|
||||||
|
|
@ -701,155 +809,146 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
||||||
|
|
||||||
return Ok(new { success = true, fileNames, fileUrls, message = $"{fileNames.Count} foto petugas berhasil diupload." });
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("ocr-timbangan")]
|
var mimeType = "image/jpeg";
|
||||||
[IgnoreAntiforgeryToken]
|
var base64 = Convert.ToBase64String(fileBytes);
|
||||||
public async Task<IActionResult> OcrTimbangan(IFormFile? Foto)
|
var dataUrl = $"data:{mimeType};base64,{base64}";
|
||||||
|
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
model = "openai/gpt-5-image-mini",
|
||||||
|
messages = new object[]
|
||||||
{
|
{
|
||||||
if (Foto == null || Foto.Length == 0)
|
new
|
||||||
{
|
{
|
||||||
return BadRequest(new { success = false, message = "Foto tidak ditemukan." });
|
role = "user",
|
||||||
}
|
content = new object[]
|
||||||
|
|
||||||
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
|
new
|
||||||
{
|
{
|
||||||
role = "user",
|
type = "text",
|
||||||
content = new object[]
|
text = @"
|
||||||
{
|
Baca angka pada display timbangan digital dari gambar ini.
|
||||||
new
|
Fokus hanya pada digit display, abaikan refleksi, cahaya merah, dan teks lain.
|
||||||
{
|
Keluarkan tepat satu baris angka saja.
|
||||||
type = "text",
|
Format output wajib NNN,NN (gunakan koma sebagai desimal dua digit).
|
||||||
text = @"
|
Jika tidak yakin atau angka tidak terbaca, keluarkan 0,00.
|
||||||
Baca angka berat timbangan digital pada foto.
|
Jangan tambahkan kata, kalimat, atau simbol lain.
|
||||||
|
"
|
||||||
Rules:
|
},
|
||||||
- Abaikan tulisan seperti ZERO, TARE, STABLE, AC, PACK, PCS, KG, ADD, HOLD.
|
new
|
||||||
- Jawab hanya angka dengan format 2 digit desimal pakai titik (contoh: 54.45).
|
{
|
||||||
- Jika tidak terbaca jawab: UNREADABLE
|
type = "image_url",
|
||||||
- Fokus pada angka layar LED merah yang menyala.
|
image_url = new { url = dataUrl }
|
||||||
- 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 json = JsonSerializer.Serialize(payload);
|
||||||
|
|
||||||
var content = doc.RootElement
|
var request = new HttpRequestMessage(HttpMethod.Post, "https://openrouter.ai/api/v1/chat/completions");
|
||||||
.GetProperty("choices")[0]
|
request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {apiKey}");
|
||||||
.GetProperty("message")
|
request.Headers.TryAddWithoutValidation("Accept", "application/json");
|
||||||
.GetProperty("content")
|
request.Headers.TryAddWithoutValidation("HTTP-Referer", "https://pesapakawan.dinaslhdki.id");
|
||||||
.GetString() ?? "";
|
request.Headers.TryAddWithoutValidation("X-Title", "eSPJ OCR Timbangan");
|
||||||
|
|
||||||
content = content.Trim();
|
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
if (content.Contains("UNREADABLE", StringComparison.OrdinalIgnoreCase))
|
var client = _httpClientFactory.CreateClient();
|
||||||
{
|
using var response = await client.SendAsync(request);
|
||||||
return Ok(new
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
{
|
|
||||||
success = false,
|
|
||||||
message = "Angka tidak terbaca.",
|
|
||||||
raw = content
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// cari format angka 2 desimal
|
if (!response.IsSuccessStatusCode)
|
||||||
var match = Regex.Match(content, @"-?\d{1,5}([.,]\d{2})");
|
{
|
||||||
|
return StatusCode((int)response.StatusCode, new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "OpenRouter request gagal.",
|
||||||
|
detail = responseText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!match.Success)
|
string content;
|
||||||
{
|
try
|
||||||
return Ok(new
|
{
|
||||||
{
|
using var doc = JsonDocument.Parse(responseText);
|
||||||
success = false,
|
var messageContent = doc.RootElement
|
||||||
message = "AI tidak menemukan angka valid.",
|
.GetProperty("choices")[0]
|
||||||
raw = content
|
.GetProperty("message")
|
||||||
});
|
.GetProperty("content");
|
||||||
}
|
content = ExtractOcrMessageContent(messageContent);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "Gagal membaca respons OCR.",
|
||||||
|
raw = responseText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var normalized = match.Value.Replace(',', '.');
|
content = content.Trim();
|
||||||
|
|
||||||
if (!decimal.TryParse(normalized, NumberStyles.Any, CultureInfo.InvariantCulture, out var weight))
|
if (content.Contains("UNREADABLE", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
success = false,
|
success = false,
|
||||||
message = "Format angka AI tidak valid.",
|
message = "Angka tidak terbaca.",
|
||||||
raw = content
|
raw = content
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(new
|
var weight = TryParseWeightFromOcrContent(content);
|
||||||
|
|
||||||
|
if (weight == null)
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "AI tidak menemukan angka valid.",
|
||||||
|
raw = content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var weightStr = weight.Value.ToString("0.00", CultureInfo.GetCultureInfo("id-ID"));
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
{
|
{
|
||||||
success = true,
|
success = true,
|
||||||
weight = weight.ToString("0.00", CultureInfo.InvariantCulture),
|
weight = weightStr,
|
||||||
raw = content
|
raw = content
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
||||||
var payload = new
|
var payload = new
|
||||||
{
|
{
|
||||||
model = "google/gemini-2.5-flash-image",
|
model = "google/gemini-2.5-flash-image",
|
||||||
temperature = 0,
|
|
||||||
messages = new object[]
|
messages = new object[]
|
||||||
{
|
{
|
||||||
new
|
new
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@
|
||||||
--text-2xl--line-height: calc(2 / 1.5);
|
--text-2xl--line-height: calc(2 / 1.5);
|
||||||
--text-3xl: 1.875rem;
|
--text-3xl: 1.875rem;
|
||||||
--text-3xl--line-height: calc(2.25 / 1.875);
|
--text-3xl--line-height: calc(2.25 / 1.875);
|
||||||
|
--font-weight-normal: 400;
|
||||||
--font-weight-medium: 500;
|
--font-weight-medium: 500;
|
||||||
--font-weight-semibold: 600;
|
--font-weight-semibold: 600;
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
|
|
@ -820,6 +821,9 @@
|
||||||
.mb-auto {
|
.mb-auto {
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
}
|
}
|
||||||
|
.ml-1 {
|
||||||
|
margin-left: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
.ml-4 {
|
.ml-4 {
|
||||||
margin-left: calc(var(--spacing) * 4);
|
margin-left: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
|
@ -1538,6 +1542,9 @@
|
||||||
.border-blue-200 {
|
.border-blue-200 {
|
||||||
border-color: var(--color-blue-200);
|
border-color: var(--color-blue-200);
|
||||||
}
|
}
|
||||||
|
.border-blue-500 {
|
||||||
|
border-color: var(--color-blue-500);
|
||||||
|
}
|
||||||
.border-cyan-300 {
|
.border-cyan-300 {
|
||||||
border-color: var(--color-cyan-300);
|
border-color: var(--color-cyan-300);
|
||||||
}
|
}
|
||||||
|
|
@ -1577,6 +1584,9 @@
|
||||||
.border-green-400 {
|
.border-green-400 {
|
||||||
border-color: var(--color-green-400);
|
border-color: var(--color-green-400);
|
||||||
}
|
}
|
||||||
|
.border-green-500 {
|
||||||
|
border-color: var(--color-green-500);
|
||||||
|
}
|
||||||
.border-lime-400 {
|
.border-lime-400 {
|
||||||
border-color: var(--color-lime-400);
|
border-color: var(--color-lime-400);
|
||||||
}
|
}
|
||||||
|
|
@ -1607,6 +1617,9 @@
|
||||||
.border-red-400 {
|
.border-red-400 {
|
||||||
border-color: var(--color-red-400);
|
border-color: var(--color-red-400);
|
||||||
}
|
}
|
||||||
|
.border-red-500 {
|
||||||
|
border-color: var(--color-red-500);
|
||||||
|
}
|
||||||
.border-slate-200 {
|
.border-slate-200 {
|
||||||
border-color: var(--color-slate-200);
|
border-color: var(--color-slate-200);
|
||||||
}
|
}
|
||||||
|
|
@ -2459,6 +2472,10 @@
|
||||||
--tw-font-weight: var(--font-weight-medium);
|
--tw-font-weight: var(--font-weight-medium);
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
}
|
}
|
||||||
|
.font-normal {
|
||||||
|
--tw-font-weight: var(--font-weight-normal);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
}
|
||||||
.font-semibold {
|
.font-semibold {
|
||||||
--tw-font-weight: var(--font-weight-semibold);
|
--tw-font-weight: var(--font-weight-semibold);
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
|
|
@ -2844,6 +2861,10 @@
|
||||||
--tw-drop-shadow: drop-shadow(var(--drop-shadow-lg));
|
--tw-drop-shadow: drop-shadow(var(--drop-shadow-lg));
|
||||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||||
}
|
}
|
||||||
|
.grayscale {
|
||||||
|
--tw-grayscale: grayscale(100%);
|
||||||
|
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||||
|
}
|
||||||
.invert {
|
.invert {
|
||||||
--tw-invert: invert(100%);
|
--tw-invert: invert(100%);
|
||||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false,
|
fotoKedatanganUploaded: tps.fotoKedatanganUploaded || false,
|
||||||
timbangan: (tps.timbangan || []).map(t => ({
|
timbangan: (tps.timbangan || []).map(t => ({
|
||||||
berat: (t.berat && t.berat.length > 0) ? t.berat[0] : 0,
|
berat: (t.berat && t.berat.length > 0) ? t.berat[0] : 0,
|
||||||
jenisSampah: (t.jenisSampah && t.jenisSampah.length > 0) ? t.jenisSampah[0] : DEFAULT_JENIS,
|
jenisSampah: normalizeJenisSampahValue((t.jenisSampah && t.jenisSampah.length > 0) ? t.jenisSampah[0] : DEFAULT_JENIS),
|
||||||
fotoFileName: t.fotoFileName || '',
|
fotoFileName: t.fotoFileName || '',
|
||||||
uploaded: t.uploaded || false,
|
uploaded: t.uploaded || false,
|
||||||
ocrInfo: t.ocrInfo || ''
|
ocrInfo: t.ocrInfo || ''
|
||||||
|
|
@ -214,6 +214,11 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
} else {
|
} else {
|
||||||
tps.timbangan = [];
|
tps.timbangan = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TAB_VISUAL_ORDER = ['Residu', 'Organik', 'Anorganik'];
|
||||||
|
tps.activeTimbanganTab = TAB_VISUAL_ORDER.find(j =>
|
||||||
|
tps.timbangan.some(t => normalizeJenisSampahValue(t.jenisSampah && t.jenisSampah.length > 0 ? t.jenisSampah[0] : '') === j)
|
||||||
|
) || tps.activeTimbanganTab || DEFAULT_JENIS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRecordForCurrentSpj() {
|
async function loadRecordForCurrentSpj() {
|
||||||
|
|
@ -307,6 +312,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
fotoKedatanganFileNames: [],
|
fotoKedatanganFileNames: [],
|
||||||
fotoKedatanganUploaded: false,
|
fotoKedatanganUploaded: false,
|
||||||
timbangan: [],
|
timbangan: [],
|
||||||
|
activeTimbanganTab: DEFAULT_JENIS,
|
||||||
totalOrganik: 0,
|
totalOrganik: 0,
|
||||||
totalAnorganik: 0,
|
totalAnorganik: 0,
|
||||||
totalResidu: 0,
|
totalResidu: 0,
|
||||||
|
|
@ -376,6 +382,25 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
function renderTpsForm() {
|
function renderTpsForm() {
|
||||||
const tps = tpsData[activeTpsIndex];
|
const tps = tpsData[activeTpsIndex];
|
||||||
const submitState = getSubmitState(tps);
|
const submitState = getSubmitState(tps);
|
||||||
|
const activeJenis = normalizeJenisSampahValue(
|
||||||
|
tps.activeTimbanganTab || DEFAULT_JENIS,
|
||||||
|
);
|
||||||
|
const INACTIVE_TAB = 'bg-gray-100 text-gray-500 border-gray-200';
|
||||||
|
const TAB_CONFIG = [
|
||||||
|
{ key: 'Residu', active: 'bg-red-500 text-white border-red-500', inactive: INACTIVE_TAB },
|
||||||
|
{ key: 'Organik', active: 'bg-green-500 text-white border-green-500', inactive: INACTIVE_TAB },
|
||||||
|
{ key: 'Anorganik', active: 'bg-blue-500 text-white border-blue-500', inactive: INACTIVE_TAB },
|
||||||
|
];
|
||||||
|
const tabsMarkup = TAB_CONFIG.map((tab) => {
|
||||||
|
const count = (tps.timbangan || []).filter((item) => {
|
||||||
|
const v = item?.jenisSampah && item.jenisSampah.length > 0 ? item.jenisSampah[0] : DEFAULT_JENIS;
|
||||||
|
return normalizeJenisSampahValue(v) === tab.key;
|
||||||
|
}).length;
|
||||||
|
const isActive = activeJenis === tab.key;
|
||||||
|
const colorClass = isActive ? tab.active : tab.inactive;
|
||||||
|
return `<button type="button" class="timbangan-tab-btn flex-1 py-2 px-1 rounded-xl text-xs font-bold border transition ${colorClass}" data-jenis="${tab.key}">${tab.key}<br/><span class="timbangan-tab-count text-[10px] opacity-80">${count > 0 ? count + ' foto' : ''}</span></button>`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
const actionMarkup = tps.submitted
|
const actionMarkup = tps.submitted
|
||||||
? `
|
? `
|
||||||
<div class="flex items-center justify-center gap-2 rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm">
|
<div class="flex items-center justify-center gap-2 rounded-xl border border-green-200 bg-green-50 px-4 py-3 text-sm">
|
||||||
|
|
@ -449,11 +474,21 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tps-timbangan-repeater space-y-3"></div>
|
<div class="timbangan-tabs flex gap-2">
|
||||||
|
${tabsMarkup}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" class="tps-btn-add-timbangan w-full border border-dashed border-upst text-upst rounded-xl py-2 text-xs font-bold transition">
|
<div class="timbangan-tab-content space-y-3">
|
||||||
+ Tambah Foto Timbangan
|
<div class="tps-timbangan-repeater space-y-2"></div>
|
||||||
</button>
|
<label class="block">
|
||||||
|
<span class="text-xs font-semibold text-gray-600 mb-1 block">
|
||||||
|
+ Tambah Foto <span class="active-jenis-label font-black">${activeJenis}</span>
|
||||||
|
<span class="text-[11px] font-normal text-gray-400 ml-1">(bisa pilih banyak)</span>
|
||||||
|
</span>
|
||||||
|
<input type="file" accept="image/*" multiple class="input-foto-timbangan-multi block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" />
|
||||||
|
</label>
|
||||||
|
<div class="timbangan-multi-progress hidden text-xs text-gray-500 text-center py-1"></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
|
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
|
||||||
|
|
@ -577,7 +612,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan");
|
const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan");
|
||||||
const fotoPetugasInput = form.querySelector(".tps-foto-petugas");
|
const fotoPetugasInput = form.querySelector(".tps-foto-petugas");
|
||||||
const namaPetugasInput = form.querySelector(".tps-nama-petugas");
|
const namaPetugasInput = form.querySelector(".tps-nama-petugas");
|
||||||
const btnAddTimbangan = form.querySelector(".tps-btn-add-timbangan");
|
const tabButtons = form.querySelectorAll('.timbangan-tab-btn');
|
||||||
|
|
||||||
fotoKedatanganInput.addEventListener('change', function() {
|
fotoKedatanganInput.addEventListener('change', function() {
|
||||||
if (!this.files || !this.files[0]) return;
|
if (!this.files || !this.files[0]) return;
|
||||||
|
|
@ -633,12 +668,119 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
scheduleAutoSave();
|
scheduleAutoSave();
|
||||||
});
|
});
|
||||||
|
|
||||||
btnAddTimbangan.addEventListener("click", function () {
|
const INACTIVE_TAB_COLOR = 'bg-gray-100 text-gray-500 border-gray-200';
|
||||||
createTimbanganItem(form.querySelector(".tps-timbangan-repeater"));
|
const TAB_COLORS = {
|
||||||
syncTimbanganToTpsData();
|
'Residu': { active: 'bg-red-500 text-white border-red-500', inactive: INACTIVE_TAB_COLOR },
|
||||||
refreshSubmitButtonState(form);
|
'Organik': { active: 'bg-green-500 text-white border-green-500', inactive: INACTIVE_TAB_COLOR },
|
||||||
|
'Anorganik': { active: 'bg-blue-500 text-white border-blue-500', inactive: INACTIVE_TAB_COLOR },
|
||||||
|
};
|
||||||
|
tabButtons.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
const nextJenis = this.dataset.jenis;
|
||||||
|
const current = tpsData[activeTpsIndex];
|
||||||
|
if (!current) return;
|
||||||
|
syncTimbanganToTpsData();
|
||||||
|
current.activeTimbanganTab = nextJenis;
|
||||||
|
tabButtons.forEach(b => {
|
||||||
|
const cfg = TAB_COLORS[b.dataset.jenis] || { active: 'bg-upst text-white border-upst', inactive: INACTIVE_TAB_COLOR };
|
||||||
|
b.className = `timbangan-tab-btn flex-1 py-2 px-1 rounded-xl text-xs font-bold border transition ${b.dataset.jenis === nextJenis ? cfg.active : cfg.inactive}`;
|
||||||
|
});
|
||||||
|
const jenisLabel = form.querySelector('.active-jenis-label');
|
||||||
|
if (jenisLabel) jenisLabel.textContent = nextJenis;
|
||||||
|
renderTimbanganRepeater();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const multiFileInput = form.querySelector('.input-foto-timbangan-multi');
|
||||||
|
if (multiFileInput) {
|
||||||
|
multiFileInput.addEventListener('change', async function() {
|
||||||
|
if (!this.files || !this.files.length) return;
|
||||||
|
const currentTps = tpsData[activeTpsIndex];
|
||||||
|
const activeJenis = normalizeJenisSampahValue(currentTps.activeTimbanganTab || DEFAULT_JENIS);
|
||||||
|
const files = Array.from(this.files);
|
||||||
|
const progressEl = form.querySelector('.timbangan-multi-progress');
|
||||||
|
if (progressEl) {
|
||||||
|
progressEl.textContent = `Memproses ${files.length} foto...`;
|
||||||
|
progressEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIndex = currentTps.timbangan.length;
|
||||||
|
files.forEach(() => {
|
||||||
|
currentTps.timbangan.push(createEmptyTimbanganData(activeJenis));
|
||||||
|
currentTps.timbangan[currentTps.timbangan.length - 1].ocrInfo = 'AI: memproses...';
|
||||||
|
});
|
||||||
|
renderTimbanganRepeater();
|
||||||
|
|
||||||
|
const processFile = async (file, timbanganIndex) => {
|
||||||
|
try {
|
||||||
|
const photoNumber = timbanganIndex + 1;
|
||||||
|
const watermarkedFile = await applyWatermark(file, photoNumber);
|
||||||
|
currentTps.timbangan[timbanganIndex].file = watermarkedFile;
|
||||||
|
|
||||||
|
const currentForm = tpsContentContainer.querySelector('form');
|
||||||
|
const itemEl = currentForm ? currentForm.querySelector(`.timbangan-item[data-timbangan-index="${timbanganIndex}"]`) : null;
|
||||||
|
if (itemEl) {
|
||||||
|
const previewWrap = itemEl.querySelector('.input-preview-wrap');
|
||||||
|
const previewImg = itemEl.querySelector('.input-preview-image');
|
||||||
|
if (previewWrap && previewImg) {
|
||||||
|
const localUrl = URL.createObjectURL(watermarkedFile);
|
||||||
|
previewImg.src = localUrl;
|
||||||
|
previewWrap.classList.remove('hidden');
|
||||||
|
previewImg.onload = () => URL.revokeObjectURL(localUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const weightInputProxy = { value: '', placeholder: '' };
|
||||||
|
const ocrInfoProxy = { textContent: 'AI: memproses...' };
|
||||||
|
await autoFillWeight(file, weightInputProxy, ocrInfoProxy);
|
||||||
|
|
||||||
|
const detectedWeight = parseWeightInput(weightInputProxy.value);
|
||||||
|
currentTps.timbangan[timbanganIndex].berat = [detectedWeight];
|
||||||
|
currentTps.timbangan[timbanganIndex].ocrInfo = ocrInfoProxy.textContent;
|
||||||
|
|
||||||
|
const currentForm2 = tpsContentContainer.querySelector('form');
|
||||||
|
const itemEl2 = currentForm2 ? currentForm2.querySelector(`.timbangan-item[data-timbangan-index="${timbanganIndex}"]`) : null;
|
||||||
|
if (itemEl2) {
|
||||||
|
const ocrInfoEl = itemEl2.querySelector('.input-ocr-info');
|
||||||
|
const weightDisplay = itemEl2.querySelector('.input-berat-timbangan-display');
|
||||||
|
const weightValue = itemEl2.querySelector('.input-berat-timbangan-value');
|
||||||
|
if (ocrInfoEl) ocrInfoEl.textContent = ocrInfoProxy.textContent;
|
||||||
|
if (weightDisplay) {
|
||||||
|
if (detectedWeight > 0) {
|
||||||
|
weightDisplay.value = formatWeightDisplay(detectedWeight);
|
||||||
|
weightDisplay.placeholder = 'Berat terdeteksi otomatis';
|
||||||
|
} else {
|
||||||
|
weightDisplay.value = '';
|
||||||
|
weightDisplay.placeholder = 'Tidak terbaca, isi manual';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (weightValue) weightValue.value = detectedWeight.toFixed(2);
|
||||||
|
refreshTimbanganUploadState(itemEl2);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (currentTps.timbangan[timbanganIndex]) {
|
||||||
|
currentTps.timbangan[timbanganIndex].ocrInfo = 'AI gagal diproses.';
|
||||||
|
}
|
||||||
|
const currentForm3 = tpsContentContainer.querySelector('form');
|
||||||
|
const itemEl3 = currentForm3 ? currentForm3.querySelector(`.timbangan-item[data-timbangan-index="${timbanganIndex}"]`) : null;
|
||||||
|
if (itemEl3) {
|
||||||
|
const ocrInfoEl = itemEl3.querySelector('.input-ocr-info');
|
||||||
|
if (ocrInfoEl) ocrInfoEl.textContent = 'AI gagal diproses.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateTpsTotalTimbangan();
|
||||||
|
const currentForm4 = tpsContentContainer.querySelector('form');
|
||||||
|
if (currentForm4) refreshSubmitButtonState(currentForm4);
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(files.map((file, i) => processFile(file, startIndex + i)));
|
||||||
|
|
||||||
|
if (progressEl) progressEl.classList.add('hidden');
|
||||||
|
this.value = '';
|
||||||
|
scheduleAutoSave();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const btnUploadKedatangan = form.querySelector(
|
const btnUploadKedatangan = form.querySelector(
|
||||||
".tps-btn-upload-kedatangan",
|
".tps-btn-upload-kedatangan",
|
||||||
);
|
);
|
||||||
|
|
@ -658,15 +800,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreTpsTimbanganItems() {
|
function restoreTpsTimbanganItems() {
|
||||||
const tps = tpsData[activeTpsIndex];
|
renderTimbanganRepeater();
|
||||||
const form = tpsContentContainer.querySelector("form");
|
|
||||||
const repeater = form.querySelector(".tps-timbangan-repeater");
|
|
||||||
|
|
||||||
if (tps.timbangan.length === 0) {
|
|
||||||
createTimbanganItem(repeater);
|
|
||||||
} else {
|
|
||||||
tps.timbangan.forEach((timb) => createTimbanganItem(repeater, timb));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWaktuKedatangan() {
|
function updateWaktuKedatangan() {
|
||||||
|
|
@ -1001,26 +1135,38 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
if (ocrInfoEl) ocrInfoEl.textContent = "AI: memproses gambar...";
|
if (ocrInfoEl) ocrInfoEl.textContent = "AI: memproses gambar...";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const img = await readFileAsImage(file);
|
|
||||||
let bestRawText = "";
|
let bestRawText = "";
|
||||||
let isSuccess = false;
|
let isSuccess = false;
|
||||||
|
|
||||||
for (const area of OCR_AREAS) {
|
const fullResult = await requestOpenRouterWeight(file);
|
||||||
const cropCanvas = createCropCanvas(img, area);
|
if (fullResult && fullResult.success && fullResult.weight) {
|
||||||
const cropFile = await canvasToJpegFile(
|
guessedWeight = parseWeightInput(fullResult.weight);
|
||||||
cropCanvas,
|
bestRawText = fullResult.raw || fullResult.weight;
|
||||||
`crop-${area.id}.jpg`,
|
isSuccess = guessedWeight > 0;
|
||||||
);
|
} else if (fullResult && fullResult.raw) {
|
||||||
const aiResult = await requestOpenRouterWeight(cropFile);
|
bestRawText = fullResult.raw;
|
||||||
|
}
|
||||||
|
|
||||||
if (aiResult && aiResult.success && aiResult.weight) {
|
if (!isSuccess) {
|
||||||
guessedWeight = parseWeightInput(aiResult.weight);
|
const img = await readFileAsImage(file);
|
||||||
bestRawText = aiResult.raw || aiResult.weight;
|
|
||||||
isSuccess = guessedWeight > 0;
|
for (const area of OCR_AREAS) {
|
||||||
if (isSuccess) break;
|
const cropCanvas = createCropCanvas(img, area);
|
||||||
|
const cropFile = await canvasToJpegFile(
|
||||||
|
cropCanvas,
|
||||||
|
`crop-${area.id}.jpg`,
|
||||||
|
);
|
||||||
|
const aiResult = await requestOpenRouterWeight(cropFile);
|
||||||
|
|
||||||
|
if (aiResult && aiResult.success && aiResult.weight) {
|
||||||
|
guessedWeight = parseWeightInput(aiResult.weight);
|
||||||
|
bestRawText = aiResult.raw || aiResult.weight;
|
||||||
|
isSuccess = guessedWeight > 0;
|
||||||
|
if (isSuccess) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aiResult && aiResult.raw) bestRawText = aiResult.raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aiResult && aiResult.raw) bestRawText = aiResult.raw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ocrInfoEl) {
|
if (ocrInfoEl) {
|
||||||
|
|
@ -1054,19 +1200,19 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
let totalOrganik = 0.0;
|
let totalOrganik = 0.0;
|
||||||
let totalAnorganik = 0.0;
|
let totalAnorganik = 0.0;
|
||||||
let totalResidu = 0.0;
|
let totalResidu = 0.0;
|
||||||
const repeater = form.querySelector(".tps-timbangan-repeater");
|
|
||||||
const items = repeater.querySelectorAll(".timbangan-item");
|
|
||||||
|
|
||||||
items.forEach(function (item) {
|
(tps.timbangan || []).forEach(function (item) {
|
||||||
const weightInput = item.querySelector(".input-berat-timbangan-value");
|
const value = item?.berat && item.berat.length > 0
|
||||||
const jenisSampahSelect = item.querySelector(".input-jenis-sampah");
|
? Number(item.berat[0]) || 0
|
||||||
if (weightInput && jenisSampahSelect) {
|
: 0;
|
||||||
const value = parseWeightInput(weightInput.value || "0");
|
const jenis = normalizeJenisSampahValue(
|
||||||
const jenis = jenisSampahSelect.value;
|
item?.jenisSampah && item.jenisSampah.length > 0
|
||||||
if (jenis === "Organik") totalOrganik += value;
|
? item.jenisSampah[0]
|
||||||
else if (jenis === "Anorganik") totalAnorganik += value;
|
: DEFAULT_JENIS,
|
||||||
else totalResidu += value;
|
);
|
||||||
}
|
if (jenis === "Organik") totalOrganik += value;
|
||||||
|
else if (jenis === "Anorganik") totalAnorganik += value;
|
||||||
|
else totalResidu += value;
|
||||||
});
|
});
|
||||||
|
|
||||||
tps.totalOrganik = totalOrganik;
|
tps.totalOrganik = totalOrganik;
|
||||||
|
|
@ -1259,8 +1405,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
const stateContainer = item.querySelector(".timbangan-upload-state");
|
const stateContainer = item.querySelector(".timbangan-upload-state");
|
||||||
if (!stateContainer) return;
|
if (!stateContainer) return;
|
||||||
|
|
||||||
const repeater = item.parentElement;
|
const itemIndex = parseInt(item.dataset.timbanganIndex || '-1', 10);
|
||||||
const itemIndex = repeater ? Array.from(repeater.children).indexOf(item) : -1;
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
const tps = tpsData[activeTpsIndex];
|
||||||
const currentData = itemIndex >= 0 ? tps.timbangan[itemIndex] : null;
|
const currentData = itemIndex >= 0 ? tps.timbangan[itemIndex] : null;
|
||||||
const fileInput = item.querySelector('.input-foto-timbangan');
|
const fileInput = item.querySelector('.input-foto-timbangan');
|
||||||
|
|
@ -1281,9 +1426,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
const uploadBtn = stateContainer.querySelector(".btn-upload-timbangan");
|
const uploadBtn = stateContainer.querySelector(".btn-upload-timbangan");
|
||||||
if (uploadBtn) {
|
if (uploadBtn) {
|
||||||
uploadBtn.addEventListener("click", function () {
|
uploadBtn.addEventListener("click", function () {
|
||||||
const latestIndex = repeater
|
const latestIndex = parseInt(item.dataset.timbanganIndex || '-1', 10);
|
||||||
? Array.from(repeater.children).indexOf(item)
|
|
||||||
: -1;
|
|
||||||
uploadSingleFotoTimbangan(latestIndex, item);
|
uploadSingleFotoTimbangan(latestIndex, item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1307,48 +1450,96 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTimbanganItem(repeater, existingData = null) {
|
function createEmptyTimbanganData(jenis) {
|
||||||
const photoNumber = repeater.children.length + 1;
|
const normalizedJenis = normalizeJenisSampahValue(jenis || DEFAULT_JENIS);
|
||||||
|
return {
|
||||||
|
file: null,
|
||||||
|
fotoFileName: '',
|
||||||
|
berat: [0],
|
||||||
|
jenisSampah: [normalizedJenis],
|
||||||
|
lokasiAngkut: [],
|
||||||
|
uploaded: false,
|
||||||
|
ocrInfo: 'OCR: belum diproses.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTimbanganRepeater() {
|
||||||
|
const tps = tpsData[activeTpsIndex];
|
||||||
|
const form = tpsContentContainer.querySelector('form');
|
||||||
|
if (!form) return;
|
||||||
|
const repeater = form.querySelector('.tps-timbangan-repeater');
|
||||||
|
if (!repeater) return;
|
||||||
|
|
||||||
|
const activeTab = normalizeJenisSampahValue(tps.activeTimbanganTab || DEFAULT_JENIS);
|
||||||
|
repeater.innerHTML = '';
|
||||||
|
|
||||||
|
(tps.timbangan || []).forEach((timb, index) => {
|
||||||
|
const jenis = normalizeJenisSampahValue(
|
||||||
|
timb?.jenisSampah && timb.jenisSampah.length > 0 ? timb.jenisSampah[0] : DEFAULT_JENIS,
|
||||||
|
);
|
||||||
|
if (jenis === activeTab) {
|
||||||
|
createTimbanganItem(repeater, index, timb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
form.querySelectorAll('.timbangan-tab-btn').forEach((btn) => {
|
||||||
|
const jenis = btn.dataset.jenis;
|
||||||
|
const count = (tps.timbangan || []).filter((timb) => {
|
||||||
|
const v = timb?.jenisSampah && timb.jenisSampah.length > 0 ? timb.jenisSampah[0] : DEFAULT_JENIS;
|
||||||
|
return normalizeJenisSampahValue(v) === jenis;
|
||||||
|
}).length;
|
||||||
|
const span = btn.querySelector('.timbangan-tab-count');
|
||||||
|
if (span) span.textContent = count > 0 ? count + ' foto' : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTpsTotalTimbangan();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTimbanganItem(repeater, timbanganIndex, existingData = null) {
|
||||||
|
const photoNumber = timbanganIndex + 1;
|
||||||
const item = document.createElement("div");
|
const item = document.createElement("div");
|
||||||
item.className =
|
item.className =
|
||||||
"timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50";
|
"timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50";
|
||||||
item.dataset.photoNumber = photoNumber;
|
item.dataset.photoNumber = photoNumber;
|
||||||
|
item.dataset.timbanganIndex = String(timbanganIndex);
|
||||||
|
|
||||||
|
const activeJenis = normalizeJenisSampahValue((tpsData[activeTpsIndex] && tpsData[activeTpsIndex].activeTimbanganTab) || DEFAULT_JENIS);
|
||||||
const weight = existingData ? (existingData.berat && existingData.berat.length > 0 ? existingData.berat[0] : 0) : 0;
|
const weight = existingData ? (existingData.berat && existingData.berat.length > 0 ? existingData.berat[0] : 0) : 0;
|
||||||
const jenisSampah = existingData ? (existingData.jenisSampah && existingData.jenisSampah.length > 0 ? existingData.jenisSampah[0] : DEFAULT_JENIS) : DEFAULT_JENIS;
|
const jenisSampah = existingData
|
||||||
const hasFileBlob = Boolean(existingData?.file);
|
? normalizeJenisSampahValue(existingData.jenisSampah && existingData.jenisSampah.length > 0 ? existingData.jenisSampah[0] : activeJenis)
|
||||||
|
: activeJenis;
|
||||||
|
item.dataset.jenisSampah = jenisSampah;
|
||||||
const hasFile = Boolean(existingData?.file || existingData?.fotoFileName);
|
const hasFile = Boolean(existingData?.file || existingData?.fotoFileName);
|
||||||
const isUploaded = Boolean(existingData?.uploaded);
|
const isUploaded = Boolean(existingData?.uploaded);
|
||||||
const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.');
|
const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.');
|
||||||
|
|
||||||
|
const BADGE_COLORS = {
|
||||||
|
'Organik': 'bg-green-100 text-green-700',
|
||||||
|
'Anorganik': 'bg-blue-100 text-blue-700',
|
||||||
|
'Residu': 'bg-red-100 text-red-700',
|
||||||
|
};
|
||||||
|
const badgeColor = BADGE_COLORS[jenisSampah] || 'bg-gray-100 text-gray-600';
|
||||||
|
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<p class="text-xs font-bold text-gray-600">Item Timbangan #${photoNumber}</p>
|
<div class="flex items-center gap-2">
|
||||||
<button type="button" class="btn-remove-timbangan text-[11px] font-bold text-red-500">Hapus</button>
|
<p class="text-xs font-bold text-gray-600">Foto #${photoNumber}</p>
|
||||||
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-black ${badgeColor}">${jenisSampah}</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-remove-timbangan text-[11px] font-bold text-red-500">Hapus</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="file" name="FotoTimbangan" accept="image/*" class="input-foto-timbangan block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" />
|
<div class="${hasFile ? '' : 'hidden'} input-preview-wrap relative rounded-xl overflow-hidden border border-gray-200 bg-black">
|
||||||
<div class="input-preview-wrap ${(hasFileBlob || (hasFile && existingData?.fotoFileName?.startsWith('/'))) ? '' : 'hidden'} relative rounded-xl overflow-hidden border border-gray-200 bg-black">
|
|
||||||
<img class="input-preview-image w-full h-44 object-contain" alt="Preview foto timbangan" />
|
<img class="input-preview-image w-full h-44 object-contain" alt="Preview foto timbangan" />
|
||||||
<div class="input-crop-overlay absolute inset-0 pointer-events-none"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[11px] text-gray-500 input-ocr-info">${ocrInfoText}</p>
|
<p class="text-[11px] text-gray-500 input-ocr-info">${ocrInfoText}</p>
|
||||||
<div class="grid grid-cols-1 gap-2">
|
<div>
|
||||||
<div>
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Berat (kg)</label>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Jenis Sampah</label>
|
<input type="text" inputmode="decimal" class="input-berat-timbangan-display w-full rounded-xl border border-gray-200 px-3 py-2 text-sm" placeholder="Contoh: 54,45" value="${weight > 0 ? formatWeightDisplay(weight) : ""}" />
|
||||||
<select class="input-jenis-sampah w-full rounded-xl border border-gray-200 px-3 py-2 text-sm">
|
<input type="hidden" class="input-berat-timbangan-value" value="${weight.toFixed(2)}" />
|
||||||
${JENIS_SAMPAH.map((js) => `<option value="${js}" ${js === jenisSampah ? "selected" : ""}>${js}</option>`).join("")}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Berat (kg)</label>
|
|
||||||
<input type="text" inputmode="decimal" class="input-berat-timbangan-display w-full rounded-xl border border-gray-200 px-3 py-2 text-sm" placeholder="Contoh: 54,45" value="${weight > 0 ? formatWeightDisplay(weight) : ""}" />
|
|
||||||
<input type="hidden" class="input-berat-timbangan-value" value="${weight.toFixed(2)}" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="timbangan-upload-state">${getTimbanganUploadStateMarkup(hasFile, isUploaded, weight > 0)}</div>
|
<div class="timbangan-upload-state">${getTimbanganUploadStateMarkup(hasFile, isUploaded, weight > 0)}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const fileInput = item.querySelector(".input-foto-timbangan");
|
|
||||||
const previewWrap = item.querySelector(".input-preview-wrap");
|
const previewWrap = item.querySelector(".input-preview-wrap");
|
||||||
const previewImage = item.querySelector(".input-preview-image");
|
const previewImage = item.querySelector(".input-preview-image");
|
||||||
const ocrInfoEl = item.querySelector(".input-ocr-info");
|
const ocrInfoEl = item.querySelector(".input-ocr-info");
|
||||||
|
|
@ -1356,7 +1547,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
".input-berat-timbangan-display",
|
".input-berat-timbangan-display",
|
||||||
);
|
);
|
||||||
const weightInputValue = item.querySelector(".input-berat-timbangan-value");
|
const weightInputValue = item.querySelector(".input-berat-timbangan-value");
|
||||||
const jenisSampahSelect = item.querySelector(".input-jenis-sampah");
|
|
||||||
const removeBtn = item.querySelector(".btn-remove-timbangan");
|
const removeBtn = item.querySelector(".btn-remove-timbangan");
|
||||||
|
|
||||||
if (existingData && existingData.file) {
|
if (existingData && existingData.file) {
|
||||||
|
|
@ -1369,38 +1559,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
previewWrap.classList.remove('hidden');
|
previewWrap.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInput.addEventListener('change', async function() {
|
|
||||||
if (fileInput.files && fileInput.files[0]) {
|
|
||||||
const originalFile = fileInput.files[0];
|
|
||||||
const photoNumber = Number(item.dataset.photoNumber || (Array.from(repeater.children).indexOf(item) + 1));
|
|
||||||
const watermarkedFile = await applyWatermark(originalFile, photoNumber);
|
|
||||||
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
dataTransfer.items.add(watermarkedFile);
|
|
||||||
fileInput.files = dataTransfer.files;
|
|
||||||
|
|
||||||
const localUrl = URL.createObjectURL(watermarkedFile);
|
|
||||||
previewImage.src = localUrl;
|
|
||||||
previewWrap.classList.remove("hidden");
|
|
||||||
previewImage.onload = function () {
|
|
||||||
URL.revokeObjectURL(localUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
await autoFillWeight(watermarkedFile, weightInputDisplay, ocrInfoEl);
|
|
||||||
const parsed = parseWeightInput(weightInputDisplay.value);
|
|
||||||
weightInputValue.value = parsed.toFixed(2);
|
|
||||||
updateTpsTotalTimbangan();
|
|
||||||
syncTimbanganToTpsData();
|
|
||||||
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
const itemIndex = Array.from(repeater.children).indexOf(item);
|
|
||||||
if (itemIndex >= 0 && tps.timbangan[itemIndex]) {
|
|
||||||
tps.timbangan[itemIndex].uploaded = false;
|
|
||||||
refreshTimbanganUploadState(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
weightInputDisplay.addEventListener("input", function () {
|
weightInputDisplay.addEventListener("input", function () {
|
||||||
const cleaned = this.value.replace(/[^0-9.,]/g, "");
|
const cleaned = this.value.replace(/[^0-9.,]/g, "");
|
||||||
this.value = cleaned;
|
this.value = cleaned;
|
||||||
|
|
@ -1431,24 +1589,16 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
scheduleAutoSave();
|
scheduleAutoSave();
|
||||||
});
|
});
|
||||||
|
|
||||||
jenisSampahSelect.addEventListener('change', function() {
|
|
||||||
updateTpsTotalTimbangan();
|
|
||||||
syncTimbanganToTpsData();
|
|
||||||
const form = tpsContentContainer.querySelector('form');
|
|
||||||
if (form) refreshSubmitButtonState(form);
|
|
||||||
});
|
|
||||||
|
|
||||||
removeBtn.addEventListener('click', function() {
|
removeBtn.addEventListener('click', function() {
|
||||||
item.remove();
|
const tps = tpsData[activeTpsIndex];
|
||||||
|
const idx = parseInt(item.dataset.timbanganIndex || '-1', 10);
|
||||||
|
if (!Number.isNaN(idx) && idx >= 0) {
|
||||||
|
tps.timbangan.splice(idx, 1);
|
||||||
|
}
|
||||||
|
renderTimbanganRepeater();
|
||||||
const form = tpsContentContainer.querySelector('form');
|
const form = tpsContentContainer.querySelector('form');
|
||||||
const rep = form ? form.querySelector('.tps-timbangan-repeater') : null;
|
|
||||||
if (rep) {
|
|
||||||
renumberTimbanganItems(rep);
|
|
||||||
if (rep.children.length === 0) createTimbanganItem(rep);
|
|
||||||
}
|
|
||||||
updateTpsTotalTimbangan();
|
|
||||||
syncTimbanganToTpsData();
|
|
||||||
if (form) refreshSubmitButtonState(form);
|
if (form) refreshSubmitButtonState(form);
|
||||||
|
scheduleAutoSave();
|
||||||
});
|
});
|
||||||
|
|
||||||
repeater.appendChild(item);
|
repeater.appendChild(item);
|
||||||
|
|
@ -1463,25 +1613,25 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
|
||||||
const repeater = form.querySelector(".tps-timbangan-repeater");
|
const repeater = form.querySelector(".tps-timbangan-repeater");
|
||||||
const items = repeater.querySelectorAll(".timbangan-item");
|
const items = repeater.querySelectorAll(".timbangan-item");
|
||||||
const previousTimbangan = [...tps.timbangan];
|
items.forEach((item) => {
|
||||||
|
const dataIndex = parseInt(item.dataset.timbanganIndex || '-1', 10);
|
||||||
tps.timbangan = [];
|
if (Number.isNaN(dataIndex) || dataIndex < 0) return;
|
||||||
items.forEach((item, index) => {
|
|
||||||
const fileInput = item.querySelector('.input-foto-timbangan');
|
|
||||||
const weightValue = item.querySelector('.input-berat-timbangan-value');
|
const weightValue = item.querySelector('.input-berat-timbangan-value');
|
||||||
const weight = parseWeightInput(weightValue.value);
|
const weight = parseWeightInput(weightValue.value);
|
||||||
const jenisSampah = item.querySelector('.input-jenis-sampah').value;
|
const jenisSampah = normalizeJenisSampahValue(item.dataset.jenisSampah || DEFAULT_JENIS);
|
||||||
const ocrInfo = item.querySelector('.input-ocr-info')?.textContent || 'OCR: belum diproses.';
|
const ocrInfo = item.querySelector('.input-ocr-info')?.textContent || 'OCR: belum diproses.';
|
||||||
|
|
||||||
tps.timbangan.push({
|
const existing = tps.timbangan[dataIndex] || createEmptyTimbanganData(jenisSampah);
|
||||||
file: fileInput.files[0] || previousTimbangan[index]?.file || null,
|
tps.timbangan[dataIndex] = {
|
||||||
fotoFileName: previousTimbangan[index]?.fotoFileName || '',
|
...existing,
|
||||||
|
file: existing.file || null,
|
||||||
|
fotoFileName: existing.fotoFileName || '',
|
||||||
berat: [weight],
|
berat: [weight],
|
||||||
jenisSampah: [jenisSampah],
|
jenisSampah: [jenisSampah],
|
||||||
lokasiAngkut: [],
|
lokasiAngkut: [],
|
||||||
uploaded: previousTimbangan[index]?.uploaded ?? false,
|
uploaded: existing.uploaded ?? false,
|
||||||
ocrInfo
|
ocrInfo
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1548,13 +1698,9 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
|
||||||
if (!targetItem) {
|
if (!targetItem) {
|
||||||
const form = tpsContentContainer.querySelector("form");
|
const form = tpsContentContainer.querySelector("form");
|
||||||
const repeater = form
|
targetItem = form
|
||||||
? form.querySelector(".tps-timbangan-repeater")
|
? form.querySelector(`.timbangan-item[data-timbangan-index="${itemIndex}"]`)
|
||||||
: null;
|
: null;
|
||||||
const items = repeater
|
|
||||||
? repeater.querySelectorAll(".timbangan-item")
|
|
||||||
: [];
|
|
||||||
targetItem = items[itemIndex] || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadBtn = targetItem ? targetItem.querySelector('.btn-upload-timbangan') : null;
|
const uploadBtn = targetItem ? targetItem.querySelector('.btn-upload-timbangan') : null;
|
||||||
|
|
|
||||||
|
|
@ -201,10 +201,12 @@ const DetailPenjemputan = (function () {
|
||||||
item.fotoFileName,
|
item.fotoFileName,
|
||||||
);
|
);
|
||||||
const weight = Number(item.weight ?? getFirstValue(item.berat) ?? 0) || 0;
|
const weight = Number(item.weight ?? getFirstValue(item.berat) ?? 0) || 0;
|
||||||
const jenisSampah =
|
const _rawJenis = getFirstValue(item.jenisSampah);
|
||||||
getFirstValue(item.jenisSampah) ||
|
const jenisSampah = (_rawJenis != null)
|
||||||
item.JenisSampah ||
|
? (typeof _rawJenis === 'number'
|
||||||
CONFIG.DEFAULT_JENIS;
|
? (CONFIG.JENIS_SAMPAH[_rawJenis] || CONFIG.DEFAULT_JENIS)
|
||||||
|
: CONFIG.JENIS_SAMPAH.find(j => j.toLowerCase() === String(_rawJenis).trim().toLowerCase()) || CONFIG.DEFAULT_JENIS)
|
||||||
|
: (item.JenisSampah && CONFIG.JENIS_SAMPAH.find(j => j.toLowerCase() === String(item.JenisSampah).trim().toLowerCase()) || CONFIG.DEFAULT_JENIS);
|
||||||
const uploaded = Boolean(
|
const uploaded = Boolean(
|
||||||
item.uploaded ??
|
item.uploaded ??
|
||||||
item.isUploaded ??
|
item.isUploaded ??
|
||||||
|
|
@ -269,6 +271,9 @@ const DetailPenjemputan = (function () {
|
||||||
? apiTimbangan.map(normalizeTimbanganItem)
|
? apiTimbangan.map(normalizeTimbanganItem)
|
||||||
: currentTps.timbangan;
|
: currentTps.timbangan;
|
||||||
|
|
||||||
|
const TAB_VISUAL_ORDER = ['Residu', 'Organik', 'Anorganik'];
|
||||||
|
const restoredActiveTab = TAB_VISUAL_ORDER.find(j => timbangan.some(t => t.jenisSampah === j)) || currentTps.activeTimbanganTab || CONFIG.DEFAULT_JENIS;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...currentTps,
|
...currentTps,
|
||||||
nomorSpj: apiTps.nomorSpj || apiTps.NomorSpj || currentTps.nomorSpj,
|
nomorSpj: apiTps.nomorSpj || apiTps.NomorSpj || currentTps.nomorSpj,
|
||||||
|
|
@ -331,6 +336,7 @@ const DetailPenjemputan = (function () {
|
||||||
),
|
),
|
||||||
submittedAt:
|
submittedAt:
|
||||||
apiTps.submittedAt || apiTps.SubmittedAt || currentTps.submittedAt,
|
apiTps.submittedAt || apiTps.SubmittedAt || currentTps.submittedAt,
|
||||||
|
activeTimbanganTab: restoredActiveTab,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -627,6 +633,7 @@ const DetailPenjemputan = (function () {
|
||||||
fotoKedatanganFileNames: [],
|
fotoKedatanganFileNames: [],
|
||||||
fotoKedatanganUploaded: false,
|
fotoKedatanganUploaded: false,
|
||||||
timbangan: [],
|
timbangan: [],
|
||||||
|
activeTimbanganTab: CONFIG.DEFAULT_JENIS,
|
||||||
totalOrganik: 0,
|
totalOrganik: 0,
|
||||||
totalAnorganik: 0,
|
totalAnorganik: 0,
|
||||||
totalResidu: 0,
|
totalResidu: 0,
|
||||||
|
|
@ -829,23 +836,47 @@ const DetailPenjemputan = (function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSection2Timbangan(tps, showTpsName) {
|
function renderSection2Timbangan(tps, showTpsName) {
|
||||||
|
const activeTab = tps.activeTimbanganTab || CONFIG.DEFAULT_JENIS;
|
||||||
|
const INACTIVE_TAB = 'bg-gray-100 text-gray-500 border-gray-200';
|
||||||
|
const TAB_CONFIG = [
|
||||||
|
{ key: 'Residu', active: 'bg-red-500 text-white border-red-500', inactive: INACTIVE_TAB },
|
||||||
|
{ key: 'Organik', active: 'bg-green-500 text-white border-green-500', inactive: INACTIVE_TAB },
|
||||||
|
{ key: 'Anorganik', active: 'bg-blue-500 text-white border-blue-500', inactive: INACTIVE_TAB },
|
||||||
|
];
|
||||||
|
const tabsHtml = TAB_CONFIG.map(tab => {
|
||||||
|
const count = (tps.timbangan || []).filter(t => (t.jenisSampah || CONFIG.DEFAULT_JENIS) === tab.key).length;
|
||||||
|
const isActive = tab.key === activeTab;
|
||||||
|
const colorClass = isActive ? tab.active : tab.inactive;
|
||||||
|
return `<button type="button" class="timbangan-tab-btn flex-1 py-2 px-1 rounded-xl text-xs font-bold border transition ${colorClass}" data-jenis="${tab.key}">
|
||||||
|
${tab.key}<br/><span class="timbangan-tab-count text-[10px] opacity-80">${count > 0 ? count + ' foto' : ''}</span>
|
||||||
|
</button>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
|
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4" data-section="timbangan">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-8 h-8 rounded-full bg-upst text-white font-black text-sm flex items-center justify-center">2</div>
|
<div class="w-8 h-8 rounded-full bg-upst text-white font-black text-sm flex items-center justify-center">2</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-black text-gray-800">Foto Timbang Sampah</h3>
|
<h3 class="font-black text-gray-800">Foto Timbang Sampah</h3>
|
||||||
<p class="text-xs text-gray-500">Upload foto timbangan, berat auto terisi</p>
|
<p class="text-xs text-gray-500">Pilih tab, upload banyak foto sekaligus</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="timbangan-tabs flex gap-2">
|
||||||
<div class="tps-timbangan-repeater space-y-3"></div>
|
${tabsHtml}
|
||||||
|
</div>
|
||||||
<button type="button" class="tps-btn-add-timbangan w-full border border-dashed border-upst text-upst rounded-xl py-2 text-xs font-bold transition">
|
<div class="timbangan-tab-content space-y-3">
|
||||||
+ Tambah Foto Timbangan
|
<div class="tps-timbangan-repeater space-y-2"></div>
|
||||||
</button>
|
<label class="block">
|
||||||
</section>
|
<span class="text-xs font-semibold text-gray-600 mb-1 block">
|
||||||
`;
|
+ Tambah Foto <span class="active-jenis-label font-black">${activeTab}</span>
|
||||||
|
<span class="text-[11px] font-normal text-gray-400 ml-1">(bisa pilih banyak)</span>
|
||||||
|
</span>
|
||||||
|
<input type="file" accept="image/*" multiple class="input-foto-timbangan-multi block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" />
|
||||||
|
</label>
|
||||||
|
<div class="timbangan-multi-progress hidden text-xs text-gray-500 text-center py-1"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSection3Petugas(tps) {
|
function renderSection3Petugas(tps) {
|
||||||
|
|
@ -881,7 +912,6 @@ const DetailPenjemputan = (function () {
|
||||||
const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan");
|
const fotoKedatanganInput = form.querySelector(".tps-foto-kedatangan");
|
||||||
const fotoPetugasInput = form.querySelector(".tps-foto-petugas");
|
const fotoPetugasInput = form.querySelector(".tps-foto-petugas");
|
||||||
const namaPetugasInput = form.querySelector(".tps-nama-petugas");
|
const namaPetugasInput = form.querySelector(".tps-nama-petugas");
|
||||||
const btnAddTimbangan = form.querySelector(".tps-btn-add-timbangan");
|
|
||||||
|
|
||||||
fotoKedatanganInput.addEventListener('change', function() {
|
fotoKedatanganInput.addEventListener('change', function() {
|
||||||
if (!this.files || !this.files[0]) return;
|
if (!this.files || !this.files[0]) return;
|
||||||
|
|
@ -937,11 +967,123 @@ const DetailPenjemputan = (function () {
|
||||||
scheduleAutoSave(state.activeTpsIndex);
|
scheduleAutoSave(state.activeTpsIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
btnAddTimbangan.addEventListener('click', function() {
|
const INACTIVE_TAB_COLOR = 'bg-gray-100 text-gray-500 border-gray-200';
|
||||||
createTimbanganItem(form.querySelector('.tps-timbangan-repeater'));
|
const TAB_COLORS = {
|
||||||
syncTimbanganToTpsData();
|
'Residu': { active: 'bg-red-500 text-white border-red-500', inactive: INACTIVE_TAB_COLOR },
|
||||||
refreshSubmitButtonState(form);
|
'Organik': { active: 'bg-green-500 text-white border-green-500', inactive: INACTIVE_TAB_COLOR },
|
||||||
|
'Anorganik': { active: 'bg-blue-500 text-white border-blue-500', inactive: INACTIVE_TAB_COLOR },
|
||||||
|
};
|
||||||
|
const tabBtns = form.querySelectorAll('.timbangan-tab-btn');
|
||||||
|
tabBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const jenis = this.dataset.jenis;
|
||||||
|
const currentTps = state.tpsData[state.activeTpsIndex];
|
||||||
|
currentTps.activeTimbanganTab = jenis;
|
||||||
|
tabBtns.forEach(b => {
|
||||||
|
const cfg = TAB_COLORS[b.dataset.jenis] || { active: 'bg-upst text-white border-upst', inactive: 'bg-gray-50 text-gray-600 border-gray-200' };
|
||||||
|
b.className = `timbangan-tab-btn flex-1 py-2 px-1 rounded-xl text-xs font-bold border transition ${b.dataset.jenis === jenis ? cfg.active : cfg.inactive}`;
|
||||||
});
|
});
|
||||||
|
const jenisLabel = form.querySelector('.active-jenis-label');
|
||||||
|
if (jenisLabel) jenisLabel.textContent = jenis;
|
||||||
|
renderTimbanganRepeater();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const multiFileInput = form.querySelector('.input-foto-timbangan-multi');
|
||||||
|
if (multiFileInput) {
|
||||||
|
multiFileInput.addEventListener('change', async function() {
|
||||||
|
if (!this.files || !this.files.length) return;
|
||||||
|
const currentTps = state.tpsData[state.activeTpsIndex];
|
||||||
|
const activeJenis = currentTps.activeTimbanganTab || CONFIG.DEFAULT_JENIS;
|
||||||
|
const files = Array.from(this.files);
|
||||||
|
const progressEl = form.querySelector('.timbangan-multi-progress');
|
||||||
|
if (progressEl) {
|
||||||
|
progressEl.textContent = `Memproses ${files.length} foto...`;
|
||||||
|
progressEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIndex = currentTps.timbangan.length;
|
||||||
|
files.forEach(() => {
|
||||||
|
currentTps.timbangan.push({
|
||||||
|
file: null,
|
||||||
|
fotoFileName: '',
|
||||||
|
weight: 0,
|
||||||
|
jenisSampah: activeJenis,
|
||||||
|
uploaded: false,
|
||||||
|
ocrInfo: 'AI: memproses...',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
renderTimbanganRepeater();
|
||||||
|
|
||||||
|
const processFile = async (file, timbanganIndex) => {
|
||||||
|
try {
|
||||||
|
const photoNumber = timbanganIndex + 1;
|
||||||
|
const watermarkedFile = await applyWatermark(file, photoNumber);
|
||||||
|
currentTps.timbangan[timbanganIndex].file = watermarkedFile;
|
||||||
|
|
||||||
|
const currentForm = elements.tpsContentContainer.querySelector('form');
|
||||||
|
const itemEl = currentForm ? currentForm.querySelector(`.timbangan-item[data-timbangan-index="${timbanganIndex}"]`) : null;
|
||||||
|
if (itemEl) {
|
||||||
|
const previewWrap = itemEl.querySelector('.input-preview-wrap');
|
||||||
|
const previewImg = itemEl.querySelector('.input-preview-image');
|
||||||
|
if (previewWrap && previewImg) {
|
||||||
|
const localUrl = URL.createObjectURL(watermarkedFile);
|
||||||
|
previewImg.src = localUrl;
|
||||||
|
previewWrap.classList.remove('hidden');
|
||||||
|
previewImg.onload = () => URL.revokeObjectURL(localUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const weightInputProxy = { value: '', placeholder: '' };
|
||||||
|
const ocrInfoProxy = { textContent: 'AI: memproses...' };
|
||||||
|
await autoFillWeight(file, weightInputProxy, ocrInfoProxy);
|
||||||
|
|
||||||
|
const detectedWeight = parseWeightInput(weightInputProxy.value);
|
||||||
|
currentTps.timbangan[timbanganIndex].weight = detectedWeight;
|
||||||
|
currentTps.timbangan[timbanganIndex].ocrInfo = ocrInfoProxy.textContent;
|
||||||
|
|
||||||
|
const currentForm2 = elements.tpsContentContainer.querySelector('form');
|
||||||
|
const itemEl2 = currentForm2 ? currentForm2.querySelector(`.timbangan-item[data-timbangan-index="${timbanganIndex}"]`) : null;
|
||||||
|
if (itemEl2) {
|
||||||
|
const ocrInfoEl = itemEl2.querySelector('.input-ocr-info');
|
||||||
|
const weightDisplay = itemEl2.querySelector('.input-berat-timbangan-display');
|
||||||
|
const weightValue = itemEl2.querySelector('.input-berat-timbangan-value');
|
||||||
|
if (ocrInfoEl) ocrInfoEl.textContent = ocrInfoProxy.textContent;
|
||||||
|
if (weightDisplay) {
|
||||||
|
if (detectedWeight > 0) {
|
||||||
|
weightDisplay.value = formatWeightDisplay(detectedWeight);
|
||||||
|
weightDisplay.placeholder = 'Berat terdeteksi otomatis';
|
||||||
|
} else {
|
||||||
|
weightDisplay.value = '';
|
||||||
|
weightDisplay.placeholder = 'Tidak terbaca, isi manual';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (weightValue) weightValue.value = detectedWeight.toFixed(2);
|
||||||
|
refreshTimbanganUploadState(itemEl2);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (currentTps.timbangan[timbanganIndex]) {
|
||||||
|
currentTps.timbangan[timbanganIndex].ocrInfo = 'AI gagal diproses.';
|
||||||
|
}
|
||||||
|
const currentForm3 = elements.tpsContentContainer.querySelector('form');
|
||||||
|
const itemEl3 = currentForm3 ? currentForm3.querySelector(`.timbangan-item[data-timbangan-index="${timbanganIndex}"]`) : null;
|
||||||
|
if (itemEl3) {
|
||||||
|
const ocrInfoEl = itemEl3.querySelector('.input-ocr-info');
|
||||||
|
if (ocrInfoEl) ocrInfoEl.textContent = 'AI gagal diproses.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateTpsTotalTimbangan();
|
||||||
|
const currentForm4 = elements.tpsContentContainer.querySelector('form');
|
||||||
|
if (currentForm4) refreshSubmitButtonState(currentForm4);
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(files.map((file, i) => processFile(file, startIndex + i)));
|
||||||
|
|
||||||
|
if (progressEl) progressEl.classList.add('hidden');
|
||||||
|
this.value = '';
|
||||||
|
scheduleAutoSave(state.activeTpsIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const btnUploadKedatangan = form.querySelector(
|
const btnUploadKedatangan = form.querySelector(
|
||||||
".tps-btn-upload-kedatangan",
|
".tps-btn-upload-kedatangan",
|
||||||
|
|
@ -962,17 +1104,7 @@ const DetailPenjemputan = (function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreTpsTimbanganItems() {
|
function restoreTpsTimbanganItems() {
|
||||||
const tps = state.tpsData[state.activeTpsIndex];
|
renderTimbanganRepeater();
|
||||||
const form = elements.tpsContentContainer.querySelector("form");
|
|
||||||
const repeater = form.querySelector(".tps-timbangan-repeater");
|
|
||||||
|
|
||||||
if (tps.timbangan.length === 0) {
|
|
||||||
createTimbanganItem(repeater);
|
|
||||||
} else {
|
|
||||||
tps.timbangan.forEach((timb) => {
|
|
||||||
createTimbanganItem(repeater, timb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function restorePhotoPreview() {
|
function restorePhotoPreview() {
|
||||||
|
|
@ -1210,155 +1342,124 @@ const DetailPenjemputan = (function () {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTimbanganItem(repeater, existingData = null) {
|
function renderTimbanganRepeater() {
|
||||||
const photoNumber = repeater.children.length + 1;
|
const tps = state.tpsData[state.activeTpsIndex];
|
||||||
|
const form = elements.tpsContentContainer.querySelector('form');
|
||||||
const item = document.createElement('div');
|
if (!form) return;
|
||||||
item.className = 'timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50';
|
const repeater = form.querySelector('.tps-timbangan-repeater');
|
||||||
item.dataset.photoNumber = photoNumber;
|
if (!repeater) return;
|
||||||
|
const activeTab = tps.activeTimbanganTab || CONFIG.DEFAULT_JENIS;
|
||||||
const weight = existingData ? (existingData.weight || 0) : 0;
|
repeater.innerHTML = '';
|
||||||
const jenisSampah = existingData ? (existingData.jenisSampah || CONFIG.DEFAULT_JENIS) : CONFIG.DEFAULT_JENIS;
|
tps.timbangan.forEach((timb, index) => {
|
||||||
const hasFileBlob = isBrowserFile(existingData?.file);
|
if ((timb.jenisSampah || CONFIG.DEFAULT_JENIS) === activeTab) {
|
||||||
|
createTimbanganItem(repeater, index, timb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
form.querySelectorAll('.timbangan-tab-btn').forEach(btn => {
|
||||||
|
const jenis = btn.dataset.jenis;
|
||||||
|
const count = tps.timbangan.filter(t => (t.jenisSampah || CONFIG.DEFAULT_JENIS) === jenis).length;
|
||||||
|
const countSpan = btn.querySelector('.timbangan-tab-count');
|
||||||
|
if (countSpan) countSpan.textContent = count > 0 ? count + ' foto' : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTimbanganItem(repeater, timbanganIndex, existingData) {
|
||||||
|
const tps = state.tpsData[state.activeTpsIndex];
|
||||||
|
const weight = existingData?.weight || 0;
|
||||||
|
const jenisSampah = existingData?.jenisSampah || CONFIG.DEFAULT_JENIS;
|
||||||
const hasFile = Boolean(hasStoredPhoto(existingData?.file) || existingData?.fotoFileName);
|
const hasFile = Boolean(hasStoredPhoto(existingData?.file) || existingData?.fotoFileName);
|
||||||
const isUploaded = Boolean(existingData?.uploaded);
|
const isUploaded = Boolean(existingData?.uploaded);
|
||||||
const ocrInfoText = existingData && existingData.ocrInfo ? existingData.ocrInfo : (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.');
|
const ocrInfoText = existingData?.ocrInfo || (hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.');
|
||||||
|
const photoNumber = timbanganIndex + 1;
|
||||||
item.innerHTML = `
|
const BADGE_COLORS = {
|
||||||
<div class="flex items-center justify-between">
|
'Organik': 'bg-green-100 text-green-700',
|
||||||
<p class="text-xs font-bold text-gray-600">Item Timbangan #${photoNumber}</p>
|
'Anorganik': 'bg-blue-100 text-blue-700',
|
||||||
<button type="button" class="btn-remove-timbangan text-[11px] font-bold text-red-500">Hapus</button>
|
'Residu': 'bg-red-100 text-red-700',
|
||||||
</div>
|
};
|
||||||
<input type="file" name="FotoTimbangan" accept="image/*" class="input-foto-timbangan block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" />
|
const badgeColor = BADGE_COLORS[jenisSampah] || 'bg-gray-100 text-gray-600';
|
||||||
<div class="${(hasFileBlob || (hasFile && getStoredPhotoUrl(existingData?.file || existingData?.fotoFileName))) ? '' : 'hidden'} input-preview-wrap relative rounded-xl overflow-hidden border border-gray-200 bg-black">
|
|
||||||
<img class="input-preview-image w-full h-44 object-contain" alt="Preview foto timbangan" />
|
|
||||||
</div>
|
|
||||||
<p class="text-[11px] text-gray-500 input-ocr-info">${ocrInfoText}</p>
|
|
||||||
<div class="grid grid-cols-1 gap-2">
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Jenis Sampah</label>
|
|
||||||
<select class="input-jenis-sampah w-full rounded-xl border border-gray-200 px-3 py-2 text-sm">
|
|
||||||
${CONFIG.JENIS_SAMPAH.map((js) => `<option value="${js}" ${js === jenisSampah ? "selected" : ""}>${js}</option>`).join("")}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Berat (kg)</label>
|
|
||||||
<input type="text" inputmode="decimal" class="input-berat-timbangan-display w-full rounded-xl border border-gray-200 px-3 py-2 text-sm" placeholder="Contoh: 54,45" value="${weight > 0 ? formatWeightDisplay(weight) : ""}" />
|
|
||||||
<input type="hidden" class="input-berat-timbangan-value" value="${weight.toFixed(2)}" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="timbangan-upload-state">${getTimbanganUploadStateMarkup(hasFile, isUploaded, weight > 0)}</div>
|
const item = document.createElement('div');
|
||||||
|
item.className = 'timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50';
|
||||||
|
item.dataset.timbanganIndex = String(timbanganIndex);
|
||||||
|
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<p class="text-xs font-bold text-gray-600">Foto #${photoNumber}</p>
|
||||||
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-black ${badgeColor}">${jenisSampah}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="btn-remove-timbangan text-[11px] font-bold text-red-500">Hapus</button>
|
||||||
|
</div>
|
||||||
|
<div class="${hasFile ? '' : 'hidden'} input-preview-wrap relative rounded-xl overflow-hidden border border-gray-200 bg-black">
|
||||||
|
<img class="input-preview-image w-full h-44 object-contain" alt="Preview foto timbangan" />
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] text-gray-500 input-ocr-info">${ocrInfoText}</p>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Berat (kg)</label>
|
||||||
|
<input type="text" inputmode="decimal" class="input-berat-timbangan-display w-full rounded-xl border border-gray-200 px-3 py-2 text-sm" placeholder="Contoh: 54,45" value="${weight > 0 ? formatWeightDisplay(weight) : ''}" />
|
||||||
|
<input type="hidden" class="input-berat-timbangan-value" value="${weight.toFixed(2)}" />
|
||||||
|
</div>
|
||||||
|
<div class="timbangan-upload-state">${getTimbanganUploadStateMarkup(hasFile, isUploaded, weight > 0)}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const fileInput = item.querySelector(".input-foto-timbangan");
|
const previewWrap = item.querySelector('.input-preview-wrap');
|
||||||
const previewWrap = item.querySelector(".input-preview-wrap");
|
const previewImage = item.querySelector('.input-preview-image');
|
||||||
const previewImage = item.querySelector(".input-preview-image");
|
const weightInputDisplay = item.querySelector('.input-berat-timbangan-display');
|
||||||
const ocrInfoEl = item.querySelector(".input-ocr-info");
|
const weightInputValue = item.querySelector('.input-berat-timbangan-value');
|
||||||
const weightInputDisplay = item.querySelector(
|
const removeBtn = item.querySelector('.btn-remove-timbangan');
|
||||||
".input-berat-timbangan-display",
|
|
||||||
);
|
|
||||||
const weightInputValue = item.querySelector(".input-berat-timbangan-value");
|
|
||||||
const jenisSampahSelect = item.querySelector(".input-jenis-sampah");
|
|
||||||
const removeBtn = item.querySelector(".btn-remove-timbangan");
|
|
||||||
|
|
||||||
if (existingData && hasStoredPhoto(existingData.file)) {
|
if (existingData && hasStoredPhoto(existingData.file)) {
|
||||||
const localUrl = getStoredPhotoUrl(existingData.file);
|
const localUrl = getStoredPhotoUrl(existingData.file);
|
||||||
previewImage.src = localUrl;
|
previewImage.src = localUrl;
|
||||||
previewWrap.classList.remove('hidden');
|
previewWrap.classList.remove('hidden');
|
||||||
previewImage.onload = function() {
|
previewImage.onload = function() {
|
||||||
if (!isBrowserFile(resolveStoredPhoto(existingData.file))) return;
|
if (!isBrowserFile(resolveStoredPhoto(existingData.file))) return;
|
||||||
URL.revokeObjectURL(localUrl);
|
URL.revokeObjectURL(localUrl);
|
||||||
};
|
};
|
||||||
} else if (existingData && existingData.fotoFileName && existingData.fotoFileName.startsWith('/')) {
|
} else if (existingData && existingData.fotoFileName && existingData.fotoFileName.startsWith('/')) {
|
||||||
previewImage.src = existingData.fotoFileName;
|
previewImage.src = existingData.fotoFileName;
|
||||||
previewWrap.classList.remove('hidden');
|
previewWrap.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInput.addEventListener('change', async function() {
|
|
||||||
if (fileInput.files && fileInput.files[0]) {
|
|
||||||
const originalFile = fileInput.files[0];
|
|
||||||
|
|
||||||
const watermarkedFile = await applyWatermark(originalFile, photoNumber);
|
|
||||||
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
dataTransfer.items.add(watermarkedFile);
|
|
||||||
fileInput.files = dataTransfer.files;
|
|
||||||
|
|
||||||
const localUrl = URL.createObjectURL(watermarkedFile);
|
|
||||||
previewImage.src = localUrl;
|
|
||||||
previewWrap.classList.remove('hidden');
|
|
||||||
previewImage.onload = function() {
|
|
||||||
URL.revokeObjectURL(localUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
await autoFillWeight(watermarkedFile, weightInputDisplay, ocrInfoEl);
|
|
||||||
const parsed = parseWeightInput(weightInputDisplay.value);
|
|
||||||
weightInputValue.value = parsed.toFixed(2);
|
|
||||||
updateTpsTotalTimbangan();
|
|
||||||
syncTimbanganToTpsData();
|
|
||||||
|
|
||||||
const tps = state.tpsData[state.activeTpsIndex];
|
|
||||||
const itemIndex = Array.from(repeater.children).indexOf(item);
|
|
||||||
if (itemIndex >= 0 && tps.timbangan[itemIndex]) {
|
|
||||||
tps.timbangan[itemIndex].uploaded = false;
|
|
||||||
tps.timbangan[itemIndex].fotoFileName = '';
|
|
||||||
refreshTimbanganUploadState(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
weightInputDisplay.addEventListener('input', function() {
|
weightInputDisplay.addEventListener('input', function() {
|
||||||
const cleaned = this.value.replace(/[^0-9.,]/g, '');
|
const cleaned = this.value.replace(/[^0-9.,]/g, '');
|
||||||
this.value = cleaned;
|
this.value = cleaned;
|
||||||
const parsed = parseWeightInput(cleaned);
|
const parsed = parseWeightInput(cleaned);
|
||||||
weightInputValue.value = parsed.toFixed(2);
|
weightInputValue.value = parsed.toFixed(2);
|
||||||
updateTpsTotalTimbangan();
|
const idx = parseInt(item.dataset.timbanganIndex, 10);
|
||||||
syncTimbanganToTpsData();
|
if (tps.timbangan[idx]) tps.timbangan[idx].weight = parsed;
|
||||||
refreshTimbanganUploadState(item);
|
updateTpsTotalTimbangan();
|
||||||
const form = elements.tpsContentContainer.querySelector('form');
|
refreshTimbanganUploadState(item);
|
||||||
if (form) refreshSubmitButtonState(form);
|
const form = elements.tpsContentContainer.querySelector('form');
|
||||||
scheduleAutoSave(state.activeTpsIndex);
|
if (form) refreshSubmitButtonState(form);
|
||||||
|
scheduleAutoSave(state.activeTpsIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
weightInputDisplay.addEventListener("blur", function () {
|
weightInputDisplay.addEventListener('blur', function() {
|
||||||
const parsed = parseWeightInput(this.value);
|
const parsed = parseWeightInput(this.value);
|
||||||
if (parsed > 0) {
|
if (parsed > 0) {
|
||||||
this.value = formatWeightDisplay(parsed);
|
this.value = formatWeightDisplay(parsed);
|
||||||
weightInputValue.value = parsed.toFixed(2);
|
weightInputValue.value = parsed.toFixed(2);
|
||||||
} else {
|
} else {
|
||||||
this.value = "";
|
this.value = '';
|
||||||
weightInputValue.value = "0.00";
|
weightInputValue.value = '0.00';
|
||||||
}
|
}
|
||||||
updateTpsTotalTimbangan();
|
const idx = parseInt(item.dataset.timbanganIndex, 10);
|
||||||
syncTimbanganToTpsData();
|
if (tps.timbangan[idx]) tps.timbangan[idx].weight = parsed;
|
||||||
refreshTimbanganUploadState(item);
|
updateTpsTotalTimbangan();
|
||||||
const form = elements.tpsContentContainer.querySelector("form");
|
refreshTimbanganUploadState(item);
|
||||||
if (form) refreshSubmitButtonState(form);
|
const form = elements.tpsContentContainer.querySelector('form');
|
||||||
});
|
if (form) refreshSubmitButtonState(form);
|
||||||
|
|
||||||
jenisSampahSelect.addEventListener('change', function() {
|
|
||||||
updateTpsTotalTimbangan();
|
|
||||||
syncTimbanganToTpsData();
|
|
||||||
const form = elements.tpsContentContainer.querySelector('form');
|
|
||||||
if (form) refreshSubmitButtonState(form);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
removeBtn.addEventListener('click', function() {
|
removeBtn.addEventListener('click', function() {
|
||||||
item.remove();
|
const idx = parseInt(item.dataset.timbanganIndex, 10);
|
||||||
const form = elements.tpsContentContainer.querySelector('form');
|
tps.timbangan.splice(idx, 1);
|
||||||
const repeater = form ? form.querySelector('.tps-timbangan-repeater') : null;
|
renderTimbanganRepeater();
|
||||||
|
updateTpsTotalTimbangan();
|
||||||
if (repeater) {
|
const form = elements.tpsContentContainer.querySelector('form');
|
||||||
renumberTimbanganItems(repeater);
|
if (form) refreshSubmitButtonState(form);
|
||||||
if (repeater.children.length === 0) {
|
scheduleAutoSave(state.activeTpsIndex);
|
||||||
createTimbanganItem(repeater);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTpsTotalTimbangan();
|
|
||||||
syncTimbanganToTpsData();
|
|
||||||
if (form) refreshSubmitButtonState(form);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
repeater.appendChild(item);
|
repeater.appendChild(item);
|
||||||
|
|
@ -1368,7 +1469,7 @@ const DetailPenjemputan = (function () {
|
||||||
|
|
||||||
function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) {
|
function getTimbanganUploadStateMarkup(hasFile, isUploaded, hasValidWeight) {
|
||||||
if (!hasFile) {
|
if (!hasFile) {
|
||||||
return '<p class="text-[11px] text-gray-400">Pilih foto timbangan terlebih dahulu</p>';
|
return '<p class="text-[11px] text-gray-400">Foto sedang diproses AI (processing...)</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUploaded) {
|
if (isUploaded) {
|
||||||
|
|
@ -1535,12 +1636,10 @@ const DetailPenjemputan = (function () {
|
||||||
const stateContainer = item.querySelector(".timbangan-upload-state");
|
const stateContainer = item.querySelector(".timbangan-upload-state");
|
||||||
if (!stateContainer) return;
|
if (!stateContainer) return;
|
||||||
|
|
||||||
const repeater = item.parentElement;
|
|
||||||
const itemIndex = repeater ? Array.from(repeater.children).indexOf(item) : -1;
|
|
||||||
const tps = state.tpsData[state.activeTpsIndex];
|
const tps = state.tpsData[state.activeTpsIndex];
|
||||||
const currentData = itemIndex >= 0 ? tps.timbangan[itemIndex] : null;
|
const itemIndex = parseInt(item.dataset.timbanganIndex, 10);
|
||||||
const fileInput = item.querySelector('.input-foto-timbangan');
|
const currentData = !isNaN(itemIndex) ? tps.timbangan[itemIndex] : null;
|
||||||
const hasFile = Boolean(currentData?.file || fileInput?.files?.[0] || currentData?.fotoFileName);
|
const hasFile = Boolean(currentData?.file || currentData?.fotoFileName);
|
||||||
const isUploaded = Boolean(currentData?.uploaded);
|
const isUploaded = Boolean(currentData?.uploaded);
|
||||||
const weightInputValue = item.querySelector('.input-berat-timbangan-value');
|
const weightInputValue = item.querySelector('.input-berat-timbangan-value');
|
||||||
const currentWeight = currentData?.weight ?? parseWeightInput(weightInputValue?.value || '0');
|
const currentWeight = currentData?.weight ?? parseWeightInput(weightInputValue?.value || '0');
|
||||||
|
|
@ -1555,9 +1654,7 @@ const DetailPenjemputan = (function () {
|
||||||
const uploadBtn = stateContainer.querySelector(".btn-upload-timbangan");
|
const uploadBtn = stateContainer.querySelector(".btn-upload-timbangan");
|
||||||
if (uploadBtn) {
|
if (uploadBtn) {
|
||||||
uploadBtn.addEventListener("click", function () {
|
uploadBtn.addEventListener("click", function () {
|
||||||
const latestIndex = repeater
|
const latestIndex = parseInt(item.dataset.timbanganIndex, 10);
|
||||||
? Array.from(repeater.children).indexOf(item)
|
|
||||||
: -1;
|
|
||||||
uploadSingleFotoTimbangan(latestIndex, item);
|
uploadSingleFotoTimbangan(latestIndex, item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1755,27 +1852,39 @@ const DetailPenjemputan = (function () {
|
||||||
if (ocrInfoEl) ocrInfoEl.textContent = "AI: memproses gambar...";
|
if (ocrInfoEl) ocrInfoEl.textContent = "AI: memproses gambar...";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const img = await readFileAsImage(file);
|
|
||||||
let bestRawText = "";
|
let bestRawText = "";
|
||||||
let isSuccess = false;
|
let isSuccess = false;
|
||||||
|
|
||||||
for (const area of CONFIG.OCR_AREAS) {
|
const fullResult = await requestOpenRouterWeight(file);
|
||||||
const cropCanvas = createCropCanvas(img, area);
|
if (fullResult && fullResult.success && fullResult.weight) {
|
||||||
const cropFile = await canvasToJpegFile(
|
guessedWeight = parseWeightInput(fullResult.weight);
|
||||||
cropCanvas,
|
bestRawText = fullResult.raw || fullResult.weight;
|
||||||
`crop-${area.id}.jpg`,
|
isSuccess = guessedWeight > 0;
|
||||||
);
|
} else if (fullResult && fullResult.raw) {
|
||||||
const aiResult = await requestOpenRouterWeight(cropFile);
|
bestRawText = fullResult.raw;
|
||||||
|
}
|
||||||
|
|
||||||
if (aiResult && aiResult.success && aiResult.weight) {
|
if (!isSuccess) {
|
||||||
guessedWeight = parseWeightInput(aiResult.weight);
|
const img = await readFileAsImage(file);
|
||||||
bestRawText = aiResult.raw || aiResult.weight;
|
|
||||||
isSuccess = guessedWeight > 0;
|
|
||||||
if (isSuccess) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aiResult && aiResult.raw) {
|
for (const area of CONFIG.OCR_AREAS) {
|
||||||
bestRawText = aiResult.raw;
|
const cropCanvas = createCropCanvas(img, area);
|
||||||
|
const cropFile = await canvasToJpegFile(
|
||||||
|
cropCanvas,
|
||||||
|
`crop-${area.id}.jpg`,
|
||||||
|
);
|
||||||
|
const aiResult = await requestOpenRouterWeight(cropFile);
|
||||||
|
|
||||||
|
if (aiResult && aiResult.success && aiResult.weight) {
|
||||||
|
guessedWeight = parseWeightInput(aiResult.weight);
|
||||||
|
bestRawText = aiResult.raw || aiResult.weight;
|
||||||
|
isSuccess = guessedWeight > 0;
|
||||||
|
if (isSuccess) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aiResult && aiResult.raw) {
|
||||||
|
bestRawText = aiResult.raw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1868,31 +1977,34 @@ const DetailPenjemputan = (function () {
|
||||||
|
|
||||||
function updateTpsTotalTimbangan() {
|
function updateTpsTotalTimbangan() {
|
||||||
const tps = state.tpsData[state.activeTpsIndex];
|
const tps = state.tpsData[state.activeTpsIndex];
|
||||||
const form = elements.tpsContentContainer.querySelector("form");
|
|
||||||
if (!form) return;
|
|
||||||
|
|
||||||
let totalOrganik = 0.0;
|
let totalOrganik = 0.0;
|
||||||
let totalAnorganik = 0.0;
|
let totalAnorganik = 0.0;
|
||||||
let totalResidu = 0.0;
|
let totalResidu = 0.0;
|
||||||
|
|
||||||
const repeater = form.querySelector(".tps-timbangan-repeater");
|
const form = elements.tpsContentContainer.querySelector("form");
|
||||||
const items = repeater.querySelectorAll(".timbangan-item");
|
if (form) {
|
||||||
|
const repeater = form.querySelector(".tps-timbangan-repeater");
|
||||||
|
if (repeater) {
|
||||||
|
repeater.querySelectorAll(".timbangan-item").forEach(function (item) {
|
||||||
|
const idx = parseInt(item.dataset.timbanganIndex, 10);
|
||||||
|
const weightInput = item.querySelector(".input-berat-timbangan-value");
|
||||||
|
if (!isNaN(idx) && tps.timbangan[idx] && weightInput) {
|
||||||
|
tps.timbangan[idx].weight = parseWeightInput(weightInput.value || "0");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items.forEach(function (item) {
|
tps.timbangan.forEach(function (timb) {
|
||||||
const weightInput = item.querySelector(".input-berat-timbangan-value");
|
const value = timb.weight || 0;
|
||||||
const jenisSampahSelect = item.querySelector(".input-jenis-sampah");
|
const jenis = timb.jenisSampah || CONFIG.DEFAULT_JENIS;
|
||||||
|
if (jenis === "Organik") {
|
||||||
if (weightInput && jenisSampahSelect) {
|
totalOrganik += value;
|
||||||
const value = parseWeightInput(weightInput.value || "0");
|
} else if (jenis === "Anorganik") {
|
||||||
const jenis = jenisSampahSelect.value;
|
totalAnorganik += value;
|
||||||
|
} else {
|
||||||
if (jenis === "Organik") {
|
totalResidu += value;
|
||||||
totalOrganik += value;
|
|
||||||
} else if (jenis === "Anorganik") {
|
|
||||||
totalAnorganik += value;
|
|
||||||
} else if (jenis === "Residu") {
|
|
||||||
totalResidu += value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1901,23 +2013,16 @@ const DetailPenjemputan = (function () {
|
||||||
tps.totalResidu = totalResidu;
|
tps.totalResidu = totalResidu;
|
||||||
tps.totalTimbangan = totalOrganik + totalAnorganik + totalResidu;
|
tps.totalTimbangan = totalOrganik + totalAnorganik + totalResidu;
|
||||||
|
|
||||||
const displayTotalOrganik = form.querySelector(
|
if (form) {
|
||||||
".tps-display-total-organik",
|
const displayTotalOrganik = form.querySelector(".tps-display-total-organik");
|
||||||
);
|
const displayTotalAnorganik = form.querySelector(".tps-display-total-anorganik");
|
||||||
const displayTotalAnorganik = form.querySelector(
|
const displayTotalResidu = form.querySelector(".tps-display-total-residu");
|
||||||
".tps-display-total-anorganik",
|
const displayTotal = form.querySelector(".tps-display-total");
|
||||||
);
|
if (displayTotalOrganik) displayTotalOrganik.textContent = formatWeightDisplay(totalOrganik);
|
||||||
const displayTotalResidu = form.querySelector(".tps-display-total-residu");
|
if (displayTotalAnorganik) displayTotalAnorganik.textContent = formatWeightDisplay(totalAnorganik);
|
||||||
const displayTotal = form.querySelector(".tps-display-total");
|
if (displayTotalResidu) displayTotalResidu.textContent = formatWeightDisplay(totalResidu);
|
||||||
|
if (displayTotal) displayTotal.textContent = formatWeightDisplay(tps.totalTimbangan);
|
||||||
if (displayTotalOrganik)
|
}
|
||||||
displayTotalOrganik.textContent = formatWeightDisplay(totalOrganik);
|
|
||||||
if (displayTotalAnorganik)
|
|
||||||
displayTotalAnorganik.textContent = formatWeightDisplay(totalAnorganik);
|
|
||||||
if (displayTotalResidu)
|
|
||||||
displayTotalResidu.textContent = formatWeightDisplay(totalResidu);
|
|
||||||
if (displayTotal)
|
|
||||||
displayTotal.textContent = formatWeightDisplay(tps.totalTimbangan);
|
|
||||||
|
|
||||||
updateAllTotals();
|
updateAllTotals();
|
||||||
saveState();
|
saveState();
|
||||||
|
|
@ -1956,31 +2061,19 @@ const DetailPenjemputan = (function () {
|
||||||
|
|
||||||
function syncTimbanganToTpsData() {
|
function syncTimbanganToTpsData() {
|
||||||
const tps = state.tpsData[state.activeTpsIndex];
|
const tps = state.tpsData[state.activeTpsIndex];
|
||||||
const form = elements.tpsContentContainer.querySelector("form");
|
const form = elements.tpsContentContainer.querySelector('form');
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
|
const repeater = form.querySelector('.tps-timbangan-repeater');
|
||||||
const repeater = form.querySelector('.tps-timbangan-repeater');
|
if (!repeater) return;
|
||||||
const items = repeater.querySelectorAll('.timbangan-item');
|
repeater.querySelectorAll('.timbangan-item').forEach(item => {
|
||||||
const previousTimbangan = [...tps.timbangan];
|
const idx = parseInt(item.dataset.timbanganIndex, 10);
|
||||||
|
if (isNaN(idx) || !tps.timbangan[idx]) return;
|
||||||
tps.timbangan = [];
|
const weightValue = item.querySelector('.input-berat-timbangan-value');
|
||||||
items.forEach((item, index) => {
|
const ocrInfo = item.querySelector('.input-ocr-info');
|
||||||
const fileInput = item.querySelector('.input-foto-timbangan');
|
if (weightValue) tps.timbangan[idx].weight = parseWeightInput(weightValue.value);
|
||||||
const weightValue = item.querySelector('.input-berat-timbangan-value');
|
if (ocrInfo) tps.timbangan[idx].ocrInfo = ocrInfo.textContent;
|
||||||
const jenisSampahSelect = item.querySelector('.input-jenis-sampah');
|
});
|
||||||
const ocrInfo = item.querySelector('.input-ocr-info')?.textContent || 'OCR: belum diproses.';
|
}
|
||||||
const existingData = previousTimbangan[index];
|
|
||||||
|
|
||||||
tps.timbangan.push({
|
|
||||||
file: fileInput.files[0] || (existingData ? existingData.file : null),
|
|
||||||
fotoFileName: existingData ? existingData.fotoFileName || '' : '',
|
|
||||||
weight: parseWeightInput(weightValue.value),
|
|
||||||
jenisSampah: jenisSampahSelect.value,
|
|
||||||
uploaded: existingData ? existingData.uploaded : false,
|
|
||||||
ocrInfo
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadSingleFotoTimbangan(itemIndex, targetItem = null) {
|
async function uploadSingleFotoTimbangan(itemIndex, targetItem = null) {
|
||||||
const tps = state.tpsData[state.activeTpsIndex];
|
const tps = state.tpsData[state.activeTpsIndex];
|
||||||
|
|
@ -2025,8 +2118,6 @@ const DetailPenjemputan = (function () {
|
||||||
if (res.ok && data.success) {
|
if (res.ok && data.success) {
|
||||||
timbanganItem.uploaded = true;
|
timbanganItem.uploaded = true;
|
||||||
timbanganItem.fotoFileName = data.fileUrl || data.fileName || '';
|
timbanganItem.fotoFileName = data.fileUrl || data.fileName || '';
|
||||||
const fotoInputTimb = targetItem?.querySelector('.input-foto-timbangan');
|
|
||||||
if (fotoInputTimb) fotoInputTimb.value = '';
|
|
||||||
showToast(data.message || `Foto timbangan #${itemIndex + 1} berhasil diupload.`, 'success');
|
showToast(data.message || `Foto timbangan #${itemIndex + 1} berhasil diupload.`, 'success');
|
||||||
} else {
|
} else {
|
||||||
showToast(data.message || 'Gagal upload foto timbangan.', 'error');
|
showToast(data.message || 'Gagal upload foto timbangan.', 'error');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue