332 lines
12 KiB
HTML
332 lines
12 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}الوثائق المكررة — سجل العقارات{% endblock %}
|
||
|
||
{% block head %}
|
||
<style>
|
||
.dup-group {
|
||
background: var(--surface, #fff);
|
||
border: 1px solid var(--border, #e5e7eb);
|
||
border-radius: 12px;
|
||
padding: 1rem 1.25rem 1.25rem;
|
||
margin-bottom: 1.5rem;
|
||
box-shadow: 0 1px 2px rgba(0,0,0,.04);
|
||
}
|
||
.dup-group-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: .75rem;
|
||
padding-bottom: .5rem;
|
||
border-bottom: 1px dashed var(--border, #e5e7eb);
|
||
}
|
||
.dup-group-title { font-weight: 600; font-size: .95rem; color: #475569; }
|
||
.dup-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
.dup-card {
|
||
border: 1px solid var(--border, #e5e7eb);
|
||
border-radius: 10px;
|
||
padding: .75rem;
|
||
background: #fafafa;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: .5rem;
|
||
}
|
||
.dup-card.keeper { border-color: #10b981; background: #f0fdf4; }
|
||
.dup-card.duplicate { border-color: #f59e0b; background: #fffbeb; }
|
||
.dup-card-label {
|
||
font-size: .75rem;
|
||
font-weight: 700;
|
||
letter-spacing: .05em;
|
||
}
|
||
.dup-card.keeper .dup-card-label { color: #047857; }
|
||
.dup-card.duplicate .dup-card-label { color: #b45309; }
|
||
.dup-card-img {
|
||
width: 100%;
|
||
aspect-ratio: 1 / 1.3;
|
||
object-fit: cover;
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
border: 1px solid #e5e7eb;
|
||
cursor: zoom-in;
|
||
}
|
||
.dup-meta { font-size: .85rem; line-height: 1.5; color: #334155; }
|
||
.dup-meta div { overflow: hidden; text-overflow: ellipsis; }
|
||
.dup-meta strong { color: #0f172a; }
|
||
.dup-card-actions {
|
||
display: flex;
|
||
gap: .35rem;
|
||
flex-wrap: wrap;
|
||
margin-top: auto;
|
||
}
|
||
.dup-lightbox {
|
||
position: fixed; inset: 0;
|
||
background: rgba(0,0,0,.85);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
padding: 1rem;
|
||
}
|
||
.dup-lightbox.open { display: flex; }
|
||
.dup-lightbox img { max-width: 100%; max-height: 100%; border-radius: 6px; }
|
||
.dup-lightbox-close {
|
||
position: absolute; top: 1rem; left: 1rem;
|
||
background: #fff; border: none; border-radius: 50%;
|
||
width: 40px; height: 40px; font-size: 1.25rem; cursor: pointer;
|
||
}
|
||
.btn-confirming {
|
||
background: #dc2626 !important;
|
||
color: #fff !important;
|
||
animation: dup-pulse .8s ease-in-out infinite;
|
||
}
|
||
@keyframes dup-pulse {
|
||
0%, 100% { box-shadow: 0 0 0 0 rgba(220,38,38,.45); }
|
||
50% { box-shadow: 0 0 0 6px rgba(220,38,38,0); }
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="page-header">
|
||
<div>
|
||
<span class="page-kicker">مراجعة التكرارات</span>
|
||
<h1>الوثائق المكررة</h1>
|
||
<p class="subtitle">قارن كل وثيقة مكررة مع نسختها الأصلية واحذف ما تريد.</p>
|
||
</div>
|
||
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
|
||
<button class="btn btn-secondary" data-action="scan">فحص التكرارات الآن</button>
|
||
{% if total_duplicates %}
|
||
<button class="btn btn-danger" data-action="delete-all">حذف جميع المكررات ({{ total_duplicates }})</button>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{% if not groups %}
|
||
<div class="no-results">
|
||
<p>لا توجد وثائق مكررة حالياً.</p>
|
||
<p><button class="btn btn-primary" data-action="scan">فحص قاعدة البيانات عن التكرارات</button></p>
|
||
</div>
|
||
{% else %}
|
||
|
||
{% macro doc_card(d, role) %}
|
||
<div class="dup-card {{ role }}" id="card-{{ d.id }}">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<span class="dup-card-label">
|
||
{% if role == 'keeper' %}★ النسخة الأصلية{% else %}مكرر{% endif %}
|
||
</span>
|
||
<span style="font-size:.8rem;color:#64748b">#{{ d.id }}</span>
|
||
</div>
|
||
<img src="/thumbs/{{ d.image_path }}?size=480"
|
||
data-full="/uploads/{{ d.image_path }}"
|
||
class="dup-card-img"
|
||
alt="وثيقة {{ d.id }}"
|
||
loading="lazy" decoding="async"
|
||
data-action="lightbox">
|
||
<div class="dup-meta">
|
||
<div><strong>الشخص:</strong>
|
||
{% if d.person_id %}
|
||
<a href="/persons/{{ d.person_id }}">{{ d.first_name or '' }} {{ d.family_name or '' }}</a>
|
||
{% else %}—{% endif %}
|
||
</div>
|
||
<div><strong>رقم الطلب:</strong> {{ d.request_number or '—' }}</div>
|
||
{% if d.page_info %}<div><strong>الصفحة:</strong> {{ d.page_info }}</div>{% endif %}
|
||
{% if d.search_scope %}<div><strong>النطاق:</strong> {{ d.search_scope }}</div>{% endif %}
|
||
<div><strong>الحالة:</strong> <span class="status-badge status-{{ d.status }}">{{ d.status }}</span></div>
|
||
{% if d.created_at %}<div><strong>أُنشئت:</strong> <small>{{ d.created_at[:16] }}</small></div>{% endif %}
|
||
</div>
|
||
<div class="dup-card-actions">
|
||
<a href="/review/{{ d.id }}" class="btn btn-sm btn-secondary">فتح</a>
|
||
{% if role == 'duplicate' %}
|
||
<button class="btn btn-sm" data-action="unflag" data-id="{{ d.id }}">ليست مكررة</button>
|
||
{% endif %}
|
||
<button class="btn btn-sm btn-danger" data-action="delete" data-id="{{ d.id }}">حذف</button>
|
||
</div>
|
||
</div>
|
||
{% endmacro %}
|
||
|
||
{% for g in groups %}
|
||
<div class="dup-group" id="group-{{ g.keeper.id }}">
|
||
<div class="dup-group-header">
|
||
<div class="dup-group-title">
|
||
مجموعة تكرار — {{ g.duplicates|length + 1 }} نسخ
|
||
{% if g.keeper.request_number %} · طلب رقم {{ g.keeper.request_number }}{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="dup-cards">
|
||
{{ doc_card(g.keeper, 'keeper') }}
|
||
{% for d in g.duplicates %}
|
||
{{ doc_card(d, 'duplicate') }}
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
|
||
<div class="dup-lightbox" id="lightbox" data-action="close-lightbox">
|
||
<button class="dup-lightbox-close" aria-label="إغلاق" data-action="close-lightbox">×</button>
|
||
<img id="lightbox-img" src="" alt="">
|
||
</div>
|
||
|
||
{% endif %}
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
(function () {
|
||
console.log('[duplicates] script loaded');
|
||
|
||
function openLightbox(imgEl) {
|
||
const full = imgEl.dataset.full || imgEl.src;
|
||
document.getElementById('lightbox-img').src = full;
|
||
document.getElementById('lightbox').classList.add('open');
|
||
}
|
||
function closeLightbox() {
|
||
const lb = document.getElementById('lightbox');
|
||
if (lb) lb.classList.remove('open');
|
||
const img = document.getElementById('lightbox-img');
|
||
if (img) img.src = '';
|
||
}
|
||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeLightbox(); });
|
||
|
||
async function scanDuplicates(btn) {
|
||
const original = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.textContent = 'جارٍ الفحص...';
|
||
try {
|
||
const res = await fetch('/documents/scan-duplicates', { method: 'POST' });
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
alert(`اكتمل الفحص:\n- تم احتساب hash لـ ${data.hashed} وثيقة\n- تكرارات (نفس الصورة): ${data.flagged_by_hash}\n- تكرارات (نفس رقم الطلب): ${data.flagged_by_request}\n- الإجمالي المكرر: ${data.total_duplicates}`);
|
||
location.reload();
|
||
return;
|
||
}
|
||
alert(`فشل الفحص: HTTP ${res.status}`);
|
||
} catch (e) {
|
||
alert(`خطأ في الشبكة: ${e.message}`);
|
||
}
|
||
btn.disabled = false;
|
||
btn.textContent = original;
|
||
}
|
||
|
||
async function deleteDoc(docId, btn) {
|
||
console.log('[duplicates] deleteDoc', docId);
|
||
// Two-click confirm: first click turns button into "تأكيد الحذف"; second click executes.
|
||
if (btn.dataset.confirming !== '1') {
|
||
const original = btn.textContent;
|
||
btn.dataset.confirming = '1';
|
||
btn.dataset.original = original;
|
||
btn.textContent = 'اضغط للتأكيد';
|
||
btn.classList.add('btn-confirming');
|
||
setTimeout(() => {
|
||
if (btn.dataset.confirming === '1') {
|
||
btn.dataset.confirming = '';
|
||
btn.textContent = btn.dataset.original || original;
|
||
btn.classList.remove('btn-confirming');
|
||
}
|
||
}, 4000);
|
||
return;
|
||
}
|
||
btn.dataset.confirming = '';
|
||
const original = btn.dataset.original || btn.textContent;
|
||
btn.disabled = true;
|
||
btn.textContent = '...';
|
||
try {
|
||
const res = await fetch(`/documents/${docId}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Accept': 'application/json' },
|
||
});
|
||
if (res.ok) { location.reload(); return; }
|
||
let msg = `HTTP ${res.status}`;
|
||
try { const j = await res.json(); if (j.error) msg += ` — ${j.error}`; } catch (_) {}
|
||
alert(`فشل الحذف: ${msg}`);
|
||
} catch (e) {
|
||
alert(`خطأ في الشبكة: ${e.message}`);
|
||
}
|
||
btn.disabled = false;
|
||
btn.textContent = original;
|
||
btn.classList.remove('btn-confirming');
|
||
}
|
||
|
||
async function unflagDuplicate(docId, btn) {
|
||
console.log('[duplicates] unflagDuplicate', docId);
|
||
const original = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.textContent = '...';
|
||
try {
|
||
const res = await fetch(`/documents/${docId}/unflag-duplicate`, {
|
||
method: 'POST',
|
||
headers: { 'Accept': 'application/json' },
|
||
});
|
||
if (res.ok) {
|
||
const card = document.getElementById(`card-${docId}`);
|
||
if (card) card.remove();
|
||
return;
|
||
}
|
||
let msg = `HTTP ${res.status}`;
|
||
try { const j = await res.json(); if (j.error) msg += ` — ${j.error}`; } catch (_) {}
|
||
alert(`فشل التحديث: ${msg}`);
|
||
} catch (e) {
|
||
alert(`خطأ في الشبكة: ${e.message}`);
|
||
}
|
||
btn.disabled = false;
|
||
btn.textContent = original;
|
||
}
|
||
|
||
async function deleteAllDuplicates(btn) {
|
||
if (btn.dataset.confirming !== '1') {
|
||
btn.dataset.confirming = '1';
|
||
btn.dataset.original = btn.textContent;
|
||
btn.textContent = 'اضغط للتأكيد — حذف الجميع';
|
||
btn.classList.add('btn-confirming');
|
||
setTimeout(() => {
|
||
if (btn.dataset.confirming === '1') {
|
||
btn.dataset.confirming = '';
|
||
btn.textContent = btn.dataset.original;
|
||
btn.classList.remove('btn-confirming');
|
||
}
|
||
}, 4000);
|
||
return;
|
||
}
|
||
btn.dataset.confirming = '';
|
||
const original = btn.dataset.original || btn.textContent;
|
||
btn.disabled = true;
|
||
btn.textContent = 'جارٍ الحذف...';
|
||
try {
|
||
const res = await fetch('/documents/delete-duplicates', { method: 'POST' });
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
alert(`تم حذف ${data.deleted} وثيقة مكررة.`);
|
||
location.reload();
|
||
return;
|
||
}
|
||
alert(`فشل الحذف: HTTP ${res.status}`);
|
||
} catch (e) {
|
||
alert(`خطأ في الشبكة: ${e.message}`);
|
||
}
|
||
btn.disabled = false;
|
||
btn.textContent = original;
|
||
btn.classList.remove('btn-confirming');
|
||
}
|
||
|
||
// Single delegated click handler — immune to macro scope / inline-handler issues.
|
||
document.addEventListener('click', function (ev) {
|
||
const target = ev.target.closest('[data-action]');
|
||
if (!target) return;
|
||
const action = target.dataset.action;
|
||
const id = target.dataset.id;
|
||
console.log('[duplicates] click', action, id);
|
||
switch (action) {
|
||
case 'lightbox': openLightbox(target); break;
|
||
case 'close-lightbox': closeLightbox(); break;
|
||
case 'scan': ev.preventDefault(); scanDuplicates(target); break;
|
||
case 'delete': ev.preventDefault(); deleteDoc(parseInt(id, 10), target); break;
|
||
case 'unflag': ev.preventDefault(); unflagDuplicate(parseInt(id, 10), target); break;
|
||
case 'delete-all': ev.preventDefault(); deleteAllDuplicates(target); break;
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|