stat2025 commited on
Commit
05ec4ff
·
verified ·
1 Parent(s): c133f8a

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +369 -0
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()