EGYADMIN commited on
Commit
2d12c28
·
verified ·
1 Parent(s): 531b190

Update modules/document_analysis/document_app.py

Browse files
modules/document_analysis/document_app.py CHANGED
@@ -1,887 +1,415 @@
1
  """
2
- وحدة تحليل المستندات - التطبيق الرئيسي
3
  """
4
 
5
- # استيراد المكتبات القياسية
6
- import os
7
- import sys
8
- import logging
9
- import base64
10
- import json
11
- import time
12
- from io import BytesIO
13
- from pathlib import Path
14
- from urllib.parse import urlparse
15
- from tempfile import NamedTemporaryFile
16
-
17
- # استيراد مكتبة Streamlit
18
  import streamlit as st
19
-
20
- # استيراد المكتبات الإضافية
21
  import pandas as pd
22
  import numpy as np
23
  import matplotlib.pyplot as plt
24
  import plotly.express as px
25
  import plotly.graph_objects as go
26
- import requests
27
- from PIL import Image
28
-
29
- # محاولة استيراد خدمات تحليل المستندات
30
- try:
31
- from .services.text_extractor import TextExtractor
32
- from .services.item_extractor import ItemExtractor
33
- from .services.document_parser import DocumentParser
34
- except ImportError:
35
- try:
36
- from modules.document_analysis.services.text_extractor import TextExtractor
37
- from modules.document_analysis.services.item_extractor import ItemExtractor
38
- from modules.document_analysis.services.document_parser import DocumentParser
39
- except ImportError:
40
- # تعريف فئات وهمية في حالة عدم وجود الخدمات
41
- class TextExtractor:
42
- def __init__(self, config=None):
43
- self.config = config or {}
44
-
45
- def extract_from_pdf(self, file_path):
46
- return "نص مستخرج مؤقت من PDF"
47
-
48
- def extract_from_docx(self, file_path):
49
- return "نص مستخرج مؤقت من DOCX"
50
-
51
- def extract_from_image(self, file_path):
52
- return "نص مستخرج مؤقت من صورة"
53
-
54
- def extract(self, file_path):
55
- _, ext = os.path.splitext(file_path)
56
- ext = ext.lower()
57
-
58
- if ext == '.pdf':
59
- return self.extract_from_pdf(file_path)
60
- elif ext in ('.doc', '.docx'):
61
- return self.extract_from_docx(file_path)
62
- elif ext in ('.jpg', '.jpeg', '.png'):
63
- return self.extract_from_image(file_path)
64
- else:
65
- return "نوع ملف غير مدعوم"
66
-
67
- class ItemExtractor:
68
- def __init__(self, config=None):
69
- self.config = config or {}
70
-
71
- def extract_tables(self, document):
72
- return [{"عنوان": "جدول مؤقت", "بيانات": []}]
73
-
74
- def extract_items(self, document):
75
- return [
76
- {"رقم البند": "A1", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", "الوحدة": "م3", "الكمية": 250.0},
77
- {"رقم البند": "A2", "وصف البند": "توريد وتركيب حديد التسليح للأساسات", "الوحدة": "طن", "الكمية": 25.0},
78
- {"رقم البند": "A3", "وصف البند": "أعمال العزل المائي للأساسات", "الوحدة": "م2", "الكمية": 500.0}
79
- ]
80
-
81
- class DocumentParser:
82
- def __init__(self, config=None):
83
- self.config = config or {}
84
-
85
- def parse_document(self, file_path):
86
- return {
87
- "metadata": {
88
- "title": "مستند مؤقت",
89
- "author": "غير معروف",
90
- "date": "2024-01-01",
91
- "pages": 10
92
- },
93
- "content": "محتوى مؤقت للمستند",
94
- "tables": [],
95
- "items": []
96
- }
97
-
98
- def extract_metadata(self, file_path):
99
- return {
100
- "title": "مستند مؤقت",
101
- "author": "غير معروف",
102
- "date": "2024-01-01",
103
- "pages": 10
104
- }
105
-
106
 
107
- class DocumentAnalysisApp:
108
- """وحدة تحليل المستندات"""
109
 
110
  def __init__(self):
111
- """تهيئة وحدة تحليل المستندات"""
112
-
113
- # تهيئة خدمات تحليل المستندات
114
- self.text_extractor = TextExtractor()
115
- self.item_extractor = ItemExtractor()
116
- self.document_parser = DocumentParser()
117
 
118
  # تهيئة حالة الجلسة
119
- if 'analyzed_documents' not in st.session_state:
120
- st.session_state.analyzed_documents = []
121
 
122
- if 'extracted_items' not in st.session_state:
123
- st.session_state.extracted_items = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
- # إنشاء مجلد مؤقت للملفات
126
- self.temp_dir = Path("temp_documents")
127
- self.temp_dir.mkdir(exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  def render(self):
130
- """عرض واجهة وحدة تحليل المستندات"""
131
 
132
- st.markdown("<h1 class='module-title'>وحدة تحليل المستندات</h1>", unsafe_allow_html=True)
133
 
134
  tabs = st.tabs([
135
- "تحليل المستندات",
136
- "استخراج البنود والكميات",
137
- "تحليل الصور والمخططات",
138
- "مكتبة المستندات",
139
- "الإعدادات"
140
  ])
141
 
142
  with tabs[0]:
143
- self._render_document_analysis_tab()
144
 
145
  with tabs[1]:
146
- self._render_item_extraction_tab()
147
 
148
  with tabs[2]:
149
- self._render_image_analysis_tab()
150
 
151
  with tabs[3]:
152
- self._render_document_library_tab()
153
 
154
  with tabs[4]:
155
- self._render_settings_tab()
156
-
157
- def _render_document_analysis_tab(self):
158
- """عرض تبويب تحليل المستندات"""
159
-
160
- st.markdown("### تحليل المستندات")
161
-
162
- # رفع المستند
163
- uploaded_file = st.file_uploader("رفع مستند للتحليل", type=["pdf", "docx", "txt", "jpg", "jpeg", "png"], key="document_upload")
164
-
165
- if uploaded_file is not None:
166
- # حفظ الملف مؤقتاً
167
- file_path = self._save_uploaded_file(uploaded_file)
168
-
169
- if file_path:
170
- st.success(f"تم رفع الملف: {uploaded_file.name}")
171
-
172
- # عرض معلومات الملف
173
- file_info = self._get_file_info(file_path)
174
-
175
- col1, col2, col3 = st.columns(3)
176
-
177
- with col1:
178
- st.metric("نوع الملف", file_info["type"])
179
-
180
- with col2:
181
- st.metric("حجم الملف", file_info["size"])
182
-
183
- with col3:
184
- if "pages" in file_info:
185
- st.metric("عدد الصفحات", file_info["pages"])
186
-
187
- # خيارات التحليل
188
- analysis_options = st.multiselect(
189
- "اختر خيارات التحليل",
190
- [
191
- "استخراج النص",
192
- "استخراج الجداول",
193
- "استخراج البنود والكميات",
194
- "استخراج المعلومات الرئيسية",
195
- "تحليل هيكل المستند"
196
- ],
197
- default=["استخراج النص", "استخراج البنود والكميات"],
198
- key="analysis_options"
199
- )
200
-
201
- # زر بدء التحليل
202
- if st.button("بدء التحليل", key="start_analysis_button"):
203
- with st.spinner("جاري تحليل المستند..."):
204
- # محاكاة وقت التحليل
205
- time.sleep(2)
206
-
207
- # تنفيذ التحليل المطلوب
208
- analysis_results = {}
209
-
210
- if "استخراج النص" in analysis_options:
211
- analysis_results["text"] = self.text_extractor.extract(file_path)
212
-
213
- if "استخراج الجداول" in analysis_options:
214
- # محاكاة استخراج الجداول
215
- tables = self.item_extractor.extract_tables(file_path)
216
- analysis_results["tables"] = tables
217
-
218
- if "استخراج البنود والكميات" in analysis_options:
219
- # محاكاة استخراج البنود
220
- items = self.item_extractor.extract_items(file_path)
221
- analysis_results["items"] = items
222
-
223
- # حفظ البنود المستخرجة في حالة الجلسة
224
- st.session_state.extracted_items = items
225
-
226
- if "استخراج المعلومات الرئيسية" in analysis_options:
227
- # محاكاة استخراج المعلومات الرئيسية
228
- metadata = self.document_parser.extract_metadata(file_path)
229
- analysis_results["metadata"] = metadata
230
-
231
- if "تحليل هيكل المستند" in analysis_options:
232
- # محاكاة تحليل هيكل المستند
233
- structure = {
234
- "sections": [
235
- {"title": "مقدمة", "level": 1, "page": 1},
236
- {"title": "نطاق العمل", "level": 1, "page": 2},
237
- {"title": "المواصفات الفنية", "level": 1, "page": 3},
238
- {"title": "جدول الكميات", "level": 1, "page": 5},
239
- {"title": "الشروط الخاصة", "level": 1, "page": 7}
240
- ]
241
- }
242
- analysis_results["structure"] = structure
243
-
244
- # حفظ نتائج التحليل في حالة الجلسة
245
- st.session_state.analyzed_documents.append({
246
- "file_name": uploaded_file.name,
247
- "file_path": str(file_path),
248
- "analysis_options": analysis_options,
249
- "results": analysis_results,
250
- "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
251
- })
252
-
253
- st.success("تم الانتهاء من تحليل المستند!")
254
-
255
- # عرض نتائج التحليل
256
- self._display_analysis_results(analysis_results)
257
-
258
- # عرض سجل التحليلات السابقة
259
- if st.session_state.analyzed_documents:
260
- st.markdown("### سجل التحليلات السابقة")
261
-
262
- for i, doc in enumerate(reversed(st.session_state.analyzed_documents)):
263
- with st.expander(f"{doc['file_name']} ({doc['timestamp']})"):
264
- st.markdown(f"**خيارات التحليل:** {', '.join(doc['analysis_options'])}")
265
-
266
- # عرض نتائج التحليل
267
- self._display_analysis_results(doc['results'])
268
-
269
- # أزرار العمليات
270
- col1, col2 = st.columns(2)
271
-
272
- with col1:
273
- if st.button("إرسال إلى وحدة التسعير", key=f"send_to_pricing_{i}"):
274
- st.success("تم إرسال البيانات إلى وحدة التسعير بنجاح!")
275
-
276
- with col2:
277
- if st.button("تصدير النتائج", key=f"export_results_{i}"):
278
- st.success("تم تصدير النتائج بنجاح!")
279
-
280
- def _render_item_extraction_tab(self):
281
- """عرض تبويب استخراج البنود والكميات"""
282
-
283
- st.markdown("### استخراج البنود والكميات")
284
-
285
- # التحقق من وجود بنود مستخرجة
286
- if not st.session_state.extracted_items:
287
- st.warning("لا توجد بنود مستخرجة. يرجى تحليل مستند أولاً.")
288
-
289
- # عرض بيانات افتراضية للتوضيح
290
- st.markdown("### مثال توضيحي")
291
-
292
- # بيانات افتراضية
293
- sample_items = [
294
- {"رقم البند": "A1", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", "الوحدة": "م3", "الكمية": 250.0},
295
- {"رقم البند": "A2", "وصف البند": "توريد وتركيب حديد التسليح للأساسات", "الوحدة": "طن", "الكمية": 25.0},
296
- {"رقم البند": "A3", "وصف البند": "أعمال العزل المائي للأساسات", "الوحدة": "م2", "الكمية": 500.0},
297
- {"رقم البند": "A4", "وصف البند": "أعمال الردم والدك للأساسات", "الوحدة": "م3", "الكمية": 300.0},
298
- {"رقم البند": "A5", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", "الوحدة": "م3", "الكمية": 120.0}
299
- ]
300
-
301
- # عرض البنود كجدول
302
- items_df = pd.DataFrame(sample_items)
303
- st.dataframe(items_df, use_container_width=True, hide_index=True)
304
-
305
- # زر لاستخدام البيانات التوضيحية
306
- if st.button("استخدام البيانات التوضيحية", key="use_sample_data_button"):
307
- st.session_state.extracted_items = sample_items
308
- st.success("تم استخدام البيانات التوضيحية!")
309
- st.rerun()
310
- else:
311
- # عرض البنود المستخرجة
312
- items_df = pd.DataFrame(st.session_state.extracted_items)
313
-
314
- # إضافة عمود سعر الوحدة والإجمالي إذا لم يكن موجوداً
315
- if "سعر الوحدة" not in items_df.columns:
316
- items_df["سعر الوحدة"] = 0.0
317
-
318
- if "الإجمالي" not in items_df.columns:
319
- items_df["الإجمالي"] = 0.0
320
-
321
- # عرض البنود كجدول قابل للتعديل
322
- st.markdown("### البنود المستخرجة")
323
- edited_df = st.data_editor(items_df, use_container_width=True, hide_index=True, key="items_editor")
324
-
325
- # تحديث البنود المستخرجة بعد التعديل
326
- st.session_state.extracted_items = edited_df.to_dict('records')
327
-
328
- # أزرار العمليات
329
- col1, col2, col3 = st.columns(3)
330
-
331
- with col1:
332
- if st.button("إرسال إلى وحدة التسعير", key="send_to_pricing_button"):
333
- # محاكاة إرسال البيانات إلى وحدة التسعير
334
- if 'current_pricing' not in st.session_state:
335
- st.session_state.current_pricing = {
336
- 'name': "مناقصة جديدة",
337
- 'number': "T-" + time.strftime("%Y-%m-%d"),
338
- 'client': "",
339
- 'location': "",
340
- 'method': "التسعير القياسي",
341
- 'submission_date': None,
342
- 'items': edited_df,
343
- 'status': 'جديد',
344
- 'created_at': time.strftime("%Y-%m-%d %H:%M:%S")
345
- }
346
- else:
347
- st.session_state.current_pricing['items'] = edited_df
348
-
349
- st.success("تم إرسال البنود إلى وحدة التسعير بنجاح!")
350
-
351
- with col2:
352
- if st.button("تصدير إلى Excel", key="export_to_excel_button"):
353
- st.success("تم تصدير البنود إلى Excel بنجاح!")
354
-
355
- with col3:
356
- if st.button("مسح البنود", key="clear_items_button"):
357
- st.session_state.extracted_items = []
358
- st.warning("تم مسح البنود!")
359
- st.rerun()
360
-
361
- # عرض إحصائيات البنود
362
- st.markdown("### إحصائيات البنود")
363
-
364
- col1, col2, col3 = st.columns(3)
365
-
366
- with col1:
367
- st.metric("عدد البنود", len(edited_df))
368
-
369
- with col2:
370
- units_count = edited_df['الوحدة'].value_counts()
371
- most_common_unit = units_count.index[0] if not units_count.empty else "غير متوفر"
372
- st.metric("الوحدة الأكثر استخداماً", most_common_unit)
373
-
374
- with col3:
375
- total_quantity = edited_df['الكمية'].sum()
376
- st.metric("إجمالي الكميات", f"{total_quantity:,.2f}")
377
-
378
- # رسم بياني لتوزيع البنود حسب الوحدة
379
- st.markdown("### توزيع البنود حسب الوحدة")
380
-
381
- units_df = pd.DataFrame(units_count).reset_index()
382
- units_df.columns = ['الوحدة', 'العدد']
383
-
384
- fig = px.pie(
385
- units_df,
386
- values='العدد',
387
- names='الوحدة',
388
- title='توزيع البنود حسب الوحدة',
389
- hole=0.4
390
- )
391
-
392
- st.plotly_chart(fig, use_container_width=True)
393
 
394
- def _render_image_analysis_tab(self):
395
- """عرض تبويب تحليل الصور والمخططات"""
396
 
397
- st.markdown("### تحليل الصور والمخططات")
398
 
399
- # رفع الصورة
400
- uploaded_image = st.file_uploader("رفع صورة أو مخطط للتحليل", type=["jpg", "jpeg", "png", "tif", "tiff"], key="image_upload")
401
 
402
- if uploaded_image is not None:
403
- # عرض الصورة
404
- image = Image.open(uploaded_image)
405
- st.image(image, caption=uploaded_image.name, use_column_width=True)
406
-
407
- # خيارات التحليل
408
- analysis_type = st.selectbox(
409
- "نوع التحليل",
410
- [
411
- "استخراج النص من الصورة",
412
- "تحليل المخططات الهندسية",
413
- "قياس المساحات والأبعاد",
414
- "تحليل مخصص"
415
- ],
416
- key="image_analysis_type"
417
- )
418
-
419
- # زر بدء التحليل
420
- if st.button("بدء التحليل", key="start_image_analysis_button"):
421
- with st.spinner("جاري تحليل الصورة..."):
422
- # محاكاة وقت التحليل
423
- time.sleep(2)
424
-
425
- if analysis_type == "استخراج النص من الصورة":
426
- # محاكاة استخراج النص
427
- extracted_text = "نص مستخرج من الصورة (محاكاة):\n\n"
428
- extracted_text += "مواصفات المشروع:\n"
429
- extracted_text += "- مساحة الأرض: 1000 م2\n"
430
- extracted_text += "- عدد الطوابق: 3\n"
431
- extracted_text += "- ارتفاع المبنى: 12 م\n"
432
-
433
- st.markdown("### النص المستخرج من الصورة")
434
- st.text_area("النص المستخرج", extracted_text, height=200)
435
-
436
- elif analysis_type == "تحليل المخططات الهندسية":
437
- # محاكاة تحليل المخططات
438
- st.markdown("### نتائج تحليل المخطط الهندسي")
439
-
440
- # محاكاة رسم تخطيطي للمخطط
441
- fig, ax = plt.subplots(figsize=(10, 6))
442
- ax.imshow(image)
443
-
444
- # إضافة تعليقات توضيحية
445
- ax.annotate('غرفة المعيشة', xy=(100, 100), xytext=(150, 50),
446
- arrowprops=dict(facecolor='red', shrink=0.05))
447
-
448
- ax.annotate('المطبخ', xy=(300, 150), xytext=(350, 100),
449
- arrowprops=dict(facecolor='blue', shrink=0.05))
450
-
451
- ax.annotate('غرفة النوم', xy=(200, 300), xytext=(250, 350),
452
- arrowprops=dict(facecolor='green', shrink=0.05))
453
-
454
- st.pyplot(fig)
455
-
456
- # عرض معلومات المخطط
457
- st.markdown("### معلومات المخطط")
458
-
459
- col1, col2, col3 = st.columns(3)
460
-
461
- with col1:
462
- st.metric("المساحة الإجمالية", "150 م2")
463
-
464
- with col2:
465
- st.metric("عدد الغرف", "3")
466
-
467
- with col3:
468
- st.metric("عدد الحمامات", "2")
469
-
470
- elif analysis_type == "قياس المساحات والأبعاد":
471
- # محاكاة قياس المساحات
472
- st.markdown("### نتائج قياس المساحات والأبعاد")
473
-
474
- # محاكاة رسم تخطيطي للمساحات
475
- fig, ax = plt.subplots(figsize=(10, 6))
476
- ax.imshow(image)
477
-
478
- # إضافة قياسات
479
- ax.plot([50, 250], [50, 50], 'r-', linewidth=2)
480
- ax.text(150, 40, '10 م', color='red', fontsize=12, ha='center')
481
-
482
- ax.plot([50, 50], [50, 250], 'b-', linewidth=2)
483
- ax.text(40, 150, '8 م', color='blue', fontsize=12, va='center', rotation=90)
484
-
485
- st.pyplot(fig)
486
-
487
- # عرض جدول القياسات
488
- measurements = pd.DataFrame({
489
- 'العنصر': ['الطول', 'العرض', 'المساحة', 'المحيط'],
490
- 'القيمة': ['10 م', '8 م', '80 م2', '36 م']
491
- })
492
-
493
- st.dataframe(measurements, use_container_width=True, hide_index=True)
494
-
495
- else: # تحليل مخصص
496
- st.markdown("### نتائج التحليل المخصص")
497
- st.info("تم تحليل الصورة بنجاح. يمكنك تخصيص التحليل حسب احتياجاتك.")
498
-
499
- # خيارات التصدير
500
- col1, col2 = st.columns(2)
501
-
502
- with col1:
503
- if st.button("تصدير نتائج التحليل", key="export_image_analysis_button"):
504
- st.success("تم تصدير نتائج التحليل بنجاح!")
505
-
506
- with col2:
507
- if st.button("إرسال إلى وحدة التسعير", key="send_image_to_pricing_button"):
508
- st.success("تم إرسال نتائج التحليل إلى وحدة التسعير بنجاح!")
509
-
510
- def _render_document_library_tab(self):
511
- """عرض تبويب مكتبة المستندات"""
512
 
513
- st.markdown("### مكتبة المستندات")
 
 
 
 
 
514
 
515
- # بيانات افتراضية للمستندات
516
- if 'document_library' not in st.session_state:
517
- st.session_state.document_library = [
518
- {
519
- "id": 1,
520
- "name": "كراسة شروط مشروع توسعة مستشفى الملك فهد",
521
- "type": "PDF",
522
- "size": "5.2 MB",
523
- "pages": 120,
524
- "upload_date": "2024-01-15",
525
- "category": "كراسات الشروط",
526
- "tags": ["صحي", "مستشفى", "توسعة"]
527
- },
528
- {
529
- "id": 2,
530
- "name": "جدول كميات صيانة محطات المياه",
531
- "type": "Excel",
532
- "size": "1.8 MB",
533
- "pages": None,
534
- "upload_date": "2024-02-10",
535
- "category": "جداول الكميات",
536
- "tags": ["مياه", "صيانة", "محطات"]
537
- },
538
- {
539
- "id": 3,
540
- "name": "مخططات إنشاء مدرسة ثانوية",
541
- "type": "PDF",
542
- "size": "12.5 MB",
543
- "pages": 45,
544
- "upload_date": "2024-02-25",
545
- "category": "مخططات",
546
- "tags": ["تعليم", "مدرسة", "إنشاء"]
547
- },
548
- {
549
- "id": 4,
550
- "name": "عقد إنشاء طريق دائري",
551
- "type": "Word",
552
- "size": "0.9 MB",
553
- "pages": 28,
554
- "upload_date": "2024-03-05",
555
- "category": "عقود",
556
- "tags": ["طرق", "إنشاء", "دائري"]
557
- },
558
- {
559
- "id": 5,
560
- "name": "تقرير فني لمشروع تطوير شبكة مياه",
561
- "type": "PDF",
562
- "size": "3.7 MB",
563
- "pages": 65,
564
- "upload_date": "2024-03-15",
565
- "category": "تقارير فنية",
566
- "tags": ["مياه", "شبكة", "تطوير"]
567
- }
568
- ]
569
-
570
- # البحث في المكتبة
571
- search_query = st.text_input("البحث في المكتبة", key="library_search")
572
-
573
- col1, col2, col3 = st.columns(3)
574
 
575
  with col1:
576
- category_filter = st.selectbox(
577
- "تصفية حسب الفئة",
578
- ["الكل", "كراسات الشروط", "جداول الكميات", "مخططات", "عقود", "تقارير فنية"],
579
- key="category_filter"
580
- )
581
 
582
  with col2:
583
- type_filter = st.selectbox(
584
- "تصفية حسب النوع",
585
- ["الكل", "PDF", "Word", "Excel", "Image"],
586
- key="type_filter"
587
- )
588
 
589
  with col3:
590
- sort_by = st.selectbox(
591
- "ترتيب حسب",
592
- ["تاريخ الرفع (الأحدث أولاً)", "تاريخ الرفع (الأقدم أولاً)", "الاسم (أ-ي)", "الاسم (ي-أ)", "الحجم (الأكبر أولاً)", "الحجم (الأصغر أولاً)"],
593
- key="sort_by"
594
- )
595
-
596
- # تطبيق التصفية والبحث
597
- filtered_documents = st.session_state.document_library.copy()
598
-
599
- # تطبيق البحث
600
- if search_query:
601
- filtered_documents = [doc for doc in filtered_documents if search_query.lower() in doc["name"].lower() or
602
- any(search_query.lower() in tag.lower() for tag in doc["tags"])]
603
-
604
- # تطبيق تصفية الفئة
605
- if category_filter != "الكل":
606
- filtered_documents = [doc for doc in filtered_documents if doc["category"] == category_filter]
607
-
608
- # تطبيق تصفية النوع
609
- if type_filter != "الكل":
610
- filtered_documents = [doc for doc in filtered_documents if doc["type"] == type_filter]
611
-
612
- # تطبيق الترتيب
613
- if sort_by == "تاريخ الرفع (الأحدث أولاً)":
614
- filtered_documents.sort(key=lambda x: x["upload_date"], reverse=True)
615
- elif sort_by == "تاريخ الرفع (الأقدم أولاً)":
616
- filtered_documents.sort(key=lambda x: x["upload_date"])
617
- elif sort_by == "الاسم (أ-ي)":
618
- filtered_documents.sort(key=lambda x: x["name"])
619
- elif sort_by == "الاسم (ي-أ)":
620
- filtered_documents.sort(key=lambda x: x["name"], reverse=True)
621
- elif sort_by == "الحجم (الأكبر أولاً)":
622
- filtered_documents.sort(key=lambda x: float(x["size"].split()[0]), reverse=True)
623
- elif sort_by == "الحجم (الأصغر أولاً)":
624
- filtered_documents.sort(key=lambda x: float(x["size"].split()[0]))
625
-
626
- # عرض المستندات
627
- st.markdown(f"### المستندات ({len(filtered_documents)})")
628
-
629
- if not filtered_documents:
630
- st.info("لا توجد مستندات تطابق معايير البحث.")
631
- else:
632
- # عرض المستندات كبطاقات
633
- for i, doc in enumerate(filtered_documents):
634
- with st.container():
635
- col1, col2, col3 = st.columns([3, 1, 1])
636
-
637
- with col1:
638
- st.markdown(f"**{doc['name']}**")
639
- st.markdown(f"الفئة: {doc['category']} | النوع: {doc['type']} | الحجم: {doc['size']} | تاريخ الرفع: {doc['upload_date']}")
640
- st.markdown(f"الوسوم: {', '.join(doc['tags'])}")
641
-
642
- with col2:
643
- if st.button("عرض", key=f"view_doc_{i}"):
644
- st.session_state.selected_document = doc
645
- st.success(f"جاري عرض المستند: {doc['name']}")
646
-
647
- with col3:
648
- if st.button("تحليل", key=f"analyze_doc_{i}"):
649
- st.session_state.selected_document = doc
650
- st.success(f"جاري تحليل المستند: {doc['name']}")
651
-
652
- st.markdown("---")
653
-
654
- # رفع مستند جديد
655
- st.markdown("### رفع مستند جديد")
656
-
657
- uploaded_file = st.file_uploader("اختر ملفاً للرفع", type=["pdf", "docx", "xlsx", "jpg", "jpeg", "png"], key="library_upload")
658
-
659
- if uploaded_file is not None:
660
- col1, col2 = st.columns(2)
661
-
662
- with col1:
663
- doc_category = st.selectbox(
664
- "فئة المستند",
665
- ["كراسات الشروط", "جداول الكميات", "مخططات", "عقود", "تقارير فنية", "أخرى"],
666
- key="doc_category"
667
- )
668
-
669
- with col2:
670
- doc_tags = st.text_input("الوسوم (مفصولة بفواصل)", key="doc_tags")
671
-
672
- if st.button("رفع المستند", key="upload_to_library_button"):
673
- # محاكاة رفع المستند
674
- new_doc = {
675
- "id": len(st.session_state.document_library) + 1,
676
- "name": uploaded_file.name,
677
- "type": uploaded_file.name.split(".")[-1].upper(),
678
- "size": f"{uploaded_file.size / (1024 * 1024):.1f} MB",
679
- "pages": None,
680
- "upload_date": time.strftime("%Y-%m-%d"),
681
- "category": doc_category,
682
- "tags": [tag.strip() for tag in doc_tags.split(",") if tag.strip()]
683
- }
684
-
685
- st.session_state.document_library.append(new_doc)
686
- st.success(f"تم رفع المستند: {uploaded_file.name}")
687
- st.rerun()
688
-
689
- def _render_settings_tab(self):
690
- """عرض تبويب الإعدادات"""
691
-
692
- st.markdown("### إعدادات تحليل المستندات")
693
-
694
- # إعدادات استخراج النص
695
- with st.expander("إعدادات استخراج النص", expanded=True):
696
- st.markdown("#### إعدادات استخراج النص")
697
-
698
- ocr_engine = st.selectbox(
699
- "محرك التعرف الضوئي على النصوص",
700
- ["Tesseract OCR", "Google Cloud Vision", "Amazon Textract", "Microsoft Azure OCR"],
701
- index=0,
702
- key="ocr_engine"
703
- )
704
-
705
- language = st.selectbox(
706
- "لغة المستندات",
707
- ["العربية", "الإنجليزية", "العربية والإنجليزية"],
708
- index=0,
709
- key="ocr_language"
710
- )
711
-
712
- dpi = st.slider(
713
- "دقة المسح (DPI)",
714
- min_value=100,
715
- max_value=600,
716
- value=300,
717
- step=50,
718
- key="ocr_dpi"
719
- )
720
-
721
- if st.button("حفظ إعدادات استخراج النص", key="save_ocr_settings"):
722
- st.success("تم حفظ إعدادات استخراج النص بنجاح!")
723
-
724
- # إعدادات استخراج البنود
725
- with st.expander("إعدادات استخراج البنود", expanded=True):
726
- st.markdown("#### إعدادات استخراج البنود")
727
-
728
- extraction_method = st.selectbox(
729
- "طريقة استخراج البنود",
730
- ["تحليل الجداول", "تحليل النص", "الذكاء الاصطناعي", "مزيج"],
731
- index=3,
732
- key="extraction_method"
733
- )
734
-
735
- auto_detect_units = st.checkbox(
736
- "اكتشاف الوحدات تلقائياً",
737
- value=True,
738
- key="auto_detect_units"
739
- )
740
-
741
- normalize_quantities = st.checkbox(
742
- "توحيد صيغة الكميات",
743
- value=True,
744
- key="normalize_quantities"
745
- )
746
-
747
- if st.button("حفظ إعدادات استخراج البنود", key="save_extraction_settings"):
748
- st.success("تم حفظ إعدادات استخراج البنود بنجاح!")
749
-
750
- # إعدادات تحليل الصور
751
- with st.expander("إعدادات تحليل الصور", expanded=True):
752
- st.markdown("#### إعدادات تحليل الصور")
753
-
754
- image_analysis_engine = st.selectbox(
755
- "محرك تحليل الصور",
756
- ["OpenCV", "Google Cloud Vision", "Amazon Rekognition", "Microsoft Azure Computer Vision"],
757
- index=0,
758
- key="image_analysis_engine"
759
- )
760
-
761
- image_resolution = st.slider(
762
- "دقة تحليل الصور",
763
- min_value=1,
764
- max_value=10,
765
- value=5,
766
- key="image_resolution"
767
- )
768
-
769
- if st.button("حفظ إعدادات تحليل الصور", key="save_image_analysis_settings"):
770
- st.success("تم حفظ إعدادات تحليل الصور بنجاح!")
771
-
772
- # إعدادات متقدمة
773
- with st.expander("إعدادات متقدمة", expanded=False):
774
- st.markdown("#### إعدادات متقدمة")
775
-
776
- temp_files_retention = st.slider(
777
- "مدة الاحتفاظ بالملفات المؤقتة (أيام)",
778
- min_value=1,
779
- max_value=30,
780
- value=7,
781
- key="temp_files_retention"
782
- )
783
-
784
- max_file_size = st.slider(
785
- "الحد الأقصى لحجم الملف (ميجابايت)",
786
- min_value=5,
787
- max_value=100,
788
- value=50,
789
- key="max_file_size"
790
- )
791
-
792
- parallel_processing = st.checkbox(
793
- "تفعيل المعالجة المتوازية",
794
- value=True,
795
- key="parallel_processing"
796
- )
797
-
798
- if st.button("حفظ الإعدادات المتقدمة", key="save_advanced_settings"):
799
- st.success("تم حفظ الإعدادات المتقدمة بنجاح!")
800
-
801
- def _save_uploaded_file(self, uploaded_file):
802
- """حفظ الملف المرفوع في مجلد مؤقت"""
803
- try:
804
- file_path = self.temp_dir / uploaded_file.name
805
- with open(file_path, "wb") as f:
806
- f.write(uploaded_file.getbuffer())
807
- return file_path
808
- except Exception as e:
809
- st.error(f"حدث خطأ أثناء حفظ الملف: {str(e)}")
810
- return None
811
-
812
- def _get_file_info(self, file_path):
813
- """الحصول على معلومات الملف"""
814
- file_info = {
815
- "type": file_path.suffix[1:].upper(),
816
- "size": f"{file_path.stat().st_size / (1024 * 1024):.2f} MB"
817
- }
818
-
819
- # محاولة الحصول على عدد الصفحات للملفات المدعومة
820
- if file_path.suffix.lower() == ".pdf":
821
- # محاكاة عدد الصفحات
822
- file_info["pages"] = 10
823
-
824
- return file_info
825
-
826
- def _display_analysis_results(self, results):
827
- """عرض نتائج التحليل"""
828
-
829
- if not results:
830
- st.info("لا توجد نتائج للعرض.")
831
- return
832
-
833
- # عرض النص المستخرج
834
- if "text" in results:
835
- with st.expander("النص المستخرج", expanded=False):
836
- st.text_area("النص", results["text"], height=200)
837
-
838
- # عرض الجداول المستخرجة
839
- if "tables" in results and results["tables"]:
840
- with st.expander("الجداول المستخرجة", expanded=True):
841
- for i, table in enumerate(results["tables"]):
842
- st.markdown(f"**جدول {i+1}: {table.get('عنوان', 'بدون عنوان')}**")
843
-
844
- if "بيانات" in table and table["بيانات"]:
845
- # محاولة عرض البيانات كجدول
846
- try:
847
- df = pd.DataFrame(table["بيانات"])
848
- st.dataframe(df, use_container_width=True, hide_index=True)
849
- except Exception:
850
- st.text(str(table["بيانات"]))
851
- else:
852
- st.info("لا توجد بيانات في هذا الجدول.")
853
-
854
- # عرض البنود المستخرجة
855
- if "items" in results and results["items"]:
856
- with st.expander("البنود المستخرجة", expanded=True):
857
- items_df = pd.DataFrame(results["items"])
858
- st.dataframe(items_df, use_container_width=True, hide_index=True)
859
-
860
- # زر لإرسال البنود إلى وحدة التسعير
861
- if st.button("إرسال البنود إلى وحدة التسعير", key="send_extracted_items_button"):
862
- st.session_state.extracted_items = results["items"]
863
- st.success("تم إرسال البنود المستخرجة إلى وحدة التسعير!")
864
-
865
- # عرض المعلومات الرئيسية
866
- if "metadata" in results:
867
- with st.expander("المعلومات الرئيسية", expanded=True):
868
- metadata = results["metadata"]
869
-
870
- col1, col2 = st.columns(2)
871
-
872
- with col1:
873
- st.markdown(f"**عنوان المستند:** {metadata.get('title', 'غير متوفر')}")
874
- st.markdown(f"**المؤلف:** {metadata.get('author', 'غير متوفر')}")
875
-
876
- with col2:
877
- st.markdown(f"**التاريخ:** {metadata.get('date', 'غير متوفر')}")
878
- st.markdown(f"**عدد الصفحات:** {metadata.get('pages', 'غير متوفر')}")
879
-
880
- # عرض هيكل المستند
881
- if "structure" in results and "sections" in results["structure"]:
882
- with st.expander("هيكل المستند", expanded=False):
883
- sections = results["structure"]["sections"]
884
-
885
- for section in sections:
886
- indent = "&nbsp;" * (section["level"] * 4)
887
- st.markdown(f"{indent}• **{section['title']}** (صفحة {section['page']})", unsafe_allow_html=True)
 
1
  """
2
+ وحدة تحليل البيانات - التطبيق الرئيسي
3
  """
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import streamlit as st
 
 
6
  import pandas as pd
7
  import numpy as np
8
  import matplotlib.pyplot as plt
9
  import plotly.express as px
10
  import plotly.graph_objects as go
11
+ from datetime import datetime
12
+ import time
13
+ import io
14
+ import os
15
+ import json
16
+ import base64
17
+ from pathlib import Path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ class DataAnalysisApp:
20
+ """وحدة تحليل البيانات"""
21
 
22
  def __init__(self):
23
+ """تهيئة وحدة تحليل البيانات"""
 
 
 
 
 
24
 
25
  # تهيئة حالة الجلسة
26
+ if 'uploaded_data' not in st.session_state:
27
+ st.session_state.uploaded_data = None
28
 
29
+ if 'data_sources' not in st.session_state:
30
+ st.session_state.data_sources = [
31
+ {
32
+ 'id': 1,
33
+ 'name': 'بيانات المناقصات السابقة',
34
+ 'type': 'CSV',
35
+ 'rows': 250,
36
+ 'columns': 15,
37
+ 'last_updated': '2024-03-01',
38
+ 'description': 'بيانات المناقصات السابقة للشركة خلال الثلاث سنوات الماضية'
39
+ },
40
+ {
41
+ 'id': 2,
42
+ 'name': 'بيانات المنافسين',
43
+ 'type': 'Excel',
44
+ 'rows': 120,
45
+ 'columns': 10,
46
+ 'last_updated': '2024-02-15',
47
+ 'description': 'بيانات المنافسين الرئيسيين في السوق وأسعارهم التنافسية'
48
+ },
49
+ {
50
+ 'id': 3,
51
+ 'name': 'بيانات أسعار المواد',
52
+ 'type': 'CSV',
53
+ 'rows': 500,
54
+ 'columns': 8,
55
+ 'last_updated': '2024-03-10',
56
+ 'description': 'بيانات أسعار المواد الرئيسية المستخدمة في المشاريع'
57
+ },
58
+ {
59
+ 'id': 4,
60
+ 'name': 'بيانات الموردين',
61
+ 'type': 'Excel',
62
+ 'rows': 80,
63
+ 'columns': 12,
64
+ 'last_updated': '2024-02-20',
65
+ 'description': 'بيانات الموردين الرئيسيين وأسعارهم وجودة منتجاتهم'
66
+ },
67
+ {
68
+ 'id': 5,
69
+ 'name': 'بيانات المشاريع المنجزة',
70
+ 'type': 'CSV',
71
+ 'rows': 150,
72
+ 'columns': 20,
73
+ 'last_updated': '2024-03-15',
74
+ 'description': 'بيانات المشاريع المنجزة وتكاليفها الفعلية ومدة تنفيذها'
75
+ }
76
+ ]
77
 
78
+ if 'sample_data' not in st.session_state:
79
+ # إنشاء بيانات افتراضية للمناقصات السابقة
80
+ np.random.seed(42)
81
+
82
+ # إنشاء بيانات المناقصات السابقة
83
+ n_tenders = 50
84
+ tender_ids = [f"T-{2021 + i//20}-{i%20 + 1:03d}" for i in range(n_tenders)]
85
+ tender_types = np.random.choice(["مبنى إداري", "مبنى سكني", "مدرسة", "مستشفى", "طرق", "جسور", "بنية تحتية"], n_tenders)
86
+ tender_locations = np.random.choice(["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك"], n_tenders)
87
+ tender_areas = np.random.randint(1000, 10000, n_tenders)
88
+ tender_durations = np.random.randint(6, 36, n_tenders)
89
+ tender_budgets = np.random.randint(1000000, 50000000, n_tenders)
90
+ tender_costs = np.array([budget * np.random.uniform(0.8, 1.1) for budget in tender_budgets])
91
+ tender_profits = tender_budgets - tender_costs
92
+ tender_profit_margins = tender_profits / tender_budgets * 100
93
+ tender_statuses = np.random.choice(["فائز", "خاسر", "قيد التنفيذ", "منجز"], n_tenders)
94
+ tender_dates = [f"202{1 + i//20}-{np.random.randint(1, 13):02d}-{np.random.randint(1, 29):02d}" for i in range(n_tenders)]
95
+
96
+ # إنشاء DataFrame للمناقصات السابقة
97
+ tenders_data = {
98
+ "رقم المناقصة": tender_ids,
99
+ "نوع المشروع": tender_types,
100
+ "الموقع": tender_locations,
101
+ "المساحة (م2)": tender_areas,
102
+ "المدة (شهر)": tender_durations,
103
+ "الميزانية (ريال)": tender_budgets,
104
+ "التكلفة (ريال)": tender_costs,
105
+ "الربح (ريال)": tender_profits,
106
+ "هامش الربح (%)": tender_profit_margins,
107
+ "الحالة": tender_statuses,
108
+ "تاريخ التقديم": tender_dates
109
+ }
110
+
111
+ st.session_state.sample_data = {
112
+ "tenders": pd.DataFrame(tenders_data)
113
+ }
114
+
115
+ # إنشاء بيانات أسعار المو��د
116
+ n_materials = 30
117
+ material_ids = [f"M-{i+1:03d}" for i in range(n_materials)]
118
+ material_names = [
119
+ "خرسانة جاهزة", "حديد تسليح", "طابوق", "أسمنت", "رمل", "بحص", "خشب", "ألمنيوم", "زجاج", "دهان",
120
+ "سيراميك", "رخام", "جبس", "عازل مائي", "عازل حراري", "أنابيب PVC", "أسلاك كهربائية", "مفاتيح كهربائية",
121
+ "إنارة", "تكييف", "مصاعد", "أبواب خشبية", "أبواب حديدية", "نوافذ ألمنيوم", "نوافذ زجاجية",
122
+ "أرضيات خشبية", "أرضيات بلاط", "أرضيات رخام", "أرضيات سيراميك", "أرضيات بورسلين"
123
+ ]
124
+ material_units = np.random.choice(["م3", "طن", "م2", "كجم", "لتر", "قطعة", "متر"], n_materials)
125
+ material_prices_2021 = np.random.randint(50, 5000, n_materials)
126
+ material_prices_2022 = np.array([price * np.random.uniform(1.0, 1.2) for price in material_prices_2021])
127
+ material_prices_2023 = np.array([price * np.random.uniform(1.0, 1.15) for price in material_prices_2022])
128
+ material_prices_2024 = np.array([price * np.random.uniform(0.95, 1.1) for price in material_prices_2023])
129
+
130
+ # إنشاء DataFrame لأسعار المواد
131
+ materials_data = {
132
+ "رمز المادة": material_ids,
133
+ "اسم المادة": material_names,
134
+ "الوحدة": material_units,
135
+ "سعر 2021 (ريال)": material_prices_2021,
136
+ "سعر 2022 (ريال)": material_prices_2022,
137
+ "سعر 2023 (ريال)": material_prices_2023,
138
+ "سعر 2024 (ريال)": material_prices_2024,
139
+ "نسبة التغير 2021-2024 (%)": (material_prices_2024 - material_prices_2021) / material_prices_2021 * 100
140
+ }
141
+
142
+ st.session_state.sample_data["materials"] = pd.DataFrame(materials_data)
143
+
144
+ # إنشاء بيانات المنافسين
145
+ n_competitors = 10
146
+ competitor_ids = [f"C-{i+1:02d}" for i in range(n_competitors)]
147
+ competitor_names = [
148
+ "شركة الإنشاءات المتطورة", "شركة البناء الحديث", "شركة التطوير العمراني", "شركة الإعمار الدولية",
149
+ "شركة البنية التحتية المتكاملة", "شركة المقاولات العامة", "شركة التشييد والبناء", "شركة الهندسة والإنشاءات",
150
+ "شركة المشاريع الكبرى", "شركة التطوير العقاري"
151
+ ]
152
+ competitor_specialties = np.random.choice(["مباني", "طرق", "جسور", "بنية تحتية", "متعددة"], n_competitors)
153
+ competitor_sizes = np.random.choice(["صغيرة", "متوسطة", "كبيرة"], n_competitors)
154
+ competitor_market_shares = np.random.uniform(1, 15, n_competitors)
155
+ competitor_win_rates = np.random.uniform(10, 60, n_competitors)
156
+ competitor_avg_margins = np.random.uniform(5, 20, n_competitors)
157
+
158
+ # إنشاء DataFrame للمنافسين
159
+ competitors_data = {
160
+ "رمز المنافس": competitor_ids,
161
+ "اسم المنافس": competitor_names,
162
+ "التخصص": competitor_specialties,
163
+ "الحجم": competitor_sizes,
164
+ "حصة السوق (%)": competitor_market_shares,
165
+ "معدل الفوز (%)": competitor_win_rates,
166
+ "متوسط هامش الربح (%)": competitor_avg_margins
167
+ }
168
+
169
+ st.session_state.sample_data["competitors"] = pd.DataFrame(competitors_data)
170
+
171
+ def run(self):
172
+ """
173
+ تشغيل وحدة تحليل البيانات
174
+
175
+ هذه الدالة هي نقطة الدخول الرئيسية لوحدة تحليل البيانات.
176
+ تقوم بتهيئة واجهة المستخدم وعرض البيانات والتحليلات.
177
+ """
178
+ try:
179
+ # تعيين عنوان الصفحة
180
+ st.set_page_config(
181
+ page_title="وحدة تحليل البيانات - نظام المناقصات",
182
+ page_icon="📊",
183
+ layout="wide",
184
+ initial_sidebar_state="expanded"
185
+ )
186
+
187
+ # تطبيق التنسيق المخصص
188
+ st.markdown("""
189
+ <style>
190
+ .module-title {
191
+ color: #2c3e50;
192
+ text-align: center;
193
+ font-size: 2.5rem;
194
+ margin-bottom: 1rem;
195
+ padding-bottom: 1rem;
196
+ border-bottom: 2px solid #3498db;
197
+ }
198
+ .stTabs [data-baseweb="tab-list"] {
199
+ gap: 10px;
200
+ }
201
+ .stTabs [data-baseweb="tab"] {
202
+ height: 50px;
203
+ white-space: pre-wrap;
204
+ background-color: #f8f9fa;
205
+ border-radius: 4px 4px 0px 0px;
206
+ gap: 1px;
207
+ padding-top: 10px;
208
+ padding-bottom: 10px;
209
+ }
210
+ .stTabs [aria-selected="true"] {
211
+ background-color: #3498db;
212
+ color: white;
213
+ }
214
+ </style>
215
+ """, unsafe_allow_html=True)
216
+
217
+ # عرض الشريط الجانبي
218
+ with st.sidebar:
219
+ st.image("/home/ubuntu/tender_system/tender_system/assets/images/logo.png", width=200)
220
+ st.markdown("## نظام تحليل المناقصات")
221
+ st.markdown("### وحدة تحليل البيانات")
222
+
223
+ st.markdown("---")
224
+
225
+ # إضافة خيارات التصفية العامة
226
+ st.markdown("### خيارات التصفية العامة")
227
+
228
+ # إضافة مزيد من الخيارات حسب الحاجة
229
+ st.markdown("---")
230
+
231
+ # إضافة معلومات المستخدم
232
+ st.markdown("### معلومات المستخدم")
233
+ st.markdown("**المستخدم:** مهندس تامر الجوهري")
234
+ st.markdown("**الدور:** محلل بيانات")
235
+ st.markdown("**تاريخ آخر دخول:** " + datetime.now().strftime("%Y-%m-%d %H:%M"))
236
+
237
+ # عرض واجهة وحدة تحليل البيانات
238
+ self.render()
239
+
240
+ # إضافة معلومات في أسفل الصفحة
241
+ st.markdown("---")
242
+ st.markdown("### نظام تحليل المناقصات - وحدة تحليل البيانات")
243
+ st.markdown("**الإصدار:** 2.0.0")
244
+ st.markdown("**تاريخ التحديث:** 2024-03-31")
245
+ st.markdown("**جميع الحقوق محفوظة © 2024**")
246
+
247
+ return True
248
+
249
+ except Exception as e:
250
+ st.error(f"حدث خطأ أثناء تشغيل وحدة تحليل البيانات: {str(e)}")
251
+ return False
252
 
253
  def render(self):
254
+ """عرض واجهة وحدة تحليل البيانات"""
255
 
256
+ st.markdown("<h1 class='module-title'>وحدة تحليل البيانات</h1>", unsafe_allow_html=True)
257
 
258
  tabs = st.tabs([
259
+ "لوحة المعلومات",
260
+ "تحليل المناقصات",
261
+ "تحليل الأسعار",
262
+ "تحليل المنافسين",
263
+ "استيراد وتصدير البيانات"
264
  ])
265
 
266
  with tabs[0]:
267
+ self._render_dashboard_tab()
268
 
269
  with tabs[1]:
270
+ self._render_tenders_analysis_tab()
271
 
272
  with tabs[2]:
273
+ self._render_price_analysis_tab()
274
 
275
  with tabs[3]:
276
+ self._render_competitors_analysis_tab()
277
 
278
  with tabs[4]:
279
+ self._render_import_export_tab()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
+ def _render_dashboard_tab(self):
282
+ """عرض تبويب لوحة المعلومات"""
283
 
284
+ st.markdown("### لوحة المعلومات")
285
 
286
+ # عرض مؤشرات الأداء الرئيسية
287
+ st.markdown("#### مؤشرات الأداء الرئيسية")
288
 
289
+ # استخراج البيانات اللازمة للمؤشرات
290
+ tenders_df = st.session_state.sample_data["tenders"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
+ # حساب المؤشرات
293
+ total_tenders = len(tenders_df)
294
+ won_tenders = len(tenders_df[tenders_df["الحالة"] == "فائز"])
295
+ win_rate = won_tenders / total_tenders * 100
296
+ avg_profit_margin = tenders_df["هامش الربح (%)"].mean()
297
+ total_profit = tenders_df["الربح (ريال)"].sum()
298
 
299
+ # عرض المؤشرات
300
+ col1, col2, col3, col4 = st.columns(4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
  with col1:
303
+ st.metric("إجمالي المناقصات", f"{total_tenders}")
 
 
 
 
304
 
305
  with col2:
306
+ st.metric("معدل الفوز", f"{win_rate:.1f}%")
 
 
 
 
307
 
308
  with col3:
309
+ st.metric("متوسط هامش الربح", f"{avg_profit_margin:.1f}%")
310
+
311
+ with col4:
312
+ st.metric("إجمالي الربح", f"{total_profit:,.0f} ريال")
313
+
314
+ # عرض توزيع المناقصات حسب الحالة
315
+ st.markdown("#### توزيع المناقصات حسب الحالة")
316
+
317
+ status_counts = tenders_df["الحالة"].value_counts().reset_index()
318
+ status_counts.columns = ["الحالة", "العدد"]
319
+
320
+ fig = px.pie(
321
+ status_counts,
322
+ values="العدد",
323
+ names="الحالة",
324
+ title="توزيع المناقصات حسب الحالة",
325
+ color="الحالة",
326
+ color_discrete_map={
327
+ "فائز": "#2ecc71",
328
+ "خاسر": "#e74c3c",
329
+ "قيد التنفيذ": "#3498db",
330
+ "منجز": "#f39c12"
331
+ }
332
+ )
333
+
334
+ st.plotly_chart(fig, use_container_width=True)
335
+
336
+ # عرض توزيع المناقصات حسب نوع المشروع
337
+ st.markdown("#### توزيع المناقصات حسب نوع المشروع")
338
+
339
+ type_counts = tenders_df["نوع المشروع"].value_counts().reset_index()
340
+ type_counts.columns = ["نوع المشروع", "العدد"]
341
+
342
+ fig = px.bar(
343
+ type_counts,
344
+ x="نوع المشروع",
345
+ y="العدد",
346
+ title="توزيع المناقصات حسب نوع المشروع",
347
+ color="نوع المشروع",
348
+ text_auto=True
349
+ )
350
+
351
+ st.plotly_chart(fig, use_container_width=True)
352
+
353
+ # عرض تطور هامش الربح عبر الزمن
354
+ st.markdown("#### تطور هامش الربح عبر الزمن")
355
+
356
+ # إضافة عمود السنة
357
+ tenders_df["السنة"] = tenders_df["تاريخ التقديم"].str[:4]
358
+
359
+ # حساب متوسط هامش الربح لكل سنة
360
+ profit_margin_by_year = tenders_df.groupby("السنة")["هامش الربح (%)"].mean().reset_index()
361
+
362
+ fig = px.line(
363
+ profit_margin_by_year,
364
+ x="السنة",
365
+ y="هامش الربح (%)",
366
+ title="تطور متوسط هامش الربح عبر السنوات",
367
+ markers=True
368
+ )
369
+
370
+ st.plotly_chart(fig, use_container_width=True)
371
+
372
+ # عرض توزيع المناقصات حسب الموقع
373
+ st.markdown("#### توزيع المناقصات حسب الموقع")
374
+
375
+ location_counts = tenders_df["الموقع"].value_counts().reset_index()
376
+ location_counts.columns = ["الموقع", "العدد"]
377
+
378
+ fig = px.bar(
379
+ location_counts,
380
+ x="الموقع",
381
+ y="العدد",
382
+ title="توزيع المناقصات حسب الموقع",
383
+ color="الموقع",
384
+ text_auto=True
385
+ )
386
+
387
+ st.plotly_chart(fig, use_container_width=True)
388
+
389
+ # عرض العلاقة بين الميزانية والتكلفة
390
+ st.markdown("#### العلاقة بين الميزانية والتكلفة")
391
+
392
+ fig = px.scatter(
393
+ tenders_df,
394
+ x="الميزانية (ريال)",
395
+ y="التكلفة (ريال)",
396
+ color="الحالة",
397
+ size="المساحة (م2)",
398
+ hover_name="رقم المناقصة",
399
+ hover_data=["نوع المشروع", "الموقع", "هامش الربح (%)"],
400
+ title="العلاقة بين الميزانية والتكلفة",
401
+ color_discrete_map={
402
+ "فائز": "#2ecc71",
403
+ "خاسر": "#e74c3c",
404
+ "قيد التنفيذ": "#3498db",
405
+ "منجز": "#f39c12"
406
+ }
407
+ )
408
+
409
+ # إضافة خط الميزانية = التكلفة
410
+ max_value = max(tenders_df["الميزانية (ريال)"].max(), tenders_df["التكلفة (ريال)"].max())
411
+ fig.add_trace(
412
+ go.Scatter(
413
+ x=[0, max_value],
414
+ y=[0, m
415
+ (Content truncated due to size limit. Use line ranges to read in chunks)