update OCR KTP

parent 7c74f037
......@@ -87,8 +87,8 @@ class AuditLog(models.Model):
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
verbose_name = _('Audit Log (Rekam Jejak MPWN)')
verbose_name_plural = _('Audit Logs (Rekam Jejak MPWN)')
verbose_name = _('Audit Trail MPWN')
verbose_name_plural = _('Audit Trail MPWN')
ordering = ['-timestamp']
def __str__(self):
......
......@@ -48,13 +48,37 @@ def status_badge_class(status_code):
@register.filter(name='urlencode_text')
def urlencode_text(value):
"""
URL encode teks untuk tautan pesan WhatsApp wa.me/?text=...
URL encode teks untuk tautan WhatsApp wa.me/?text=...
"""
if not value:
return ''
return urllib.parse.quote(str(value))
_SISTEM_LABEL_ID = {
'Group results': 'Hasil Grup',
'Task results': 'Hasil Task',
'Groups': 'Grup',
'Tasks': 'Tugas',
'group results': 'Hasil Grup',
'task results': 'Hasil Task',
'group result': 'Hasil Grup',
'task result': 'Hasil Task',
'Celery Results': 'Hasil Celery',
'Django Celery Results': 'Hasil Celery',
}
@register.filter(name='bahasa')
def bahasa(value):
"""
Terjemahkan label admin berbahasa Inggris (umumnya dari modul pihak ketiga
seperti django_celery_results) ke Bahasa Indonesia pada lapisan penyajian.
Nilai yang tidak dikenal dikembalikan apa adanya.
"""
return _SISTEM_LABEL_ID.get(str(value), value)
@register.simple_tag
def get_legal_dashboard_stats(user=None):
"""
......
......@@ -24,12 +24,174 @@ class OCREngineService:
image = image.filter(ImageFilter.SHARPEN)
return image
@classmethod
def _extract_via_apple_vision(cls, image_bytes: bytes) -> str:
"""
Engine OCR bawaan Apple Vision Framework (macOS Monterey, Ventura, Sonoma, Sequoia+).
Berjalan native melalui ctypes tanpa perlu instalasi binary eksternal di macOS.
"""
import sys
if sys.platform != 'darwin' or not image_bytes:
return ""
try:
import ctypes
import ctypes.util
objc_path = ctypes.util.find_library('objc')
if not objc_path:
return ""
objc = ctypes.cdll.LoadLibrary(objc_path)
objc.objc_getClass.restype = ctypes.c_void_p
objc.objc_getClass.argtypes = [ctypes.c_char_p]
objc.sel_registerName.restype = ctypes.c_void_p
objc.sel_registerName.argtypes = [ctypes.c_char_p]
ctypes.cdll.LoadLibrary('/System/Library/Frameworks/Foundation.framework/Foundation')
ctypes.cdll.LoadLibrary('/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics')
ctypes.cdll.LoadLibrary('/System/Library/Frameworks/ImageIO.framework/ImageIO')
ctypes.cdll.LoadLibrary('/System/Library/Frameworks/Vision.framework/Vision')
def msg(target, sel_name, *args, restype=ctypes.c_void_p, argtypes=None):
if argtypes is None:
argtypes = [ctypes.c_void_p] * len(args)
func = objc.objc_msgSend
func.restype = restype
func.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + argtypes
return func(target, objc.sel_registerName(sel_name.encode('utf-8')), *args)
NSData = objc.objc_getClass(b'NSData')
ns_data = msg(NSData, 'dataWithBytes:length:', image_bytes, len(image_bytes), argtypes=[ctypes.c_char_p, ctypes.c_size_t])
if not ns_data:
return ""
VNImageRequestHandler = objc.objc_getClass(b'VNImageRequestHandler')
handler = msg(msg(VNImageRequestHandler, 'alloc'), 'initWithData:options:', ns_data, None)
if not handler:
return ""
VNRecognizeTextRequest = objc.objc_getClass(b'VNRecognizeTextRequest')
req = msg(msg(VNRecognizeTextRequest, 'alloc'), 'init')
if not req:
return ""
# Level 0 = accurate
msg(req, 'setRecognitionLevel:', 0, restype=None, argtypes=[ctypes.c_long])
msg(req, 'setUsesLanguageCorrection:', 1, restype=None, argtypes=[ctypes.c_bool])
class CGRect(ctypes.Structure):
_fields_ = [('x', ctypes.c_double), ('y', ctypes.c_double), ('w', ctypes.c_double), ('h', ctypes.c_double)]
NSArray = objc.objc_getClass(b'NSArray')
reqs = msg(NSArray, 'arrayWithObject:', req)
error = ctypes.c_void_p()
success = msg(handler, 'performRequests:error:', reqs, ctypes.byref(error), restype=ctypes.c_bool, argtypes=[ctypes.c_void_p, ctypes.c_void_p])
if not success:
return ""
results = msg(req, 'results')
count = msg(results, 'count', restype=ctypes.c_ulong)
if not count:
return ""
items = []
for i in range(count):
obs = msg(results, 'objectAtIndex:', i, restype=ctypes.c_void_p, argtypes=[ctypes.c_ulong])
cands = msg(obs, 'topCandidates:', 1, restype=ctypes.c_void_p, argtypes=[ctypes.c_ulong])
if not cands:
continue
cand = msg(cands, 'objectAtIndex:', 0, restype=ctypes.c_void_p, argtypes=[ctypes.c_ulong])
ns_str = msg(cand, 'string')
utf8 = msg(ns_str, 'UTF8String', restype=ctypes.c_char_p)
text = utf8.decode('utf-8') if utf8 else ''
func_box = objc.objc_msgSend
func_box.restype = CGRect
func_box.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
box = func_box(obs, objc.sel_registerName(b'boundingBox'))
top = 1.0 - (box.y + box.h)
items.append({'text': text, 'x': box.x, 'top': top})
# Kelompokkan teks berdasarkan baris horizontal (top) dan urutkan kiri ke kanan (x)
sorted_items = sorted(items, key=lambda it: it['top'])
lines = []
current_line = []
current_top = None
for it in sorted_items:
# Abaikan teks cap/tanda tangan di pojok kanan bawah
if it['x'] > 0.68 and it['top'] > 0.68:
continue
if current_top is None or abs(it['top'] - current_top) < 0.025:
current_line.append(it)
if current_top is None:
current_top = it['top']
else:
current_line.sort(key=lambda x: x['x'])
lines.append(' '.join(x['text'] for x in current_line))
current_line = [it]
current_top = it['top']
if current_line:
current_line.sort(key=lambda x: x['x'])
lines.append(' '.join(x['text'] for x in current_line))
return '\n'.join(lines)
except Exception as exc:
logger.debug(f"Apple Vision extraction failed: {exc}")
return ""
@classmethod
def _extract_via_tesseract_cli(cls, image_bytes: bytes) -> str:
"""
Fallback eksekusi langsung binary tesseract CLI tanpa dependency modul pytesseract.
Cocok untuk environment Docker Linux Debian/Ubuntu (tesseract-ocr).
"""
import shutil, subprocess, tempfile
tesseract_bin = shutil.which('tesseract')
if not tesseract_bin or not image_bytes:
return ""
try:
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=True) as tmp:
tmp.write(image_bytes)
tmp.flush()
try:
res = subprocess.run(
[tesseract_bin, tmp.name, 'stdout', '-l', 'ind+eng'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=10,
check=True
)
text = res.stdout.decode('utf-8', errors='ignore')
if len(text.strip()) > 10:
return text
except Exception:
res = subprocess.run(
[tesseract_bin, tmp.name, 'stdout'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=10,
check=True
)
text = res.stdout.decode('utf-8', errors='ignore')
if len(text.strip()) > 10:
return text
except Exception as exc:
logger.debug(f"Tesseract CLI extraction failed: {exc}")
return ""
@classmethod
def extract_raw_text(cls, image_bytes: bytes) -> str:
"""
Ekstraksi teks mentah dari berkas dokumen (PDF / Citra).
Mendukung pypdf untuk berkas PDF, serta Pytesseract / EasyOCR untuk berkas citra.
Menyediakan fallback terisolasi jika library biner C++ belum diinisialisasi di OS host.
Ekstraksi teks cerdas multi-engine dari berkas dokumen (PDF / Citra).
Prioritas urutan engine:
1. pypdf untuk berkas PDF elektronik.
2. Apple Vision Framework (macOS native).
3. Pytesseract Python library.
4. Tesseract CLI binary langsung (/usr/bin/tesseract di Linux/Docker).
5. EasyOCR library (jika terpasang).
6. UTF-8 raw text decode fallback.
"""
if not image_bytes:
return ""
......@@ -50,7 +212,12 @@ class OCREngineService:
except Exception as e:
logger.debug(f"pypdf extraction failed or PDF encrypted: {e}")
# 2. Coba ekstraksi Pytesseract jika binary Tesseract terpasang
# 2. Engine macOS Apple Vision (Kinerja tinggi & akurat untuk KTP/dokumen di Mac)
vision_text = cls._extract_via_apple_vision(image_bytes)
if vision_text and len(vision_text.strip()) > 10:
return vision_text
# 3. Coba ekstraksi Pytesseract jika modul terpasang
try:
import pytesseract
img = cls.preprocess_image(image_bytes)
......@@ -60,7 +227,12 @@ class OCREngineService:
except Exception as e:
logger.debug(f"Pytesseract not available or failed: {e}")
# 3. Coba ekstraksi EasyOCR jika modul terpasang
# 4. Coba eksekusi langsung Tesseract CLI binary
cli_text = cls._extract_via_tesseract_cli(image_bytes)
if cli_text and len(cli_text.strip()) > 10:
return cli_text
# 5. Coba ekstraksi EasyOCR jika modul terpasang
try:
import easyocr
reader = easyocr.Reader(['id', 'en'], gpu=False)
......@@ -71,8 +243,7 @@ class OCREngineService:
except Exception as e:
logger.debug(f"EasyOCR not available or failed: {e}")
# 4. Jika berkas adalah teks mentah (misal UTF-8 payload atau fixture)
# Pastikan BUKAN citra biner terkompresi sebelum mencoba decode utf-8
# 6. Jika berkas adalah teks mentah (misal UTF-8 payload atau fixture)
is_binary_format = (
image_bytes.startswith(b'\xff\xd8\xff') or # JPEG
image_bytes.startswith(b'\x89PNG') or # PNG
......@@ -96,8 +267,8 @@ class OCREngineService:
def parse_ktp(cls, raw_text: str) -> Dict[str, Any]:
"""
Parser Khusus e-KTP Indonesia:
Mengekstrak NIK (16 digit), Nama, Tempat/Tgl Lahir, Alamat, RT/RW,
Kel/Desa, Kecamatan, Agama, Status Perkawinan, Pekerjaan.
Mengekstrak NIK (16 digit), Nama (termasuk multi-line), Tempat/Tgl Lahir,
Alamat, RT/RW, Kel/Desa, Kecamatan, Agama, Status Perkawinan, Pekerjaan, Golongan Darah.
"""
data: Dict[str, Any] = {
'document_type': 'KTP',
......@@ -105,7 +276,9 @@ class OCREngineService:
'nama': None,
'tempat_tanggal_lahir': None,
'jenis_kelamin': None,
'golongan_darah': None,
'alamat': None,
'alamat_lengkap': None,
'rt_rw': None,
'kel_desa': None,
'kecamatan': None,
......@@ -117,48 +290,91 @@ class OCREngineService:
'raw_text_length': len(raw_text)
}
# NIK (16 digit angka)
nik_match = re.search(r'(?:NIK|Nomor|NIK\s*:?)\s*([0-9]{16})', raw_text, re.IGNORECASE)
# 1. NIK (16 digit angka)
nik_match = re.search(r'(?:NIK|Nomor|NIK\s*:?)\s*[:;]?\s*([0-9]{16})', raw_text, re.IGNORECASE)
if not nik_match:
nik_match = re.search(r'\b([0-9]{16})\b', raw_text)
if nik_match:
data['nik'] = nik_match.group(1)
# Nama
nama_match = re.search(r'Nama\s*[:;]?\s*([A-Z\s\.,\']+?)(?=\n|Tempat|Jenis|Lahir|$)', raw_text, re.IGNORECASE)
# 2. Nama (Mendukung nama panjang multi-line sampai batas label berikutnya)
nama_match = re.search(r'Nama\s*[:;]?\s*([A-Z\s\.,\'-]+?)(?=\n\s*(?:Tempat|Jenis|Lahir|Alamat)|$)', raw_text, re.IGNORECASE)
if nama_match:
data['nama'] = nama_match.group(1).strip()
clean_nama = ' '.join(nama_match.group(1).split())
data['nama'] = clean_nama.strip()
# Tempat / Tgl Lahir
ttl_match = re.search(r'(?:Tempat/Tgl Lahir|Tempat.*?Lahir)\s*[:;]?\s*([^\n]+)', raw_text, re.IGNORECASE)
# 3. Tempat / Tgl Lahir
ttl_match = re.search(r'(?:Tempat/Tg[il]\s*Lahir|Tempat.*?Lahir)\s*[:;]?\s*([^\n]+)', raw_text, re.IGNORECASE)
if ttl_match:
data['tempat_tanggal_lahir'] = ttl_match.group(1).strip()
# Jenis Kelamin
# 4. Jenis Kelamin
jk_match = re.search(r'(LAKI-LAKI|PEREMPUAN|PRIA|WANITA)', raw_text, re.IGNORECASE)
if jk_match:
data['jenis_kelamin'] = jk_match.group(1).upper()
# Alamat
# 5. Golongan Darah
gol_match = re.search(r'Gol\.?\s*Darah\s*[:;]?\s*([A-Z0-9\+\-]+)', raw_text, re.IGNORECASE)
if gol_match:
data['golongan_darah'] = gol_match.group(1).strip()
# 6. Alamat Pokok
alamat_match = re.search(r'Alamat\s*[:;]?\s*([^\n]+)', raw_text, re.IGNORECASE)
if alamat_match:
data['alamat'] = alamat_match.group(1).strip()
# RT/RW
rtrw_match = re.search(r'RT/RW\s*[:;]?\s*([0-9]{2,3}/[0-9]{2,3})', raw_text, re.IGNORECASE)
# 7. RT/RW
rtrw_match = re.search(r'RT/?RW\s*[:;]?\s*([0-9]{2,3}\s*/\s*[0-9]{2,3})', raw_text, re.IGNORECASE)
if rtrw_match:
data['rt_rw'] = rtrw_match.group(1)
# Status Perkawinan
data['rt_rw'] = rtrw_match.group(1).replace(' ', '')
# 8. Kel/Desa
kel_match = re.search(r'(?:Kel/?Desa|Kelurahan|Desa)\s*[:;]?\s*([A-Za-z0-9\s\.\-]+?)(?=\n|Kecamatan|$)', raw_text, re.IGNORECASE)
if kel_match:
data['kel_desa'] = kel_match.group(1).strip()
# 9. Kecamatan
kec_match = re.search(r'Kecamatan\s*[:;]?\s*([A-Za-z0-9\s\.\-]+?)(?=\n|Agama|$)', raw_text, re.IGNORECASE)
if kec_match:
data['kecamatan'] = kec_match.group(1).strip()
# Alamat Lengkap Terkonsolidasi
addr_parts = []
if data['alamat']:
addr_parts.append(data['alamat'])
if data['rt_rw']:
addr_parts.append(f"RT {data['rt_rw']}")
if data['kel_desa']:
addr_parts.append(f"Desa/Kel. {data['kel_desa']}")
if data['kecamatan']:
addr_parts.append(f"Kec. {data['kecamatan']}")
data['alamat_lengkap'] = ", ".join(addr_parts) if addr_parts else data['alamat']
# 10. Agama
agama_match = re.search(r'Agama\s*[:;]?\s*([A-Za-z]+)', raw_text, re.IGNORECASE)
if agama_match:
data['agama'] = agama_match.group(1).upper().strip()
# 11. Status Perkawinan
status_match = re.search(r'(BELUM KAWIN|KAWIN|CERAI HIDUP|CERAI MATI)', raw_text, re.IGNORECASE)
if status_match:
data['status_perkawinan'] = status_match.group(1).upper()
# Pekerjaan
# 12. Pekerjaan
pekerjaan_match = re.search(r'Pekerjaan\s*[:;]?\s*([^\n]+)', raw_text, re.IGNORECASE)
if pekerjaan_match:
data['pekerjaan'] = pekerjaan_match.group(1).strip()
# 13. Kewarganegaraan
kwn_match = re.search(r'Kewarganegaraan\s*[:;]?\s*([A-Za-z]+)', raw_text, re.IGNORECASE)
if kwn_match:
data['kewarganegaraan'] = kwn_match.group(1).upper().strip()
# 14. Berlaku Hingga
berlaku_match = re.search(r'Berlaku\s*Hingga\s*[:;]?\s*([^\n]+)', raw_text, re.IGNORECASE)
if berlaku_match:
data['berlaku_hingga'] = berlaku_match.group(1).strip()
return data
@classmethod
......
......@@ -224,6 +224,37 @@ class OCRAndEKYCPipelineTest(TestCase):
self.assertEqual(res.data['data']['extracted_data']['nik'], '3171012304850001')
self.assertEqual(res.data['data']['extracted_data']['nama'], 'AHMAD FAUZI')
def test_ocr_scan_real_ktp_image_file(self):
"""Uji scan OCR berkas citra digital KTP asli via API."""
import os
from django.core.files.uploadedfile import SimpleUploadedFile
photo_path = '/Users/trendy/Downloads/photo_2026-07-10 10.55.05.jpeg'
if not os.path.exists(photo_path):
self.skipTest("Berkas foto KTP pengujian tidak ditemukan di Downloads.")
with open(photo_path, 'rb') as f:
uploaded_img = SimpleUploadedFile(
name='ktp_test.jpg',
content=f.read(),
content_type='image/jpeg'
)
client = APIClient()
client.force_authenticate(user=self.staff)
res = client.post('/api/v1/ocr/scan/', {
'file': uploaded_img,
'doc_type': 'KTP'
}, format='multipart')
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(res.data['status'], 'success')
extracted = res.data['data']['extracted_data']
self.assertEqual(extracted['nik'], '3277031812900014')
self.assertIn('TRENDY DWI ANUGRAH', extracted['nama'])
self.assertEqual(extracted['jenis_kelamin'], 'LAKI-LAKI')
self.assertEqual(extracted['status_perkawinan'], 'KAWIN')
self.assertEqual(extracted['pekerjaan'], 'KARYAWAN SWASTA')
def test_kbli_check_api_endpoint(self):
"""US-06: Endpoint API validasi KBLI 2020."""
client = APIClient()
......
......@@ -143,7 +143,8 @@ class OCRScanView(APIView):
pass
return Response({
'status': 'success',
'status': 'success' if result.get('status') == 'SUCCESS' else 'error',
'message': result.get('message', ''),
'data': result
}, status=status.HTTP_200_OK)
......
......@@ -848,6 +848,13 @@ fieldset.module h2 {
color: #ffffff !important;
}
#toggle-nav-sidebar:focus-visible,
.toggle-nav-sidebar:focus-visible {
box-shadow: inset 0 0 0 2px #3b82f6 !important;
outline: 1px solid transparent !important;
outline-offset: -2px !important;
}
.toggle-icon {
font-size: 16px;
font-weight: 700;
......@@ -911,7 +918,7 @@ fieldset.module h2 {
margin-bottom: 10px;
}
.sidebar-workdesk-link {
#nav-sidebar .sidebar-workdesk-link {
display: flex;
align-items: center;
justify-content: space-between;
......@@ -920,6 +927,7 @@ fieldset.module h2 {
border-radius: 6px;
padding: 7px 10px;
text-decoration: none;
color: #5fa3c7;
transition: all 0.15s ease;
}
......
......@@ -9,7 +9,7 @@
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'admin/css/custom_admin.css' %}?v=2.4">
<link rel="stylesheet" href="{% static 'admin/css/custom_admin.css' %}?v=2.5">
{% endblock %}
{% block extrahead %}
......
{% extends "admin/change_list.html" %}
{% load i18n admin_urls static admin_list %}
{% load i18n admin_urls static admin_list legal_tags %}
{% block extrastyle %}
{{ block.super }}
......@@ -29,3 +29,13 @@
}
</style>
{% endblock %}
{% if not is_popup %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=cl.opts.app_label %}">{{ cl.opts.app_config.verbose_name|bahasa }}</a>
&rsaquo; {{ cl.opts.verbose_name_plural|capfirst|bahasa }}
</div>
{% endblock %}
{% endif %}
......@@ -356,7 +356,7 @@
{% elif app.app_label == 'ahu_integration' %}
🏛 &nbsp;Ditjen AHU & BPN
{% else %}
📁 &nbsp;<span class="app-system-tag">Sistem Teknis</span> {{ app.name }}
📁 &nbsp;<span class="app-system-tag">Sistem Teknis</span> {{ app.name|bahasa }}
{% endif %}
</h2>
<a href="{{ app.app_url }}" style="font-size:11px;color:var(--slate-500);text-decoration:none;font-weight:600;">Lihat Semua →</a>
......@@ -367,9 +367,9 @@
<tr>
<td class="model-link">
{% if model.admin_url %}
<a href="{{ model.admin_url }}">{{ model.name }}</a>
<a href="{{ model.admin_url }}">{{ model.name|bahasa }}</a>
{% else %}
{{ model.name }}
{{ model.name|bahasa }}
{% endif %}
</td>
<td class="action-links" style="text-align:right;">
......
......@@ -292,7 +292,8 @@
<!-- Toggle Password Visibility Button -->
<button type="button"
@click="isPasswordVisible = !isPasswordVisible"
class="absolute inset-y-0 right-0 flex items-center pr-3.5 text-slate-400 hover:text-judicial-navy transition-colors focus:outline-none"
:aria-label="isPasswordVisible ? 'Sembunyikan kata sandi' : 'Tampilkan kata sandi'"
class="absolute inset-y-0 right-0 flex items-center pr-3.5 text-slate-400 hover:text-judicial-navy transition-colors focus:outline-none focus:ring-2 focus:ring-judicial-gold rounded-judicial"
tabindex="-1"
title="Tampilkan/Sembunyikan Kata Sandi">
<!-- Eye Off Icon -->
......
{% load i18n static %}
{% load i18n static legal_tags %}
<button class="sticky toggle-nav-sidebar" id="toggle-nav-sidebar" aria-label="{% translate 'Toggle navigation' %}">
<span class="toggle-icon">‹</span>
......@@ -50,7 +50,7 @@
<th scope="row">
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}aria-current="page"{% endif %}>
<span class="model-bullet"></span>
<span class="model-name">{{ model.name }}</span>
<span class="model-name">{{ model.name|bahasa }}</span>
</a>
</th>
{% if model.add_url %}
......@@ -83,7 +83,7 @@
<th scope="row">
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}aria-current="page"{% endif %}>
<span class="model-bullet"></span>
<span class="model-name">{{ model.name }}</span>
<span class="model-name">{{ model.name|bahasa }}</span>
</a>
</th>
{% if model.add_url %}
......@@ -116,7 +116,7 @@
<th scope="row">
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}aria-current="page"{% endif %}>
<span class="model-bullet"></span>
<span class="model-name">{{ model.name }}</span>
<span class="model-name">{{ model.name|bahasa }}</span>
</a>
</th>
{% if model.add_url %}
......@@ -149,7 +149,7 @@
<th scope="row">
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}aria-current="page"{% endif %}>
<span class="model-bullet"></span>
<span class="model-name">{{ model.name }}</span>
<span class="model-name">{{ model.name|bahasa }}</span>
</a>
</th>
{% if model.add_url %}
......@@ -173,7 +173,7 @@
<div class="app-group app-group-system">
<div class="app-group-header">
<span class="group-icon">⚙️</span>
<span class="group-title">{{ app.name }}</span>
<span class="group-title">{{ app.name|bahasa }}</span>
</div>
<table class="nav-table">
<tbody>
......@@ -182,7 +182,7 @@
<th scope="row">
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}aria-current="page"{% endif %}>
<span class="model-bullet"></span>
<span class="model-name">{{ model.name }}</span>
<span class="model-name">{{ model.name|bahasa }}</span>
</a>
</th>
{% if model.add_url %}
......
{% load admin_list %}
{% load i18n legal_tags %}
<p class="paginator">
{% if pagination_required %}
{% for i in page_range %}
{% paginator_number cl i %}
{% endfor %}
{% endif %}
{{ cl.result_count }} {% if cl.result_count == 1 %}{{ cl.opts.verbose_name|bahasa }}{% else %}{{ cl.opts.verbose_name_plural|bahasa }}{% endif %}
{% if show_all_url %}<a href="{{ show_all_url }}" class="showall">{% translate 'Show all' %}</a>{% endif %}
{% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="{% translate 'Save' %}">{% endif %}
</p>
\ No newline at end of file
{% load i18n static %}
{% if cl.search_fields %}
<div id="toolbar"><form id="changelist-search" method="get" role="search">
<div><!-- DIV needed for valid HTML -->
<label for="searchbar"><img src="{% static "admin/img/search.svg" %}" alt="" aria-hidden="true"><span style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;">{% translate "Cari dalam daftar" %}</span></label>
<input type="text" size="40" name="{{ search_var }}" value="{{ cl.query }}" id="searchbar" aria-label="{% translate "Cari dalam daftar" %}"{% if cl.search_help_text %} aria-describedby="searchbar_helptext"{% endif %}>
<input type="submit" value="{% translate 'Search' %}">
{% if show_result_count %}
<span class="small quiet">{% blocktranslate count counter=cl.result_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktranslate %} (<a href="?{% if cl.is_popup %}{{ is_popup_var }}=1{% if cl.add_facets %}&{% endif %}{% endif %}{% if cl.add_facets %}{{ is_facets_var }}{% endif %}">{% if cl.show_full_result_count %}{% blocktranslate with full_result_count=cl.full_result_count %}{{ full_result_count }} total{% endblocktranslate %}{% else %}{% translate "Show all" %}{% endif %}</a>)</span>
{% endif %}
{% for pair in cl.params.items %}
{% if pair.0 != search_var %}<input type="hidden" name="{{ pair.0 }}" value="{{ pair.1 }}">{% endif %}
{% endfor %}
</div>
{% if cl.search_help_text %}
<br class="clear">
<div class="help" id="searchbar_helptext">{{ cl.search_help_text }}</div>
{% endif %}
</form></div>
{% endif %}
\ No newline at end of file
......@@ -1014,6 +1014,11 @@
})
.then(d => {
ocrIsProcessing = false;
if (d.status === 'error' || (d.data && d.data.status === 'FAILED')) {
ocrError = d.message || (d.data && d.data.message) || 'Teks dokumen tidak terbaca.';
ocrResult = null;
return;
}
ocrResult = d.data;
})
.catch(err => {
......@@ -1057,7 +1062,7 @@
</div>
<div class="flex justify-between p-1 bg-white rounded border border-slate-200">
<span class="text-slate-500 font-sans">Alamat:</span>
<span class="text-slate-800 text-right truncate max-w-[220px]" x-text="ocrResult.extracted_data.alamat || '-'"></span>
<span class="text-slate-800 text-right truncate max-w-[240px]" :title="ocrResult.extracted_data.alamat_lengkap || ocrResult.extracted_data.alamat" x-text="ocrResult.extracted_data.alamat_lengkap || ocrResult.extracted_data.alamat || '-'"></span>
</div>
<div class="grid grid-cols-2 gap-1 pt-0.5">
<div class="p-1 bg-white rounded border border-slate-200">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment