Spaces:
Running
Running
Upload app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
منصة معالجة التذاكر — ISIC Helper (اندكس + واجهة فاتحة ومتجاوبة)
|
| 5 |
+
- اندكس ترحيبي + خطوات 1/2/3 + زر ابدأ الآن
|
| 6 |
+
- لصق عدة تذاكر دفعة واحدة
|
| 7 |
+
- استخراج الحقول الأساسية بأي ترتيب
|
| 8 |
+
- جدول واضح + ترويسة ملوّنة + محتوى مُوسّط
|
| 9 |
+
- تصدير Excel عربي (RTL) بتحميل تلقائي (Ticket_التاريخ)
|
| 10 |
+
"""
|
| 11 |
+
import os, re, tempfile, shutil
|
| 12 |
+
import gradio as gr
|
| 13 |
+
import pandas as pd
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
from openpyxl.styles import Alignment, Font, PatternFill
|
| 16 |
+
from openpyxl.utils import get_column_letter
|
| 17 |
+
|
| 18 |
+
# ============================ إعدادات الحقول ============================
|
| 19 |
+
ARABIC_DIGITS = str.maketrans("٠١٢٣٤٥٦٧٨٩", "0123456789")
|
| 20 |
+
FIELD_ALIASES = {
|
| 21 |
+
"نوع المشكلة": ["نوع المشكله", "نوع المشكلة", "المشكلة"],
|
| 22 |
+
"وقت حدوث المشكلة": ["وقت حدوث المشكله", "وقت حدوث المشكلة", "وقت المشكلة", "وقت حدوث"],
|
| 23 |
+
"اسم صاحب المشكلة": ["اسم صاحب المشكله", "اسم صاحب المشكلة", "اسم صاحب البلاغ", "الاسم"],
|
| 24 |
+
"رقم الهوية": ["رقم الهويه", "رقم الهوية", "الهوية"],
|
| 25 |
+
"رقم الجهاز": ["رقم الجهاز", "الجهاز"],
|
| 26 |
+
"رقم الجوال": ["رقم الجوال", "الجوال", "الهاتف"],
|
| 27 |
+
"المسح": ["المسح", "اسم المسح"],
|
| 28 |
+
"المنطقة": ["المنطقة", "المنطقه", "المدينة", "المحافظة"],
|
| 29 |
+
}
|
| 30 |
+
EXPORT_COLUMNS = [
|
| 31 |
+
"نوع المشكلة","وقت حدوث المشكلة","اسم صاحب المشكلة",
|
| 32 |
+
"رقم الهوية","رقم الجهاز","رقم الجوال","المسح","المنطقة"
|
| 33 |
+
]
|
| 34 |
+
DISPLAY_COLUMNS = EXPORT_COLUMNS[:]
|
| 35 |
+
|
| 36 |
+
# ============================ أدوات مساعدة ============================
|
| 37 |
+
LABEL_SEP = r"(?:[::]\s*)?"
|
| 38 |
+
def compile_field_patterns():
|
| 39 |
+
pats = {}
|
| 40 |
+
for canonical, labels in FIELD_ALIASES.items():
|
| 41 |
+
lbls = "|".join(map(re.escape, labels))
|
| 42 |
+
pats[canonical] = [
|
| 43 |
+
re.compile(rf"(?:^|\n)\s*(?:{lbls})\s*{LABEL_SEP}(.+)$", re.MULTILINE),
|
| 44 |
+
re.compile(rf"(?:^|\n)\s*(?:{lbls})\s*{LABEL_SEP}\n\s*(.+)", re.MULTILINE),
|
| 45 |
+
]
|
| 46 |
+
return pats
|
| 47 |
+
FIELD_PATTERNS = compile_field_patterns()
|
| 48 |
+
|
| 49 |
+
# فواصل التذاكر (🔴 أو سطر فارغ/فواصل)
|
| 50 |
+
TICKET_SEP = re.compile(r"\n\s*(?:\n|—+|-{3,}|={3,}|🔴+)+\s*\n")
|
| 51 |
+
|
| 52 |
+
def normalize_text(s: str) -> str:
|
| 53 |
+
if not isinstance(s, str): return ""
|
| 54 |
+
s2 = s.translate(ARABIC_DIGITS)
|
| 55 |
+
s2 = re.sub(r"[\u200f\u200e\u2066\u2067\u2068\u2069\u00a0]", " ", s2)
|
| 56 |
+
s2 = re.sub(r"[ــ]+", "", s2)
|
| 57 |
+
return s2.strip()
|
| 58 |
+
|
| 59 |
+
def normalize_time(val: str) -> str:
|
| 60 |
+
m = re.search(r"(\d{1,2})[:٫\.:\-](\d{2})\s*(ص|م)?", (val or "").strip(), flags=re.I)
|
| 61 |
+
if not m: return (val or "").strip()
|
| 62 |
+
h, mn, ampm = int(m.group(1)), m.group(2), m.group(3)
|
| 63 |
+
if ampm:
|
| 64 |
+
if ampm in ["م","pm","PM"] and h < 12: h += 12
|
| 65 |
+
if ampm in ["ص","am","AM"] and h == 12: h = 0
|
| 66 |
+
return f"{h:02d}:{mn}"
|
| 67 |
+
|
| 68 |
+
def normalize_date(val: str) -> str:
|
| 69 |
+
v = (val or "").strip()
|
| 70 |
+
m = re.search(r"(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})", v)
|
| 71 |
+
if m:
|
| 72 |
+
d, mth, y = int(m.group(1)), int(m.group(2)), int(m.group(3))
|
| 73 |
+
if y < 100: y += 2000
|
| 74 |
+
return f"{y:04d}-{mth:02d}-{d:02d}"
|
| 75 |
+
m = re.search(r"(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})", v)
|
| 76 |
+
if m:
|
| 77 |
+
y, mth, d = int(m.group(1)), int(m.group(2)), int(m.group(3))
|
| 78 |
+
return f"{y:04d}-{mth:02d}-{d:02d}"
|
| 79 |
+
return v
|
| 80 |
+
|
| 81 |
+
def split_tickets(raw: str):
|
| 82 |
+
raw = normalize_text(raw)
|
| 83 |
+
if not raw: return []
|
| 84 |
+
parts = re.split(TICKET_SEP, raw)
|
| 85 |
+
if len(parts) == 1:
|
| 86 |
+
parts = [p for p in re.split(r"\n\s*\n+", raw) if p.strip()]
|
| 87 |
+
return [p.strip() for p in parts if p.strip()]
|
| 88 |
+
|
| 89 |
+
def extract_fields(ticket_text: str) -> dict:
|
| 90 |
+
data = {k: "" for k in FIELD_ALIASES.keys()}
|
| 91 |
+
text = normalize_text(ticket_text)
|
| 92 |
+
for fname, patterns in FIELD_PATTERNS.items():
|
| 93 |
+
for pat in patterns:
|
| 94 |
+
m = pat.search(text)
|
| 95 |
+
if m:
|
| 96 |
+
val = normalize_text(m.group(1))
|
| 97 |
+
if fname == "وقت حدوث المشكلة": val = normalize_time(val)
|
| 98 |
+
if not data[fname]: data[fname] = val
|
| 99 |
+
break
|
| 100 |
+
if not data["رقم الجهاز"]:
|
| 101 |
+
m = re.search(r"(?:رقم\s*الجهاز|الجهاز)\D*([0-9][0-9\-\s]{2,})", text, flags=re.I)
|
| 102 |
+
if m: data["رقم الجهاز"] = re.sub(r"\D", "", m.group(1))[:20]
|
| 103 |
+
if not data["رقم الجوال"]:
|
| 104 |
+
m = re.search(r"(05\s*\d[\s\-]*\d[\s\-]*\d[\س\-]*\d[\س\-]*\d[\س\-]*\د[\س\-]*\د[\س\-]*\د)", text)
|
| 105 |
+
if m: data["رقم الجوال"] = re.sub(r"\D", "", m.group(1))
|
| 106 |
+
if not data["رقم الهوية"]:
|
| 107 |
+
m = re.search(r"(1\s*\d[\س\-]*\d[\س\-]*\د[\س\-]*\د[\س\-]*\د[\س\-]*\د[\س\-]*\د[\س\-]*\د)", text)
|
| 108 |
+
if m: data["رقم الهوية"] = re.sub(r"\D", "", m.group(1))
|
| 109 |
+
if not data["المسح"]:
|
| 110 |
+
m = re.search(r"(?:اسم\s*المسح|المسح)\s*[::]?\s*(.+)", text)
|
| 111 |
+
if m: data["المسح"] = normalize_text(m.group(1).splitlines()[0])
|
| 112 |
+
mdate = re.search(r"(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}|\d{4}[\/\-]\د{1,2}[\/\-]\د{1,2})", text)
|
| 113 |
+
if mdate:
|
| 114 |
+
date_norm = normalize_date(mdate.group(1))
|
| 115 |
+
tm = data.get("وقت حدوث المشكلة", "")
|
| 116 |
+
data["وقت حدوث المشكلة"] = f"{date_norm} {tm}".strip()
|
| 117 |
+
return data
|
| 118 |
+
|
| 119 |
+
def parse_tickets(raw_text: str) -> pd.DataFrame:
|
| 120 |
+
tickets = split_tickets(raw_text or "")
|
| 121 |
+
rows = [extract_fields(tk) for tk in tickets]
|
| 122 |
+
df = pd.DataFrame(rows) if rows else pd.DataFrame(columns=EXPORT_COLUMNS)
|
| 123 |
+
for c in EXPORT_COLUMNS:
|
| 124 |
+
if c not in df.columns: df[c] = ""
|
| 125 |
+
return df[EXPORT_COLUMNS]
|
| 126 |
+
|
| 127 |
+
# ============================ Excel RTL احترافي ============================
|
| 128 |
+
def _arabic_excel_format(writer, df):
|
| 129 |
+
ws = writer.sheets["التذاكر"]
|
| 130 |
+
ws.sheet_view.rightToLeft = True
|
| 131 |
+
header_font = Font(bold=True, color="FFFFFF")
|
| 132 |
+
header_fill = PatternFill(fill_type="solid", fgColor="4137A8") # لون ترويسة الجدول
|
| 133 |
+
for cell in ws[1]:
|
| 134 |
+
cell.font = header_font
|
| 135 |
+
cell.fill = header_fill
|
| 136 |
+
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
| 137 |
+
for col_idx, col_name in enumerate(df.columns, start=1):
|
| 138 |
+
vals = [str(col_name)] + ["" if pd.isna(v) else str(v) for v in df[col_name].tolist()]
|
| 139 |
+
width = min(max(len(v) for v in vals) + 2, 50)
|
| 140 |
+
ws.column_dimensions[get_column_letter(col_idx)].width = width
|
| 141 |
+
for cell in ws.iter_cols(min_col=col_idx, max_col=col_idx, min_row=2, max_row=ws.max_row):
|
| 142 |
+
for c in cell:
|
| 143 |
+
c.alignment = Alignment(horizontal="center", vertical="top", wrap_text=True)
|
| 144 |
+
ws.freeze_panes = "A2"
|
| 145 |
+
|
| 146 |
+
def ensure_filename_path(prefix: str = "Ticket") -> str:
|
| 147 |
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 148 |
+
base = (prefix or "Ticket").strip() or "Ticket"
|
| 149 |
+
return os.path.join("/tmp", f"{base}_{ts}.xlsx")
|
| 150 |
+
|
| 151 |
+
def export_excel_to_path(df: pd.DataFrame, filename_prefix: str = "Ticket") -> str:
|
| 152 |
+
target = ensure_filename_path(filename_prefix)
|
| 153 |
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
|
| 154 |
+
tmp.close()
|
| 155 |
+
with pd.ExcelWriter(tmp.name, engine="openpyxl") as writer:
|
| 156 |
+
df.to_excel(writer, index=False, sheet_name="التذاكر")
|
| 157 |
+
_arabic_excel_format(writer, df)
|
| 158 |
+
shutil.move(tmp.name, target)
|
| 159 |
+
return target
|
| 160 |
+
|
| 161 |
+
# ============================ CSS (ألوانك + اندكس) ============================
|
| 162 |
+
CUSTOM_CSS = """
|
| 163 |
+
@import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap');
|
| 164 |
+
|
| 165 |
+
/* Palette from client */
|
| 166 |
+
:root{
|
| 167 |
+
--primary:#4137A8; --primary2:#1CADE4; --success:#00B050;
|
| 168 |
+
--success2:#42BA97; --gray1:#5C6E88; --violet:#7030A0;
|
| 169 |
+
--turq:#27CED7; --yellow:#FFC000; --danger:#F5554A;
|
| 170 |
+
|
| 171 |
+
--bg:#F6F8FB; --card:#FFFFFF; --text:#1B2559; --muted:#667085; --border:#E5E7EB;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
html, body, .gradio-container { direction: rtl; }
|
| 175 |
+
.gradio-container{
|
| 176 |
+
font-family:'Cairo', system-ui,-apple-system,'Segoe UI',Tahoma,Arial,sans-serif!important;
|
| 177 |
+
max-width:1100px!important; margin:0 auto!important; padding:16px!important;
|
| 178 |
+
color:var(--text)!important; background:var(--bg)!important;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/* ========== Index (Hero + Steps) ========== */
|
| 182 |
+
.hero{
|
| 183 |
+
border-radius:16px; padding:20px 22px;
|
| 184 |
+
background:
|
| 185 |
+
radial-gradient(900px 200px at 5% -10%, rgba(65,55,168,.14), transparent 60%),
|
| 186 |
+
radial-gradient(900px 200px at 95% -10%, rgba(28,222,228,.14), transparent 60%),
|
| 187 |
+
linear-gradient(180deg,#fff,#fbfdff);
|
| 188 |
+
border:1px solid var(--border);
|
| 189 |
+
box-shadow: 0 8px 26px rgba(16,24,40,.06);
|
| 190 |
+
margin-bottom:14px;
|
| 191 |
+
}
|
| 192 |
+
.hero h1{ margin:0 0 6px 0; font-weight:700; font-size:clamp(20px,2.2vw,28px); color:var(--primary); }
|
| 193 |
+
.hero .sub{ font-size:clamp(13px,1.6vw,15px); color:var(--gray1); }
|
| 194 |
+
.hero .sub b{ color:var(--violet); }
|
| 195 |
+
|
| 196 |
+
.index-card{
|
| 197 |
+
background:var(--card); border:1px solid var(--border);
|
| 198 |
+
border-radius:14px; padding:16px; box-shadow:0 6px 18px rgba(16,24,40,.05); margin-bottom:16px;
|
| 199 |
+
}
|
| 200 |
+
.steps{ display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }
|
| 201 |
+
.step{
|
| 202 |
+
background:#f9fbff; border:1px dashed var(--border); border-radius:12px; padding:12px;
|
| 203 |
+
display:flex; align-items:center; gap:10px;
|
| 204 |
+
}
|
| 205 |
+
.step .num{
|
| 206 |
+
width:34px; height:34px; border-radius:50%; color:#fff; font-weight:700;
|
| 207 |
+
display:flex; align-items:center; justify-content:center;
|
| 208 |
+
background:var(--primary2);
|
| 209 |
+
}
|
| 210 |
+
.step:nth-child(2) .num{ background:var(--success); }
|
| 211 |
+
.step:nth-child(3) .num{ background:var(--yellow); color:#111; }
|
| 212 |
+
|
| 213 |
+
.start-btn{ margin-top:10px; }
|
| 214 |
+
|
| 215 |
+
/* ========== Work Card ========== */
|
| 216 |
+
.wrap{ background:var(--card); border:1px solid var(--border); border-radius:14px; padding:16px; box-shadow:0 6px 18px rgba(16,24,40,.05); }
|
| 217 |
+
.label{ color:var(--text)!important; font-weight:600!important; }
|
| 218 |
+
.hint{ color:var(--muted); font-size:13px; margin-top:6px; }
|
| 219 |
+
|
| 220 |
+
/* Inputs */
|
| 221 |
+
.gr-textbox textarea{
|
| 222 |
+
background:#fff!important; color:var(--text)!important;
|
| 223 |
+
border:1px solid var(--border)!important; border-radius:12px!important;
|
| 224 |
+
min-height:clamp(160px,40vh,420px); line-height:1.8;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
/* Buttons row near table */
|
| 228 |
+
.btn-row{ gap:10px; flex-wrap:wrap; justify-content:flex-start; }
|
| 229 |
+
button, .gr-button{ border-radius:12px!important; font-weight:600!important; position:relative; }
|
| 230 |
+
.gr-button-primary{
|
| 231 |
+
background:linear-gradient(90deg,var(--primary),var(--primary2))!important;
|
| 232 |
+
color:#fff!important; border:none!important; box-shadow:0 8px 18px rgba(65,55,168,.18);
|
| 233 |
+
}
|
| 234 |
+
.gr-button-secondary{
|
| 235 |
+
background:#f8fafc!important; color:var(--text)!important; border:1px solid var(--border)!important;
|
| 236 |
+
}
|
| 237 |
+
.gr-button-primary:hover{ filter:brightness(1.05); }
|
| 238 |
+
|
| 239 |
+
/* Numbers on buttons */
|
| 240 |
+
#btn-parse::before, #btn-export::before, #btn-clear::before{
|
| 241 |
+
content: attr(data-step);
|
| 242 |
+
position:absolute; top:-8px; inset-inline-start:-8px;
|
| 243 |
+
background:var(--primary2); color:#fff; width:22px; height:22px;
|
| 244 |
+
border-radius:50%; display:flex; align-items:center; justify-content:center;
|
| 245 |
+
font-size:12px; font-weight:700; box-shadow:0 2px 8px rgba(0,0,0,.15);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
/* Dataframe */
|
| 249 |
+
.dataframe{ background:#fff!important; border-radius:10px!important; }
|
| 250 |
+
.dataframe thead th{
|
| 251 |
+
background:var(--primary)!important; color:#fff!important;
|
| 252 |
+
text-align:center; position:sticky; top:0; z-index:2;
|
| 253 |
+
border-bottom:1px solid var(--border)!important;
|
| 254 |
+
}
|
| 255 |
+
.dataframe td, .dataframe th{ border-color:var(--border)!important; }
|
| 256 |
+
.dataframe td{ text-align:center !important; }
|
| 257 |
+
|
| 258 |
+
/* Footer */
|
| 259 |
+
.footer{ color:var(--muted); font-size:13.5px; text-align:center; margin-top:18px; }
|
| 260 |
+
.footer b{ color:var(--primary); }
|
| 261 |
+
|
| 262 |
+
/* Responsive */
|
| 263 |
+
@media (max-width:768px){
|
| 264 |
+
.gradio-container{ padding:12px!important; }
|
| 265 |
+
.hero{ padding:16px; }
|
| 266 |
+
.hero h1{ font-size:clamp(18px,5vw,24px); }
|
| 267 |
+
.gr-textbox textarea{ min-height:clamp(140px,34vh,360px); }
|
| 268 |
+
.steps{ grid-template-columns:1fr; }
|
| 269 |
+
}
|
| 270 |
+
"""
|
| 271 |
+
|
| 272 |
+
# ============================ الواجهة ============================
|
| 273 |
+
with gr.Blocks(title="منصة معالجة التذاكر — ISIC Helper",
|
| 274 |
+
theme=gr.themes.Soft(),
|
| 275 |
+
css=CUSTOM_CSS) as demo:
|
| 276 |
+
|
| 277 |
+
# ====== INDEX ======
|
| 278 |
+
gr.HTML("""
|
| 279 |
+
<div class="hero">
|
| 280 |
+
<h1>منصة معالجة التذاكر</h1>
|
| 281 |
+
<div class="sub">تطوير وإعداد — <b>نوف الناصر</b></div>
|
| 282 |
+
</div>
|
| 283 |
+
""")
|
| 284 |
+
|
| 285 |
+
# بطاقة اندكس: خطوات + زر ابدأ الآن (ينزل للقسم السفلي)
|
| 286 |
+
gr.HTML("""
|
| 287 |
+
<div class="index-card">
|
| 288 |
+
<div class="steps">
|
| 289 |
+
<div class="step"><div class="num">1</div><div>ألصقي نص التذاكر كما هو (يمكن فواصل 🔴🔴🔴 أو سطر فارغ).</div></div>
|
| 290 |
+
<div class="step"><div class="num">2</div><div>راجعي الجدول وعدّلي إن لزم — ثم صدّري إلى Excel.</div></div>
|
| 291 |
+
<div class="step"><div class="num">3</div><div>سجّلي الملف وشاركيه مع الفريق.</div></div>
|
| 292 |
+
</div>
|
| 293 |
+
<div class="start-btn"><button id="goWork" class="gr-button gr-button-primary">ابدأ الآن</button></div>
|
| 294 |
+
</div>
|
| 295 |
+
<script>
|
| 296 |
+
setTimeout(()=>{ const b=document.getElementById('goWork'); if(!b) return;
|
| 297 |
+
b.onclick=()=>{ const el=document.getElementById('work'); if(el) el.scrollIntoView({behavior:'smooth', block:'start'}); };
|
| 298 |
+
},50);
|
| 299 |
+
</script>
|
| 300 |
+
""")
|
| 301 |
+
|
| 302 |
+
# ====== WORK (المعالجة) ======
|
| 303 |
+
with gr.Group(elem_classes=["wrap"], elem_id="work"):
|
| 304 |
+
raw = gr.Textbox(
|
| 305 |
+
label="الصق التذاكر هنا",
|
| 306 |
+
lines=16,
|
| 307 |
+
placeholder="الصق نص التذاكر كما هو… افصل بين التذاكر بسطر فارغ أو 🔴🔴🔴 أو ---",
|
| 308 |
+
scale=12
|
| 309 |
+
)
|
| 310 |
+
gr.Markdown('<div class="hint">تلميح: لا يلزم ترتيب معيّن للحقول داخل كل تذكرة.</div>')
|
| 311 |
+
|
| 312 |
+
with gr.Row(elem_classes=["btn-row"]):
|
| 313 |
+
parse_btn = gr.Button("تحليل التذاكر", variant="secondary", elem_id="btn-parse")
|
| 314 |
+
export_btn = gr.DownloadButton("تصدير Excel", variant="primary", elem_id="btn-export")
|
| 315 |
+
clear_btn = gr.Button("مسح الكل", variant="secondary", elem_id="btn-clear")
|
| 316 |
+
fname = gr.Textbox(label="اسم ملف التصدير", value="Ticket", scale=3)
|
| 317 |
+
|
| 318 |
+
gr.HTML("""
|
| 319 |
+
<script>
|
| 320 |
+
setTimeout(()=>{
|
| 321 |
+
const p=document.getElementById('btn-parse'); if(p) p.setAttribute('data-step','1');
|
| 322 |
+
const e=document.getElementById('btn-export'); if(e) e.setAttribute('data-step','2');
|
| 323 |
+
const c=document.getElementById('btn-clear'); if(c) c.setAttribute('data-step','3');
|
| 324 |
+
}, 50);
|
| 325 |
+
</script>
|
| 326 |
+
""")
|
| 327 |
+
|
| 328 |
+
df_out = gr.Dataframe(
|
| 329 |
+
headers=DISPLAY_COLUMNS,
|
| 330 |
+
row_count=(1, "dynamic"),
|
| 331 |
+
interactive=True,
|
| 332 |
+
label="نتيجة التحليل (قابلة للتعديل قبل التصدير)"
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
# مثال صغير
|
| 336 |
+
sample = """
|
| 337 |
+
🔴🔴🔴
|
| 338 |
+
نوع المشكلة : لا أقدر أكمل الدخول
|
| 339 |
+
وقت حدوث المشكلة: 21/8/2025 7:00
|
| 340 |
+
اسم صاحب المشكلة : محمد بن علي
|
| 341 |
+
رقم الهوية: 1068891991
|
| 342 |
+
رقم الجهاز: 01438
|
| 343 |
+
رقم الجوال: 0556665323
|
| 344 |
+
اسم المسح: الجبيل
|
| 345 |
+
منطقة: الشرقية
|
| 346 |
+
""".strip()
|
| 347 |
+
|
| 348 |
+
# الأحداث
|
| 349 |
+
def on_parse(raw_text):
|
| 350 |
+
df = parse_tickets(raw_text or sample)
|
| 351 |
+
return df[DISPLAY_COLUMNS]
|
| 352 |
+
|
| 353 |
+
def on_export(raw_text, prefix):
|
| 354 |
+
df = parse_tickets(raw_text or sample)
|
| 355 |
+
path = export_excel_to_path(df, prefix or "Ticket")
|
| 356 |
+
return path
|
| 357 |
+
|
| 358 |
+
def on_clear():
|
| 359 |
+
empty_df = pd.DataFrame(columns=DISPLAY_COLUMNS)
|
| 360 |
+
return "", empty_df
|
| 361 |
+
|
| 362 |
+
parse_btn.click(on_parse, inputs=[raw], outputs=[df_out])
|
| 363 |
+
export_btn.click(on_export, inputs=[raw, fname], outputs=[export_btn])
|
| 364 |
+
clear_btn.click(on_clear, outputs=[raw, df_out])
|
| 365 |
+
|
| 366 |
+
gr.HTML('<div class="footer">© جميع الحقوق محفوظة — تطوير وإعداد <b>نوف الناصر</b></div>')
|
| 367 |
+
|
| 368 |
+
if __name__ == "__main__":
|
| 369 |
+
demo.launch()
|