Mobile UI: compact card layout for tables; switch Gemini default to 2.5-pro
This commit is contained in:
@@ -20,8 +20,10 @@ SECRET_KEY = os.environ.get("SECRET_KEY", "")
|
|||||||
DATABASE_PATH = os.environ.get("DB_PATH", str(BASE_DIR / "data" / "realestate.db"))
|
DATABASE_PATH = os.environ.get("DB_PATH", str(BASE_DIR / "data" / "realestate.db"))
|
||||||
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", str(BASE_DIR / "uploads"))
|
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", str(BASE_DIR / "uploads"))
|
||||||
MAX_CONCURRENT_EXTRACTIONS = int(os.environ.get("MAX_CONCURRENT", "3"))
|
MAX_CONCURRENT_EXTRACTIONS = int(os.environ.get("MAX_CONCURRENT", "3"))
|
||||||
CLAUDE_MODEL = "claude-sonnet-4-6"
|
CLAUDE_MODEL = os.environ.get("CLAUDE_MODEL", "claude-sonnet-4-6")
|
||||||
GEMINI_MODEL = "gemini-2.5-flash"
|
# Gemini 2.5 Pro is more reliable than Flash for Arabic OCR/reasoning
|
||||||
|
# (Flash occasionally returns refusals / "absolute" style hedges). Override via env if needed.
|
||||||
|
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-pro")
|
||||||
|
|
||||||
# Production mode — set to "production" on VPS
|
# Production mode — set to "production" on VPS
|
||||||
ENVIRONMENT = os.environ.get("ENVIRONMENT", "development")
|
ENVIRONMENT = os.environ.get("ENVIRONMENT", "development")
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('data/realestate.db')
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
# All documents
|
||||||
|
rows = conn.execute('''
|
||||||
|
SELECT d.id, d.person_id, d.request_number, d.search_scope, d.page_info,
|
||||||
|
d.page_number, d.pdf_group_id, d.status,
|
||||||
|
p.first_name, p.family_name
|
||||||
|
FROM documents d
|
||||||
|
LEFT JOIN persons p ON p.id = d.person_id
|
||||||
|
ORDER BY d.id
|
||||||
|
''').fetchall()
|
||||||
|
|
||||||
|
print("=== ALL documents ===")
|
||||||
|
for r in rows:
|
||||||
|
d = dict(r)
|
||||||
|
print("id=%s status=%s person_id=%s req=%r scope=%r page_info=%r pdf=%r name=%s %s" % (
|
||||||
|
d['id'], d['status'], d['person_id'], d['request_number'],
|
||||||
|
d['search_scope'], d['page_info'], d['pdf_group_id'],
|
||||||
|
d['first_name'], d['family_name']))
|
||||||
|
|
||||||
|
# Persons
|
||||||
|
print("\n=== Persons ===")
|
||||||
|
persons = conn.execute('SELECT id, first_name, father_name, family_name FROM persons ORDER BY id').fetchall()
|
||||||
|
for p in persons:
|
||||||
|
d = dict(p)
|
||||||
|
print("id=%s %s %s %s" % (d['id'], d['first_name'], d['father_name'], d['family_name']))
|
||||||
|
|
||||||
|
# Potential duplicate persons
|
||||||
|
print("\n=== Potential duplicate persons ===")
|
||||||
|
dups = conn.execute('''
|
||||||
|
SELECT p1.id AS pid1, p2.id AS pid2, p1.first_name, p1.family_name
|
||||||
|
FROM persons p1
|
||||||
|
JOIN persons p2 ON p1.id < p2.id
|
||||||
|
AND p1.first_name = p2.first_name
|
||||||
|
AND COALESCE(p1.family_name,'') = COALESCE(p2.family_name,'')
|
||||||
|
''').fetchall()
|
||||||
|
for d in dups:
|
||||||
|
print(dict(d))
|
||||||
@@ -70,6 +70,16 @@ def _find_page1_candidate(conn, doc: dict) -> tuple[bool, dict | None]:
|
|||||||
LIMIT 1""",
|
LIMIT 1""",
|
||||||
(req_num, doc_scope, doc["id"]),
|
(req_num, doc_scope, doc["id"]),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
else:
|
||||||
|
sibling_doc = conn.execute(
|
||||||
|
"""SELECT * FROM documents
|
||||||
|
WHERE request_number=? AND (search_scope IS NULL OR TRIM(search_scope)='') AND id != ?
|
||||||
|
ORDER BY CASE WHEN page_number=1 THEN 0 ELSE 1 END,
|
||||||
|
CASE WHEN person_id IS NOT NULL THEN 0 ELSE 1 END,
|
||||||
|
id
|
||||||
|
LIMIT 1""",
|
||||||
|
(req_num, doc["id"]),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
return is_subsequent_page, dict(sibling_doc) if sibling_doc else None
|
return is_subsequent_page, dict(sibling_doc) if sibling_doc else None
|
||||||
|
|
||||||
@@ -727,6 +737,11 @@ async def retrigger_extraction(doc_id: int, provider: str = ""):
|
|||||||
use_provider = provider or doc["provider"] or get_default_provider()
|
use_provider = provider or doc["provider"] or get_default_provider()
|
||||||
|
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
|
old_person_id = conn.execute(
|
||||||
|
"SELECT person_id FROM documents WHERE id=?", (doc_id,)
|
||||||
|
).fetchone()
|
||||||
|
old_person_id = old_person_id["person_id"] if old_person_id else None
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""UPDATE documents
|
"""UPDATE documents
|
||||||
SET status='pending',
|
SET status='pending',
|
||||||
@@ -750,6 +765,16 @@ async def retrigger_extraction(doc_id: int, provider: str = ""):
|
|||||||
)
|
)
|
||||||
conn.execute("DELETE FROM properties WHERE document_id=?", (doc_id,))
|
conn.execute("DELETE FROM properties WHERE document_id=?", (doc_id,))
|
||||||
|
|
||||||
|
# Clean up orphaned person if this was their only document
|
||||||
|
if old_person_id:
|
||||||
|
remaining = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS n FROM documents WHERE person_id=?",
|
||||||
|
(old_person_id,),
|
||||||
|
).fetchone()["n"]
|
||||||
|
if remaining == 0:
|
||||||
|
conn.execute("DELETE FROM properties WHERE person_id=?", (old_person_id,))
|
||||||
|
conn.execute("DELETE FROM persons WHERE id=?", (old_person_id,))
|
||||||
|
|
||||||
from routers.upload import _extract_and_save
|
from routers.upload import _extract_and_save
|
||||||
asyncio.create_task(_extract_and_save(doc_id, doc["image_path"], use_provider))
|
asyncio.create_task(_extract_and_save(doc_id, doc["image_path"], use_provider))
|
||||||
return JSONResponse({"ok": True, "message": "Extraction started"})
|
return JSONResponse({"ok": True, "message": "Extraction started"})
|
||||||
|
|||||||
+140
-71
@@ -1382,77 +1382,6 @@ a.stat.active { border-color: rgba(15,118,110,.24); background: var(--primary-so
|
|||||||
.doc-thumb img { height: 70px; }
|
.doc-thumb img { height: 70px; }
|
||||||
.stats-bar .stat { flex-basis: 100%; }
|
.stats-bar .stat { flex-basis: 100%; }
|
||||||
|
|
||||||
.responsive-table {
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.responsive-table thead {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.responsive-table tbody,
|
|
||||||
.responsive-table tr,
|
|
||||||
.responsive-table td {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.responsive-table tbody {
|
|
||||||
padding: .35rem .75rem .85rem;
|
|
||||||
}
|
|
||||||
.responsive-table tr {
|
|
||||||
margin-bottom: .75rem;
|
|
||||||
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);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.responsive-table td {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(92px, 110px) 1fr;
|
|
||||||
gap: .65rem;
|
|
||||||
align-items: center;
|
|
||||||
padding: .7rem .9rem;
|
|
||||||
text-align: right;
|
|
||||||
border-bottom: 1px solid rgba(23,37,44,.06);
|
|
||||||
}
|
|
||||||
.responsive-table td:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.responsive-table td[data-label]::before {
|
|
||||||
display: block;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: .76rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.responsive-table .doc-actions,
|
|
||||||
.responsive-table td[data-label="الإجراءات"] {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: stretch;
|
|
||||||
gap: .5rem;
|
|
||||||
}
|
|
||||||
.responsive-table td[data-label="الإجراءات"]::before {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.responsive-table .doc-actions .btn,
|
|
||||||
.responsive-table td[data-label="الإجراءات"] .btn,
|
|
||||||
.responsive-table td[data-label="الإجراءات"] .inline-form,
|
|
||||||
.responsive-table td[data-label="الإجراءات"] .inline-form .btn {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.responsive-table .doc-thumb-mini {
|
|
||||||
width: 72px;
|
|
||||||
height: 54px;
|
|
||||||
}
|
|
||||||
.ownership-alert td {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.ownership-alert {
|
|
||||||
border-color: #fecaca;
|
|
||||||
background: #fff8f8;
|
|
||||||
}
|
|
||||||
.mobile-dock {
|
.mobile-dock {
|
||||||
right: .5rem;
|
right: .5rem;
|
||||||
left: .5rem;
|
left: .5rem;
|
||||||
@@ -1465,6 +1394,146 @@ a.stat.active { border-color: rgba(15,118,110,.24); background: var(--primary-so
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================
|
||||||
|
Responsive tables — compact list cards on mobile/tablet
|
||||||
|
Each row becomes a card with property number prominent on top
|
||||||
|
and other fields laid out 2-per-row for density.
|
||||||
|
============================ */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.responsive-table {
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.responsive-table thead { display: none; }
|
||||||
|
.responsive-table tbody,
|
||||||
|
.responsive-table tr,
|
||||||
|
.responsive-table td { display: block; width: 100%; }
|
||||||
|
|
||||||
|
.responsive-table tbody { padding: .25rem .5rem .6rem; }
|
||||||
|
|
||||||
|
.responsive-table tr {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
row-gap: 0;
|
||||||
|
column-gap: .5rem;
|
||||||
|
margin-bottom: .55rem;
|
||||||
|
padding: .5rem .65rem .6rem;
|
||||||
|
border: 1px solid rgba(23,37,44,.08);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255,255,255,.96);
|
||||||
|
box-shadow: 0 6px 16px rgba(23,37,44,.06);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-table td {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: .1rem;
|
||||||
|
padding: .3rem .15rem;
|
||||||
|
text-align: right;
|
||||||
|
border: none;
|
||||||
|
font-size: .85rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.responsive-table td[data-label]::before {
|
||||||
|
display: block;
|
||||||
|
content: attr(data-label);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: .68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .01em;
|
||||||
|
margin-bottom: .05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prominent / full-width fields */
|
||||||
|
.responsive-table td[data-label="رقم العقار"] {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
order: -1;
|
||||||
|
padding-top: .15rem;
|
||||||
|
padding-bottom: .4rem;
|
||||||
|
border-bottom: 1px solid rgba(23,37,44,.08);
|
||||||
|
margin-bottom: .3rem;
|
||||||
|
}
|
||||||
|
.responsive-table td[data-label="رقم العقار"]::before {
|
||||||
|
font-size: .7rem;
|
||||||
|
}
|
||||||
|
.responsive-table td[data-label="رقم العقار"] {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-table td[data-label="المالك"],
|
||||||
|
.responsive-table td[data-label="اسم الفريق"],
|
||||||
|
.responsive-table td[data-label="المنطقة العقارية"],
|
||||||
|
.responsive-table td[data-label="الإجراءات"] {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row index: small corner badge, hide label */
|
||||||
|
.responsive-table td[data-label="#"] {
|
||||||
|
order: -2;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 0 .15rem;
|
||||||
|
font-size: .7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.responsive-table td[data-label="#"]::before {
|
||||||
|
content: '#';
|
||||||
|
margin-inline-end: .25rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: .7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ownership-alert rows stay visually flagged */
|
||||||
|
.ownership-alert {
|
||||||
|
border-color: #fecaca !important;
|
||||||
|
background: #fff8f8 !important;
|
||||||
|
}
|
||||||
|
.ownership-alert td { background: transparent; }
|
||||||
|
|
||||||
|
/* Actions cell: buttons flow full width */
|
||||||
|
.responsive-table .doc-actions,
|
||||||
|
.responsive-table td[data-label="الإجراءات"] {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: .4rem;
|
||||||
|
padding-top: .4rem;
|
||||||
|
border-top: 1px dashed rgba(23,37,44,.08);
|
||||||
|
margin-top: .2rem;
|
||||||
|
}
|
||||||
|
.responsive-table td[data-label="الإجراءات"]::before {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: .2rem;
|
||||||
|
}
|
||||||
|
.responsive-table .doc-actions .btn,
|
||||||
|
.responsive-table td[data-label="الإجراءات"] .btn,
|
||||||
|
.responsive-table td[data-label="الإجراءات"] .inline-form,
|
||||||
|
.responsive-table td[data-label="الإجراءات"] .inline-form .btn {
|
||||||
|
flex: 1 1 calc(50% - .2rem);
|
||||||
|
min-width: 0;
|
||||||
|
font-size: .8rem;
|
||||||
|
}
|
||||||
|
.responsive-table .doc-thumb-mini {
|
||||||
|
width: 64px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table wrapper padding tweaks */
|
||||||
|
.table-wrapper.table-wrapper-spaced,
|
||||||
|
.table-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================
|
/* ============================
|
||||||
Upload & Staging (mobile-first)
|
Upload & Staging (mobile-first)
|
||||||
============================ */
|
============================ */
|
||||||
|
|||||||
Reference in New Issue
Block a user