Initial commit: Real Estate Registry app

This commit is contained in:
Georges Haddad
2026-04-10 15:24:22 +03:00
commit 7305e54f66
32 changed files with 3612 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}سجل العقارات اللبناني{% endblock %}</title>
<link rel="stylesheet" href="/static/css/main.css">
{% block head %}{% endblock %}
</head>
<body>
{% block navbar %}
<nav class="navbar">
<div class="nav-container">
<a class="nav-brand" href="/">🏠 سجل العقارات</a>
<div class="nav-links">
<a href="/" class="nav-link">رفع وثائق</a>
<a href="/review/next" class="nav-link">مراجعة</a>
<a href="/search" class="nav-link">بحث</a>
<a href="/documents" class="nav-link">قائمة الوثائق</a>
<a href="/auth/users" class="nav-link">الإدارة</a>
<a href="/auth/logout" class="nav-link">تسجيل خروج</a>
</div>
</div>
</nav>
{% endblock %}
<main class="main-content">
{% block content %}{% endblock %}
</main>
{% block scripts %}{% endblock %}
</body>
</html>
+105
View File
@@ -0,0 +1,105 @@
{% extends "base.html" %}
{% block title %}قائمة الوثائق — سجل العقارات{% endblock %}
{% block content %}
<div class="page-header">
<h1>قائمة الوثائق</h1>
<a href="/" class="btn btn-primary">+ رفع وثائق جديدة</a>
</div>
{% if uploaded %}
<div class="success-banner">تم رفع الملفات بنجاح وبدأ الاستخراج. قد يستغرق بعض الوقت.</div>
{% endif %}
<div class="stats-bar">
<a href="/documents" class="stat {{ 'active' if not current_status }}">
<span class="stat-num">{{ stats.total or 0 }}</span><span class="stat-label">الكل</span>
</a>
<a href="/documents?status=extracted" class="stat pending {{ 'active' if current_status=='extracted' }}">
<span class="stat-num">{{ stats.pending_review or 0 }}</span><span class="stat-label">بانتظار المراجعة</span>
</a>
<a href="/documents?status=confirmed" class="stat confirmed {{ 'active' if current_status=='confirmed' }}">
<span class="stat-num">{{ stats.confirmed or 0 }}</span><span class="stat-label">مؤكّدة</span>
</a>
<a href="/documents?status=pending" class="stat {{ 'active' if current_status=='pending' }}">
<span class="stat-num">{{ stats.processing or 0 }}</span><span class="stat-label">جارٍ المعالجة</span>
</a>
{% if stats.errors %}
<a href="/documents?status=error" class="stat error {{ 'active' if current_status=='error' }}">
<span class="stat-num">{{ stats.errors }}</span><span class="stat-label">أخطاء</span>
</a>
{% endif %}
</div>
{% if stats.pending_review %}
<div class="alert-banner">
<a href="/review/next">▶ ابدأ مراجعة {{ stats.pending_review }} وثيقة</a>
</div>
{% endif %}
<div class="table-wrapper">
<table class="results-table">
<thead>
<tr>
<th>#</th>
<th>الصورة</th>
<th>الشخص</th>
<th>رقم الطلب</th>
<th>الحالة</th>
<th>التاريخ</th>
<th></th>
</tr>
</thead>
<tbody>
{% for d in documents %}
<tr>
<td>{{ d.id }}</td>
<td>
<a href="/review/{{ d.id }}">
<img src="/uploads/{{ d.image_path }}" class="doc-thumb-mini" alt="">
</a>
</td>
<td>
{% if d.person_id %}
<a href="/persons/{{ d.person_id }}">{{ d.first_name or '' }} {{ d.family_name or '' }}</a>
{% else %}—{% endif %}
</td>
<td>{{ d.request_number or '—' }}</td>
<td><span class="status-badge status-{{ d.status }}">{{ d.status }}</span></td>
<td>{{ d.created_at[:10] if d.created_at else '' }}</td>
<td class="doc-actions">
<a href="/review/{{ d.id }}" class="btn btn-sm btn-secondary">
{% if d.status == 'confirmed' %}عرض{% else %}مراجعة{% endif %}
</a>
<button class="btn btn-sm btn-danger" onclick="deleteDoc({{ d.id }}, this)">حذف</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if not documents %}
<div class="no-results">
<p>لا توجد وثائق. <a href="/">ارفع وثائق جديدة</a></p>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
async function deleteDoc(docId, btn) {
if (!confirm('حذف هذه الوثيقة نهائياً؟ لا يمكن التراجع.')) return;
btn.disabled = true;
btn.textContent = '...';
const res = await fetch(`/documents/${docId}`, { method: 'DELETE' });
if (res.ok) {
btn.closest('tr').remove();
} else {
alert('حدث خطأ أثناء الحذف.');
btn.disabled = false;
btn.textContent = 'حذف';
}
}
</script>
{% endblock %}
+119
View File
@@ -0,0 +1,119 @@
{% extends "base.html" %}
{% block title %}رفع وثائق — سجل العقارات{% endblock %}
{% block content %}
<div class="page-header">
<h1>بطاقات معلومات الملكية العقارية</h1>
<p class="subtitle">ارفع صور أو ملفات PDF للوثائق — اختر محرك الاستخراج (مجاني أو ذكاء اصطناعي)</p>
</div>
{% if stats and stats.total %}
<div class="stats-bar">
<div class="stat"><span class="stat-num">{{ stats.total }}</span><span class="stat-label">إجمالي الوثائق</span></div>
<div class="stat confirmed"><span class="stat-num">{{ stats.confirmed or 0 }}</span><span class="stat-label">مؤكّدة</span></div>
<div class="stat pending"><span class="stat-num">{{ stats.pending_review or 0 }}</span><span class="stat-label">بانتظار المراجعة</span></div>
{% if stats.errors %}<div class="stat error"><span class="stat-num">{{ stats.errors }}</span><span class="stat-label">أخطاء</span></div>{% endif %}
</div>
{% if stats.pending_review %}
<div class="alert-banner">
<a href="/review/next">▶ ابدأ مراجعة {{ stats.pending_review }} وثيقة</a>
</div>
{% endif %}
{% endif %}
<div class="upload-card">
<form id="uploadForm" action="/upload" method="post" enctype="multipart/form-data">
<div class="drop-zone" id="dropZone">
<div class="drop-icon">📄</div>
<p class="drop-text">اسحب الملفات هنا أو انقر للاختيار</p>
<p class="drop-hint">JPG، PNG، PDF — يمكن رفع عدة ملفات دفعة واحدة</p>
<input type="file" name="files" id="fileInput" multiple accept=".jpg,.jpeg,.png,.webp,.pdf" class="file-input">
</div>
<div id="fileList" class="file-list hidden"></div>
<!-- Provider selector -->
<div id="providerSection" class="provider-section hidden">
<label class="provider-label">محرك الاستخراج:</label>
<div class="provider-options">
{% for p in providers %}
<label class="provider-option">
<input type="radio" name="provider" value="{{ p.id }}"
{{ 'checked' if p.id == default_provider else '' }}>
<span class="provider-card {{ 'provider-free' if p.id == 'easyocr' else 'provider-ai' }}">
<span class="provider-name">{{ p.name }}</span>
<span class="provider-model">{{ p.model }}</span>
</span>
</label>
{% endfor %}
</div>
</div>
<div id="uploadActions" class="upload-actions hidden">
<button type="submit" class="btn btn-primary btn-large">
⬆ رفع ومعالجة
</button>
<button type="button" class="btn btn-secondary" onclick="clearFiles()">إلغاء</button>
</div>
</form>
</div>
<div class="quick-links">
<a href="/search" class="quick-link-card">
<span class="ql-icon">🔍</span>
<span>البحث في قاعدة البيانات</span>
</a>
<a href="/documents" class="quick-link-card">
<span class="ql-icon">📋</span>
<span>قائمة جميع الوثائق</span>
</a>
</div>
{% endblock %}
{% block scripts %}
<script>
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const uploadActions = document.getElementById('uploadActions');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', e => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('drag-over');
fileInput.files = e.dataTransfer.files;
showFiles(fileInput.files);
});
fileInput.addEventListener('change', () => showFiles(fileInput.files));
function showFiles(files) {
if (!files.length) return;
fileList.innerHTML = Array.from(files).map(f =>
`<div class="file-item">${f.name.endsWith('.pdf')?'📕':'📄'} ${f.name} <span class="file-size">${(f.size/1024).toFixed(0)} KB</span></div>`
).join('');
fileList.classList.remove('hidden');
document.getElementById('providerSection').classList.remove('hidden');
uploadActions.classList.remove('hidden');
}
function clearFiles() {
fileInput.value = '';
fileList.innerHTML = '';
fileList.classList.add('hidden');
document.getElementById('providerSection').classList.add('hidden');
uploadActions.classList.add('hidden');
}
document.getElementById('uploadForm').addEventListener('submit', function() {
document.querySelector('.btn-primary').textContent = '⏳ جارٍ الرفع...';
document.querySelector('.btn-primary').disabled = true;
});
</script>
{% endblock %}
+31
View File
@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block title %}تسجيل الدخول - سجل العقارات اللبناني{% endblock %}
{% block navbar %}{% endblock %}
{% block content %}
<div class="login-container" style="max-width: 400px; margin: 50px auto; padding: 20px; border: 1px solid #ccc; border-radius: 8px;">
<h2>تسجيل الدخول</h2>
{% if error %}
<div style="color: red; margin-bottom: 10px;">{{ error }}</div>
{% endif %}
<form method="post" action="/auth/login">
<div style="margin-bottom: 15px;">
<label for="username" style="display: block; margin-bottom: 5px;">اسم المستخدم:</label>
<input type="text" id="username" name="username" required style="width: 100%; padding: 8px;">
</div>
<div style="margin-bottom: 15px;">
<label for="password" style="display: block; margin-bottom: 5px;">كلمة المرور:</label>
<input type="password" id="password" name="password" required style="width: 100%; padding: 8px;">
</div>
<button type="submit" style="width: 100%; padding: 10px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
دخول
</button>
</form>
</div>
{% endblock %}
+119
View File
@@ -0,0 +1,119 @@
{% extends "base.html" %}
{% block title %}{{ person.first_name }} {{ person.family_name or '' }} — سجل العقارات{% endblock %}
{% block content %}
<div class="page-header">
<div>
<a href="/search" class="back-link">← العودة للبحث</a>
<h1>{{ person.first_name }} {{ person.father_name or '' }} {{ person.family_name or '' }}</h1>
</div>
<div>
{% set scopes = documents | selectattr('search_scope') | map(attribute='search_scope') | unique | list %}
<div class="export-dropdown" style="position: relative; display: inline-block;">
<button class="btn btn-primary" onclick="this.nextElementSibling.classList.toggle('show')">📥 تصدير كملف إكسل ▾</button>
<div class="export-menu" style="display:none; position:absolute; left:0; top:100%; background:#fff; border:1px solid #ddd; border-radius:8px; box-shadow:0 4px 12px rgba(0,0,0,.15); z-index:100; min-width:220px; margin-top:4px;">
<a href="/persons/{{ person.id }}/export" class="export-option" download style="display:block; padding:10px 16px; text-decoration:none; color:#333; border-bottom:1px solid #eee;">📥 تصدير الكل</a>
{% for scope in scopes %}
{% if scope %}
<a href="/persons/{{ person.id }}/export?qaza={{ scope|urlencode }}" class="export-option" download style="display:block; padding:10px 16px; text-decoration:none; color:#333;">📥 {{ scope }}</a>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
<div class="person-info-card">
<div class="info-grid">
<div class="info-item"><span class="info-label">الاسم الكامل</span><span>{{ person.first_name }} {{ person.father_name or '' }} {{ person.family_name or '' }}</span></div>
{% if person.mother_name %}<div class="info-item"><span class="info-label">اسم الأم</span><span>{{ person.mother_name }}</span></div>{% endif %}
{% if person.family_origin %}<div class="info-item"><span class="info-label">الهوا</span><span>{{ person.family_origin }}</span></div>{% endif %}
{% if person.nationality %}<div class="info-item"><span class="info-label">الجنسية</span><span>{{ person.nationality }}</span></div>{% endif %}
{% if person.birth_date %}<div class="info-item"><span class="info-label">تاريخ الولادة</span><span>{{ person.birth_date }}</span></div>{% endif %}
{% if person.registry_number %}<div class="info-item"><span class="info-label">رقم السجل</span><span>{{ person.registry_number }}</span></div>{% endif %}
{% if person.registry_place %}<div class="info-item"><span class="info-label">مكان السجل</span><span>{{ person.registry_place }}</span></div>{% endif %}
{% set scopes = documents | selectattr('search_scope') | map(attribute='search_scope') | unique | list %}
{% if scopes %}<div class="info-item"><span class="info-label">القضاء</span><span>{{ scopes | join('، ') }}</span></div>{% endif %}
</div>
</div>
<div class="section-header">
<h2>العقارات المملوكة ({{ properties|length }})</h2>
</div>
{% if properties %}
<div class="table-wrapper">
<table class="results-table props-full">
<thead>
<tr>
<th>#</th>
<th>اسم الفريق</th>
<th>رقم العقار</th>
<th>القسم</th>
<th>البلوك</th>
<th>المنطقة العقارية</th>
<th>القضاء</th>
<th>عدد الأسهم</th>
<th>نوع الملكية</th>
</tr>
</thead>
<tbody>
{% for pr in properties %}
<tr {% if pr.ownership_type == 'لا يملك' %}style="background-color: #fff3f3; color: #dc3545; font-weight: bold;"{% endif %}>
<td class="row-num">{{ loop.index }}</td>
<td>{{ pr.party_name or '' }}</td>
<td class="prop-num">{{ pr.property_number or '' }}</td>
<td>{{ pr.section or '' }}</td>
<td>{{ pr.block or '' }}</td>
<td>{{ pr.real_estate_district or '' }}</td>
<td>{{ pr.qaza or '' }}</td>
<td>{{ pr.num_shares or '' }}</td>
<td>{{ pr.ownership_type or '' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div style="margin: 1rem 0; padding: 15px; background-color: #fff3f3; border: 1px solid #ffcaca; border-radius: 8px; color: #dc3545; font-weight: bold; text-align: center;">
⚠️ نتيجة البحث: لا يملك أي عقار في نطاق البحث المذكور.
</div>
{% endif %}
{% if documents %}
<div class="docs-section">
<h3>الوثائق الأصلية</h3>
<div class="doc-thumbnails">
{% for d in documents %}
<a href="/review/{{ d.id }}" class="doc-thumb">
<img src="/uploads/{{ d.image_path }}" alt="وثيقة">
<div class="doc-thumb-info">
{% if d.request_number %}طلب {{ d.request_number }}{% endif %}
{% if d.request_date %}<br>{{ d.request_date }}{% endif %}
{% if d.search_scope %}<br>القضاء: {{ d.search_scope }}{% endif %}
{% if d.page_info %}<br><span class="doc-page-info">{{ d.page_info }}</span>{% endif %}
</div>
</a>
{% endfor %}
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
// Export dropdown toggle
document.addEventListener('click', function(e) {
document.querySelectorAll('.export-menu').forEach(function(menu) {
if (!menu.parentElement.contains(e.target)) {
menu.classList.remove('show');
}
});
});
var style = document.createElement('style');
style.textContent = '.export-menu.show { display: block !important; } .export-option:hover { background-color: #f0f0f0; }';
document.head.appendChild(style);
</script>
{% endblock %}
+358
View File
@@ -0,0 +1,358 @@
{% 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">
<h2>مراجعة الوثيقة #{{ doc.id }}</h2>
<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 }})">🗑 حذف الوثيقة</button>
</div>
</div>
{% 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 onclick="zoomIn()" class="btn btn-sm">🔍+</button>
<button onclick="zoomOut()" class="btn btn-sm">🔍-</button>
<button onclick="resetZoom()" class="btn btn-sm"></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">
<!-- Person Info -->
<section class="form-section">
<h3>بيانات الشخص</h3>
<div class="form-grid">
<div class="form-group">
<label>الاسم *</label>
<input type="text" name="first_name" value="{{ doc.person.get('first_name') or '' }}" required>
</div>
<div class="form-group">
<label>اسم الأب</label>
<input type="text" name="father_name" value="{{ doc.person.get('father_name') or '' }}">
</div>
<div class="form-group">
<label>اسم الأم</label>
<input type="text" name="mother_name" value="{{ doc.person.get('mother_name') or '' }}">
</div>
<div class="form-group">
<label>اللقب</label>
<input type="text" name="family_name" value="{{ doc.person.get('family_name') or '' }}">
</div>
<div class="form-group">
<label>الهوا / المنشأ</label>
<input type="text" name="family_origin" value="{{ doc.person.get('family_origin') or '' }}">
</div>
<div class="form-group">
<label>الجنسية</label>
<input type="text" name="nationality" value="{{ doc.person.get('nationality') or '' }}">
</div>
<div class="form-group">
<label>تاريخ الولادة</label>
<input type="text" name="birth_date" value="{{ doc.person.get('birth_date') or '' }}">
</div>
<div class="form-group">
<label>رقم السجل</label>
<input type="text" name="registry_number" value="{{ doc.person.get('registry_number') or '' }}">
</div>
<div class="form-group">
<label>مكان السجل</label>
<input type="text" name="registry_place" value="{{ doc.person.get('registry_place') or '' }}">
</div>
</div>
<!-- Merge suggestion (populated by JS) -->
<div id="mergeSuggestion" class="merge-banner hidden">
<h4>شخص مشابه موجود في قاعدة البيانات:</h4>
<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>رقم الطلب</label>
<input type="text" name="request_number" value="{{ doc.request_number or '' }}">
</div>
<div class="form-group">
<label>تاريخ الطلب</label>
<input type="text" name="request_date" value="{{ doc.request_date or '' }}">
</div>
<div class="form-group">
<label>معلومات الصفحة</label>
<input type="text" name="page_info" value="{{ doc.page_info or '' }}" placeholder="مثال: 1 من 3">
</div>
<div class="form-group">
<label>نطاق البحث (القضاء)</label>
<input type="text" name="search_scope" value="{{ doc.search_scope or '' }}" 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 id="propertiesTable" class="props-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><input type="text" value="{{ prop.party_name or '' }}"></td>
<td><input type="text" value="{{ prop.property_number or '' }}"></td>
<td><input type="text" value="{{ prop.section or '' }}"></td>
<td><input type="text" value="{{ prop.block or '' }}"></td>
<td><input type="text" value="{{ prop.real_estate_district or '' }}"></td>
<td><input type="text" value="{{ prop.qaza or '' }}"></td>
<td><input type="text" value="{{ prop.num_shares or '' }}"></td>
<td><input type="text" value="{{ prop.ownership_type or '' }}"></td>
<td><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">
✓ تأكيد والانتقال للتالية
</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><input type="text" value=""></td>
<td><input type="text" value=""></td>
<td><input type="text" value=""></td>
<td><input type="text" value=""></td>
<td><input type="text" value=""></td>
<td><input type="text" value=""></td>
<td><input type="text" value=""></td>
<td><input type="text" value=""></td>
<td><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();
if (!first) return;
const params = new URLSearchParams({first_name: first, father_name: father, family_name: family});
const res = await fetch('/api/check-duplicate?' + params);
const data = await res.json();
const banner = document.getElementById('mergeSuggestion');
const matchesDiv = document.getElementById('mergeMatches');
const mergeIdInput = document.getElementById('mergePersonId');
if (data.matches && data.matches.length > 0) {
matchesDiv.innerHTML = data.matches.map(m =>
`<div class="merge-person" data-person-id="${m.id}">
<span class="mp-name">${m.first_name} ${m.father_name||''} ${m.family_name||''}</span>
<span class="mp-count">${m.property_count} عقار</span>
${m.family_origin ? '<span class="mp-count">'+m.family_origin+'</span>' : ''}
</div>`
).join('');
mergeIdInput.value = data.matches[0].id;
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)';
mergeIdInput.value = el.dataset.personId;
form.querySelector('[name=merge_action][value=merge]').checked = true;
});
});
} else {
banner.classList.add('hidden');
mergeIdInput.value = '';
}
}, 500);
}
// Attach duplicate check to name fields
document.querySelectorAll('[name=first_name],[name=father_name],[name=family_name]').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('حدث خطأ: ' + JSON.stringify(data));
btn.disabled = false;
btn.textContent = '✓ تأكيد والانتقال للتالية';
}
} catch (e) {
alert('خطأ في الشبكة: ' + e);
btn.disabled = false;
btn.textContent = '✓ تأكيد والانتقال للتالية';
}
}
async function retriggerExtraction(docId) {
if (!confirm('إعادة استخراج البيانات بالذكاء الاصطناعي؟')) return;
await fetch(`/extract/${docId}`, { method: 'POST' });
location.reload();
}
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 %}
+232
View File
@@ -0,0 +1,232 @@
{% extends "base.html" %}
{% block title %}بحث — سجل العقارات{% endblock %}
{% block content %}
<div class="page-header">
<h1>البحث في قاعدة البيانات</h1>
</div>
<div class="search-card">
<form method="get" action="/search" id="searchForm">
<div class="search-tabs">
<button type="button" class="tab-btn active" onclick="showTab('person')">بحث بالاسم</button>
<button type="button" class="tab-btn" onclick="showTab('property')">بحث بالعقار</button>
</div>
<div id="tab-person" class="tab-content">
<div class="search-row">
<input type="text" name="q" id="qInput" value="{{ q }}"
placeholder="اكتب اسم الشخص..." class="search-input" autocomplete="off">
<button type="submit" class="btn btn-primary">🔍 بحث</button>
</div>
</div>
<div id="tab-property" class="tab-content hidden">
<div class="search-grid">
<div class="form-group">
<label>رقم العقار</label>
<input type="text" name="property_number" value="{{ property_number }}" placeholder="مثال: 162">
</div>
<div class="form-group">
<label>المنطقة العقارية</label>
<input type="text" name="district" value="{{ district }}" placeholder="مثال: مراح الزيتات">
</div>
<div class="form-group">
<label>البلوك</label>
<input type="text" name="block" value="{{ block }}" placeholder="رقم البلوك">
</div>
</div>
<button type="submit" class="btn btn-primary">🔍 بحث</button>
</div>
</form>
</div>
{% if error %}
<div class="error-banner">{{ error }}</div>
{% endif %}
<!-- Person results -->
{% if persons %}
<div class="results-section">
<h2>نتائج البحث بالاسم ({{ persons|length }} شخص)</h2>
{% if selected_person %}
<!-- Single person: show full details inline -->
<div class="person-info-card">
<div class="info-grid">
<div class="info-item"><span class="info-label">الاسم الكامل</span><span>{{ selected_person.person.first_name }} {{ selected_person.person.father_name or '' }} {{ selected_person.person.family_name or '' }}</span></div>
{% if selected_person.person.family_origin %}<div class="info-item"><span class="info-label">الهوا</span><span>{{ selected_person.person.family_origin }}</span></div>{% endif %}
{% if selected_person.person.nationality %}<div class="info-item"><span class="info-label">الجنسية</span><span>{{ selected_person.person.nationality }}</span></div>{% endif %}
{% if selected_person.person.registry_number %}<div class="info-item"><span class="info-label">رقم السجل</span><span>{{ selected_person.person.registry_number }}</span></div>{% endif %}
{% set scopes = selected_person.documents | selectattr('search_scope') | map(attribute='search_scope') | unique | list %}
{% if scopes %}<div class="info-item"><span class="info-label">القضاء</span><span>{{ scopes | join('، ') }}</span></div>{% endif %}
</div>
<div style="margin-top:1rem">
<a href="/persons/{{ selected_person.person.id }}" class="btn btn-secondary">عرض التفاصيل كاملة</a>
{% set scopes = selected_person.documents | selectattr('search_scope') | map(attribute='search_scope') | unique | list %}
<div class="export-dropdown" style="position: relative; display: inline-block;">
<button class="btn btn-primary" onclick="this.nextElementSibling.classList.toggle('show')">📥 تصدير كملف إكسل ▾</button>
<div class="export-menu" style="display:none; position:absolute; left:0; top:100%; background:#fff; border:1px solid #ddd; border-radius:8px; box-shadow:0 4px 12px rgba(0,0,0,.15); z-index:100; min-width:220px; margin-top:4px;">
<a href="/persons/{{ selected_person.person.id }}/export" class="export-option" download style="display:block; padding:10px 16px; text-decoration:none; color:#333; border-bottom:1px solid #eee;">📥 تصدير الكل</a>
{% for scope in scopes %}
{% if scope %}
<a href="/persons/{{ selected_person.person.id }}/export?qaza={{ scope|urlencode }}" class="export-option" download style="display:block; padding:10px 16px; text-decoration:none; color:#333;">📥 {{ scope }}</a>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
{% if selected_person.properties %}
<div class="table-wrapper" style="margin-top:1rem">
<table class="results-table">
<thead>
<tr>
<th>#</th>
<th>اسم الفريق</th>
<th>رقم العقار</th>
<th>القسم</th>
<th>البلوك</th>
<th>المنطقة العقارية</th>
<th>القضاء</th>
<th>عدد الأسهم</th>
<th>نوع الملكية</th>
</tr>
</thead>
<tbody>
{% for pr in selected_person.properties %}
<tr {% if pr.ownership_type == 'لا يملك' %}style="background-color: #fff3f3; color: #dc3545; font-weight: bold;"{% endif %}>
<td>{{ loop.index }}</td>
<td>{{ pr.party_name or '' }}</td>
<td>{{ pr.property_number or '' }}</td>
<td>{{ pr.section or '' }}</td>
<td>{{ pr.block or '' }}</td>
<td>{{ pr.real_estate_district or '' }}</td>
<td>{{ pr.qaza or '' }}</td>
<td>{{ pr.num_shares or '' }}</td>
<td>{{ pr.ownership_type or '' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div style="margin-top: 1rem; padding: 15px; background-color: #fff3f3; border: 1px solid #ffcaca; border-radius: 8px; color: #dc3545; font-weight: bold; text-align: center;">
⚠️ نتيجة البحث: لا يملك أي عقار في القضاء المذكور.
</div>
{% endif %}
{% else %}
<div class="persons-grid">
{% for p in persons %}
<a href="/persons/{{ p.id }}" class="person-card">
<div class="person-name">{{ p.first_name }} {{ p.father_name or '' }} {{ p.family_name or '' }}</div>
<div class="person-meta">
{% if p.family_origin %}{{ p.family_origin }} · {% endif %}
{% if p.property_count > 0 %}
{{ p.property_count }} عقار
{% else %}
<span style="color: #dc3545; font-weight: bold;">لا يملك عقارات</span>
{% endif %}
</div>
{% if p.search_scopes %}
<div class="person-reg">نطاق البحث: {{ p.search_scopes | replace(',', '، ') }}</div>
{% endif %}
{% if p.registry_number %}
<div class="person-reg">سجل: {{ p.registry_number }}</div>
{% endif %}
</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
<!-- Property results -->
{% if properties %}
<div class="results-section">
<h2>نتائج البحث بالعقار ({{ properties|length }} سجل)</h2>
<div class="table-wrapper">
<table class="results-table">
<thead>
<tr>
<th>المالك</th>
<th>اسم الفريق</th>
<th>رقم العقار</th>
<th>القسم</th>
<th>البلوك</th>
<th>المنطقة العقارية</th>
<th>القضاء</th>
<th>عدد الأسهم</th>
<th>نوع الملكية</th>
</tr>
</thead>
<tbody>
{% for pr in properties %}
<tr>
<td>
{% if pr.person_id %}
<a href="/persons/{{ pr.person_id }}">{{ pr.first_name or '' }} {{ pr.family_name or '' }}</a>
{% else %}—{% endif %}
</td>
<td>{{ pr.party_name or '' }}</td>
<td class="prop-num">{{ pr.property_number or '' }}</td>
<td>{{ pr.section or '' }}</td>
<td>{{ pr.block or '' }}</td>
<td>{{ pr.real_estate_district or '' }}</td>
<td>{{ pr.qaza or '' }}</td>
<td>{{ pr.num_shares or '' }}</td>
<td>{{ pr.ownership_type or '' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if (q or property_number or district or block) and not persons and not properties %}
<div class="no-results">
<p>لا توجد نتائج للبحث.</p>
<p><a href="/search">بحث جديد</a></p>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
function showTab(tab) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
document.getElementById('tab-' + tab).classList.remove('hidden');
event.target.classList.add('active');
}
// Export dropdown toggle
document.addEventListener('click', function(e) {
document.querySelectorAll('.export-menu').forEach(function(menu) {
if (!menu.parentElement.contains(e.target)) {
menu.classList.remove('show');
}
});
});
// CSS for show class
var style = document.createElement('style');
style.textContent = '.export-menu.show { display: block !important; } .export-option:hover { background-color: #f0f0f0; }';
document.head.appendChild(style);
// Auto-select correct tab based on query params
window.addEventListener('DOMContentLoaded', () => {
const params = new URLSearchParams(location.search);
if (params.get('property_number') || params.get('district') || params.get('block')) {
showTab('property');
document.querySelectorAll('.tab-btn')[1].classList.add('active');
document.querySelectorAll('.tab-btn')[0].classList.remove('active');
} else {
document.getElementById('qInput').focus();
}
});
</script>
{% endblock %}
+64
View File
@@ -0,0 +1,64 @@
{% extends "base.html" %}
{% block title %}إدارة المستخدمين - سجل العقارات اللبناني{% endblock %}
{% block content %}
<div class="users-container" style="max-width: 800px; margin: 20px auto; padding: 20px;">
<h2>إدارة النظام والمستخدمين</h2>
{% if backup_msg %}
<div style="padding: 10px; background-color: #d4edda; color: #155724; border-radius: 4px; margin-bottom: 15px;">
{{ backup_msg }}
</div>
{% endif %}
<div style="margin-bottom: 30px; padding: 15px; border: 1px solid #ddd; border-radius: 8px;">
<h3>نسخ احتياطي لقاعدة البيانات</h3>
<p>قم بإنشاء نسخة احتياطية من قاعدة البيانات الحالية.</p>
<a href="/auth/backup" style="display: inline-block; padding: 8px 15px; background-color: #28a745; color: white; text-decoration: none; border-radius: 4px;">إنشاء نسخة احتياطية</a>
</div>
<div style="margin-bottom: 30px; padding: 15px; border: 1px solid #ddd; border-radius: 8px;">
<h3>إضافة مستخدم جديد</h3>
<form method="post" action="/auth/users/create" style="display: flex; gap: 10px; align-items: flex-end;">
<div>
<label for="username" style="display: block; margin-bottom: 5px;">اسم المستخدم</label>
<input type="text" id="username" name="username" required style="padding: 8px;">
</div>
<div>
<label for="password" style="display: block; margin-bottom: 5px;">كلمة المرور</label>
<input type="password" id="password" name="password" required style="padding: 8px;">
</div>
<button type="submit" style="padding: 9px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">إضافة</button>
</form>
</div>
<div style="padding: 15px; border: 1px solid #ddd; border-radius: 8px;">
<h3>المستخدمين</h3>
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
<thead>
<tr style="background-color: #f8f9fa;">
<th style="padding: 10px; border-bottom: 2px solid #dee2e6; text-align: right;">الرقم</th>
<th style="padding: 10px; border-bottom: 2px solid #dee2e6; text-align: right;">اسم المستخدم</th>
<th style="padding: 10px; border-bottom: 2px solid #dee2e6; text-align: right;">تاريخ الإنشاء</th>
<th style="padding: 10px; border-bottom: 2px solid #dee2e6; text-align: right;">إجراءات</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;">{{ user.id }}</td>
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;">{{ user.username }}</td>
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;">{{ user.created_at }}</td>
<td style="padding: 10px; border-bottom: 1px solid #dee2e6;">
<form method="post" action="/auth/users/delete/{{ user.id }}" onsubmit="return confirm('هل أنت متأكد من حذف هذا المستخدم؟');" style="display: inline;">
<button type="submit" style="padding: 5px 10px; background-color: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer;">حذف</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}