UI beautification: SVG icons, polished buttons, glass navbar, refined login, animated orbs, modern cards
This commit is contained in:
+8
-5
@@ -70,11 +70,14 @@ async def users_list(request: Request, _=Depends(get_current_user)):
|
|||||||
return templates.TemplateResponse(request=request, name="users.html", context={"users": users})
|
return templates.TemplateResponse(request=request, name="users.html", context={"users": users})
|
||||||
|
|
||||||
@router.post("/users/create")
|
@router.post("/users/create")
|
||||||
async def add_user(username: str = Form(...), password: str = Form(...), _=Depends(get_current_user)):
|
async def add_user(request: Request, username: str = Form(...), password: str = Form(...), _=Depends(get_current_user)):
|
||||||
try:
|
if len(password) < 8:
|
||||||
create_user(username, password)
|
users = get_all_users()
|
||||||
except Exception:
|
return templates.TemplateResponse(request=request, name="users.html", context={"users": users, "error": "Password must be at least 8 characters."})
|
||||||
pass # Probably duplicate
|
if get_user_by_username(username):
|
||||||
|
users = get_all_users()
|
||||||
|
return templates.TemplateResponse(request=request, name="users.html", context={"users": users, "error": f"User '{username}' already exists."})
|
||||||
|
create_user(username, password)
|
||||||
return RedirectResponse(url="/auth/users", status_code=status.HTTP_303_SEE_OTHER)
|
return RedirectResponse(url="/auth/users", status_code=status.HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
@router.post("/users/delete/{user_id}")
|
@router.post("/users/delete/{user_id}")
|
||||||
|
|||||||
+266
-82
@@ -1,18 +1,20 @@
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter, HTTPException, Request, status
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from config import UPLOAD_DIR
|
from config import UPLOAD_DIR
|
||||||
from database.connection import get_db
|
from database.connection import get_db
|
||||||
from services.extractor import extract_document
|
from services.extractor import extract_document
|
||||||
from services.search_service import normalize_arabic
|
from services.search_service import normalize_arabic, _normalize_scope
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
REVIEWABLE_DOCUMENT_STATUSES = {"extracted", "confirmed", "error"}
|
||||||
|
|
||||||
|
|
||||||
def _get_document(doc_id: int) -> dict | None:
|
def _get_document(doc_id: int) -> dict | None:
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
@@ -43,6 +45,98 @@ def _get_document(doc_id: int) -> dict | None:
|
|||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_optional_bool(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return bool(value)
|
||||||
|
|
||||||
|
normalized = str(value).strip().lower()
|
||||||
|
if normalized in {"1", "true", "yes", "on"}:
|
||||||
|
return True
|
||||||
|
if normalized in {"0", "false", "no", "off", ""}:
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_merge_candidates(
|
||||||
|
first_name: str,
|
||||||
|
father_name: str = "",
|
||||||
|
family_name: str = "",
|
||||||
|
search_scope: str = "",
|
||||||
|
registry_number: str = "",
|
||||||
|
) -> list[dict]:
|
||||||
|
if not first_name:
|
||||||
|
return []
|
||||||
|
|
||||||
|
first_norm = normalize_arabic(first_name.strip())
|
||||||
|
family_norm = normalize_arabic(family_name.strip()) if family_name else ""
|
||||||
|
normalized_scope = _normalize_scope(search_scope)
|
||||||
|
normalized_registry = (registry_number or "").strip()
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
if family_norm:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT p.*,
|
||||||
|
COUNT(DISTINCT pr.id) AS property_count,
|
||||||
|
COUNT(DISTINCT d.id) AS document_count,
|
||||||
|
GROUP_CONCAT(DISTINCT NULLIF(TRIM(d.search_scope), '')) AS search_scopes
|
||||||
|
FROM persons p
|
||||||
|
LEFT JOIN documents d ON d.person_id = p.id
|
||||||
|
LEFT JOIN properties pr ON pr.person_id = p.id
|
||||||
|
WHERE p.first_name_norm = ? AND p.family_name_norm = ?
|
||||||
|
GROUP BY p.id""",
|
||||||
|
(first_norm, family_norm),
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""SELECT p.*,
|
||||||
|
COUNT(DISTINCT pr.id) AS property_count,
|
||||||
|
COUNT(DISTINCT d.id) AS document_count,
|
||||||
|
GROUP_CONCAT(DISTINCT NULLIF(TRIM(d.search_scope), '')) AS search_scopes
|
||||||
|
FROM persons p
|
||||||
|
LEFT JOIN documents d ON d.person_id = p.id
|
||||||
|
LEFT JOIN properties pr ON pr.person_id = p.id
|
||||||
|
WHERE p.first_name_norm = ?
|
||||||
|
GROUP BY p.id""",
|
||||||
|
(first_norm,),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
matches = []
|
||||||
|
normalized_father = normalize_arabic(father_name.strip()) if father_name else ""
|
||||||
|
for row in rows:
|
||||||
|
person = dict(row)
|
||||||
|
if normalized_father and person.get("father_name"):
|
||||||
|
if normalize_arabic(person["father_name"]) != normalized_father:
|
||||||
|
continue
|
||||||
|
scope_values = [scope.strip() for scope in (person.get("search_scopes") or "").split(",") if scope.strip()]
|
||||||
|
same_scope = bool(normalized_scope and normalized_scope in scope_values)
|
||||||
|
registry_match = bool(
|
||||||
|
normalized_registry
|
||||||
|
and person.get("registry_number")
|
||||||
|
and person["registry_number"].strip() == normalized_registry
|
||||||
|
)
|
||||||
|
person["search_scope_list"] = scope_values
|
||||||
|
person["same_scope"] = same_scope
|
||||||
|
person["registry_match"] = registry_match
|
||||||
|
person["merge_allowed"] = same_scope or registry_match
|
||||||
|
matches.append(person)
|
||||||
|
|
||||||
|
matches.sort(
|
||||||
|
key=lambda person: (
|
||||||
|
0 if person.get("merge_allowed") else 1,
|
||||||
|
0 if person.get("same_scope") else 1,
|
||||||
|
0 if person.get("registry_match") else 1,
|
||||||
|
-(person.get("document_count") or 0),
|
||||||
|
-(person.get("property_count") or 0),
|
||||||
|
person.get("id") or 0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
@router.get("/review/next")
|
@router.get("/review/next")
|
||||||
async def review_next():
|
async def review_next():
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
@@ -100,49 +194,34 @@ async def check_duplicate(
|
|||||||
first_name: str = "",
|
first_name: str = "",
|
||||||
father_name: str = "",
|
father_name: str = "",
|
||||||
family_name: str = "",
|
family_name: str = "",
|
||||||
|
search_scope: str = "",
|
||||||
|
registry_number: str = "",
|
||||||
):
|
):
|
||||||
"""Check if a person with similar name already exists. Called via AJAX from review page."""
|
"""Check if a person with similar name already exists. Called via AJAX from review page."""
|
||||||
if not first_name:
|
if not first_name:
|
||||||
return JSONResponse({"matches": []})
|
return JSONResponse({"matches": []})
|
||||||
|
|
||||||
first_norm = normalize_arabic(first_name.strip())
|
matches = []
|
||||||
family_norm = normalize_arabic(family_name.strip()) if family_name else ""
|
for candidate in _get_merge_candidates(
|
||||||
|
first_name,
|
||||||
with get_db() as conn:
|
father_name,
|
||||||
if family_norm:
|
family_name,
|
||||||
rows = conn.execute(
|
search_scope,
|
||||||
"""SELECT p.*, COUNT(DISTINCT pr.id) AS property_count
|
registry_number,
|
||||||
FROM persons p
|
):
|
||||||
LEFT JOIN properties pr ON pr.person_id = p.id
|
matches.append({
|
||||||
WHERE p.first_name_norm = ? AND p.family_name_norm = ?
|
"id": candidate["id"],
|
||||||
GROUP BY p.id""",
|
"first_name": candidate["first_name"],
|
||||||
(first_norm, family_norm),
|
"father_name": candidate.get("father_name"),
|
||||||
).fetchall()
|
"family_name": candidate.get("family_name"),
|
||||||
else:
|
"family_origin": candidate.get("family_origin"),
|
||||||
rows = conn.execute(
|
"property_count": candidate.get("property_count", 0),
|
||||||
"""SELECT p.*, COUNT(DISTINCT pr.id) AS property_count
|
"document_count": candidate.get("document_count", 0),
|
||||||
FROM persons p
|
"search_scopes": candidate.get("search_scope_list", []),
|
||||||
LEFT JOIN properties pr ON pr.person_id = p.id
|
"same_scope": candidate.get("same_scope", False),
|
||||||
WHERE p.first_name_norm = ?
|
"registry_match": candidate.get("registry_match", False),
|
||||||
GROUP BY p.id""",
|
"merge_allowed": candidate.get("merge_allowed", False),
|
||||||
(first_norm,),
|
})
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
# Further filter by father_name if provided
|
|
||||||
matches = []
|
|
||||||
for r in rows:
|
|
||||||
d = dict(r)
|
|
||||||
if father_name and d.get("father_name"):
|
|
||||||
if normalize_arabic(father_name.strip()) != normalize_arabic(d["father_name"]):
|
|
||||||
continue
|
|
||||||
matches.append({
|
|
||||||
"id": d["id"],
|
|
||||||
"first_name": d["first_name"],
|
|
||||||
"father_name": d.get("father_name"),
|
|
||||||
"family_name": d.get("family_name"),
|
|
||||||
"family_origin": d.get("family_origin"),
|
|
||||||
"property_count": d["property_count"],
|
|
||||||
})
|
|
||||||
|
|
||||||
return JSONResponse({"matches": matches})
|
return JSONResponse({"matches": matches})
|
||||||
|
|
||||||
@@ -155,34 +234,104 @@ async def confirm_document(doc_id: int, request: Request):
|
|||||||
properties_data = body.get("properties", [])
|
properties_data = body.get("properties", [])
|
||||||
merge_person_id = body.get("merge_person_id") # If user chose to merge
|
merge_person_id = body.get("merge_person_id") # If user chose to merge
|
||||||
|
|
||||||
first_name = (person_data.get("first_name") or "").strip()
|
if not isinstance(person_data, dict):
|
||||||
registry_number = (person_data.get("registry_number") or "").strip() or None
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid person payload")
|
||||||
|
if not isinstance(properties_data, list):
|
||||||
# We should update document fields as well since user might have edited them
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid properties payload")
|
||||||
request_number = (body.get("request_number") or "").strip() or None
|
|
||||||
request_date = (body.get("request_date") or "").strip() or None
|
|
||||||
page_info = (body.get("page_info") or "").strip() or None
|
|
||||||
search_scope = (body.get("search_scope") or "").strip() or None
|
|
||||||
request_purpose = (body.get("request_purpose") or "").strip() or None
|
|
||||||
data_valid_until = (body.get("data_valid_until") or "").strip() or None
|
|
||||||
registry_office = (body.get("registry_office") or "").strip() or None
|
|
||||||
owns_properties = body.get("owns_properties")
|
|
||||||
if owns_properties is not None:
|
|
||||||
owns_properties = bool(owns_properties)
|
|
||||||
declared_property_count = body.get("declared_property_count")
|
|
||||||
if declared_property_count is not None:
|
|
||||||
try:
|
|
||||||
declared_property_count = int(declared_property_count)
|
|
||||||
except ValueError:
|
|
||||||
declared_property_count = None
|
|
||||||
|
|
||||||
|
first_name = (person_data.get("first_name") or "").strip()
|
||||||
|
if not first_name:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="First name is required")
|
||||||
|
|
||||||
|
registry_number = (person_data.get("registry_number") or "").strip() or None
|
||||||
|
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
|
current_doc = conn.execute(
|
||||||
|
"SELECT * FROM documents WHERE id=?",
|
||||||
|
(doc_id,),
|
||||||
|
).fetchone()
|
||||||
|
if not current_doc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
||||||
|
|
||||||
|
current_doc = dict(current_doc)
|
||||||
|
if current_doc["status"] == "pending":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Document is still being processed",
|
||||||
|
)
|
||||||
|
if current_doc["status"] not in REVIEWABLE_DOCUMENT_STATUSES:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail=f"Document status '{current_doc['status']}' cannot be confirmed",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _body_or_existing(field_name: str):
|
||||||
|
if field_name in body:
|
||||||
|
value = body.get(field_name)
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.strip()
|
||||||
|
return value or None
|
||||||
|
return current_doc.get(field_name)
|
||||||
|
|
||||||
|
request_number = _body_or_existing("request_number")
|
||||||
|
request_date = _body_or_existing("request_date")
|
||||||
|
page_info = _body_or_existing("page_info")
|
||||||
|
search_scope = _body_or_existing("search_scope")
|
||||||
|
request_purpose = _body_or_existing("request_purpose")
|
||||||
|
data_valid_until = _body_or_existing("data_valid_until")
|
||||||
|
registry_office = _body_or_existing("registry_office")
|
||||||
|
normalized_search_scope = _normalize_scope(search_scope) or ""
|
||||||
|
|
||||||
|
if "owns_properties" in body:
|
||||||
|
owns_properties = _parse_optional_bool(body.get("owns_properties"))
|
||||||
|
else:
|
||||||
|
owns_properties = current_doc.get("owns_properties")
|
||||||
|
|
||||||
|
if "declared_property_count" in body:
|
||||||
|
declared_property_count = body.get("declared_property_count")
|
||||||
|
if declared_property_count is not None and str(declared_property_count).strip() != "":
|
||||||
|
try:
|
||||||
|
declared_property_count = int(declared_property_count)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid declared property count",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
declared_property_count = None
|
||||||
|
else:
|
||||||
|
declared_property_count = current_doc.get("declared_property_count")
|
||||||
|
|
||||||
person_id = None
|
person_id = None
|
||||||
|
|
||||||
# Option 1: User explicitly chose to merge with an existing person
|
# Option 1: User explicitly chose to merge with an existing person
|
||||||
if merge_person_id:
|
if merge_person_id:
|
||||||
person_id = int(merge_person_id)
|
try:
|
||||||
|
person_id = int(merge_person_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid merge target")
|
||||||
|
|
||||||
|
existing_person = conn.execute(
|
||||||
|
"SELECT * FROM persons WHERE id=?",
|
||||||
|
(person_id,),
|
||||||
|
).fetchone()
|
||||||
|
if not existing_person:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Merge target not found")
|
||||||
|
|
||||||
|
allowed_merge_ids = {candidate["id"] for candidate in _get_merge_candidates(
|
||||||
|
first_name,
|
||||||
|
person_data.get("father_name") or "",
|
||||||
|
person_data.get("family_name") or "",
|
||||||
|
normalized_search_scope,
|
||||||
|
registry_number or "",
|
||||||
|
) if candidate.get("merge_allowed")}
|
||||||
|
|
||||||
|
if person_id not in allowed_merge_ids:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Merge target is not a verified match for this search scope",
|
||||||
|
)
|
||||||
|
|
||||||
# Update person info with latest data
|
# Update person info with latest data
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""UPDATE persons SET
|
"""UPDATE persons SET
|
||||||
@@ -241,31 +390,66 @@ async def confirm_document(doc_id: int, request: Request):
|
|||||||
|
|
||||||
# Option 3: Create new person
|
# Option 3: Create new person
|
||||||
if not person_id:
|
if not person_id:
|
||||||
cursor = conn.execute(
|
existing_person = None
|
||||||
"""INSERT INTO persons
|
if current_doc.get("person_id"):
|
||||||
(first_name, father_name, mother_name, family_name, family_origin,
|
existing_person = conn.execute(
|
||||||
nationality, birth_date, registry_number, registry_place,
|
"SELECT id FROM persons WHERE id=?",
|
||||||
first_name_norm, family_name_norm)
|
(current_doc["person_id"],),
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
).fetchone()
|
||||||
(
|
|
||||||
first_name,
|
if existing_person:
|
||||||
person_data.get("father_name"),
|
person_id = existing_person["id"]
|
||||||
person_data.get("mother_name"),
|
conn.execute(
|
||||||
person_data.get("family_name"),
|
"""UPDATE persons SET
|
||||||
person_data.get("family_origin"),
|
first_name=?, father_name=?, mother_name=?,
|
||||||
person_data.get("nationality"),
|
family_name=?, family_origin=?, nationality=?,
|
||||||
person_data.get("birth_date"),
|
birth_date=?, registry_number=?, registry_place=?,
|
||||||
registry_number,
|
first_name_norm=?, family_name_norm=?,
|
||||||
person_data.get("registry_place"),
|
updated_at=CURRENT_TIMESTAMP
|
||||||
normalize_arabic(first_name),
|
WHERE id=?""",
|
||||||
normalize_arabic(person_data.get("family_name") or ""),
|
(
|
||||||
),
|
first_name,
|
||||||
)
|
person_data.get("father_name"),
|
||||||
person_id = cursor.lastrowid
|
person_data.get("mother_name"),
|
||||||
|
person_data.get("family_name"),
|
||||||
|
person_data.get("family_origin"),
|
||||||
|
person_data.get("nationality"),
|
||||||
|
person_data.get("birth_date"),
|
||||||
|
registry_number,
|
||||||
|
person_data.get("registry_place"),
|
||||||
|
normalize_arabic(first_name),
|
||||||
|
normalize_arabic(person_data.get("family_name") or ""),
|
||||||
|
person_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cursor = conn.execute(
|
||||||
|
"""INSERT INTO persons
|
||||||
|
(first_name, father_name, mother_name, family_name, family_origin,
|
||||||
|
nationality, birth_date, registry_number, registry_place,
|
||||||
|
first_name_norm, family_name_norm)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
|
(
|
||||||
|
first_name,
|
||||||
|
person_data.get("father_name"),
|
||||||
|
person_data.get("mother_name"),
|
||||||
|
person_data.get("family_name"),
|
||||||
|
person_data.get("family_origin"),
|
||||||
|
person_data.get("nationality"),
|
||||||
|
person_data.get("birth_date"),
|
||||||
|
registry_number,
|
||||||
|
person_data.get("registry_place"),
|
||||||
|
normalize_arabic(first_name),
|
||||||
|
normalize_arabic(person_data.get("family_name") or ""),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
person_id = cursor.lastrowid
|
||||||
|
|
||||||
# Replace properties for this document
|
# Replace properties for this document
|
||||||
conn.execute("DELETE FROM properties WHERE document_id=?", (doc_id,))
|
conn.execute("DELETE FROM properties WHERE document_id=?", (doc_id,))
|
||||||
for i, prop in enumerate(properties_data):
|
for i, prop in enumerate(properties_data):
|
||||||
|
if not isinstance(prop, dict):
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid property payload")
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO properties
|
"""INSERT INTO properties
|
||||||
(document_id, person_id, row_order, party_name, property_number,
|
(document_id, person_id, row_order, party_name, property_number,
|
||||||
|
|||||||
+7
-7
@@ -32,7 +32,10 @@ async def search(
|
|||||||
|
|
||||||
# If exactly one person found, preload their full details
|
# If exactly one person found, preload their full details
|
||||||
if len(persons) == 1 and not properties:
|
if len(persons) == 1 and not properties:
|
||||||
selected_person = get_person_with_properties(persons[0]["id"])
|
selected_person = get_person_with_properties(
|
||||||
|
persons[0]["id"],
|
||||||
|
persons[0].get("search_scope"),
|
||||||
|
)
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
@@ -50,8 +53,8 @@ async def search(
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/persons/{person_id}")
|
@router.get("/persons/{person_id}")
|
||||||
async def person_detail(request: Request, person_id: int):
|
async def person_detail(request: Request, person_id: int, search_scope: str = ""):
|
||||||
data = get_person_with_properties(person_id)
|
data = get_person_with_properties(person_id, search_scope.strip() or None)
|
||||||
if not data:
|
if not data:
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
@@ -71,16 +74,13 @@ async def person_export_csv(person_id: int, qaza: str = ""):
|
|||||||
import io
|
import io
|
||||||
import csv
|
import csv
|
||||||
|
|
||||||
data = get_person_with_properties(person_id)
|
data = get_person_with_properties(person_id, qaza.strip() or None)
|
||||||
if not data:
|
if not data:
|
||||||
return Response("Person not found", status_code=404)
|
return Response("Person not found", status_code=404)
|
||||||
|
|
||||||
person = data["person"]
|
person = data["person"]
|
||||||
properties = data["properties"]
|
properties = data["properties"]
|
||||||
|
|
||||||
if qaza:
|
|
||||||
properties = [p for p in properties if (p.get("search_scope") or p.get("qaza") or "").strip() == qaza.strip()]
|
|
||||||
|
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
# Write BOM for Excel to open Arabic UTF-8 correctly
|
# Write BOM for Excel to open Arabic UTF-8 correctly
|
||||||
output.write('\ufeff')
|
output.write('\ufeff')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import sqlite3
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import hmac
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
@@ -11,7 +11,7 @@ def verify_password(stored_password: str, provided_password: str) -> bool:
|
|||||||
try:
|
try:
|
||||||
salt, stored_hash = stored_password.split('$')
|
salt, stored_hash = stored_password.split('$')
|
||||||
hashed = hashlib.pbkdf2_hmac('sha256', provided_password.encode('utf-8'), salt.encode('utf-8'), 100000).hex()
|
hashed = hashlib.pbkdf2_hmac('sha256', provided_password.encode('utf-8'), salt.encode('utf-8'), 100000).hex()
|
||||||
return hashed == stored_hash
|
return hmac.compare_digest(hashed, stored_hash)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
+42
-20
@@ -12,37 +12,53 @@ def normalize_arabic(text: str) -> str:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_scope(text: str | None) -> str | None:
|
||||||
|
if text is None:
|
||||||
|
return None
|
||||||
|
normalized = text.strip()
|
||||||
|
return normalized or None
|
||||||
|
|
||||||
|
|
||||||
def search_persons(query: str) -> list[dict]:
|
def search_persons(query: str) -> list[dict]:
|
||||||
"""Search persons by name (first, family, or father name)."""
|
"""Search persons by name, keeping separate result rows per search scope."""
|
||||||
norm = normalize_arabic(query.strip())
|
norm = normalize_arabic(query.strip())
|
||||||
pattern = f"%{norm}%"
|
pattern = f"%{norm}%"
|
||||||
raw_pattern = f"%{query.strip()}%"
|
raw_pattern = f"%{query.strip()}%"
|
||||||
|
scope_expr = "COALESCE(NULLIF(TRIM(d.search_scope), ''), '')"
|
||||||
|
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT p.*,
|
SELECT p.*,
|
||||||
|
{scope_expr} AS search_scope,
|
||||||
COUNT(DISTINCT pr.id) AS property_count,
|
COUNT(DISTINCT pr.id) AS property_count,
|
||||||
COUNT(DISTINCT d.id) AS document_count,
|
COUNT(DISTINCT d.id) AS document_count
|
||||||
GROUP_CONCAT(DISTINCT d.search_scope) AS search_scopes
|
|
||||||
FROM persons p
|
FROM persons p
|
||||||
LEFT JOIN properties pr ON pr.person_id = p.id
|
|
||||||
LEFT JOIN documents d ON d.person_id = p.id
|
LEFT JOIN documents d ON d.person_id = p.id
|
||||||
|
LEFT JOIN properties pr ON pr.document_id = d.id
|
||||||
WHERE p.first_name_norm LIKE ?
|
WHERE p.first_name_norm LIKE ?
|
||||||
OR p.family_name_norm LIKE ?
|
OR p.family_name_norm LIKE ?
|
||||||
OR p.father_name LIKE ?
|
OR p.father_name LIKE ?
|
||||||
OR p.first_name LIKE ?
|
OR p.first_name LIKE ?
|
||||||
OR p.family_name LIKE ?
|
OR p.family_name LIKE ?
|
||||||
GROUP BY p.id
|
GROUP BY p.id, {scope_expr}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE WHEN p.first_name_norm = ? THEN 0
|
CASE WHEN p.first_name_norm = ? THEN 0
|
||||||
WHEN p.family_name_norm = ? THEN 0
|
WHEN p.family_name_norm = ? THEN 0
|
||||||
ELSE 1 END,
|
ELSE 1 END,
|
||||||
p.first_name
|
p.first_name,
|
||||||
|
p.family_name,
|
||||||
|
search_scope
|
||||||
""",
|
""",
|
||||||
(pattern, pattern, raw_pattern, raw_pattern, raw_pattern, norm, norm),
|
(pattern, pattern, raw_pattern, raw_pattern, raw_pattern, norm, norm),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(r) for r in rows]
|
|
||||||
|
results = []
|
||||||
|
for row in rows:
|
||||||
|
person = dict(row)
|
||||||
|
person["search_scope"] = _normalize_scope(person.get("search_scope"))
|
||||||
|
results.append(person)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def search_properties(
|
def search_properties(
|
||||||
@@ -94,8 +110,9 @@ def search_properties(
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def get_person_with_properties(person_id: int) -> dict | None:
|
def get_person_with_properties(person_id: int, search_scope: str | None = None) -> dict | None:
|
||||||
"""Fetch a person and all their properties."""
|
"""Fetch a person and all their properties, optionally filtered by search scope."""
|
||||||
|
normalized_scope = _normalize_scope(search_scope)
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
person = conn.execute(
|
person = conn.execute(
|
||||||
"SELECT * FROM persons WHERE id = ?", (person_id,)
|
"SELECT * FROM persons WHERE id = ?", (person_id,)
|
||||||
@@ -103,21 +120,25 @@ def get_person_with_properties(person_id: int) -> dict | None:
|
|||||||
if not person:
|
if not person:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
props = conn.execute(
|
props_query = """
|
||||||
"""
|
|
||||||
SELECT pr.*, d.image_path, d.id AS document_id, d.search_scope
|
SELECT pr.*, d.image_path, d.id AS document_id, d.search_scope
|
||||||
FROM properties pr
|
FROM properties pr
|
||||||
LEFT JOIN documents d ON d.id = pr.document_id
|
LEFT JOIN documents d ON d.id = pr.document_id
|
||||||
WHERE pr.person_id = ?
|
WHERE pr.person_id = ?
|
||||||
ORDER BY pr.real_estate_district, pr.row_order
|
"""
|
||||||
""",
|
props_params = [person_id]
|
||||||
(person_id,),
|
if normalized_scope:
|
||||||
).fetchall()
|
props_query += " AND COALESCE(NULLIF(TRIM(d.search_scope), ''), '') = ?"
|
||||||
|
props_params.append(normalized_scope)
|
||||||
|
props_query += " ORDER BY pr.real_estate_district, pr.row_order"
|
||||||
|
props = conn.execute(props_query, props_params).fetchall()
|
||||||
|
|
||||||
docs = conn.execute(
|
docs_query = "SELECT id, image_path, request_number, request_date, status, page_info, search_scope FROM documents WHERE person_id = ?"
|
||||||
"SELECT id, image_path, request_number, request_date, status, page_info, search_scope FROM documents WHERE person_id = ?",
|
docs_params = [person_id]
|
||||||
(person_id,),
|
if normalized_scope:
|
||||||
).fetchall()
|
docs_query += " AND COALESCE(NULLIF(TRIM(search_scope), ''), '') = ?"
|
||||||
|
docs_params.append(normalized_scope)
|
||||||
|
docs = conn.execute(docs_query, docs_params).fetchall()
|
||||||
|
|
||||||
properties_list = []
|
properties_list = []
|
||||||
doc_ids_with_props = set()
|
doc_ids_with_props = set()
|
||||||
@@ -149,4 +170,5 @@ def get_person_with_properties(person_id: int) -> dict | None:
|
|||||||
"person": dict(person),
|
"person": dict(person),
|
||||||
"properties": properties_list,
|
"properties": properties_list,
|
||||||
"documents": docs_list,
|
"documents": docs_list,
|
||||||
|
"current_search_scope": normalized_scope,
|
||||||
}
|
}
|
||||||
|
|||||||
+809
-171
File diff suppressed because it is too large
Load Diff
+148
-12
@@ -1,25 +1,27 @@
|
|||||||
/* Review page — two-column sticky layout */
|
/* Review page — two-column sticky layout */
|
||||||
.review-layout {
|
.review-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1.4fr;
|
grid-template-columns: minmax(320px, .92fr) minmax(0, 1.4fr);
|
||||||
gap: 1.25rem;
|
gap: 1.4rem;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Image panel */
|
/* Image panel */
|
||||||
.image-panel {
|
.image-panel {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 70px;
|
top: 88px;
|
||||||
background: #1f2937;
|
background: linear-gradient(180deg, #1a2e38, #0f1c22);
|
||||||
border-radius: 8px;
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,.2);
|
box-shadow: 0 16px 48px rgba(15, 28, 34, .28);
|
||||||
|
border: 1px solid rgba(255,255,255,.06);
|
||||||
}
|
}
|
||||||
.image-toolbar {
|
.image-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .4rem;
|
gap: .35rem;
|
||||||
padding: .5rem .75rem;
|
padding: .65rem .75rem;
|
||||||
background: #111827;
|
background: rgba(10, 18, 22, .94);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,.06);
|
||||||
}
|
}
|
||||||
.image-container {
|
.image-container {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -27,15 +29,24 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: .5rem;
|
padding: .9rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(45deg, rgba(255,255,255,.03) 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, rgba(255,255,255,.03) 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, rgba(255,255,255,.03) 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, rgba(255,255,255,.03) 75%);
|
||||||
|
background-size: 24px 24px;
|
||||||
|
background-position: 0 0, 0 12px, 12px -12px, -12px 0;
|
||||||
}
|
}
|
||||||
.doc-image {
|
.doc-image {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
transform-origin: top center;
|
transform-origin: top center;
|
||||||
transition: transform .2s;
|
transition: transform .2s cubic-bezier(.4,0,.2,1);
|
||||||
border-radius: 4px;
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 12px 36px rgba(0,0,0,.32);
|
||||||
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form panel */
|
/* Form panel */
|
||||||
@@ -43,6 +54,27 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-wrapper-editable {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-edit-table td[data-label]::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-title {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-title .page-kicker {
|
||||||
|
margin-bottom: .45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-title h2 {
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================
|
/* ============================
|
||||||
Review — Responsive
|
Review — Responsive
|
||||||
============================ */
|
============================ */
|
||||||
@@ -57,6 +89,107 @@
|
|||||||
.image-container {
|
.image-container {
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
|
.review-nav {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.review-nav .btn {
|
||||||
|
flex: 1 1 calc(50% - .5rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.review-layout {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.image-container {
|
||||||
|
max-height: 36vh;
|
||||||
|
padding: .7rem;
|
||||||
|
}
|
||||||
|
.image-toolbar {
|
||||||
|
padding: .65rem;
|
||||||
|
}
|
||||||
|
.review-nav .btn {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
.form-section {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.table-wrapper-editable {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
margin-inline: 0;
|
||||||
|
}
|
||||||
|
.mobile-edit-table {
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.mobile-edit-table thead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mobile-edit-table tbody,
|
||||||
|
.mobile-edit-table tr,
|
||||||
|
.mobile-edit-table td {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.mobile-edit-table tr {
|
||||||
|
margin-bottom: .85rem;
|
||||||
|
padding: .85rem;
|
||||||
|
border: 1px solid rgba(23,37,44,.08);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(255,255,255,.96);
|
||||||
|
box-shadow: 0 10px 24px rgba(23,37,44,.08);
|
||||||
|
}
|
||||||
|
.mobile-edit-table td {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: .65rem;
|
||||||
|
}
|
||||||
|
.mobile-edit-table td:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.mobile-edit-table td[data-label]::before {
|
||||||
|
display: block;
|
||||||
|
font-size: .76rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
.mobile-edit-table input {
|
||||||
|
min-width: 0;
|
||||||
|
padding: .7rem .8rem;
|
||||||
|
background: rgba(248,244,236,.72);
|
||||||
|
border: 1px solid rgba(23,37,44,.08);
|
||||||
|
}
|
||||||
|
.mobile-edit-table td[data-label="حذف"] {
|
||||||
|
margin-top: .2rem;
|
||||||
|
}
|
||||||
|
.mobile-edit-table td[data-label="حذف"] .btn-del {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
background: #fff5f5;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.form-actions {
|
||||||
|
position: sticky;
|
||||||
|
bottom: calc(5.4rem + env(safe-area-inset-bottom));
|
||||||
|
z-index: 20;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: .85rem;
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
box-shadow: 0 -12px 24px rgba(23,37,44,.12);
|
||||||
|
background: rgba(255,255,255,.96);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
padding-bottom: calc(.85rem + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
.keyboard-hint {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
@@ -66,4 +199,7 @@
|
|||||||
.image-toolbar {
|
.image-toolbar {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.image-panel {
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-9
@@ -2,26 +2,62 @@
|
|||||||
<html lang="ar" dir="rtl">
|
<html lang="ar" dir="rtl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
<title>{% block title %}سجل العقارات اللبناني{% endblock %}</title>
|
<title>{% block title %}سجل العقارات اللبناني{% endblock %}</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Kufi+Arabic:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/main.css">
|
<link rel="stylesheet" href="/static/css/main.css">
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="page-backdrop" aria-hidden="true">
|
||||||
|
<span class="backdrop-orb orb-1"></span>
|
||||||
|
<span class="backdrop-orb orb-2"></span>
|
||||||
|
</div>
|
||||||
{% block navbar %}
|
{% block navbar %}
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<div class="nav-container">
|
<div class="nav-container">
|
||||||
<a class="nav-brand" href="/">🏠 سجل العقارات</a>
|
<a class="nav-brand" href="/">
|
||||||
<button class="nav-toggle" id="navToggle" aria-label="القائمة">
|
<span class="nav-brand-mark">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 21h18"/>
|
||||||
|
<path d="M5 21V7l7-4 7 4v14"/>
|
||||||
|
<path d="M9 21v-6h6v6"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="nav-brand-text">سجل العقارات</span>
|
||||||
|
</a>
|
||||||
|
<button class="nav-toggle" id="navToggle" aria-label="القائمة" aria-expanded="false" aria-controls="navLinks">
|
||||||
<span></span><span></span><span></span>
|
<span></span><span></span><span></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="nav-links" id="navLinks">
|
<div class="nav-links" id="navLinks">
|
||||||
<a href="/" class="nav-link">رفع وثائق</a>
|
<a href="/" class="nav-link">
|
||||||
<a href="/review/next" class="nav-link">مراجعة</a>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||||
<a href="/search" class="nav-link">بحث</a>
|
رفع وثائق
|
||||||
<a href="/documents" class="nav-link">قائمة الوثائق</a>
|
</a>
|
||||||
<a href="/auth/users" class="nav-link">الإدارة</a>
|
<a href="/review/next" class="nav-link">
|
||||||
<a href="/auth/logout" class="nav-link">تسجيل خروج</a>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
|
||||||
|
مراجعة
|
||||||
|
</a>
|
||||||
|
<a href="/search" class="nav-link">
|
||||||
|
<svg width="16" height="16" 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"/></svg>
|
||||||
|
بحث
|
||||||
|
</a>
|
||||||
|
<a href="/documents" class="nav-link">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
قائمة الوثائق
|
||||||
|
</a>
|
||||||
|
<a href="/auth/users" class="nav-link">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15c-4 0-7 2-7 4v1h14v-1c0-2-3-4-7-4z"/><circle cx="12" cy="8" r="4"/></svg>
|
||||||
|
الإدارة
|
||||||
|
</a>
|
||||||
|
<a href="/auth/logout" class="nav-link nav-link-logout">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||||
|
خروج
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -31,6 +67,33 @@
|
|||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<nav class="mobile-dock" aria-label="التنقل السريع">
|
||||||
|
<a href="/" class="mobile-dock-link" data-match="/">
|
||||||
|
<span class="mobile-dock-icon">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||||
|
</span>
|
||||||
|
<span>رفع</span>
|
||||||
|
</a>
|
||||||
|
<a href="/review/next" class="mobile-dock-link" data-match="/review">
|
||||||
|
<span class="mobile-dock-icon">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
|
||||||
|
</span>
|
||||||
|
<span>مراجعة</span>
|
||||||
|
</a>
|
||||||
|
<a href="/search" class="mobile-dock-link" data-match="/search,/persons">
|
||||||
|
<span class="mobile-dock-icon">
|
||||||
|
<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="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
|
</span>
|
||||||
|
<span>بحث</span>
|
||||||
|
</a>
|
||||||
|
<a href="/documents" class="mobile-dock-link" data-match="/documents">
|
||||||
|
<span class="mobile-dock-icon">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
</span>
|
||||||
|
<span>وثائق</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const navToggle = document.getElementById('navToggle');
|
const navToggle = document.getElementById('navToggle');
|
||||||
const navLinks = document.getElementById('navLinks');
|
const navLinks = document.getElementById('navLinks');
|
||||||
@@ -38,8 +101,46 @@
|
|||||||
navToggle.addEventListener('click', () => {
|
navToggle.addEventListener('click', () => {
|
||||||
navLinks.classList.toggle('open');
|
navLinks.classList.toggle('open');
|
||||||
navToggle.classList.toggle('active');
|
navToggle.classList.toggle('active');
|
||||||
|
navToggle.setAttribute('aria-expanded', String(navLinks.classList.contains('open')));
|
||||||
|
});
|
||||||
|
|
||||||
|
navLinks.querySelectorAll('a').forEach(link => {
|
||||||
|
link.addEventListener('click', () => {
|
||||||
|
navLinks.classList.remove('open');
|
||||||
|
navToggle.classList.remove('active');
|
||||||
|
navToggle.setAttribute('aria-expanded', 'false');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', event => {
|
||||||
|
document.querySelectorAll('.export-dropdown').forEach(dropdown => {
|
||||||
|
if (!dropdown.contains(event.target)) {
|
||||||
|
dropdown.querySelector('.export-menu')?.classList.remove('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-export-toggle]').forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
button.nextElementSibling?.classList.toggle('show');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
const mobileDock = document.querySelector('.mobile-dock');
|
||||||
|
if (currentPath.startsWith('/auth/login')) {
|
||||||
|
mobileDock?.setAttribute('hidden', 'hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.mobile-dock-link').forEach(link => {
|
||||||
|
const patterns = (link.dataset.match || '').split(',').filter(Boolean);
|
||||||
|
const isRoot = patterns.includes('/') && currentPath === '/';
|
||||||
|
const isMatched = isRoot || patterns.some(pattern => pattern !== '/' && currentPath.startsWith(pattern));
|
||||||
|
if (isMatched) {
|
||||||
|
link.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+19
-12
@@ -3,8 +3,15 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>قائمة الوثائق</h1>
|
<div>
|
||||||
<a href="/" class="btn btn-primary">+ رفع وثائق جديدة</a>
|
<span class="page-kicker">متابعة المعالجة</span>
|
||||||
|
<h1>قائمة الوثائق</h1>
|
||||||
|
<p class="subtitle">تابع حالة كل وثيقة وانتقل مباشرة إلى شاشة المراجعة عند الحاجة.</p>
|
||||||
|
</div>
|
||||||
|
<a href="/" class="btn btn-primary">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
|
رفع وثائق جديدة
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if uploaded %}
|
{% if uploaded %}
|
||||||
@@ -33,12 +40,12 @@
|
|||||||
|
|
||||||
{% if stats.pending_review %}
|
{% if stats.pending_review %}
|
||||||
<div class="alert-banner">
|
<div class="alert-banner">
|
||||||
<a href="/review/next">▶ ابدأ مراجعة {{ stats.pending_review }} وثيقة</a>
|
<a href="/review/next">ابدأ مراجعة {{ stats.pending_review }} وثيقة →</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="results-table">
|
<table class="results-table responsive-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
@@ -53,21 +60,21 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for d in documents %}
|
{% for d in documents %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ d.id }}</td>
|
<td data-label="#">{{ d.id }}</td>
|
||||||
<td>
|
<td data-label="الصورة">
|
||||||
<a href="/review/{{ d.id }}">
|
<a href="/review/{{ d.id }}" title="فتح الوثيقة {{ d.id }} للمراجعة" aria-label="فتح الوثيقة {{ d.id }} للمراجعة">
|
||||||
<img src="/uploads/{{ d.image_path }}" class="doc-thumb-mini" alt="">
|
<img src="/uploads/{{ d.image_path }}" class="doc-thumb-mini" alt="">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-label="الشخص">
|
||||||
{% if d.person_id %}
|
{% if d.person_id %}
|
||||||
<a href="/persons/{{ d.person_id }}">{{ d.first_name or '' }} {{ d.family_name or '' }}</a>
|
<a href="/persons/{{ d.person_id }}">{{ d.first_name or '' }} {{ d.family_name or '' }}</a>
|
||||||
{% else %}—{% endif %}
|
{% else %}—{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ d.request_number or '—' }}</td>
|
<td data-label="رقم الطلب">{{ d.request_number or '—' }}</td>
|
||||||
<td><span class="status-badge status-{{ d.status }}">{{ d.status }}</span></td>
|
<td data-label="الحالة"><span class="status-badge status-{{ d.status }}">{{ d.status }}</span></td>
|
||||||
<td>{{ d.created_at[:10] if d.created_at else '' }}</td>
|
<td data-label="التاريخ">{{ d.created_at[:10] if d.created_at else '' }}</td>
|
||||||
<td class="doc-actions">
|
<td data-label="الإجراءات" class="doc-actions">
|
||||||
<a href="/review/{{ d.id }}" class="btn btn-sm btn-secondary">
|
<a href="/review/{{ d.id }}" class="btn btn-sm btn-secondary">
|
||||||
{% if d.status == 'confirmed' %}عرض{% else %}مراجعة{% endif %}
|
{% if d.status == 'confirmed' %}عرض{% else %}مراجعة{% endif %}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
+38
-12
@@ -2,9 +2,16 @@
|
|||||||
{% block title %}رفع وثائق — سجل العقارات{% endblock %}
|
{% block title %}رفع وثائق — سجل العقارات{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header page-header-hero">
|
||||||
<h1>بطاقات معلومات الملكية العقارية</h1>
|
<div>
|
||||||
<p class="subtitle">ارفع صور أو ملفات PDF للوثائق — اختر محرك الاستخراج (مجاني أو ذكاء اصطناعي)</p>
|
<span class="page-kicker">منصة الأرشفة والمراجعة</span>
|
||||||
|
<h1>بطاقات معلومات الملكية العقارية</h1>
|
||||||
|
<p class="subtitle">ارفع صور أو ملفات PDF، ثم راجع النتائج واستكمل البحث من واجهة واحدة واضحة وسريعة.</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a href="/search" class="btn btn-secondary">فتح البحث</a>
|
||||||
|
<a href="/documents" class="btn btn-primary">عرض الوثائق</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if stats and stats.total %}
|
{% if stats and stats.total %}
|
||||||
@@ -16,16 +23,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if stats.pending_review %}
|
{% if stats.pending_review %}
|
||||||
<div class="alert-banner">
|
<div class="alert-banner">
|
||||||
<a href="/review/next">▶ ابدأ مراجعة {{ stats.pending_review }} وثيقة</a>
|
<a href="/review/next">ابدأ مراجعة {{ stats.pending_review }} وثيقة →</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="upload-card">
|
<div class="upload-card">
|
||||||
|
<div class="card-heading">
|
||||||
|
<div>
|
||||||
|
<h2>رفع ملفات جديدة</h2>
|
||||||
|
<p>اختر دفعة صور أو PDF، ثم حدّد محرك الاستخراج قبل بدء المعالجة.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form id="uploadForm" action="/upload" method="post" enctype="multipart/form-data">
|
<form id="uploadForm" action="/upload" method="post" enctype="multipart/form-data">
|
||||||
<input type="file" name="files" id="fileInput" multiple accept=".jpg,.jpeg,.png,.webp,.pdf" class="file-input">
|
<input type="file" name="files" id="fileInput" multiple accept=".jpg,.jpeg,.png,.webp,.pdf" class="file-input">
|
||||||
<label for="fileInput" class="drop-zone" id="dropZone">
|
<label for="fileInput" class="drop-zone" id="dropZone">
|
||||||
<div class="drop-icon">📄</div>
|
<div class="drop-icon">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||||
|
</div>
|
||||||
<p class="drop-text">اسحب الملفات هنا أو انقر للاختيار</p>
|
<p class="drop-text">اسحب الملفات هنا أو انقر للاختيار</p>
|
||||||
<p class="drop-hint">JPG، PNG، PDF — يمكن رفع عدة ملفات دفعة واحدة</p>
|
<p class="drop-hint">JPG، PNG، PDF — يمكن رفع عدة ملفات دفعة واحدة</p>
|
||||||
</label>
|
</label>
|
||||||
@@ -51,7 +66,8 @@
|
|||||||
|
|
||||||
<div id="uploadActions" class="upload-actions hidden">
|
<div id="uploadActions" class="upload-actions hidden">
|
||||||
<button type="submit" class="btn btn-primary btn-large">
|
<button type="submit" class="btn btn-primary btn-large">
|
||||||
⬆ رفع ومعالجة
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||||
|
رفع ومعالجة
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-secondary" onclick="clearFiles()">إلغاء</button>
|
<button type="button" class="btn btn-secondary" onclick="clearFiles()">إلغاء</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,12 +76,22 @@
|
|||||||
|
|
||||||
<div class="quick-links">
|
<div class="quick-links">
|
||||||
<a href="/search" class="quick-link-card">
|
<a href="/search" class="quick-link-card">
|
||||||
<span class="ql-icon">🔍</span>
|
<span class="ql-icon">
|
||||||
<span>البحث في قاعدة البيانات</span>
|
<svg width="20" height="20" 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"/></svg>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong>البحث في قاعدة البيانات</strong>
|
||||||
|
<small>ابحث بالأسماء أو بالعقارات مع نتائج منظّمة.</small>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/documents" class="quick-link-card">
|
<a href="/documents" class="quick-link-card">
|
||||||
<span class="ql-icon">📋</span>
|
<span class="ql-icon">
|
||||||
<span>قائمة جميع الوثائق</span>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong>قائمة جميع الوثائق</strong>
|
||||||
|
<small>تابع الحالات، افتح المراجعة، أو احذف المستندات غير المطلوبة.</small>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -96,7 +122,7 @@ fileInput.addEventListener('change', () => showFiles(fileInput.files));
|
|||||||
function showFiles(files) {
|
function showFiles(files) {
|
||||||
if (!files.length) return;
|
if (!files.length) return;
|
||||||
fileList.innerHTML = Array.from(files).map(f =>
|
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>`
|
`<div class="file-item">${f.name} <span class="file-size">${(f.size/1024).toFixed(0)} KB</span></div>`
|
||||||
).join('');
|
).join('');
|
||||||
fileList.classList.remove('hidden');
|
fileList.classList.remove('hidden');
|
||||||
document.getElementById('providerSection').classList.remove('hidden');
|
document.getElementById('providerSection').classList.remove('hidden');
|
||||||
@@ -112,7 +138,7 @@ function clearFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('uploadForm').addEventListener('submit', function() {
|
document.getElementById('uploadForm').addEventListener('submit', function() {
|
||||||
document.querySelector('.btn-primary').textContent = '⏳ جارٍ الرفع...';
|
document.querySelector('.btn-primary').textContent = 'جارٍ الرفع...';
|
||||||
document.querySelector('.btn-primary').disabled = true;
|
document.querySelector('.btn-primary').disabled = true;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+12
-2
@@ -6,7 +6,17 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<h2>🏠 تسجيل الدخول</h2>
|
<div class="login-logo">
|
||||||
|
<span class="login-logo-icon">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 21h18"/>
|
||||||
|
<path d="M5 21V7l7-4 7 4v14"/>
|
||||||
|
<path d="M9 21v-6h6v6"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2>تسجيل الدخول</h2>
|
||||||
|
<p class="login-subtitle">أدخل بياناتك للوصول إلى سجل العقارات</p>
|
||||||
|
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="login-error">{{ error }}</div>
|
<div class="login-error">{{ error }}</div>
|
||||||
@@ -23,7 +33,7 @@
|
|||||||
<input type="password" id="password" name="password" required placeholder="أدخل كلمة المرور">
|
<input type="password" id="password" name="password" required placeholder="أدخل كلمة المرور">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-large" style="width:100%; justify-content:center; margin-top:.5rem;">
|
<button type="submit" class="btn btn-primary btn-large btn-block login-submit">
|
||||||
دخول
|
دخول
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,18 +4,28 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<a href="/search" class="back-link">← العودة للبحث</a>
|
<a href="/search" class="back-link no-print">← العودة للبحث</a>
|
||||||
<h1>{{ person.first_name }} {{ person.father_name or '' }} {{ person.family_name or '' }}</h1>
|
<h1>{{ person.first_name }} {{ person.father_name or '' }} {{ person.family_name or '' }}</h1>
|
||||||
|
{% if current_search_scope %}
|
||||||
|
<p class="subtitle">عرض نتائج نطاق البحث: {{ current_search_scope }}</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="header-actions no-print">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.print()">
|
||||||
|
<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="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
|
||||||
|
طباعة
|
||||||
|
</button>
|
||||||
{% set scopes = documents | selectattr('search_scope') | map(attribute='search_scope') | unique | list %}
|
{% set scopes = documents | selectattr('search_scope') | map(attribute='search_scope') | unique | list %}
|
||||||
<div class="export-dropdown">
|
<div class="export-dropdown">
|
||||||
<button class="btn btn-primary" onclick="this.nextElementSibling.classList.toggle('show')">📥 تصدير كملف إكسل ▾</button>
|
<button type="button" class="btn btn-primary" data-export-toggle>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
|
تصدير كملف إكسل ▾
|
||||||
|
</button>
|
||||||
<div class="export-menu">
|
<div class="export-menu">
|
||||||
<a href="/persons/{{ person.id }}/export" class="export-option" download>📥 تصدير الكل</a>
|
<a href="/persons/{{ person.id }}/export" class="export-option" download>تصدير الكل</a>
|
||||||
{% for scope in scopes %}
|
{% for scope in scopes %}
|
||||||
{% if scope %}
|
{% if scope %}
|
||||||
<a href="/persons/{{ person.id }}/export?qaza={{ scope|urlencode }}" class="export-option" download>📥 {{ scope }}</a>
|
<a href="/persons/{{ person.id }}/export?qaza={{ scope|urlencode }}" class="export-option" download>{{ scope }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -35,6 +45,11 @@
|
|||||||
{% set scopes = documents | selectattr('search_scope') | map(attribute='search_scope') | unique | list %}
|
{% 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 %}
|
{% if scopes %}<div class="info-item"><span class="info-label">القضاء</span><span>{{ scopes | join('، ') }}</span></div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if current_search_scope %}
|
||||||
|
<div class="person-actions no-print">
|
||||||
|
<a href="/persons/{{ person.id }}" class="btn btn-secondary">عرض كل النطاقات</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
@@ -43,7 +58,7 @@
|
|||||||
|
|
||||||
{% if properties %}
|
{% if properties %}
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="results-table props-full">
|
<table class="results-table props-full responsive-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
@@ -59,29 +74,29 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for pr in properties %}
|
{% for pr in properties %}
|
||||||
<tr {% if pr.ownership_type == 'لا يملك' %}style="background-color: #fff3f3; color: #dc3545; font-weight: bold;"{% endif %}>
|
<tr class="{{ 'ownership-alert' if pr.ownership_type == 'لا يملك' else '' }}">
|
||||||
<td class="row-num">{{ loop.index }}</td>
|
<td data-label="#" class="row-num">{{ loop.index }}</td>
|
||||||
<td>{{ pr.party_name or '' }}</td>
|
<td data-label="اسم الفريق">{{ pr.party_name or '' }}</td>
|
||||||
<td class="prop-num">{{ pr.property_number or '' }}</td>
|
<td data-label="رقم العقار" class="prop-num">{{ pr.property_number or '' }}</td>
|
||||||
<td>{{ pr.section or '' }}</td>
|
<td data-label="القسم">{{ pr.section or '' }}</td>
|
||||||
<td>{{ pr.block or '' }}</td>
|
<td data-label="البلوك">{{ pr.block or '' }}</td>
|
||||||
<td>{{ pr.real_estate_district or '' }}</td>
|
<td data-label="المنطقة العقارية">{{ pr.real_estate_district or '' }}</td>
|
||||||
<td>{{ pr.qaza or '' }}</td>
|
<td data-label="القضاء">{{ pr.qaza or '' }}</td>
|
||||||
<td>{{ pr.num_shares or '' }}</td>
|
<td data-label="عدد الأسهم">{{ pr.num_shares or '' }}</td>
|
||||||
<td>{{ pr.ownership_type or '' }}</td>
|
<td data-label="نوع الملكية">{{ pr.ownership_type or '' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="error-banner" style="justify-content:center;">
|
<div class="error-banner centered-banner">
|
||||||
⚠️ نتيجة البحث: لا يملك أي عقار في نطاق البحث المذكور.
|
⚠️ نتيجة البحث: لا يملك أي عقار في نطاق البحث المذكور.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if documents %}
|
{% if documents %}
|
||||||
<div class="docs-section">
|
<div class="docs-section no-print">
|
||||||
<h3>الوثائق الأصلية</h3>
|
<h3>الوثائق الأصلية</h3>
|
||||||
<div class="doc-thumbnails">
|
<div class="doc-thumbnails">
|
||||||
{% for d in documents %}
|
{% for d in documents %}
|
||||||
@@ -101,19 +116,3 @@
|
|||||||
|
|
||||||
{% endblock %}
|
{% 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 %}
|
|
||||||
|
|||||||
+140
-67
@@ -8,13 +8,19 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="review-header">
|
<div class="review-header">
|
||||||
<div class="review-title">
|
<div class="review-title">
|
||||||
<h2>مراجعة الوثيقة #{{ doc.id }}</h2>
|
<div>
|
||||||
|
<span class="page-kicker">المراجعة والتحقق</span>
|
||||||
|
<h2>مراجعة الوثيقة #{{ doc.id }}</h2>
|
||||||
|
</div>
|
||||||
<span class="status-badge status-{{ doc.status }}">{{ doc.status }}</span>
|
<span class="status-badge status-{{ doc.status }}">{{ doc.status }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="review-nav">
|
<div class="review-nav">
|
||||||
<a href="/documents" class="btn btn-secondary btn-sm">← قائمة الوثائق</a>
|
<a href="/documents" class="btn btn-secondary btn-sm">← قائمة الوثائق</a>
|
||||||
<a href="/review/next" 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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -29,9 +35,15 @@
|
|||||||
<!-- Image Panel -->
|
<!-- Image Panel -->
|
||||||
<div class="image-panel">
|
<div class="image-panel">
|
||||||
<div class="image-toolbar">
|
<div class="image-toolbar">
|
||||||
<button onclick="zoomIn()" class="btn btn-sm">🔍+</button>
|
<button type="button" onclick="zoomIn()" class="btn btn-sm btn-secondary" title="تكبير">
|
||||||
<button onclick="zoomOut()" class="btn btn-sm">🔍-</button>
|
<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 onclick="resetZoom()" class="btn btn-sm">↺</button>
|
</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>
|
||||||
<div class="image-container" id="imageContainer">
|
<div class="image-container" id="imageContainer">
|
||||||
<img id="docImage" src="/uploads/{{ doc.image_path }}" alt="وثيقة" class="doc-image">
|
<img id="docImage" src="/uploads/{{ doc.image_path }}" alt="وثيقة" class="doc-image">
|
||||||
@@ -47,46 +59,47 @@
|
|||||||
<h3>بيانات الشخص</h3>
|
<h3>بيانات الشخص</h3>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>الاسم *</label>
|
<label for="first_name">الاسم *</label>
|
||||||
<input type="text" name="first_name" value="{{ doc.person.get('first_name') or '' }}" required>
|
<input id="first_name" type="text" name="first_name" value="{{ doc.person.get('first_name') or '' }}" title="الاسم" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>اسم الأب</label>
|
<label for="father_name">اسم الأب</label>
|
||||||
<input type="text" name="father_name" value="{{ doc.person.get('father_name') or '' }}">
|
<input id="father_name" type="text" name="father_name" value="{{ doc.person.get('father_name') or '' }}" title="اسم الأب">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>اسم الأم</label>
|
<label for="mother_name">اسم الأم</label>
|
||||||
<input type="text" name="mother_name" value="{{ doc.person.get('mother_name') or '' }}">
|
<input id="mother_name" type="text" name="mother_name" value="{{ doc.person.get('mother_name') or '' }}" title="اسم الأم">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>اللقب</label>
|
<label for="family_name">اللقب</label>
|
||||||
<input type="text" name="family_name" value="{{ doc.person.get('family_name') or '' }}">
|
<input id="family_name" type="text" name="family_name" value="{{ doc.person.get('family_name') or '' }}" title="اللقب">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>الهوا / المنشأ</label>
|
<label for="family_origin">الهوا / المنشأ</label>
|
||||||
<input type="text" name="family_origin" value="{{ doc.person.get('family_origin') or '' }}">
|
<input id="family_origin" type="text" name="family_origin" value="{{ doc.person.get('family_origin') or '' }}" title="الهوا أو المنشأ">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>الجنسية</label>
|
<label for="nationality">الجنسية</label>
|
||||||
<input type="text" name="nationality" value="{{ doc.person.get('nationality') or '' }}">
|
<input id="nationality" type="text" name="nationality" value="{{ doc.person.get('nationality') or '' }}" title="الجنسية">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>تاريخ الولادة</label>
|
<label for="birth_date">تاريخ الولادة</label>
|
||||||
<input type="text" name="birth_date" value="{{ doc.person.get('birth_date') or '' }}">
|
<input id="birth_date" type="text" name="birth_date" value="{{ doc.person.get('birth_date') or '' }}" title="تاريخ الولادة">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>رقم السجل</label>
|
<label for="registry_number">رقم السجل</label>
|
||||||
<input type="text" name="registry_number" value="{{ doc.person.get('registry_number') or '' }}">
|
<input id="registry_number" type="text" name="registry_number" value="{{ doc.person.get('registry_number') or '' }}" title="رقم السجل">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>مكان السجل</label>
|
<label for="registry_place">مكان السجل</label>
|
||||||
<input type="text" name="registry_place" value="{{ doc.person.get('registry_place') or '' }}">
|
<input id="registry_place" type="text" name="registry_place" value="{{ doc.person.get('registry_place') or '' }}" title="مكان السجل">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Merge suggestion (populated by JS) -->
|
<!-- Merge suggestion (populated by JS) -->
|
||||||
<div id="mergeSuggestion" class="merge-banner hidden">
|
<div id="mergeSuggestion" class="merge-banner hidden">
|
||||||
<h4>شخص مشابه موجود في قاعدة البيانات:</h4>
|
<h4>شخص مشابه موجود في قاعدة البيانات:</h4>
|
||||||
|
<p id="mergeScopeNotice" class="merge-note hidden"></p>
|
||||||
<div id="mergeMatches"></div>
|
<div id="mergeMatches"></div>
|
||||||
<div class="merge-actions">
|
<div class="merge-actions">
|
||||||
<label><input type="radio" name="merge_action" value="merge" checked> دمج مع الشخص الموجود</label>
|
<label><input type="radio" name="merge_action" value="merge" checked> دمج مع الشخص الموجود</label>
|
||||||
@@ -101,20 +114,20 @@
|
|||||||
<h3>بيانات الطلب</h3>
|
<h3>بيانات الطلب</h3>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>رقم الطلب</label>
|
<label for="request_number">رقم الطلب</label>
|
||||||
<input type="text" name="request_number" value="{{ doc.request_number or '' }}">
|
<input id="request_number" type="text" name="request_number" value="{{ doc.request_number or '' }}" title="رقم الطلب">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>تاريخ الطلب</label>
|
<label for="request_date">تاريخ الطلب</label>
|
||||||
<input type="text" name="request_date" value="{{ doc.request_date or '' }}">
|
<input id="request_date" type="text" name="request_date" value="{{ doc.request_date or '' }}" title="تاريخ الطلب">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>معلومات الصفحة</label>
|
<label for="page_info">معلومات الصفحة</label>
|
||||||
<input type="text" name="page_info" value="{{ doc.page_info or '' }}" placeholder="مثال: 1 من 3">
|
<input id="page_info" type="text" name="page_info" value="{{ doc.page_info or '' }}" title="معلومات الصفحة" placeholder="مثال: 1 من 3">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>نطاق البحث (القضاء)</label>
|
<label for="search_scope">نطاق البحث (القضاء)</label>
|
||||||
<input type="text" name="search_scope" value="{{ doc.search_scope or '' }}" placeholder="مثال: المتن">
|
<input id="search_scope" type="text" name="search_scope" value="{{ doc.search_scope or '' }}" title="نطاق البحث" placeholder="مثال: المتن">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -125,8 +138,8 @@
|
|||||||
<h3>العقارات المملوكة</h3>
|
<h3>العقارات المملوكة</h3>
|
||||||
<button type="button" class="btn btn-sm btn-success" onclick="addRow()">+ إضافة صف</button>
|
<button type="button" class="btn btn-sm btn-success" onclick="addRow()">+ إضافة صف</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper table-wrapper-editable">
|
||||||
<table id="propertiesTable" class="props-table">
|
<table id="propertiesTable" class="props-table mobile-edit-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>اسم الفريق</th>
|
<th>اسم الفريق</th>
|
||||||
@@ -143,15 +156,15 @@
|
|||||||
<tbody id="propertiesTbody">
|
<tbody id="propertiesTbody">
|
||||||
{% for prop in doc.properties %}
|
{% for prop in doc.properties %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><input type="text" value="{{ prop.party_name or '' }}"></td>
|
<td data-label="اسم الفريق"><input type="text" value="{{ prop.party_name or '' }}" title="اسم الفريق" aria-label="اسم الفريق"></td>
|
||||||
<td><input type="text" value="{{ prop.property_number or '' }}"></td>
|
<td data-label="رقم العقار"><input type="text" value="{{ prop.property_number or '' }}" title="رقم العقار" aria-label="رقم العقار"></td>
|
||||||
<td><input type="text" value="{{ prop.section or '' }}"></td>
|
<td data-label="القسم"><input type="text" value="{{ prop.section or '' }}" title="القسم" aria-label="القسم"></td>
|
||||||
<td><input type="text" value="{{ prop.block or '' }}"></td>
|
<td data-label="البلوك"><input type="text" value="{{ prop.block or '' }}" title="البلوك" aria-label="البلوك"></td>
|
||||||
<td><input type="text" value="{{ prop.real_estate_district or '' }}"></td>
|
<td data-label="المنطقة العقارية"><input type="text" value="{{ prop.real_estate_district or '' }}" title="المنطقة العقارية" aria-label="المنطقة العقارية"></td>
|
||||||
<td><input type="text" value="{{ prop.qaza or '' }}"></td>
|
<td data-label="القضاء"><input type="text" value="{{ prop.qaza or '' }}" title="القضاء" aria-label="القضاء"></td>
|
||||||
<td><input type="text" value="{{ prop.num_shares or '' }}"></td>
|
<td data-label="عدد الأسهم"><input type="text" value="{{ prop.num_shares or '' }}" title="عدد الأسهم" aria-label="عدد الأسهم"></td>
|
||||||
<td><input type="text" value="{{ prop.ownership_type or '' }}"></td>
|
<td data-label="نوع الملكية"><input type="text" value="{{ prop.ownership_type or '' }}" title="نوع الملكية" aria-label="نوع الملكية"></td>
|
||||||
<td><button type="button" class="btn-del" onclick="removeRow(this)">✕</button></td>
|
<td data-label="حذف"><button type="button" class="btn-del" onclick="removeRow(this)">✕</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -162,7 +175,8 @@
|
|||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="button" class="btn btn-primary btn-large" onclick="confirmDocument()" id="confirmBtn">
|
<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>
|
</button>
|
||||||
<span class="keyboard-hint">أو اضغط Ctrl+Enter</span>
|
<span class="keyboard-hint">أو اضغط Ctrl+Enter</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,15 +195,15 @@ function addRow() {
|
|||||||
const tbody = document.getElementById('propertiesTbody');
|
const tbody = document.getElementById('propertiesTbody');
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td><input type="text" value=""></td>
|
<td data-label="اسم الفريق"><input type="text" value="" title="اسم الفريق" aria-label="اسم الفريق"></td>
|
||||||
<td><input type="text" value=""></td>
|
<td data-label="رقم العقار"><input type="text" value="" title="رقم العقار" aria-label="رقم العقار"></td>
|
||||||
<td><input type="text" value=""></td>
|
<td data-label="القسم"><input type="text" value="" title="القسم" aria-label="القسم"></td>
|
||||||
<td><input type="text" value=""></td>
|
<td data-label="البلوك"><input type="text" value="" title="البلوك" aria-label="البلوك"></td>
|
||||||
<td><input type="text" value=""></td>
|
<td data-label="المنطقة العقارية"><input type="text" value="" title="المنطقة العقارية" aria-label="المنطقة العقارية"></td>
|
||||||
<td><input type="text" value=""></td>
|
<td data-label="القضاء"><input type="text" value="" title="القضاء" aria-label="القضاء"></td>
|
||||||
<td><input type="text" value=""></td>
|
<td data-label="عدد الأسهم"><input type="text" value="" title="عدد الأسهم" aria-label="عدد الأسهم"></td>
|
||||||
<td><input type="text" value=""></td>
|
<td data-label="نوع الملكية"><input type="text" value="" title="نوع الملكية" aria-label="نوع الملكية"></td>
|
||||||
<td><button type="button" class="btn-del" onclick="removeRow(this)">✕</button></td>
|
<td data-label="حذف"><button type="button" class="btn-del" onclick="removeRow(this)">✕</button></td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
tr.querySelector('input').focus();
|
tr.querySelector('input').focus();
|
||||||
@@ -252,25 +266,68 @@ function checkDuplicate() {
|
|||||||
const first = form.querySelector('[name=first_name]').value.trim();
|
const first = form.querySelector('[name=first_name]').value.trim();
|
||||||
const father = form.querySelector('[name=father_name]').value.trim();
|
const father = form.querySelector('[name=father_name]').value.trim();
|
||||||
const family = form.querySelector('[name=family_name]').value.trim();
|
const family = form.querySelector('[name=family_name]').value.trim();
|
||||||
if (!first) return;
|
const scope = form.querySelector('[name=search_scope]').value.trim();
|
||||||
|
const registry = form.querySelector('[name=registry_number]').value.trim();
|
||||||
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 banner = document.getElementById('mergeSuggestion');
|
||||||
const matchesDiv = document.getElementById('mergeMatches');
|
const matchesDiv = document.getElementById('mergeMatches');
|
||||||
const mergeIdInput = document.getElementById('mergePersonId');
|
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) {
|
if (data.matches && data.matches.length > 0) {
|
||||||
matchesDiv.innerHTML = data.matches.map(m =>
|
const firstAllowed = data.matches.find(m => m.merge_allowed);
|
||||||
`<div class="merge-person" data-person-id="${m.id}">
|
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-name">${m.first_name} ${m.father_name||''} ${m.family_name||''}</span>
|
||||||
<span class="mp-count">${m.property_count} عقار</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>' : ''}
|
${m.family_origin ? '<span class="mp-count">'+m.family_origin+'</span>' : ''}
|
||||||
</div>`
|
<span class="mp-count">${status}</span>
|
||||||
).join('');
|
<span class="mp-count">النطاقات: ${scopes}</span>
|
||||||
mergeIdInput.value = data.matches[0].id;
|
</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');
|
banner.classList.remove('hidden');
|
||||||
|
|
||||||
// Click to select a match
|
// Click to select a match
|
||||||
@@ -278,19 +335,35 @@ function checkDuplicate() {
|
|||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
matchesDiv.querySelectorAll('.merge-person').forEach(e => e.style.outline = '');
|
matchesDiv.querySelectorAll('.merge-person').forEach(e => e.style.outline = '');
|
||||||
el.style.outline = '2px solid var(--primary)';
|
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;
|
mergeIdInput.value = el.dataset.personId;
|
||||||
form.querySelector('[name=merge_action][value=merge]').checked = true;
|
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 {
|
} else {
|
||||||
banner.classList.add('hidden');
|
banner.classList.add('hidden');
|
||||||
mergeIdInput.value = '';
|
mergeIdInput.value = '';
|
||||||
|
mergeRadio.disabled = false;
|
||||||
|
scopeNotice.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach duplicate check to name fields
|
// Attach duplicate check to name fields
|
||||||
document.querySelectorAll('[name=first_name],[name=father_name],[name=family_name]').forEach(input => {
|
document.querySelectorAll('[name=first_name],[name=father_name],[name=family_name],[name=search_scope],[name=registry_number]').forEach(input => {
|
||||||
input.addEventListener('blur', checkDuplicate);
|
input.addEventListener('blur', checkDuplicate);
|
||||||
});
|
});
|
||||||
// Run once on load
|
// Run once on load
|
||||||
@@ -300,7 +373,7 @@ checkDuplicate();
|
|||||||
async function confirmDocument() {
|
async function confirmDocument() {
|
||||||
const btn = document.getElementById('confirmBtn');
|
const btn = document.getElementById('confirmBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '⏳ جارٍ الحفظ...';
|
btn.textContent = 'جارٍ الحفظ...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = getFormData();
|
const formData = getFormData();
|
||||||
@@ -321,7 +394,7 @@ async function confirmDocument() {
|
|||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
window.location.href = data.next;
|
window.location.href = data.next;
|
||||||
} else {
|
} else {
|
||||||
alert('حدث خطأ: ' + JSON.stringify(data));
|
alert('حدث خطأ: ' + (data.detail || JSON.stringify(data)));
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '✓ تأكيد والانتقال للتالية';
|
btn.textContent = '✓ تأكيد والانتقال للتالية';
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-59
@@ -3,21 +3,36 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>البحث في قاعدة البيانات</h1>
|
<div>
|
||||||
|
<span class="page-kicker">البحث والاستكشاف</span>
|
||||||
|
<h1>البحث في قاعدة البيانات</h1>
|
||||||
|
<p class="subtitle">اختر طريقة البحث المناسبة ثم انتقل مباشرة إلى سجل الشخص أو العقار المطلوب.</p>
|
||||||
|
</div>
|
||||||
|
{% if persons or properties %}
|
||||||
|
<div class="header-actions no-print">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.print()">
|
||||||
|
<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="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
|
||||||
|
طباعة
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-card">
|
<div class="search-card no-print">
|
||||||
<form method="get" action="/search" id="searchForm">
|
<form method="get" action="/search" id="searchForm">
|
||||||
<div class="search-tabs">
|
<div class="search-tabs">
|
||||||
<button type="button" class="tab-btn active" onclick="showTab('person')">بحث بالاسم</button>
|
<button type="button" class="tab-btn active" data-tab="person">بحث بالاسم</button>
|
||||||
<button type="button" class="tab-btn" onclick="showTab('property')">بحث بالعقار</button>
|
<button type="button" class="tab-btn" data-tab="property">بحث بالعقار</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tab-person" class="tab-content">
|
<div id="tab-person" class="tab-content">
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<input type="text" name="q" id="qInput" value="{{ q }}"
|
<input type="text" name="q" id="qInput" value="{{ q }}"
|
||||||
placeholder="اكتب اسم الشخص..." class="search-input" autocomplete="off">
|
placeholder="اكتب اسم الشخص..." class="search-input" autocomplete="off">
|
||||||
<button type="submit" class="btn btn-primary">🔍 بحث</button>
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
|
بحث
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -36,7 +51,10 @@
|
|||||||
<input type="text" name="block" value="{{ block }}" placeholder="رقم البلوك">
|
<input type="text" name="block" value="{{ block }}" placeholder="رقم البلوك">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">🔍 بحث</button>
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
|
بحث
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,16 +79,23 @@
|
|||||||
{% set scopes = selected_person.documents | selectattr('search_scope') | map(attribute='search_scope') | unique | list %}
|
{% 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 %}
|
{% if scopes %}<div class="info-item"><span class="info-label">القضاء</span><span>{{ scopes | join('، ') }}</span></div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:1rem">
|
<div class="person-actions">
|
||||||
<a href="/persons/{{ selected_person.person.id }}" class="btn btn-secondary">عرض التفاصيل كاملة</a>
|
<a href="/persons/{{ selected_person.person.id }}{% if selected_person.current_search_scope %}?search_scope={{ selected_person.current_search_scope|urlencode }}{% endif %}" class="btn btn-secondary no-print">عرض التفاصيل كاملة</a>
|
||||||
|
<button type="button" class="btn btn-secondary no-print" onclick="window.print()">
|
||||||
|
<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="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
|
||||||
|
طباعة
|
||||||
|
</button>
|
||||||
{% set scopes = selected_person.documents | selectattr('search_scope') | map(attribute='search_scope') | unique | list %}
|
{% set scopes = selected_person.documents | selectattr('search_scope') | map(attribute='search_scope') | unique | list %}
|
||||||
<div class="export-dropdown">
|
<div class="export-dropdown no-print">
|
||||||
<button class="btn btn-primary" onclick="this.nextElementSibling.classList.toggle('show')">📥 تصدير كملف إكسل ▾</button>
|
<button type="button" class="btn btn-primary" data-export-toggle>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
|
تصدير كملف إكسل ▾
|
||||||
|
</button>
|
||||||
<div class="export-menu">
|
<div class="export-menu">
|
||||||
<a href="/persons/{{ selected_person.person.id }}/export" class="export-option" download>📥 تصدير الكل</a>
|
<a href="/persons/{{ selected_person.person.id }}/export" class="export-option" download>تصدير الكل</a>
|
||||||
{% for scope in scopes %}
|
{% for scope in scopes %}
|
||||||
{% if scope %}
|
{% if scope %}
|
||||||
<a href="/persons/{{ selected_person.person.id }}/export?qaza={{ scope|urlencode }}" class="export-option" download>📥 {{ scope }}</a>
|
<a href="/persons/{{ selected_person.person.id }}/export?qaza={{ scope|urlencode }}" class="export-option" download>{{ scope }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -79,8 +104,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if selected_person.properties %}
|
{% if selected_person.properties %}
|
||||||
<div class="table-wrapper" style="margin-top:1rem">
|
<div class="table-wrapper table-wrapper-spaced">
|
||||||
<table class="results-table">
|
<table class="results-table responsive-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
@@ -96,42 +121,41 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for pr in selected_person.properties %}
|
{% for pr in selected_person.properties %}
|
||||||
<tr {% if pr.ownership_type == 'لا يملك' %}style="background-color: #fff3f3; color: #dc3545; font-weight: bold;"{% endif %}>
|
<tr class="{{ 'ownership-alert' if pr.ownership_type == 'لا يملك' else '' }}">
|
||||||
<td>{{ loop.index }}</td>
|
<td data-label="#">{{ loop.index }}</td>
|
||||||
<td>{{ pr.party_name or '' }}</td>
|
<td data-label="اسم الفريق">{{ pr.party_name or '' }}</td>
|
||||||
<td>{{ pr.property_number or '' }}</td>
|
<td data-label="رقم العقار">{{ pr.property_number or '' }}</td>
|
||||||
<td>{{ pr.section or '' }}</td>
|
<td data-label="القسم">{{ pr.section or '' }}</td>
|
||||||
<td>{{ pr.block or '' }}</td>
|
<td data-label="البلوك">{{ pr.block or '' }}</td>
|
||||||
<td>{{ pr.real_estate_district or '' }}</td>
|
<td data-label="المنطقة العقارية">{{ pr.real_estate_district or '' }}</td>
|
||||||
<td>{{ pr.qaza or '' }}</td>
|
<td data-label="القضاء">{{ pr.qaza or '' }}</td>
|
||||||
<td>{{ pr.num_shares or '' }}</td>
|
<td data-label="عدد الأسهم">{{ pr.num_shares or '' }}</td>
|
||||||
<td>{{ pr.ownership_type or '' }}</td>
|
<td data-label="نوع الملكية">{{ pr.ownership_type or '' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="error-banner" style="margin-top:1rem; justify-content:center;">
|
<div class="error-banner centered-banner banner-spaced">
|
||||||
⚠️ نتيجة البحث: لا يملك أي عقار في القضاء المذكور.
|
⚠️ نتيجة البحث: لا يملك أي عقار في القضاء المذكور.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="persons-grid">
|
<div class="persons-grid">
|
||||||
{% for p in persons %}
|
{% for p in persons %}
|
||||||
<a href="/persons/{{ p.id }}" class="person-card">
|
<a href="/persons/{{ p.id }}{% if p.search_scope %}?search_scope={{ p.search_scope|urlencode }}{% endif %}" class="person-card">
|
||||||
<div class="person-name">{{ p.first_name }} {{ p.father_name or '' }} {{ p.family_name or '' }}</div>
|
<div class="person-name">{{ p.first_name }} {{ p.father_name or '' }} {{ p.family_name or '' }}</div>
|
||||||
<div class="person-meta">
|
<div class="person-meta">
|
||||||
{% if p.family_origin %}{{ p.family_origin }} · {% endif %}
|
{% if p.family_origin %}{{ p.family_origin }} · {% endif %}
|
||||||
{% if p.property_count > 0 %}
|
{% if p.property_count > 0 %}
|
||||||
{{ p.property_count }} عقار
|
{{ p.property_count }} عقار
|
||||||
{% else %}
|
{% else %}
|
||||||
<span style="color: #dc3545; font-weight: bold;">لا يملك عقارات</span>
|
<span class="person-meta-alert">لا يملك عقارات</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if p.search_scopes %}
|
<div class="person-reg">نطاق البحث: {{ p.search_scope or 'غير محدد' }}</div>
|
||||||
<div class="person-reg">نطاق البحث: {{ p.search_scopes | replace(',', '، ') }}</div>
|
{% if p.document_count %}<div class="person-reg">عدد الطلبات: {{ p.document_count }}</div>{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% if p.registry_number %}
|
{% if p.registry_number %}
|
||||||
<div class="person-reg">سجل: {{ p.registry_number }}</div>
|
<div class="person-reg">سجل: {{ p.registry_number }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -147,7 +171,7 @@
|
|||||||
<div class="results-section">
|
<div class="results-section">
|
||||||
<h2>نتائج البحث بالعقار ({{ properties|length }} سجل)</h2>
|
<h2>نتائج البحث بالعقار ({{ properties|length }} سجل)</h2>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="results-table">
|
<table class="results-table responsive-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>المالك</th>
|
<th>المالك</th>
|
||||||
@@ -164,19 +188,19 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for pr in properties %}
|
{% for pr in properties %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td data-label="المالك">
|
||||||
{% if pr.person_id %}
|
{% if pr.person_id %}
|
||||||
<a href="/persons/{{ pr.person_id }}">{{ pr.first_name or '' }} {{ pr.family_name or '' }}</a>
|
<a href="/persons/{{ pr.person_id }}">{{ pr.first_name or '' }} {{ pr.family_name or '' }}</a>
|
||||||
{% else %}—{% endif %}
|
{% else %}—{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ pr.party_name or '' }}</td>
|
<td data-label="اسم الفريق">{{ pr.party_name or '' }}</td>
|
||||||
<td class="prop-num">{{ pr.property_number or '' }}</td>
|
<td data-label="رقم العقار" class="prop-num">{{ pr.property_number or '' }}</td>
|
||||||
<td>{{ pr.section or '' }}</td>
|
<td data-label="القسم">{{ pr.section or '' }}</td>
|
||||||
<td>{{ pr.block or '' }}</td>
|
<td data-label="البلوك">{{ pr.block or '' }}</td>
|
||||||
<td>{{ pr.real_estate_district or '' }}</td>
|
<td data-label="المنطقة العقارية">{{ pr.real_estate_district or '' }}</td>
|
||||||
<td>{{ pr.qaza or '' }}</td>
|
<td data-label="القضاء">{{ pr.qaza or '' }}</td>
|
||||||
<td>{{ pr.num_shares or '' }}</td>
|
<td data-label="عدد الأسهم">{{ pr.num_shares or '' }}</td>
|
||||||
<td>{{ pr.ownership_type or '' }}</td>
|
<td data-label="نوع الملكية">{{ pr.ownership_type or '' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -196,35 +220,29 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
function showTab(tab) {
|
function showTab(tab, button = null) {
|
||||||
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||||||
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
||||||
document.getElementById('tab-' + tab).classList.remove('hidden');
|
document.getElementById('tab-' + tab).classList.remove('hidden');
|
||||||
event.target.classList.add('active');
|
if (button) {
|
||||||
|
button.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
|
// Auto-select correct tab based on query params
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', () => showTab(button.dataset.tab, button));
|
||||||
|
});
|
||||||
|
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
|
const personButton = document.querySelector('.tab-btn[data-tab="person"]');
|
||||||
|
const propertyButton = document.querySelector('.tab-btn[data-tab="property"]');
|
||||||
|
|
||||||
if (params.get('property_number') || params.get('district') || params.get('block')) {
|
if (params.get('property_number') || params.get('district') || params.get('block')) {
|
||||||
showTab('property');
|
showTab('property', propertyButton);
|
||||||
document.querySelectorAll('.tab-btn')[1].classList.add('active');
|
|
||||||
document.querySelectorAll('.tab-btn')[0].classList.remove('active');
|
|
||||||
} else {
|
} else {
|
||||||
|
showTab('person', personButton);
|
||||||
document.getElementById('qInput').focus();
|
document.getElementById('qInput').focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+23
-10
@@ -5,7 +5,11 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="admin-container">
|
<div class="admin-container">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>إدارة النظام والمستخدمين</h1>
|
<div>
|
||||||
|
<span class="page-kicker">الإعدادات والإدارة</span>
|
||||||
|
<h1>إدارة النظام والمستخدمين</h1>
|
||||||
|
<p class="subtitle">أنشئ نسخاً احتياطية وأدر حسابات الدخول من مكان واحد.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if backup_msg %}
|
{% if backup_msg %}
|
||||||
@@ -13,13 +17,19 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
<h3>💾 نسخ احتياطي لقاعدة البيانات</h3>
|
<h3>
|
||||||
|
<svg class="section-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
||||||
|
نسخ احتياطي لقاعدة البيانات
|
||||||
|
</h3>
|
||||||
<p>قم بإنشاء نسخة احتياطية من قاعدة البيانات الحالية.</p>
|
<p>قم بإنشاء نسخة احتياطية من قاعدة البيانات الحالية.</p>
|
||||||
<a href="/auth/backup" class="btn btn-success">إنشاء نسخة احتياطية</a>
|
<a href="/auth/backup" class="btn btn-success">إنشاء نسخة احتياطية</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
<h3>➕ إضافة مستخدم جديد</h3>
|
<h3>
|
||||||
|
<svg class="section-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>
|
||||||
|
إضافة مستخدم جديد
|
||||||
|
</h3>
|
||||||
<form method="post" action="/auth/users/create" class="add-user-form">
|
<form method="post" action="/auth/users/create" class="add-user-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">اسم المستخدم</label>
|
<label for="username">اسم المستخدم</label>
|
||||||
@@ -34,9 +44,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
<h3>👥 المستخدمين</h3>
|
<h3>
|
||||||
|
<svg class="section-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||||
|
المستخدمين
|
||||||
|
</h3>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="results-table">
|
<table class="results-table responsive-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>الرقم</th>
|
<th>الرقم</th>
|
||||||
@@ -48,11 +61,11 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.id }}</td>
|
<td data-label="الرقم">{{ user.id }}</td>
|
||||||
<td>{{ user.username }}</td>
|
<td data-label="اسم المستخدم">{{ user.username }}</td>
|
||||||
<td>{{ user.created_at }}</td>
|
<td data-label="تاريخ الإنشاء">{{ user.created_at }}</td>
|
||||||
<td>
|
<td data-label="الإجراءات">
|
||||||
<form method="post" action="/auth/users/delete/{{ user.id }}" onsubmit="return confirm('هل أنت متأكد من حذف هذا المستخدم؟');" style="display:inline;">
|
<form method="post" action="/auth/users/delete/{{ user.id }}" onsubmit="return confirm('هل أنت متأكد من حذف هذا المستخدم؟');" class="inline-form">
|
||||||
<button type="submit" class="btn btn-sm btn-danger">حذف</button>
|
<button type="submit" class="btn btn-sm btn-danger">حذف</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user