Files

539 lines
25 KiB
HTML

{% extends "base.html" %}
{% block title %}مراجعة الوثيقة #{{ doc.id }}{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/review.css">
{% endblock %}
{% block content %}
<div class="review-header">
<div class="review-title">
<div>
<span class="page-kicker">المراجعة والتحقق</span>
<h2>مراجعة الوثيقة #{{ doc.id }}</h2>
</div>
<span class="status-badge status-{{ doc.status }}">{{ doc.status }}</span>
</div>
<div class="review-nav">
<a href="/documents" class="btn btn-secondary btn-sm">← قائمة الوثائق</a>
<a href="/review/next" class="btn btn-secondary btn-sm">التالية ←</a>
<button class="btn btn-danger btn-sm" onclick="deleteDoc({{ doc.id }})">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
حذف الوثيقة
</button>
</div>
</div>
{% if doc.status in ['extracted', 'confirmed', 'error'] %}
<div class="info-banner rescan-banner">
<div>
<strong>إعادة المسح</strong>
<span class="rescan-copy">إذا خلط الذكاء الاصطناعي بين أحرف مثل س و ن، يمكنك إعادة القراءة بنفس المحرك أو بمحرك آخر.</span>
</div>
<div class="rescan-controls">
<label for="providerSelect" class="sr-only">محرك الاستخراج</label>
<select id="providerSelect" title="محرك الاستخراج">
{% for provider in providers %}
<option value="{{ provider.id }}" {{ 'selected' if provider.id == current_provider else '' }}>
{{ provider.name }}
</option>
{% endfor %}
</select>
<button type="button" class="btn btn-warning btn-sm" id="rescanBtn" onclick="retriggerExtraction({{ doc.id }})">
إعادة المسح
</button>
</div>
</div>
{% endif %}
{% if doc.status == 'error' %}
<div class="error-banner">
⚠ خطأ في الاستخراج: {{ doc.extraction_error }}
<button class="btn btn-sm btn-warning" onclick="retriggerExtraction({{ doc.id }})">إعادة المحاولة</button>
</div>
{% endif %}
<div class="review-layout">
<!-- Image Panel -->
<div class="image-panel">
<div class="image-toolbar">
<button type="button" onclick="zoomIn()" class="btn btn-sm btn-secondary" title="تكبير">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
</button>
<button type="button" onclick="zoomOut()" class="btn btn-sm btn-secondary" title="تصغير">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
</button>
<button type="button" onclick="resetZoom()" class="btn btn-sm btn-secondary" title="إعادة ضبط">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
</button>
</div>
<div class="image-container" id="imageContainer">
<img id="docImage" src="/uploads/{{ doc.image_path }}" alt="وثيقة" class="doc-image">
</div>
</div>
<!-- Form Panel -->
<div class="form-panel">
<form id="reviewForm">
{% if doc.get('is_subsequent_page') %}
<div class="info-banner page-group-banner">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
<div class="page-group-copy">
<div>
هذه صفحة تكميلية ({{ doc.get('page_info') or doc.get('page_number') or '' }}) من مستند متعدد الصفحات.
{% if doc.get('inherited_from_page1') %}
بيانات الشخص مستوردة تلقائياً من الصفحة الأولى.
{% else %}
يرجى التأكد من بيانات الشخص يدوياً — لم يتم العثور على صفحة أولى مؤكدة بعد.
{% endif %}
</div>
{% if doc.get('page1_doc_id') and ai_verification_available %}
<div class="page-group-actions">
<button type="button" class="btn btn-secondary btn-sm" id="verifyCorrelationBtn" onclick="verifyCorrelation({{ doc.id }})">
تحقق بالذكاء الاصطناعي
</button>
</div>
{% endif %}
</div>
</div>
<div id="correlationResult" class="hidden"></div>
{% endif %}
<!-- Person Info -->
<section class="form-section">
<h3>بيانات الشخص</h3>
<div class="form-grid">
<div class="form-group">
<label for="first_name">الاسم *</label>
<input id="first_name" type="text" name="first_name" value="{{ doc.person.get('first_name') or '' }}" title="الاسم" required>
</div>
<div class="form-group">
<label for="father_name">اسم الأب</label>
<input id="father_name" type="text" name="father_name" value="{{ doc.person.get('father_name') or '' }}" title="اسم الأب">
</div>
<div class="form-group">
<label for="mother_name">اسم الأم</label>
<input id="mother_name" type="text" name="mother_name" value="{{ doc.person.get('mother_name') or '' }}" title="اسم الأم">
</div>
<div class="form-group">
<label for="family_name">اللقب</label>
<input id="family_name" type="text" name="family_name" value="{{ doc.person.get('family_name') or '' }}" title="اللقب">
</div>
<div class="form-group">
<label for="family_origin">الهوا / المنشأ</label>
<input id="family_origin" type="text" name="family_origin" value="{{ doc.person.get('family_origin') or '' }}" title="الهوا أو المنشأ">
</div>
<div class="form-group">
<label for="nationality">الجنسية</label>
<input id="nationality" type="text" name="nationality" value="{{ doc.person.get('nationality') or '' }}" title="الجنسية">
</div>
<div class="form-group">
<label for="birth_date">تاريخ الولادة</label>
<input id="birth_date" type="text" name="birth_date" value="{{ doc.person.get('birth_date') or '' }}" title="تاريخ الولادة">
</div>
<div class="form-group">
<label for="registry_number">رقم السجل</label>
<input id="registry_number" type="text" name="registry_number" value="{{ doc.person.get('registry_number') or '' }}" title="رقم السجل">
</div>
<div class="form-group">
<label for="registry_place">مكان السجل</label>
<input id="registry_place" type="text" name="registry_place" value="{{ doc.person.get('registry_place') or '' }}" title="مكان السجل">
</div>
</div>
<!-- Merge suggestion (populated by JS) -->
<div id="mergeSuggestion" class="merge-banner hidden">
<h4>شخص مشابه موجود في قاعدة البيانات:</h4>
<p id="mergeScopeNotice" class="merge-note hidden"></p>
<div id="mergeMatches"></div>
<div class="merge-actions">
<label><input type="radio" name="merge_action" value="merge" checked> دمج مع الشخص الموجود</label>
<label><input type="radio" name="merge_action" value="new"> إنشاء شخص جديد</label>
</div>
<input type="hidden" id="mergePersonId" value="">
</div>
</section>
<!-- Document Info -->
<section class="form-section">
<h3>بيانات الطلب</h3>
<div class="form-grid">
<div class="form-group">
<label for="request_number">رقم الطلب</label>
<input id="request_number" type="text" name="request_number" value="{{ doc.request_number or '' }}" title="رقم الطلب">
</div>
<div class="form-group">
<label for="request_date">تاريخ الطلب</label>
<input id="request_date" type="text" name="request_date" value="{{ doc.request_date or '' }}" title="تاريخ الطلب">
</div>
<div class="form-group">
<label for="page_info">معلومات الصفحة</label>
<input id="page_info" type="text" name="page_info" value="{{ doc.page_info or '' }}" title="معلومات الصفحة" placeholder="مثال: 1 من 3">
</div>
<div class="form-group">
<label for="search_scope">نطاق البحث (القضاء)</label>
<input id="search_scope" type="text" name="search_scope" value="{{ doc.search_scope or '' }}" title="نطاق البحث" placeholder="مثال: المتن">
</div>
</div>
</section>
<!-- Properties Table -->
<section class="form-section">
<div class="section-header">
<h3>العقارات المملوكة</h3>
<button type="button" class="btn btn-sm btn-success" onclick="addRow()">+ إضافة صف</button>
</div>
<div class="table-wrapper table-wrapper-editable">
<table id="propertiesTable" class="props-table mobile-edit-table">
<thead>
<tr>
<th>اسم الفريق</th>
<th>رقم العقار</th>
<th>القسم</th>
<th>البلوك</th>
<th>المنطقة العقارية</th>
<th>القضاء</th>
<th>عدد الأسهم</th>
<th>نوع الملكية</th>
<th></th>
</tr>
</thead>
<tbody id="propertiesTbody">
{% for prop in doc.properties %}
<tr>
<td data-label="اسم الفريق"><input type="text" value="{{ prop.party_name or '' }}" title="اسم الفريق" aria-label="اسم الفريق"></td>
<td data-label="رقم العقار"><input type="text" value="{{ prop.property_number or '' }}" title="رقم العقار" aria-label="رقم العقار"></td>
<td data-label="القسم"><input type="text" value="{{ prop.section or '' }}" title="القسم" aria-label="القسم"></td>
<td data-label="البلوك"><input type="text" value="{{ prop.block or '' }}" title="البلوك" aria-label="البلوك"></td>
<td data-label="المنطقة العقارية"><input type="text" value="{{ prop.real_estate_district or '' }}" title="المنطقة العقارية" aria-label="المنطقة العقارية"></td>
<td data-label="القضاء"><input type="text" value="{{ prop.qaza or '' }}" title="القضاء" aria-label="القضاء"></td>
<td data-label="عدد الأسهم"><input type="text" value="{{ prop.num_shares or '' }}" title="عدد الأسهم" aria-label="عدد الأسهم"></td>
<td data-label="نوع الملكية"><input type="text" value="{{ prop.ownership_type or '' }}" title="نوع الملكية" aria-label="نوع الملكية"></td>
<td data-label="حذف"><button type="button" class="btn-del" onclick="removeRow(this)"></button></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<!-- Actions -->
<div class="form-actions">
<button type="button" class="btn btn-primary btn-large" onclick="confirmDocument()" id="confirmBtn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
تأكيد والانتقال للتالية
</button>
<span class="keyboard-hint">أو اضغط Ctrl+Enter</span>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/review.js"></script>
<script>
const DOC_ID = {{ doc.id }};
function addRow() {
const tbody = document.getElementById('propertiesTbody');
const tr = document.createElement('tr');
tr.innerHTML = `
<td data-label="اسم الفريق"><input type="text" value="" title="اسم الفريق" aria-label="اسم الفريق"></td>
<td data-label="رقم العقار"><input type="text" value="" title="رقم العقار" aria-label="رقم العقار"></td>
<td data-label="القسم"><input type="text" value="" title="القسم" aria-label="القسم"></td>
<td data-label="البلوك"><input type="text" value="" title="البلوك" aria-label="البلوك"></td>
<td data-label="المنطقة العقارية"><input type="text" value="" title="المنطقة العقارية" aria-label="المنطقة العقارية"></td>
<td data-label="القضاء"><input type="text" value="" title="القضاء" aria-label="القضاء"></td>
<td data-label="عدد الأسهم"><input type="text" value="" title="عدد الأسهم" aria-label="عدد الأسهم"></td>
<td data-label="نوع الملكية"><input type="text" value="" title="نوع الملكية" aria-label="نوع الملكية"></td>
<td data-label="حذف"><button type="button" class="btn-del" onclick="removeRow(this)">✕</button></td>
`;
tbody.appendChild(tr);
tr.querySelector('input').focus();
}
function removeRow(btn) {
btn.closest('tr').remove();
}
function getFormData() {
const form = document.getElementById('reviewForm');
const person = {
first_name: form.querySelector('[name=first_name]').value,
father_name: form.querySelector('[name=father_name]').value,
mother_name: form.querySelector('[name=mother_name]').value,
family_name: form.querySelector('[name=family_name]').value,
family_origin: form.querySelector('[name=family_origin]').value,
nationality: form.querySelector('[name=nationality]').value,
birth_date: form.querySelector('[name=birth_date]').value,
registry_number: form.querySelector('[name=registry_number]').value,
registry_place: form.querySelector('[name=registry_place]').value,
};
const rows = document.querySelectorAll('#propertiesTbody tr');
const properties = Array.from(rows).map(row => {
const inputs = row.querySelectorAll('input');
return {
party_name: inputs[0].value,
property_number: inputs[1].value,
section: inputs[2].value,
block: inputs[3].value,
real_estate_district: inputs[4].value,
qaza: inputs[5].value,
num_shares: inputs[6].value,
ownership_type: inputs[7].value,
};
});
const reqNum = form.querySelector('[name=request_number]').value;
const reqDate = form.querySelector('[name=request_date]').value;
const pageInfo = form.querySelector('[name=page_info]').value;
const searchScope = form.querySelector('[name=search_scope]').value;
return {
person,
properties,
request_number: reqNum,
request_date: reqDate,
page_info: pageInfo,
search_scope: searchScope
};
}
// ─── Duplicate person check ────────────────────────────
let _dupTimer = null;
function checkDuplicate() {
clearTimeout(_dupTimer);
_dupTimer = setTimeout(async () => {
const form = document.getElementById('reviewForm');
const first = form.querySelector('[name=first_name]').value.trim();
const father = form.querySelector('[name=father_name]').value.trim();
const family = form.querySelector('[name=family_name]').value.trim();
const scope = form.querySelector('[name=search_scope]').value.trim();
const registry = form.querySelector('[name=registry_number]').value.trim();
const banner = document.getElementById('mergeSuggestion');
const matchesDiv = document.getElementById('mergeMatches');
const mergeIdInput = document.getElementById('mergePersonId');
const scopeNotice = document.getElementById('mergeScopeNotice');
const mergeRadio = form.querySelector('[name=merge_action][value=merge]');
const newRadio = form.querySelector('[name=merge_action][value=new]');
if (!first) {
banner.classList.add('hidden');
matchesDiv.innerHTML = '';
mergeIdInput.value = '';
mergeRadio.disabled = false;
scopeNotice.classList.add('hidden');
newRadio.checked = true;
return;
}
const params = new URLSearchParams({
first_name: first,
father_name: father,
family_name: family,
search_scope: scope,
registry_number: registry,
});
const res = await fetch('/api/check-duplicate?' + params);
const data = await res.json();
if (data.matches && data.matches.length > 0) {
const firstAllowed = data.matches.find(m => m.merge_allowed);
matchesDiv.innerHTML = data.matches.map(m => {
const status = m.registry_match
? 'مطابقة رقم سجل'
: (m.same_scope ? 'نفس نطاق البحث' : 'نطاق مختلف');
const scopes = (m.search_scopes || []).length ? (m.search_scopes || []).join('، ') : 'غير محدد';
const safeClass = m.merge_allowed ? 'merge-person-safe' : 'merge-person-unsafe';
const disabledAttr = m.merge_allowed ? '' : 'data-merge-disabled="true"';
return `<div class="merge-person ${safeClass}" data-person-id="${m.id}" ${disabledAttr}>
<span class="mp-name">${m.first_name} ${m.father_name||''} ${m.family_name||''}</span>
<span class="mp-count">${m.property_count} عقار</span>
<span class="mp-count">${m.document_count || 0} طلب</span>
${m.family_origin ? '<span class="mp-count">'+m.family_origin+'</span>' : ''}
<span class="mp-count">${status}</span>
<span class="mp-count">النطاقات: ${scopes}</span>
</div>`;
}).join('');
if (firstAllowed) {
mergeIdInput.value = firstAllowed.id;
mergeRadio.checked = true;
mergeRadio.disabled = false;
scopeNotice.textContent = 'تم تفعيل الدمج فقط للمرشحين ضمن نفس نطاق البحث أو عند تطابق رقم السجل.';
scopeNotice.classList.remove('hidden');
} else {
mergeIdInput.value = '';
newRadio.checked = true;
mergeRadio.disabled = true;
scopeNotice.textContent = 'المرشحون الموجودون يحملون الاسم نفسه لكن في نطاقات بحث مختلفة. سيتم إنشاء شخص جديد ما لم يوجد تطابق برقم السجل.';
scopeNotice.classList.remove('hidden');
}
banner.classList.remove('hidden');
// Click to select a match
matchesDiv.querySelectorAll('.merge-person').forEach(el => {
el.addEventListener('click', () => {
matchesDiv.querySelectorAll('.merge-person').forEach(e => e.style.outline = '');
el.style.outline = '2px solid var(--primary)';
if (el.dataset.mergeDisabled === 'true') {
mergeIdInput.value = '';
newRadio.checked = true;
mergeRadio.disabled = true;
return;
}
mergeIdInput.value = el.dataset.personId;
mergeRadio.disabled = false;
mergeRadio.checked = true;
});
});
if (firstAllowed) {
const selected = matchesDiv.querySelector(`[data-person-id="${firstAllowed.id}"]`);
if (selected) {
selected.style.outline = '2px solid var(--primary)';
}
}
} else {
banner.classList.add('hidden');
mergeIdInput.value = '';
mergeRadio.disabled = false;
scopeNotice.classList.add('hidden');
}
}, 500);
}
// Attach duplicate check to name fields
document.querySelectorAll('[name=first_name],[name=father_name],[name=family_name],[name=search_scope],[name=registry_number]').forEach(input => {
input.addEventListener('blur', checkDuplicate);
});
// Run once on load
checkDuplicate();
// ─── Confirm ───────────────────────────────────────────
async function confirmDocument() {
const btn = document.getElementById('confirmBtn');
btn.disabled = true;
btn.textContent = 'جارٍ الحفظ...';
try {
const formData = getFormData();
// Check if user chose to merge
const mergeAction = document.querySelector('[name=merge_action]:checked');
const mergeId = document.getElementById('mergePersonId').value;
if (mergeAction && mergeAction.value === 'merge' && mergeId) {
formData.merge_person_id = parseInt(mergeId);
}
const res = await fetch(`/confirm/${DOC_ID}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const data = await res.json();
if (data.ok) {
window.location.href = data.next;
} else {
alert('حدث خطأ: ' + (data.detail || JSON.stringify(data)));
btn.disabled = false;
btn.textContent = '✓ تأكيد والانتقال للتالية';
}
} catch (e) {
alert('خطأ في الشبكة: ' + e);
btn.disabled = false;
btn.textContent = '✓ تأكيد والانتقال للتالية';
}
}
async function retriggerExtraction(docId) {
const provider = document.getElementById('providerSelect')?.value || '';
const btn = document.getElementById('rescanBtn');
const providerLabel = document.getElementById('providerSelect')?.selectedOptions?.[0]?.text || 'المحرك الحالي';
if (!confirm(`إعادة استخراج البيانات باستخدام ${providerLabel}؟ سيتم استبدال النتائج الحالية.`)) return;
if (btn) {
btn.disabled = true;
btn.textContent = 'جارٍ إعادة المسح...';
}
try {
const params = new URLSearchParams();
if (provider) params.set('provider', provider);
const res = await fetch(`/extract/${docId}?${params.toString()}`, { method: 'POST' });
const data = await res.json();
if (!res.ok || !data.ok) {
throw new Error(data.error || data.detail || 'تعذر بدء إعادة المسح.');
}
window.location.href = `/review/${docId}?wait=1`;
} catch (e) {
alert('حدث خطأ أثناء إعادة المسح: ' + e.message);
if (btn) {
btn.disabled = false;
btn.textContent = 'إعادة المسح';
}
}
}
async function verifyCorrelation(docId) {
const btn = document.getElementById('verifyCorrelationBtn');
const resultEl = document.getElementById('correlationResult');
if (!btn || !resultEl) return;
btn.disabled = true;
btn.textContent = 'جارٍ التحقق...';
resultEl.className = 'info-banner correlation-result';
resultEl.innerHTML = 'جارٍ فحص الترابط بين الصفحة الحالية والصفحة الأولى...';
try {
const res = await fetch(`/review/${docId}/verify-correlation`, { method: 'POST' });
const data = await res.json();
if (!res.ok || !data.ok) {
throw new Error(data.error || 'تعذر التحقق من الترابط.');
}
const statusClass = data.same_document ? 'success-banner' : 'error-banner';
const reasons = (data.reasons_ar || []).map(item => `<li>${item}</li>`).join('');
const mismatches = (data.mismatch_flags || []).map(item => `<li>${item}</li>`).join('');
resultEl.className = `${statusClass} correlation-result`;
resultEl.innerHTML = `
<div class="correlation-heading">
<strong>${data.verdict_ar}</strong>
<span class="correlation-meta">الثقة: ${data.confidence} | المحرك: ${data.provider}</span>
</div>
${reasons ? `<ul class="correlation-list">${reasons}</ul>` : ''}
${mismatches ? `<ul class="correlation-list correlation-warnings">${mismatches}</ul>` : ''}
`;
} catch (e) {
resultEl.className = 'error-banner correlation-result';
resultEl.textContent = 'فشل التحقق: ' + e.message;
} finally {
btn.disabled = false;
btn.textContent = 'تحقق بالذكاء الاصطناعي';
}
}
async function deleteDoc(docId) {
if (!confirm('حذف هذه الوثيقة نهائياً؟ لا يمكن التراجع.')) return;
const res = await fetch(`/documents/${docId}`, { method: 'DELETE' });
if (res.ok) {
window.location.href = '/review/next';
} else {
alert('حدث خطأ أثناء الحذف.');
}
}
document.addEventListener('keydown', e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
confirmDocument();
}
});
</script>
{% endblock %}