update OCR KTP

parent 7c74f037
...@@ -87,8 +87,8 @@ class AuditLog(models.Model): ...@@ -87,8 +87,8 @@ class AuditLog(models.Model):
timestamp = models.DateTimeField(auto_now_add=True, db_index=True) timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta: class Meta:
verbose_name = _('Audit Log (Rekam Jejak MPWN)') verbose_name = _('Audit Trail MPWN')
verbose_name_plural = _('Audit Logs (Rekam Jejak MPWN)') verbose_name_plural = _('Audit Trail MPWN')
ordering = ['-timestamp'] ordering = ['-timestamp']
def __str__(self): def __str__(self):
......
...@@ -48,13 +48,37 @@ def status_badge_class(status_code): ...@@ -48,13 +48,37 @@ def status_badge_class(status_code):
@register.filter(name='urlencode_text') @register.filter(name='urlencode_text')
def urlencode_text(value): 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: if not value:
return '' return ''
return urllib.parse.quote(str(value)) 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 @register.simple_tag
def get_legal_dashboard_stats(user=None): def get_legal_dashboard_stats(user=None):
""" """
......
This diff is collapsed.
...@@ -224,6 +224,37 @@ class OCRAndEKYCPipelineTest(TestCase): ...@@ -224,6 +224,37 @@ class OCRAndEKYCPipelineTest(TestCase):
self.assertEqual(res.data['data']['extracted_data']['nik'], '3171012304850001') self.assertEqual(res.data['data']['extracted_data']['nik'], '3171012304850001')
self.assertEqual(res.data['data']['extracted_data']['nama'], 'AHMAD FAUZI') 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): def test_kbli_check_api_endpoint(self):
"""US-06: Endpoint API validasi KBLI 2020.""" """US-06: Endpoint API validasi KBLI 2020."""
client = APIClient() client = APIClient()
......
...@@ -143,7 +143,8 @@ class OCRScanView(APIView): ...@@ -143,7 +143,8 @@ class OCRScanView(APIView):
pass pass
return Response({ return Response({
'status': 'success', 'status': 'success' if result.get('status') == 'SUCCESS' else 'error',
'message': result.get('message', ''),
'data': result 'data': result
}, status=status.HTTP_200_OK) }, status=status.HTTP_200_OK)
......
...@@ -848,6 +848,13 @@ fieldset.module h2 { ...@@ -848,6 +848,13 @@ fieldset.module h2 {
color: #ffffff !important; 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 { .toggle-icon {
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
...@@ -911,7 +918,7 @@ fieldset.module h2 { ...@@ -911,7 +918,7 @@ fieldset.module h2 {
margin-bottom: 10px; margin-bottom: 10px;
} }
.sidebar-workdesk-link { #nav-sidebar .sidebar-workdesk-link {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
...@@ -920,6 +927,7 @@ fieldset.module h2 { ...@@ -920,6 +927,7 @@ fieldset.module h2 {
border-radius: 6px; border-radius: 6px;
padding: 7px 10px; padding: 7px 10px;
text-decoration: none; text-decoration: none;
color: #5fa3c7;
transition: all 0.15s ease; transition: all 0.15s ease;
} }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
{% block extrastyle %} {% block extrastyle %}
{{ block.super }} {{ 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 %} {% endblock %}
{% block extrahead %} {% block extrahead %}
......
{% extends "admin/change_list.html" %} {% extends "admin/change_list.html" %}
{% load i18n admin_urls static admin_list %} {% load i18n admin_urls static admin_list legal_tags %}
{% block extrastyle %} {% block extrastyle %}
{{ block.super }} {{ block.super }}
...@@ -29,3 +29,13 @@ ...@@ -29,3 +29,13 @@
} }
</style> </style>
{% endblock %} {% 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 @@ ...@@ -356,7 +356,7 @@
{% elif app.app_label == 'ahu_integration' %} {% elif app.app_label == 'ahu_integration' %}
🏛 &nbsp;Ditjen AHU & BPN 🏛 &nbsp;Ditjen AHU & BPN
{% else %} {% 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 %} {% endif %}
</h2> </h2>
<a href="{{ app.app_url }}" style="font-size:11px;color:var(--slate-500);text-decoration:none;font-weight:600;">Lihat Semua →</a> <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 @@ ...@@ -367,9 +367,9 @@
<tr> <tr>
<td class="model-link"> <td class="model-link">
{% if model.admin_url %} {% if model.admin_url %}
<a href="{{ model.admin_url }}">{{ model.name }}</a> <a href="{{ model.admin_url }}">{{ model.name|bahasa }}</a>
{% else %} {% else %}
{{ model.name }} {{ model.name|bahasa }}
{% endif %} {% endif %}
</td> </td>
<td class="action-links" style="text-align:right;"> <td class="action-links" style="text-align:right;">
......
...@@ -292,7 +292,8 @@ ...@@ -292,7 +292,8 @@
<!-- Toggle Password Visibility Button --> <!-- Toggle Password Visibility Button -->
<button type="button" <button type="button"
@click="isPasswordVisible = !isPasswordVisible" @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" tabindex="-1"
title="Tampilkan/Sembunyikan Kata Sandi"> title="Tampilkan/Sembunyikan Kata Sandi">
<!-- Eye Off Icon --> <!-- 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' %}"> <button class="sticky toggle-nav-sidebar" id="toggle-nav-sidebar" aria-label="{% translate 'Toggle navigation' %}">
<span class="toggle-icon">‹</span> <span class="toggle-icon">‹</span>
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
<th scope="row"> <th scope="row">
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}aria-current="page"{% endif %}> <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-bullet"></span>
<span class="model-name">{{ model.name }}</span> <span class="model-name">{{ model.name|bahasa }}</span>
</a> </a>
</th> </th>
{% if model.add_url %} {% if model.add_url %}
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
<th scope="row"> <th scope="row">
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}aria-current="page"{% endif %}> <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-bullet"></span>
<span class="model-name">{{ model.name }}</span> <span class="model-name">{{ model.name|bahasa }}</span>
</a> </a>
</th> </th>
{% if model.add_url %} {% if model.add_url %}
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
<th scope="row"> <th scope="row">
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}aria-current="page"{% endif %}> <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-bullet"></span>
<span class="model-name">{{ model.name }}</span> <span class="model-name">{{ model.name|bahasa }}</span>
</a> </a>
</th> </th>
{% if model.add_url %} {% if model.add_url %}
...@@ -149,7 +149,7 @@ ...@@ -149,7 +149,7 @@
<th scope="row"> <th scope="row">
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}aria-current="page"{% endif %}> <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-bullet"></span>
<span class="model-name">{{ model.name }}</span> <span class="model-name">{{ model.name|bahasa }}</span>
</a> </a>
</th> </th>
{% if model.add_url %} {% if model.add_url %}
...@@ -173,7 +173,7 @@ ...@@ -173,7 +173,7 @@
<div class="app-group app-group-system"> <div class="app-group app-group-system">
<div class="app-group-header"> <div class="app-group-header">
<span class="group-icon">⚙️</span> <span class="group-icon">⚙️</span>
<span class="group-title">{{ app.name }}</span> <span class="group-title">{{ app.name|bahasa }}</span>
</div> </div>
<table class="nav-table"> <table class="nav-table">
<tbody> <tbody>
...@@ -182,7 +182,7 @@ ...@@ -182,7 +182,7 @@
<th scope="row"> <th scope="row">
<a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}aria-current="page"{% endif %}> <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-bullet"></span>
<span class="model-name">{{ model.name }}</span> <span class="model-name">{{ model.name|bahasa }}</span>
</a> </a>
</th> </th>
{% if model.add_url %} {% 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 @@ ...@@ -1014,6 +1014,11 @@
}) })
.then(d => { .then(d => {
ocrIsProcessing = false; 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; ocrResult = d.data;
}) })
.catch(err => { .catch(err => {
...@@ -1057,7 +1062,7 @@ ...@@ -1057,7 +1062,7 @@
</div> </div>
<div class="flex justify-between p-1 bg-white rounded border border-slate-200"> <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-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>
<div class="grid grid-cols-2 gap-1 pt-0.5"> <div class="grid grid-cols-2 gap-1 pt-0.5">
<div class="p-1 bg-white rounded border border-slate-200"> <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