Files

332 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}