Files
gexp/templates/duplicates.html
T

247 lines
8.4 KiB
HTML
Raw 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;
}
</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" onclick="scanDuplicates(this)">فحص التكرارات الآن</button>
{% if total_duplicates %}
<button class="btn btn-danger" onclick="deleteAllDuplicates(this)">حذف جميع المكررات ({{ total_duplicates }})</button>
{% endif %}
</div>
</div>
{% if not groups %}
<div class="no-results">
<p>لا توجد وثائق مكررة حالياً.</p>
<p><button class="btn btn-primary" onclick="scanDuplicates(this)">فحص قاعدة البيانات عن التكرارات</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"
onclick="openLightbox(this)">
<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" onclick="unflagDuplicate({{ d.id }}, this)">ليست مكررة</button>
{% endif %}
<button class="btn btn-sm btn-danger" onclick="deleteDoc({{ d.id }}, this)">حذف</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" onclick="closeLightbox()">
<button class="dup-lightbox-close" aria-label="إغلاق">×</button>
<img id="lightbox-img" src="" alt="">
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
function openLightbox(imgEl) {
const full = imgEl.dataset.full || imgEl.src;
document.getElementById('lightbox-img').src = full;
document.getElementById('lightbox').classList.add('open');
}
function closeLightbox() {
document.getElementById('lightbox').classList.remove('open');
document.getElementById('lightbox-img').src = '';
}
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeLightbox(); });
async function scanDuplicates(btn) {
btn.disabled = true;
const original = btn.textContent;
btn.textContent = 'جارٍ الفحص...';
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();
} else {
alert('فشل الفحص.');
btn.disabled = false;
btn.textContent = original;
}
}
async function deleteDoc(docId, btn) {
if (!confirm('حذف هذه الوثيقة نهائياً؟')) return;
btn.disabled = true;
const res = await fetch(`/documents/${docId}`, { method: 'DELETE' });
if (res.ok) {
// Reload so keeper-promotion (if the keeper was deleted) is reflected.
location.reload();
} else {
alert('فشل الحذف.');
btn.disabled = false;
}
}
async function unflagDuplicate(docId, btn) {
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) {
document.getElementById(`card-${docId}`)?.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 (!confirm('حذف جميع الوثائق المكررة نهائياً؟ (سيتم الاحتفاظ بالنسخة الأصلية)')) return;
btn.disabled = true;
btn.textContent = 'جارٍ الحذف...';
const res = await fetch('/documents/delete-duplicates', { method: 'POST' });
if (res.ok) {
const data = await res.json();
alert(`تم حذف ${data.deleted} وثيقة مكررة.`);
location.reload();
} else {
alert('فشل الحذف.');
btn.disabled = false;
}
}
</script>
{% endblock %}