EGYADMIN commited on
Commit
25d2b3e
·
verified ·
1 Parent(s): e6c6e90

Upload 114 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. app.py +975 -151
  2. config.py +62 -229
  3. data/achievements/user_1_achievements.json +82 -0
  4. data/achievements/user_1_languages.json +3 -0
  5. data/achievements/user_1_risks.json +3 -0
  6. data/project_tracker/project_1_kpis.json +63 -0
  7. data/project_tracker/project_1_status.json +169 -0
  8. data/projects/.gitkeep +1 -0
  9. database/db_connector.py +55 -317
  10. database/models.py +279 -626
  11. demo_pricing.py +449 -0
  12. docs/technical_docs.md +165 -0
  13. docs/user_manual.md +594 -0
  14. fonts/Amiri-Bold.ttf +0 -0
  15. fonts/Amiri-Regular.ttf +0 -0
  16. huggingface_app.py +83 -0
  17. models/README.md +52 -0
  18. models/datasets/README.md +61 -0
  19. models/trained/README.md +27 -0
  20. modules/achievements/__init__.py +4 -0
  21. modules/achievements/achievement_system.py +1033 -0
  22. modules/achievements/achievements_app.py +49 -0
  23. modules/ai_assistant/__init__.py +5 -0
  24. modules/ai_assistant/ai_assistant.py +773 -0
  25. modules/ai_assistant/ai_assistant_app.py +0 -0
  26. modules/ai_assistant/assistant_app.py +44 -0
  27. modules/ai_finetuning/__init__.py +1 -0
  28. modules/ai_finetuning/finetuning_app.py +53 -0
  29. modules/ai_finetuning/model_finetuning.py +0 -0
  30. modules/document_analysis/document_analysis_app.py +1114 -0
  31. modules/document_analysis/services/__init__.py +22 -0
  32. modules/document_analysis/services/document_parser.py +219 -0
  33. modules/document_analysis/services/item_extractor.py +131 -0
  34. modules/document_analysis/services/text_extractor.py +105 -0
  35. modules/document_comparison/__init__.py +4 -0
  36. modules/document_comparison/comparison_app.py +43 -0
  37. modules/document_comparison/document_comparator.py +1503 -0
  38. modules/maps/README.md +45 -0
  39. modules/maps/__init__.py +1 -0
  40. modules/maps/interactive_map.py +1671 -0
  41. modules/maps/interactive_map.py.bak +1647 -0
  42. modules/maps/maps_app.py +36 -439
  43. modules/notifications/__init__.py +1 -0
  44. modules/notifications/notifications_app.py +39 -658
  45. modules/notifications/smart_notifications.py +1237 -0
  46. modules/pricing/constants.py +113 -0
  47. modules/pricing/construction_calculator.py +787 -0
  48. modules/pricing/exceptions.py +42 -0
  49. modules/pricing/price_analysis_component.py +932 -0
  50. modules/pricing/pricing_app.py +0 -0
app.py CHANGED
@@ -1,168 +1,992 @@
1
- import streamlit as st
 
 
 
 
 
 
 
 
2
  import sys
3
- from pathlib import Path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- # إضافة مسار المشروع للنظام
6
- sys.path.append(str(Path(__file__).parent.parent))
 
7
 
8
- # استيراد الوحدات
9
- from modules.document_analysis.document_app import DocumentAnalysisApp
10
  from modules.pricing.pricing_app import PricingApp
 
11
  from modules.resources.resources_app import ResourcesApp
12
- from modules.risk_analysis.risk_analyzer import RiskAnalysisApp
13
- from modules.project_management.project_management_app import ProjectsApp
14
  from modules.maps.maps_app import MapsApp
15
  from modules.notifications.notifications_app import NotificationsApp
16
- from modules.document_comparison.document_comparison_app import DocumentComparisonApp
17
- from modules.translation.translation_app import TranslationApp
18
- from modules.ai_assistant.ai_app import AIAssistantApp
19
- from modules.data_analysis.data_analysis_app import DataAnalysisApp
20
- from styling.enhanced_ui import UIEnhancer
21
-
22
- # تكوين الصفحة
23
- st.set_page_config(
24
- page_title="نظام تحليل المناقصات",
25
- page_icon="📊",
26
- layout="wide",
27
- initial_sidebar_state="expanded",
28
- menu_items={
29
- 'Get Help': 'https://www.example.com/help',
30
- 'Report a bug': "https://www.example.com/bug",
31
- 'About': "### نظام تحليل المناقصات\nالإصدار 2.0.0"
32
- }
33
- )
34
-
35
- # تطبيق التنسيق العام
36
- ui_enhancer = UIEnhancer(page_title="نظام تحليل المناقصات", page_icon="📊")
37
- ui_enhancer.apply_global_styles()
38
-
39
- # إنشاء قائمة العناصر
40
- menu_items = [
41
- {"name": "لوحة المعلومات", "icon": "house"},
42
- {"name": "المناقصات والعقود", "icon": "file-text"},
43
- {"name": "تحليل المستندات", "icon": "file-earmark-text"},
44
- {"name": "نظام التسعير", "icon": "calculator"},
45
- {"name": "حاسبة تكاليف البناء", "icon": "building"},
46
- {"name": "الموارد والتكاليف", "icon": "people"},
47
- {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
48
- {"name": "إدارة المشاريع", "icon": "kanban"},
49
- {"name": "الخرائط والمواقع", "icon": "geo-alt"},
50
- {"name": "الجدول الزمني", "icon": "calendar3"},
51
- {"name": "الإشعارات", "icon": "bell"},
52
- {"name": "مقارنة المستندات", "icon": "files"},
53
- {"name": "الترجمة", "icon": "translate"},
54
- {"name": "المساعد الذكي", "icon": "robot"},
55
- {"name": "تحليل البيانات", "icon": "bar-chart"},
56
- {"name": "الإعدادات", "icon": "gear"}
57
- ]
58
-
59
- # إنشاء الشريط الجانبي
60
- selected = ui_enhancer.create_sidebar(menu_items)
61
-
62
- # تحديد الوحدة المطلوبة بناءً على اختيار المستخدم
63
- if selected == "لوحة المعلومات":
64
- ui_enhancer.create_header("لوحة المعلومات", "نظرة عامة على المناقصات والمشاريع")
65
-
66
- # عرض لوحة المعلومات
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  col1, col2, col3 = st.columns(3)
68
 
69
  with col1:
70
- ui_enhancer.create_metric_card("المناقصات النشطة", "12", "+3", ui_enhancer.COLORS['primary'])
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  with col2:
73
- ui_enhancer.create_metric_card("المشاريع قيد التنفيذ", "8", "+1", ui_enhancer.COLORS['success'])
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  with col3:
76
- ui_enhancer.create_metric_card("المناقصات المقدمة", "24", "+5", ui_enhancer.COLORS['info'])
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- # عرض الإشعارات الأخيرة
79
- st.markdown("### الإشعارات الأخيرة")
80
 
81
- notifications = [
82
- {"title": "موعد تقديم مناقصة", "project": "إنشاء مبنى مستشفى الولادة والأطفال", "date": "2025-04-05", "priority": "عالية"},
83
- {"title": "تحديث مستندات", "project": "صيانة وتطوير طريق الملك عبدالله", "date": "2025-03-28", "priority": "متوسطة"},
84
- {"title": "اجتماع مراجعة التسعير", "project": "إنشاء محطة معالجة مياه الصرف الصحي", "date": "2025-03-25", "priority": "عالية"}
85
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- for notification in notifications:
88
- with st.container():
89
- col1, col2 = st.columns([4, 1])
90
- with col1:
91
- st.markdown(f"**{notification['title']}** - {notification['project']}")
92
- st.caption(f"التاريخ: {notification['date']} | الأولوية: {notification['priority']}")
93
- with col2:
94
- st.button("عرض", key=f"view_{notification['title']}")
95
- st.divider()
96
-
97
- elif selected == "تحليل المستندات":
98
- document_app = DocumentAnalysisApp()
99
- document_app.run()
100
-
101
- elif selected == "نظام التسعير":
102
- pricing_app = PricingApp()
103
- pricing_app.run()
104
-
105
- elif selected == "الموارد والتكاليف":
106
- resources_app = ResourcesApp()
107
- resources_app.run()
108
-
109
- elif selected == "تحليل المخاطر":
110
- risk_app = RiskAnalysisApp()
111
- risk_app.run()
112
-
113
- elif selected == "إدارة المشاريع":
114
- projects_app = ProjectsApp()
115
- projects_app.run()
116
-
117
- elif selected == "الخرائط والمواقع":
118
- maps_app = MapsApp()
119
- maps_app.run()
120
-
121
- elif selected == "الإشعارات":
122
- notifications_app = NotificationsApp()
123
- notifications_app.run()
124
-
125
- elif selected == "مقارنة المستندات":
126
- document_comparison_app = DocumentComparisonApp()
127
- document_comparison_app.run()
128
-
129
- elif selected == "الترجمة":
130
- translation_app = TranslationApp()
131
- translation_app.run()
132
-
133
- elif selected == "المساعد الذكي":
134
- ai_app = AIAssistantApp()
135
- ai_app.run()
136
-
137
- elif selected == "تحليل البيانات":
138
- data_analysis_app = DataAnalysisApp()
139
- data_analysis_app.run()
140
-
141
- elif selected == "الإعدادات":
142
- ui_enhancer.create_header("الإعدادات", "إعدادات النظام والحساب")
143
-
144
- # عرض إعدادات النظام
145
- st.markdown("### إعدادات النظام")
146
-
147
- tabs = st.tabs(["إعدادات عامة", "الواجهة", "الأمان", "مفاتيح API"])
148
-
149
- with tabs[0]:
150
- st.checkbox("تفعيل الإشعارات", value=True)
151
- st.checkbox("حفظ تلقائي للبيانات", value=True)
152
- st.selectbox("اللغة", ["العربية", "English"])
153
- st.selectbox("المنطقة الزمنية", ["توقيت الرياض (GMT+3)", "توقيت جرينتش (GMT)"])
154
-
155
- with tabs[1]:
156
- st.radio("النمط", ["فاتح", "داكن", "تلقائي (حسب نظام التشغيل)"])
157
- st.slider("حجم الخط", 12, 20, 16)
158
- st.color_picker("لون التمييز", "#1E88E5")
159
-
160
- with tabs[2]:
161
- st.checkbox("تفعيل المصادقة الثنائية", value=False)
162
- st.number_input("مدة الجلسة (دقائق)", min_value=5, max_value=120, value=30)
163
- st.button("تغيير كلمة المرور")
164
-
165
- with tabs[3]:
166
- st.text_input("مفتاح OpenAI API", type="password")
167
- st.text_input("مفتاح Google Maps API", type="password")
168
- st.button("حفظ مفاتيح API")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ نظام واهبي للذكاء الاصطناعي لتحليل العقود والمناقصات
6
+ تطبيق Streamlit الرئيسي الذي يجمع جميع الوحدات والمكونات
7
+ """
8
+
9
+ import os
10
  import sys
11
+ import streamlit as st
12
+ import pandas as pd
13
+ import numpy as np
14
+
15
+ # تهيئة حالة الجلسة لكل وحدات النظام
16
+ if 'page' not in st.session_state:
17
+ st.session_state.page = 'home'
18
+ if 'analysis_type' not in st.session_state:
19
+ st.session_state.analysis_type = None
20
+ if 'show_document_upload' not in st.session_state:
21
+ st.session_state.show_document_upload = False
22
+ if 'report_type' not in st.session_state:
23
+ st.session_state.report_type = None
24
+ if 'show_report_form' not in st.session_state:
25
+ st.session_state.show_report_form = False
26
+ if 'analysis_result' not in st.session_state:
27
+ st.session_state.analysis_result = None
28
+ if 'current_document' not in st.session_state:
29
+ st.session_state.current_document = None
30
+ if 'current_document_text' not in st.session_state:
31
+ st.session_state.current_document_text = None
32
+ if 'loaded_files' not in st.session_state:
33
+ st.session_state.loaded_files = []
34
+ if 'notifications' not in st.session_state:
35
+ st.session_state.notifications = []
36
+
37
+ # وظيفة لتهيئة حزم NLTK المطلوبة عند بدء التطبيق
38
+ def initialize_nltk_resources():
39
+ """تنزيل وتهيئة موارد NLTK المطلوبة"""
40
+ try:
41
+ # محاولة تنزيل حزم NLTK الأساسية
42
+ import nltk
43
+
44
+ # تحديد المسار المخصص لتنزيل NLTK data
45
+ nltk_data_path = os.path.join(os.path.expanduser("~"), "nltk_data")
46
+ os.makedirs(nltk_data_path, exist_ok=True)
47
+ nltk.data.path.append(nltk_data_path)
48
+
49
+ # قائمة بالحزم المطلوبة
50
+ required_packages = ['punkt', 'stopwords', 'wordnet', 'omw-1.4']
51
+ for package in required_packages:
52
+ try:
53
+ if package == 'punkt':
54
+ nltk.data.find('tokenizers/punkt')
55
+ elif package == 'stopwords':
56
+ nltk.data.find('corpora/stopwords')
57
+ elif package == 'wordnet':
58
+ nltk.data.find('corpora/wordnet')
59
+ else:
60
+ nltk.data.find(f'corpora/{package}')
61
+ except LookupError:
62
+ print(f"تنزيل حزمة NLTK: {package}")
63
+ nltk.download(package, download_dir=nltk_data_path, quiet=False)
64
+
65
+ print("تم تهيئة موارد NLTK بنجاح.")
66
+ except Exception as e:
67
+ print(f"خطأ في تهيئة NLTK: {e}")
68
+ st.error(f"حدث خطأ أثناء تهيئة موارد NLTK: {e}")
69
+
70
+ # تهيئة موارد NLTK عند بدء التطبيق
71
+ initialize_nltk_resources()
72
+
73
+ # مسار نسبي للملفات الثابتة (للتأكد من العمل في بيئات مختلفة)
74
+ def get_static_path(file_path):
75
+ """مسار ملف ثابت يعمل سواء كان التشغيل من المجلد الرئيسي أو من المجلد الفرعي"""
76
+ # قائمة المسارات المحتملة
77
+ possible_paths = [
78
+ # المسار المباشر (في حالة تشغيل التطبيق من نفس المجلد)
79
+ file_path,
80
+ # المسار النسبي من مجلد التطبيق (tender-analysis-system)
81
+ os.path.join(os.path.dirname(__file__), file_path),
82
+ # المسار النسبي من المجلد الأعلى
83
+ os.path.join(os.path.dirname(os.path.dirname(__file__)), "tender-analysis-system", file_path),
84
+ ]
85
+
86
+ # اختبار كل مسار محتمل
87
+ for path in possible_paths:
88
+ if os.path.exists(path):
89
+ return path
90
+
91
+ # إذا لم يتم العثور على الملف، إعادة المسار الأصلي
92
+ return file_path
93
+
94
+ # إعداد إعدادات الصفحة
95
+ try:
96
+ st.set_page_config(
97
+ page_title="نظام WAHBi للذكاء الاصطناعي | التعاقدات والمناقصات",
98
+ page_icon="📊",
99
+ layout="wide",
100
+ initial_sidebar_state="expanded"
101
+ )
102
+ except Exception as e:
103
+ print(f"خطأ في إعداد الصفحة: {e}")
104
+ # يحدث هذا غالبًا عند استخدام st.set_page_config أكثر من مرة
105
+
106
+ # استيراد ملفات CSS الجديدة
107
+ try:
108
+ # تحديد مسارات الملفات
109
+ main_css_path = get_static_path("utils/css/main.css")
110
+ rtl_css_path = get_static_path("utils/css/rtl.css")
111
+ enhanced_css_path = get_static_path("utils/css/enhanced.css")
112
+
113
+ # تحميل ملف CSS الرئيسي
114
+ if os.path.exists(main_css_path):
115
+ with open(main_css_path, "r", encoding='utf-8') as f:
116
+ main_css = f.read()
117
+ st.markdown(f"<style>{main_css}</style>", unsafe_allow_html=True)
118
+ print(f"تم تحميل ملف CSS الرئيسي بنجاح من: {main_css_path}")
119
+ else:
120
+ print(f"تعذر العثور على ملف CSS الرئيسي: {main_css_path}")
121
+
122
+ # تحميل ملف دعم الاتجاه من اليمين إلى اليسار
123
+ if os.path.exists(rtl_css_path):
124
+ with open(rtl_css_path, "r", encoding='utf-8') as f:
125
+ rtl_css = f.read()
126
+ st.markdown(f"<style>{rtl_css}</style>", unsafe_allow_html=True)
127
+ print(f"تم تحميل ملف CSS للتوجيه RTL بنجاح من: {rtl_css_path}")
128
+ else:
129
+ print(f"تعذر العثور على ملف CSS للتوجيه RTL: {rtl_css_path}")
130
+
131
+ # تحميل ملف التحسينات المتقدمة
132
+ if os.path.exists(enhanced_css_path):
133
+ with open(enhanced_css_path, "r", encoding='utf-8') as f:
134
+ enhanced_css = f.read()
135
+ st.markdown(f"<style>{enhanced_css}</style>", unsafe_allow_html=True)
136
+ print(f"تم تحميل ملف CSS المحسن بنجاح من: {enhanced_css_path}")
137
+ else:
138
+ print(f"تعذر العثور على ملف CSS المحسن: {enhanced_css_path}")
139
+
140
+ except Exception as e:
141
+ st.warning(f"حدث خطأ أثناء تحميل ملفات CSS: {str(e)}")
142
+ print(f"خطأ في تحميل ملفات CSS: {str(e)}")
143
+
144
+ # إضافة Font Awesome وأي أصول خارجية أخرى
145
+ st.markdown("""
146
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
147
+ """, unsafe_allow_html=True)
148
+
149
+ # إضافة CSS المخصص
150
+ st.markdown("""
151
+ <style>
152
+ /* تعديل الاتجاه للدعم العربي */
153
+ .css-18e3th9, .css-1d391kg, .stMarkdown, .stTextArea, .stButton, .stTextInput, .stSelectbox, .stRadio {
154
+ direction: rtl;
155
+ text-align: right;
156
+ }
157
+
158
+ /* تحسين مظهر العناوين */
159
+ h1, h2, h3, h4 {
160
+ color: #1E88E5;
161
+ }
162
+
163
+ /* تخصيص عنوان التطبيق */
164
+ .app-title {
165
+ font-size: 2.2rem;
166
+ font-weight: bold;
167
+ text-align: center;
168
+ color: #1E88E5;
169
+ margin-bottom: 1rem;
170
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
171
+ background: linear-gradient(90deg, #1976D2, #64B5F6);
172
+ -webkit-background-clip: text;
173
+ -webkit-text-fill-color: transparent;
174
+ }
175
+
176
+ /* تخصيص الشريط الجانبي */
177
+ .sidebar .sidebar-content {
178
+ background-color: #f8f9fa;
179
+ }
180
+
181
+ /* تخصيص الأقسام */
182
+ .section-card {
183
+ background-color: #f9f9f9;
184
+ border-radius: 10px;
185
+ padding: 20px;
186
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
187
+ margin-bottom: 20px;
188
+ }
189
+
190
+ /* تخصيص الأزرار */
191
+ .stButton>button {
192
+ border-radius: 5px;
193
+ background-color: #1E88E5;
194
+ color: white;
195
+ font-weight: bold;
196
+ border: none;
197
+ padding: 0.5rem 1rem;
198
+ transition: all 0.3s ease;
199
+ }
200
+
201
+ .stButton>button:hover {
202
+ background-color: #1565C0;
203
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
204
+ }
205
+
206
+ /* تخصيص المؤشرات */
207
+ .indicator {
208
+ padding: 1rem;
209
+ border-radius: 10px;
210
+ background-color: #f5f5f5;
211
+ text-align: center;
212
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
213
+ }
214
+
215
+ .indicator-value {
216
+ font-size: 2rem;
217
+ font-weight: bold;
218
+ margin-bottom: 0.5rem;
219
+ }
220
+
221
+ .indicator-label {
222
+ font-size: 1rem;
223
+ color: #666;
224
+ }
225
+
226
+ /* تخصيص البطاقات */
227
+ .card {
228
+ background-color: white;
229
+ border-radius: 10px;
230
+ padding: 15px;
231
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
232
+ margin-bottom: 15px;
233
+ border-right: 4px solid #1E88E5;
234
+ }
235
+
236
+ .card-title {
237
+ font-weight: bold;
238
+ color: #1E88E5;
239
+ margin-bottom: 10px;
240
+ }
241
+
242
+ .card-metrics {
243
+ display: flex;
244
+ justify-content: space-between;
245
+ }
246
+
247
+ .card-metric {
248
+ text-align: center;
249
+ }
250
+
251
+ .card-metric-value {
252
+ font-weight: bold;
253
+ font-size: 1.5rem;
254
+ }
255
+
256
+ .card-metric-label {
257
+ font-size: 0.8rem;
258
+ color: #666;
259
+ }
260
+ </style>
261
+ """, unsafe_allow_html=True)
262
 
263
+ # استيراد المكونات والوحدات
264
+ from utils.components.sidebar import render_sidebar
265
+ from utils.helpers import create_directory_if_not_exists, get_data_folder
266
 
267
+ # استيراد وحدات التطبيق
 
268
  from modules.pricing.pricing_app import PricingApp
269
+ from modules.projects.projects_app import ProjectsApp
270
  from modules.resources.resources_app import ResourcesApp
271
+ from modules.risk_assessment.risk_assessment_app import RiskAssessmentApp
272
+ from modules.project_tracker.tracker_app import TrackerApp
273
  from modules.maps.maps_app import MapsApp
274
  from modules.notifications.notifications_app import NotificationsApp
275
+ from modules.voice_narration.voice_narration_app import VoiceNarrationApp
276
+ from modules.achievements.achievements_app import AchievementsApp
277
+ from modules.ai_finetuning.finetuning_app import FinetuningApp
278
+ from modules.document_comparison.comparison_app import DocumentComparisonApp
279
+
280
+ # إنشاء مجلدات البيانات الضرورية
281
+ create_directory_if_not_exists(get_data_folder())
282
+ create_directory_if_not_exists(os.path.join(get_data_folder(), "projects"))
283
+ create_directory_if_not_exists(os.path.join(get_data_folder(), "documents"))
284
+ create_directory_if_not_exists(os.path.join(get_data_folder(), "analysis"))
285
+
286
+ def main():
287
+ """الدالة الرئيسية للتطبيق"""
288
+
289
+ # تقديم الشريط الجانبي وتلقي الوحدة المختارة
290
+ selected_module = render_sidebar()
291
+
292
+ # إذا كان المستخدم غير مصرح له، قم بإظهار شاشة تسجيل الدخول
293
+ if "is_authenticated" in st.session_state and not st.session_state.is_authenticated:
294
+ render_login_screen()
295
+ return
296
+
297
+ # إظهار الوحدة المختارة
298
+ if selected_module == "الرئيسية":
299
+ render_homepage()
300
+
301
+ elif selected_module == "إدارة المشاريع":
302
+ projects_app = ProjectsApp()
303
+ projects_app.render()
304
+
305
+ elif selected_module == "التسعير المتكاملة":
306
+ pricing_app = PricingApp()
307
+ pricing_app.render()
308
+
309
+ elif selected_module == "الموارد والتكاليف":
310
+ resources_app = ResourcesApp()
311
+ resources_app.render()
312
+
313
+ elif selected_module == "تحليل المستندات":
314
+ # تقديم واجهة تحليل المستندات
315
+ render_document_analysis()
316
+
317
+ elif selected_module == "مقارنة المستندات":
318
+ # تقديم واجهة مقارنة المستندات
319
+ comparison_app = DocumentComparisonApp()
320
+ comparison_app.render()
321
+
322
+ elif selected_module == "تقييم مخاطر العقود":
323
+ risk_app = RiskAssessmentApp()
324
+ risk_app.render()
325
+
326
+ elif selected_module == "التقارير والتحليلات":
327
+ # تقديم واجهة التقارير والتحليلات
328
+ render_reports_and_analytics()
329
+
330
+ elif selected_module == "متتبع حالة المشروع":
331
+ tracker_app = TrackerApp()
332
+ tracker_app.render()
333
+
334
+ elif selected_module == "خريطة المشاريع":
335
+ maps_app = MapsApp()
336
+ maps_app.render()
337
+
338
+ elif selected_module == "نظام الإشعارات":
339
+ notifications_app = NotificationsApp()
340
+ notifications_app.render()
341
+
342
+ elif selected_module == "الترجمة الصوتية":
343
+ voice_app = VoiceNarrationApp()
344
+ voice_app.render()
345
+
346
+ elif selected_module == "نظام الإنجازات":
347
+ achievements_app = AchievementsApp()
348
+ achievements_app.render()
349
+
350
+ elif selected_module == "المساعد الذكي":
351
+ # تقديم واجهة المساعد الذكي
352
+ render_ai_assistant()
353
+
354
+ elif selected_module == "ضبط نماذج الذكاء الاصطناعي":
355
+ finetuning_app = FinetuningApp()
356
+ finetuning_app.render()
357
+
358
+ else:
359
+ st.error("الوحدة المطلوبة غير موجودة")
360
+
361
+
362
+ def render_login_screen():
363
+ """عرض شاشة تسجيل الدخول"""
364
+ st.markdown("<h1 class='app-title'>نظام WAHBi للذكاء الاصطناعي</h1>", unsafe_allow_html=True)
365
+
366
+ st.markdown("""
367
+ <div class="section-card">
368
+ <h2>تسجيل الدخول</h2>
369
+ <p>يرجى إدخال بيانات الاعتماد الخاصة بك للوصول إلى النظام.</p>
370
+ </div>
371
+ """, unsafe_allow_html=True)
372
+
373
+ col1, col2, col3 = st.columns([1, 2, 1])
374
+
375
+ with col2:
376
+ username = st.text_input("اسم المستخدم")
377
+ password = st.text_input("كلمة المرور", type="password")
378
+
379
+ if st.button("تسجيل الدخول"):
380
+ # تنفيذ منطق المصادقة
381
+ if username == "admin" and password == "admin": # بيانات اعتماد مؤقتة للتطوير
382
+ st.session_state.is_authenticated = True
383
+ st.session_state.user_info = {
384
+ "id": 1,
385
+ "username": "admin",
386
+ "full_name": "مدير النظام",
387
+ "email": "[email protected]",
388
+ "role": "مدير",
389
+ "department": "الإدارة",
390
+ "last_login": "2023-01-01 09:00:00"
391
+ }
392
+ st.rerun()
393
+ else:
394
+ st.error("اسم المستخدم أو كلمة المرور غير صحيحة")
395
+
396
+ st.markdown("""
397
+ <div style="text-align: center; margin-top: 50px; color: #666;">
398
+ <p>نظام WAHBi للذكاء الاصطناعي © 2025 شركة شبه الجزيرة للمقاولات</p>
399
+ <p>جميع الحقوق محفوظة</p>
400
+ </div>
401
+ """, unsafe_allow_html=True)
402
+
403
+
404
+ def render_homepage():
405
+ """عرض الصفحة الرئيسية للتطبيق"""
406
+ st.markdown("<h1 class='app-title'>نظام WAHBi للذكاء الاصطناعي</h1>", unsafe_allow_html=True)
407
+ st.markdown("<div style='text-align: center; margin-bottom: 20px;'>نظام متكامل لتحليل العقود والمناقصات باستخدام تقنيات الذكاء الاصطناعي المتقدمة</div>", unsafe_allow_html=True)
408
+
409
+ # عرض مؤشرات الأداء الرئيسية
410
+ col1, col2, col3, col4 = st.columns(4)
411
+
412
+ with col1:
413
+ st.markdown("""
414
+ <div class="indicator">
415
+ <div class="indicator-value" style="color: #1E88E5;">24</div>
416
+ <div class="indicator-label">المناقصات النشطة</div>
417
+ </div>
418
+ """, unsafe_allow_html=True)
419
+
420
+ with col2:
421
+ st.markdown("""
422
+ <div class="indicator">
423
+ <div class="indicator-value" style="color: #43A047;">8</div>
424
+ <div class="indicator-label">مشاريع قيد التنفيذ</div>
425
+ </div>
426
+ """, unsafe_allow_html=True)
427
+
428
+ with col3:
429
+ st.markdown("""
430
+ <div class="indicator">
431
+ <div class="indicator-value" style="color: #FB8C00;">12</div>
432
+ <div class="indicator-label">مستندات قيد التحليل</div>
433
+ </div>
434
+ """, unsafe_allow_html=True)
435
+
436
+ with col4:
437
+ st.markdown("""
438
+ <div class="indicator">
439
+ <div class="indicator-value" style="color: #E53935;">5</div>
440
+ <div class="indicator-label">تنبيهات تتطلب الاهتمام</div>
441
+ </div>
442
+ """, unsafe_allow_html=True)
443
+
444
+ # عرض المشاريع الأخيرة والوصول السريع
445
+ col1, col2 = st.columns([2, 1])
446
+
447
+ with col1:
448
+ st.markdown("### المناقصات الأخيرة")
449
+
450
+ st.markdown("""
451
+ <div class="card">
452
+ <div class="card-title">إنشاء طريق سريع بمنطقة الرياض</div>
453
+ <div>رقم المناقصة: TR-2025-134</div>
454
+ <div>الجهة المالكة: وزارة النقل</div>
455
+ <div>تاريخ الإغلاق: 15 أبريل 2025</div>
456
+ <div class="card-metrics" style="margin-top: 10px;">
457
+ <div class="card-metric">
458
+ <div class="card-metric-value" style="color: #4CAF50;">85%</div>
459
+ <div class="card-metric-label">نسبة الإنجاز</div>
460
+ </div>
461
+ <div class="card-metric">
462
+ <div class="card-metric-value" style="color: #FFC107;">متوسطة</div>
463
+ <div class="card-metric-label">المخاطر</div>
464
+ </div>
465
+ <div class="card-metric">
466
+ <div class="card-metric-value" style="color: #2196F3;">مرتفعة</div>
467
+ <div class="card-metric-label">الأولوية</div>
468
+ </div>
469
+ </div>
470
+ </div>
471
+
472
+ <div class="card">
473
+ <div class="card-title">تطوير شبكة الصرف الصحي بالمنطقة الشرقية</div>
474
+ <div>رقم المناقصة: WS-2025-089</div>
475
+ <div>الجهة المالكة: وزارة المياه</div>
476
+ <div>تاريخ الإغلاق: 22 أبريل 2025</div>
477
+ <div class="card-metrics" style="margin-top: 10px;">
478
+ <div class="card-metric">
479
+ <div class="card-metric-value" style="color: #4CAF50;">62%</div>
480
+ <div class="card-metric-label">نسبة الإنجاز</div>
481
+ </div>
482
+ <div class="card-metric">
483
+ <div class="card-metric-value" style="color: #F44336;">مرتفعة</div>
484
+ <div class="card-metric-label">المخاطر</div>
485
+ </div>
486
+ <div class="card-metric">
487
+ <div class="card-metric-value" style="color: #2196F3;">مرتفعة</div>
488
+ <div class="card-metric-label">الأولوية</div>
489
+ </div>
490
+ </div>
491
+ </div>
492
+
493
+ <div class="card">
494
+ <div class="card-title">بناء 3 مدارس بمنطقة مكة المكرمة</div>
495
+ <div>رقم المناقصة: ED-2025-112</div>
496
+ <div>الجهة المالكة: وزارة التعليم</div>
497
+ <div>تاريخ الإغلاق: 5 مايو 2025</div>
498
+ <div class="card-metrics" style="margin-top: 10px;">
499
+ <div class="card-metric">
500
+ <div class="card-metric-value" style="color: #4CAF50;">38%</div>
501
+ <div class="card-metric-label">نسبة الإنجاز</div>
502
+ </div>
503
+ <div class="card-metric">
504
+ <div class="card-metric-value" style="color: #4CAF50;">منخفضة</div>
505
+ <div class="card-metric-label">المخاطر</div>
506
+ </div>
507
+ <div class="card-metric">
508
+ <div class="card-metric-value" style="color: #FFC107;">متوسطة</div>
509
+ <div class="card-metric-label">الأولوية</div>
510
+ </div>
511
+ </div>
512
+ </div>
513
+ """, unsafe_allow_html=True)
514
+
515
+ with col2:
516
+ st.markdown("### الوصول السريع")
517
+
518
+ st.markdown("""
519
+ <div style="display: grid; gap: 10px;">
520
+ <button style="background-color: #1E88E5; color: white; border: none; border-radius: 5px; padding: 10px; text-align: right; cursor: pointer; font-weight: bold;">
521
+ <i class="fas fa-file-alt" style="margin-left: 10px;"></i> تحليل مستند جديد
522
+ </button>
523
+
524
+ <button style="background-color: #43A047; color: white; border: none; border-radius: 5px; padding: 10px; text-align: right; cursor: pointer; font-weight: bold;">
525
+ <i class="fas fa-calculator" style="margin-left: 10px;"></i> حساب تكاليف مشروع
526
+ </button>
527
+
528
+ <button style="background-color: #FB8C00; color: white; border: none; border-radius: 5px; padding: 10px; text-align: right; cursor: pointer; font-weight: bold;">
529
+ <i class="fas fa-exclamation-triangle" style="margin-left: 10px;"></i> تقييم مخاطر العقد
530
+ </button>
531
+
532
+ <button style="background-color: #8E24AA; color: white; border: none; border-radius: 5px; padding: 10px; text-align: right; cursor: pointer; font-weight: bold;">
533
+ <i class="fas fa-map-marker-alt" style="margin-left: 10px;"></i> استعراض خريطة المشاريع
534
+ </button>
535
+
536
+ <button style="background-color: #546E7A; color: white; border: none; border-radius: 5px; padding: 10px; text-align: right; cursor: pointer; font-weight: bold;">
537
+ <i class="fas fa-chart-bar" style="margin-left: 10px;"></i> إنشاء تقارير تحليلية
538
+ </button>
539
+ </div>
540
+ """, unsafe_allow_html=True)
541
+
542
+ st.markdown("### آخر التنبيهات")
543
+
544
+ st.markdown("""
545
+ <div style="background-color: #FFEBEE; border-right: 3px solid #E53935; padding: 10px; margin-bottom: 10px; border-radius: 5px;">
546
+ <div style="font-weight: bold; color: #B71C1C;">انتهاء موعد تقديم المناقصة</div>
547
+ <div style="font-size: 0.9rem;">مشروع إنشاء الطريق السريع - متبقي 3 أيام</div>
548
+ </div>
549
+
550
+ <div style="background-color: #FFF8E1; border-right: 3px solid #FFA000; padding: 10px; margin-bottom: 10px; border-radius: 5px;">
551
+ <div style="font-weight: bold; color: #FF6F00;">تغيير في شروط المناقصة</div>
552
+ <div style="font-size: 0.9rem;">تم تحديث مستندات مشروع شبكة الصرف الصحي</div>
553
+ </div>
554
+
555
+ <div style="background-color: #E8F5E9; border-right: 3px solid #43A047; padding: 10px; margin-bottom: 10px; border-radius: 5px;">
556
+ <div style="font-weight: bold; color: #2E7D32;">إكمال تحليل المستند</div>
557
+ <div style="font-size: 0.9rem;">اكتمل تحليل عقد بناء المدارس بنجاح</div>
558
+ </div>
559
+ """, unsafe_allow_html=True)
560
+
561
+ # معلومات حول النظام
562
+ st.markdown("---")
563
+
564
+ st.markdown("""
565
+ <div class="section-card">
566
+ <h3>حول النظام</h3>
567
+ <p>نظام WAHBi للذكاء الاصطناعي هو نظام متكامل لتحليل العقود والمناقصات وإدارة المشاريع، مصمم خصيصاً لشركات المقاولات والبناء. يستخدم النظام تقنيات الذكاء الاصطناعي المتقدمة لتحليل المستندات واستخراج المعلومات المهمة وتقييم المخاطر ودعم اتخاذ القرار.</p>
568
+ </div>
569
+ """, unsafe_allow_html=True)
570
+
571
+ # معلومات الشركة
572
+ st.markdown("""
573
+ <div style="text-align: center; margin-top: 30px; color: #666;">
574
+ <p>هذا النظام يعمل لشركة شبه الجزيرة للمقاولات</p>
575
+ <p>جميع الحقوق محفوظة 2025</p>
576
+ </div>
577
+ """, unsafe_allow_html=True)
578
+
579
+
580
+ def render_document_analysis():
581
+ """عرض واجهة تحليل المستندات"""
582
+ st.markdown("<h1 class='app-title'>تحليل المستندات</h1>", unsafe_allow_html=True)
583
+
584
+ st.markdown("""
585
+ <div class="section-card">
586
+ <p>استخدم هذه الوحدة لتحليل مستندات العقود والمناقصات باستخدام تقنيات الذكاء الاصطناعي المتقدمة.
587
+ يمكنك تحميل المستندات بتنسيقات PDF أو Word وسيقوم النظام بتحليلها واستخراج المعلومات المهمة مثل الشروط والتكاليف والمخاطر والتزاماتك كمقاول.</p>
588
+ </div>
589
+ """, unsafe_allow_html=True)
590
+
591
+ # أدوات التحليل
592
+ st.markdown("### أدوات التحليل:", unsafe_allow_html=True)
593
+
594
  col1, col2, col3 = st.columns(3)
595
 
596
  with col1:
597
+ st.markdown("""
598
+ <div class="card">
599
+ <div class="card-title">تحليل العقد الشامل</div>
600
+ <p>تحليل شامل للعقد باستخدام Claude AI لاستخراج جميع البنود والشروط والالتزامات والمواعيد النهائية.</p>
601
+ </div>
602
+ """, unsafe_allow_html=True)
603
+
604
+ if st.button("تحليل جديد", key="btn_complete_analysis"):
605
+ # هنا سيتم استدعاء وحدة تحليل العقد الشامل
606
+ st.session_state.analysis_type = "complete"
607
+ st.session_state.show_document_upload = True
608
+ st.rerun()
609
 
610
  with col2:
611
+ st.markdown("""
612
+ <div class="card">
613
+ <div class="card-title">تحليل جداول الكميات</div>
614
+ <p>تحليل متخصص لجداول الكميات (BOQ) لاستخراج قوائم المواد والكميات والأسعار والتكاليف الإجمالية.</p>
615
+ </div>
616
+ """, unsafe_allow_html=True)
617
+
618
+ if st.button("تحليل جديد", key="btn_boq_analysis"):
619
+ # هنا سيتم استدعاء وحدة تحليل جداول الكميات
620
+ st.session_state.analysis_type = "boq"
621
+ st.session_state.show_document_upload = True
622
+ st.rerun()
623
 
624
  with col3:
625
+ st.markdown("""
626
+ <div class="card">
627
+ <div class="card-title">تحليل الشروط والأحكام</div>
628
+ <p>تحليل متخصص للشروط والأحكام في العقد لتحديد الشروط الغير عادية أو المقيدة والمخاطر القانونية.</p>
629
+ </div>
630
+ """, unsafe_allow_html=True)
631
+
632
+ if st.button("تحليل جديد", key="btn_terms_analysis"):
633
+ # هنا سيتم استدعاء وحدة تحليل الشروط والأحكام
634
+ st.session_state.analysis_type = "terms"
635
+ st.session_state.show_document_upload = True
636
+ st.rerun()
637
 
638
+ # التحليلات الأخيرة
639
+ st.markdown("### التحليلات الأخيرة")
640
 
641
+ st.markdown("""
642
+ <table style="width: 100%; border-collapse: collapse; margin-top: 20px;">
643
+ <thead>
644
+ <tr style="background-color: #f5f5f5;">
645
+ <th style="padding: 12px; text-align: right; border-bottom: 2px solid #ddd;">اسم المستند</th>
646
+ <th style="padding: 12px; text-align: right; border-bottom: 2px solid #ddd;">نوع التحليل</th>
647
+ <th style="padding: 12px; text-align: right; border-bottom: 2px solid #ddd;">تاريخ التحليل</th>
648
+ <th style="padding: 12px; text-align: right; border-bottom: 2px solid #ddd;">الحالة</th>
649
+ <th style="padding: 12px; text-align: right; border-bottom: 2px solid #ddd;">الإجراءات</th>
650
+ </tr>
651
+ </thead>
652
+ <tbody>
653
+ <tr>
654
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">عقد إنشاء طريق سريع.pdf</td>
655
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">تحليل شامل</td>
656
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">2023-03-25</td>
657
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;"><span style="color: #4CAF50; font-weight: bold;">مكتمل</span></td>
658
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">
659
+ <button style="background-color: #1E88E5; color: white; border: none; border-radius: 3px; padding: 5px 10px; margin-left: 5px; cursor: pointer;">عرض</button>
660
+ <button style="background-color: #78909C; color: white; border: none; border-radius: 3px; padding: 5px 10px; cursor: pointer;">تنزيل التقرير</button>
661
+ </td>
662
+ </tr>
663
+ <tr style="background-color: #f9f9f9;">
664
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">جداول كميات مشروع صرف صحي.xlsx</td>
665
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">تحليل جداول الكميات</td>
666
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">2023-03-23</td>
667
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;"><span style="color: #4CAF50; font-weight: bold;">مكتمل</span></td>
668
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">
669
+ <button style="background-color: #1E88E5; color: white; border: none; border-radius: 3px; padding: 5px 10px; margin-left: 5px; cursor: pointer;">عرض</button>
670
+ <button style="background-color: #78909C; color: white; border: none; border-radius: 3px; padding: 5px 10px; cursor: pointer;">تنزيل التقرير</button>
671
+ </td>
672
+ </tr>
673
+ <tr>
674
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">شروط وأحكام عقد بناء مدارس.pdf</td>
675
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">تحليل الشروط والأحكام</td>
676
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">2023-03-20</td>
677
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;"><span style="color: #4CAF50; font-weight: bold;">مكتمل</span></td>
678
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">
679
+ <button style="background-color: #1E88E5; color: white; border: none; border-radius: 3px; padding: 5px 10px; margin-left: 5px; cursor: pointer;">عرض</button>
680
+ <button style="background-color: #78909C; color: white; border: none; border-radius: 3px; padding: 5px 10px; cursor: pointer;">تنزيل التقرير</button>
681
+ </td>
682
+ </tr>
683
+ <tr style="background-color: #f9f9f9;">
684
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">ملحق عقد مشروع كباري.pdf</td>
685
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">تحليل شامل</td>
686
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">2023-03-18</td>
687
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;"><span style="color: #FB8C00; font-weight: bold;">قيد المعالجة</span></td>
688
+ <td style="padding: 12px; text-align: right; border-bottom: 1px solid #ddd;">
689
+ <button style="background-color: #9E9E9E; color: white; border: none; border-radius: 3px; padding: 5px 10px; margin-left: 5px; cursor: pointer;" disabled>عرض</button>
690
+ <button style="background-color: #9E9E9E; color: white; border: none; border-radius: 3px; padding: 5px 10px; cursor: pointer;" disabled>تنزيل التقرير</button>
691
+ </td>
692
+ </tr>
693
+ </tbody>
694
+ </table>
695
+ """, unsafe_allow_html=True)
696
 
697
+ # إحصائيات التحليل
698
+ st.markdown("### إحصائيات التحليل")
699
+
700
+ col1, col2 = st.columns(2)
701
+
702
+ with col1:
703
+ st.markdown("""
704
+ <div style="padding: 20px; background-color: #f5f5f5; border-radius: 10px; height: 100%;">
705
+ <h4 style="color: #1E88E5; margin-bottom: 15px;">توزيع أنواع المستندات</h4>
706
+ <div style="margin-bottom: 15px;">
707
+ <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
708
+ <span>عقود ومناقصات</span>
709
+ <span>45%</span>
710
+ </div>
711
+ <div style="height: 10px; background-color: #e0e0e0; border-radius: 5px;">
712
+ <div style="height: 100%; width: 45%; background-color: #1E88E5; border-radius: 5px;"></div>
713
+ </div>
714
+ </div>
715
+
716
+ <div style="margin-bottom: 15px;">
717
+ <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
718
+ <span>جداول كميات</span>
719
+ <span>30%</span>
720
+ </div>
721
+ <div style="height: 10px; background-color: #e0e0e0; border-radius: 5px;">
722
+ <div style="height: 100%; width: 30%; background-color: #43A047; border-radius: 5px;"></div>
723
+ </div>
724
+ </div>
725
+
726
+ <div style="margin-bottom: 15px;">
727
+ <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
728
+ <span>شروط وأحكام</span>
729
+ <span>15%</span>
730
+ </div>
731
+ <div style="height: 10px; background-color: #e0e0e0; border-radius: 5px;">
732
+ <div style="height: 100%; width: 15%; background-color: #FB8C00; border-radius: 5px;"></div>
733
+ </div>
734
+ </div>
735
+
736
+ <div style="margin-bottom: 15px;">
737
+ <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
738
+ <span>مستندات أخرى</span>
739
+ <span>10%</span>
740
+ </div>
741
+ <div style="height: 10px; background-color: #e0e0e0; border-radius: 5px;">
742
+ <div style="height: 100%; width: 10%; background-color: #78909C; border-radius: 5px;"></div>
743
+ </div>
744
+ </div>
745
+ </div>
746
+ """, unsafe_allow_html=True)
747
+
748
+ with col2:
749
+ st.markdown("""
750
+ <div style="padding: 20px; background-color: #f5f5f5; border-radius: 10px; height: 100%;">
751
+ <h4 style="color: #1E88E5; margin-bottom: 15px;">إحصائيات التحليل الشهرية</h4>
752
+ <div style="display: flex; justify-content: space-between; text-align: center;">
753
+ <div>
754
+ <div style="font-size: 2rem; font-weight: bold; color: #1E88E5;">42</div>
755
+ <div style="color: #666;">مستند تم تحليله</div>
756
+ </div>
757
+ <div>
758
+ <div style="font-size: 2rem; font-weight: bold; color: #43A047;">38</div>
759
+ <div style="color: #666;">تحليل ناجح</div>
760
+ </div>
761
+ <div>
762
+ <div style="font-size: 2rem; font-weight: bold; color: #FB8C00;">4</div>
763
+ <div style="color: #666;">تحليل غير مكتمل</div>
764
+ </div>
765
+ </div>
766
+
767
+ <h4 style="color: #1E88E5; margin-top: 20px; margin-bottom: 15px;">متوسط وقت المعالجة</h4>
768
+ <div style="display: flex; align-items: center; margin-bottom: 10px;">
769
+ <div style="width: 150px;">تحليل شامل:</div>
770
+ <div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 5px;">
771
+ <div style="height: 100%; width: 80%; background-color: #1E88E5; border-radius: 5px;"></div>
772
+ </div>
773
+ <div style="width: 50px; text-align: left; padding-left: 10px;">2:30</div>
774
+ </div>
775
+
776
+ <div style="display: flex; align-items: center; margin-bottom: 10px;">
777
+ <div style="width: 150px;">جداول الكميات:</div>
778
+ <div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 5px;">
779
+ <div style="height: 100%; width: 50%; background-color: #43A047; border-radius: 5px;"></div>
780
+ </div>
781
+ <div style="width: 50px; text-align: left; padding-left: 10px;">1:45</div>
782
+ </div>
783
+
784
+ <div style="display: flex; align-items: center;">
785
+ <div style="width: 150px;">الشروط والأحكام:</div>
786
+ <div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 5px;">
787
+ <div style="height: 100%; width: 60%; background-color: #FB8C00; border-radius: 5px;"></div>
788
+ </div>
789
+ <div style="width: 50px; text-align: left; padding-left: 10px;">2:00</div>
790
+ </div>
791
+ </div>
792
+ """, unsafe_allow_html=True)
793
+
794
+
795
+ def render_reports_and_analytics():
796
+ """عرض واجهة التقارير والتحليلات"""
797
+ st.markdown("<h1 class='app-title'>التقارير والتحليلات</h1>", unsafe_allow_html=True)
798
+
799
+ st.markdown("""
800
+ <div class="section-card">
801
+ <p>استخدم هذه الوحدة لإنشاء تقارير تحليلية متقدمة عن المشاريع والمناقصات والأداء العام.
802
+ يوفر النظام رؤى وتحليلات متعمقة تساعدك على فهم أداء مشاريعك وتحسين عمليات صنع القرار.</p>
803
+ </div>
804
+ """, unsafe_allow_html=True)
805
+
806
+ # أنواع التقارير
807
+ st.markdown("### أنواع التقارير")
808
+
809
+ col1, col2, col3 = st.columns(3)
810
+
811
+ with col1:
812
+ st.markdown("""
813
+ <div class="card">
814
+ <div class="card-title">تقارير المشاريع</div>
815
+ <p>تقارير تفصيلية عن حالة المشاريع وتقدمها ومؤشرات الأداء الرئيسية والمشكلات المحتملة.</p>
816
+ </div>
817
+ """, unsafe_allow_html=True)
818
+
819
+ if st.button("إنشاء تقرير", key="btn_project_report"):
820
+ # هنا سيتم استدعاء وحدة إنشاء تقارير المشاريع
821
+ st.session_state.report_type = "project"
822
+ st.session_state.show_report_form = True
823
+ st.rerun()
824
+
825
+ with col2:
826
+ st.markdown("""
827
+ <div class="card">
828
+ <div class="card-title">تقارير الأداء المالي</div>
829
+ <p>تحليل مالي للمشاريع يتضمن الإيرادات والتكاليف والأرباح والتدفقات النقدية والانحرافات عن الميزانية.</p>
830
+ </div>
831
+ """, unsafe_allow_html=True)
832
+
833
+ if st.button("إنشاء تقرير", key="btn_financial_report"):
834
+ # هنا سيتم استدعاء وحدة إنشاء تقارير الأداء المالي
835
+ st.session_state.report_type = "financial"
836
+ st.session_state.show_report_form = True
837
+ st.rerun()
838
+
839
+ with col3:
840
+ st.markdown("""
841
+ <div class="card">
842
+ <div class="card-title">تقارير المناقصات</div>
843
+ <p>تحليل شامل للمناقصات النشطة والمنتهية ونسب الفوز والمنافسين ومقارنة الأسعار.</p>
844
+ </div>
845
+ """, unsafe_allow_html=True)
846
+
847
+ if st.button("إنشاء تقرير", key="btn_tender_report"):
848
+ # هنا سيتم استدعاء وحدة إنشاء تقارير المناقصات
849
+ st.session_state.report_type = "tender"
850
+ st.session_state.show_report_form = True
851
+ st.rerun()
852
+
853
+ # لوحة البيانات
854
+ st.markdown("### لوحة البيانات التنفيذية")
855
+
856
+ col1, col2 = st.columns([2, 1])
857
+
858
+ with col1:
859
+ st.markdown("#### أداء المشاريع حسب القطاع")
860
+
861
+ # إنشاء بيانات تجريبية للرسم البياني
862
+ sectors = ['البنية التحتية', 'السكني', 'التعليمي', 'الصحي', 'النقل']
863
+ performance = [85, 72, 64, 90, 78]
864
+
865
+ # إنشاء رسم بياني شريطي
866
+ chart_data = pd.DataFrame({'القطاع': sectors, 'الأداء (%)': performance})
867
+ st.bar_chart(chart_data.set_index('القطاع'), use_container_width=True)
868
+
869
+ # عرض بيان توضيحي
870
+ st.caption("مقارنة أداء المشاريع عبر القطاعات المختلفة (نسبة الإنجاز)")
871
+
872
+ with col2:
873
+ st.markdown("#### المؤشرات الرئيسية")
874
+
875
+ # نسبة المشاريع المتأخرة
876
+ st.markdown("##### نسبة المشاريع المتأخرة")
877
+ delayed_projects = 15
878
+ st.progress(delayed_projects / 100)
879
+ st.markdown(f"<p style='text-align: center; color: #F44336; font-weight: bold; margin-top: -10px;'>{delayed_projects}%</p>", unsafe_allow_html=True)
880
+
881
+ # متوسط هامش الربح
882
+ st.markdown("##### متوسط هامش الربح")
883
+ profit_margin = 22
884
+ st.progress(profit_margin / 100)
885
+ st.markdown(f"<p style='text-align: center; color: #4CAF50; font-weight: bold; margin-top: -10px;'>{profit_margin}%</p>", unsafe_allow_html=True)
886
+
887
+ # معدل نجاح المناقصات
888
+ st.markdown("##### معدل نجاح المناقصات")
889
+ tender_success = 35
890
+ st.progress(tender_success / 100)
891
+ st.markdown(f"<p style='text-align: center; color: #2196F3; font-weight: bold; margin-top: -10px;'>{tender_success}%</p>", unsafe_allow_html=True)
892
+
893
+ # تقارير الأداء
894
+ st.markdown("### تقارير الأداء الأخيرة")
895
+
896
+ # التقرير الأول
897
+ with st.container():
898
+ col1, col2 = st.columns([3, 1])
899
+
900
+ with col1:
901
+ st.markdown("#### التقرير الشهري لمشاريع الربع الأول 2025")
902
+ st.markdown("تقرير شامل يوضح أداء جميع المشاريع النشطة خلال الربع الأول من عام 2025، بما في ذلك تحليل التكاليف والجدول الزمني والمخاطر.")
903
+ st.markdown("**تاريخ الإنشاء:** 15 مارس 2025")
904
+
905
+ with col2:
906
+ st.markdown("<br>", unsafe_allow_html=True) # إضافة مسافة
907
+ col2_1, col2_2 = st.columns(2)
908
+ with col2_1:
909
+ if st.button("عرض", key="view_report1"):
910
+ st.session_state.view_report = "quarterly_q1_2025"
911
+ st.session_state.show_report_viewer = True
912
+ with col2_2:
913
+ if st.button("تنزيل", key="download_report1"):
914
+ st.info("جاري تحميل التقرير...")
915
+
916
+ st.markdown("---")
917
+
918
+ # التقرير الثاني
919
+ with st.container():
920
+ col1, col2 = st.columns([3, 1])
921
+
922
+ with col1:
923
+ st.markdown("#### تحليل أداء المناقصات 2024-2025")
924
+ st.markdown("تحليل مقارن لنتائج المناقصات بين عامي 2024 و 2025، يوضح التحسن في معدلات النجاح وتحليل أسباب الخسارة وفرص التحسين.")
925
+ st.markdown("**تاريخ الإنشاء:** 28 فبراير 2025")
926
+
927
+ with col2:
928
+ st.markdown("<br>", unsafe_allow_html=True) # إضافة مسافة
929
+ col2_1, col2_2 = st.columns(2)
930
+ with col2_1:
931
+ if st.button("عرض", key="view_report2"):
932
+ st.session_state.view_report = "tenders_analysis_2024_2025"
933
+ st.session_state.show_report_viewer = True
934
+ with col2_2:
935
+ if st.button("تنزيل", key="download_report2"):
936
+ st.info("جاري تحميل التقرير...")
937
+
938
+ st.markdown("---")
939
+
940
+ # التقرير الثالث
941
+ with st.container():
942
+ col1, col2 = st.columns([3, 1])
943
+
944
+ with col1:
945
+ st.markdown("#### تقرير المخاطر المالية للمشاريع الجارية")
946
+ st.markdown("تقرير تفصيلي حول المخاطر المالية للمشاريع الجارية، بما في ذلك تحليل التدفقات النقدية والمستحقات المتأخرة والمطالبات المحتملة.")
947
+ st.markdown("**تاريخ الإنشاء:** 10 فبراير 2025")
948
+
949
+ with col2:
950
+ st.markdown("<br>", unsafe_allow_html=True) # إضافة مسافة
951
+ col2_1, col2_2 = st.columns(2)
952
+ with col2_1:
953
+ if st.button("عرض", key="view_report3"):
954
+ st.session_state.view_report = "financial_risks_2025"
955
+ st.session_state.show_report_viewer = True
956
+ with col2_2:
957
+ if st.button("تنزيل", key="download_report3"):
958
+ st.info("جاري تحميل التقرير...")
959
+
960
+
961
+ def render_ai_assistant():
962
+ """عرض واجهة المساعد الذكي باستخدام المكون الجديد"""
963
+ try:
964
+ from modules.ai_assistant.assistant_app import AssistantApp
965
+
966
+ # عرض العنوان والوصف
967
+ st.markdown("<h1 class='app-title'>المساعد الذكي</h1>", unsafe_allow_html=True)
968
+
969
+ st.markdown("""
970
+ <div class="section-card">
971
+ <p>المساعد الذكي هو واجهة تفاعلية مدعومة بتقنيات الذكاء الاصطناعي لمساعدتك في جميع أنشطة إدارة المشاريع والعقود.
972
+ يمكنك طرح أسئلة بلغتك الطبيعية والحصول على إجابات فورية، أو طلب مساعدة في مهام محددة مثل تحليل بنود العقد أو تقدير التكاليف.</p>
973
+ </div>
974
+ """, unsafe_allow_html=True)
975
+
976
+ # استدعاء المساعد الذكي الجديد
977
+ ai_assistant = AssistantApp()
978
+ ai_assistant.render()
979
+
980
+ except Exception as e:
981
+ st.error(f"حدث خطأ في تحميل المساعد الذكي: {str(e)}")
982
+ st.markdown("""
983
+ <div class="error-card">
984
+ <h3>😞 عذراً، واجهنا مشكلة في تحميل المساعد الذكي</h3>
985
+ <p>يرجى المحاولة مرة أخرى لاحقاً أو التواصل مع فريق الدعم الفني إذا استمرت المشكلة.</p>
986
+ </div>
987
+ """, unsafe_allow_html=True)
988
+
989
+
990
+ # تشغيل التطبيق عند تنفيذ الملف مباشرة
991
+ if __name__ == "__main__":
992
+ main()
config.py CHANGED
@@ -1,229 +1,62 @@
1
- """
2
- ملف الإعدادات لنظام إدارة المناقصات
3
- """
4
-
5
- import os
6
- import json
7
- from pathlib import Path
8
-
9
- class AppConfig:
10
- """فئة إعدادات التطبيق"""
11
-
12
- def __init__(self):
13
- """تهيئة الإعدادات"""
14
- # المسارات الأساسية
15
- self.app_dir = os.path.dirname(os.path.abspath(__file__))
16
- self.assets_dir = os.path.join(self.app_dir, "assets")
17
- self.data_dir = os.path.join(self.app_dir, "data")
18
-
19
- # إنشاء المجلدات إذا لم تكن موجودة
20
- Path(self.assets_dir).mkdir(parents=True, exist_ok=True)
21
- Path(self.data_dir).mkdir(parents=True, exist_ok=True)
22
-
23
- # مسارات الأصول
24
- self.icons_dir = os.path.join(self.assets_dir, "icons")
25
- self.images_dir = os.path.join(self.assets_dir, "images")
26
- self.fonts_dir = os.path.join(self.assets_dir, "fonts")
27
-
28
- # إنشاء مجلدات الأصول إذا لم تكن موجودة
29
- Path(self.icons_dir).mkdir(parents=True, exist_ok=True)
30
- Path(self.images_dir).mkdir(parents=True, exist_ok=True)
31
- Path(self.fonts_dir).mkdir(parents=True, exist_ok=True)
32
-
33
- # مسارات البيانات
34
- self.database_file = os.path.join(self.data_dir, "database.db")
35
- self.settings_file = os.path.join(self.data_dir, "settings.json")
36
- self.charts_dir = os.path.join(self.data_dir, "charts")
37
-
38
- # إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
39
- Path(self.charts_dir).mkdir(parents=True, exist_ok=True)
40
-
41
- # تحميل الإعدادات
42
- self.settings = self._load_settings()
43
-
44
- def _load_settings(self):
45
- """تحميل الإعدادات من ملف JSON"""
46
- default_settings = {
47
- "app": {
48
- "name": "نظام إدارة المناقصات",
49
- "version": "1.0.0",
50
- "language": "ar",
51
- "theme": "light",
52
- "font": "Cairo",
53
- "font_size": 12
54
- },
55
- "database": {
56
- "type": "sqlite",
57
- "path": self.database_file
58
- },
59
- "ui": {
60
- "window_width": 1200,
61
- "window_height": 800,
62
- "sidebar_width": 250
63
- },
64
- "notifications": {
65
- "enabled": True,
66
- "email_enabled": True,
67
- "email_server": "smtp.example.com",
68
- "email_port": 587,
69
- "email_username": "",
70
- "email_password": ""
71
- },
72
- "reports": {
73
- "default_format": "pdf",
74
- "default_path": os.path.join(self.data_dir, "reports")
75
- },
76
- "backup": {
77
- "auto_backup": True,
78
- "backup_frequency": "weekly",
79
- "backup_path": os.path.join(self.data_dir, "backups"),
80
- "max_backups": 10
81
- }
82
- }
83
-
84
- # إذا كان ملف الإعدادات موجودًا، قم بتحميله
85
- if os.path.exists(self.settings_file):
86
- try:
87
- with open(self.settings_file, "r", encoding="utf-8") as f:
88
- settings = json.load(f)
89
-
90
- # دمج الإعدادات المحملة مع الإعدادات الافتراضية
91
- self._merge_settings(default_settings, settings)
92
- return default_settings
93
- except Exception as e:
94
- print(f"خطأ في تحميل الإعدادات: {str(e)}")
95
- return default_settings
96
- else:
97
- # إنشاء ملف الإعدادات الافتراضية
98
- self._save_settings(default_settings)
99
- return default_settings
100
-
101
- def _merge_settings(self, default_settings, loaded_settings):
102
- """دمج الإعدادات المحملة مع الإعدادات الافتراضية"""
103
- for key, value in loaded_settings.items():
104
- if key in default_settings:
105
- if isinstance(value, dict) and isinstance(default_settings[key], dict):
106
- self._merge_settings(default_settings[key], value)
107
- else:
108
- default_settings[key] = value
109
-
110
- def _save_settings(self, settings=None):
111
- """حفظ الإعدادات إلى ملف JSON"""
112
- if settings is None:
113
- settings = self.settings
114
-
115
- try:
116
- with open(self.settings_file, "w", encoding="utf-8") as f:
117
- json.dump(settings, f, ensure_ascii=False, indent=4)
118
- return True
119
- except Exception as e:
120
- print(f"خطأ في حفظ الإعدادات: {str(e)}")
121
- return False
122
-
123
- def get_setting(self, section, key, default=None):
124
- """الحصول على قيمة إعداد معين"""
125
- try:
126
- return self.settings[section][key]
127
- except KeyError:
128
- return default
129
-
130
- def set_setting(self, section, key, value):
131
- """تعيين قيمة إعداد معين"""
132
- if section not in self.settings:
133
- self.settings[section] = {}
134
-
135
- self.settings[section][key] = value
136
- self._save_settings()
137
-
138
- def get_app_name(self):
139
- """الحصول على اسم التطبيق"""
140
- return self.get_setting("app", "name", "نظام إدارة المناقصات")
141
-
142
- def get_app_version(self):
143
- """الحصول على إصدار التطبيق"""
144
- return self.get_setting("app", "version", "1.0.0")
145
-
146
- def get_language(self):
147
- """الحصول على لغة التطبيق"""
148
- return self.get_setting("app", "language", "ar")
149
-
150
- def set_language(self, language):
151
- """تعيين لغة التطبيق"""
152
- self.set_setting("app", "language", language)
153
-
154
- def get_theme(self):
155
- """الحصول على نمط التطبيق"""
156
- return self.get_setting("app", "theme", "light")
157
-
158
- def set_theme(self, theme):
159
- """تعيين نمط التطبيق"""
160
- self.set_setting("app", "theme", theme)
161
-
162
- def get_font(self):
163
- """الحصول على خط التطبيق"""
164
- return self.get_setting("app", "font", "Cairo")
165
-
166
- def set_font(self, font):
167
- """تعيين خط التطبيق"""
168
- self.set_setting("app", "font", font)
169
-
170
- def get_font_size(self):
171
- """الحصول على حجم خط التطبيق"""
172
- return self.get_setting("app", "font_size", 12)
173
-
174
- def set_font_size(self, font_size):
175
- """تعيين حجم خط التطبيق"""
176
- self.set_setting("app", "font_size", font_size)
177
-
178
- def get_window_size(self):
179
- """الحصول على حجم نافذة التطبيق"""
180
- width = self.get_setting("ui", "window_width", 1200)
181
- height = self.get_setting("ui", "window_height", 800)
182
- return (width, height)
183
-
184
- def set_window_size(self, width, height):
185
- """تعيين حجم نافذة التطبيق"""
186
- self.set_setting("ui", "window_width", width)
187
- self.set_setting("ui", "window_height", height)
188
-
189
- def get_sidebar_width(self):
190
- """الحصول على عرض الشريط الجانبي"""
191
- return self.get_setting("ui", "sidebar_width", 250)
192
-
193
- def set_sidebar_width(self, width):
194
- """تعيين عرض الشريط الجانبي"""
195
- self.set_setting("ui", "sidebar_width", width)
196
-
197
- def get_database_config(self):
198
- """الحصول على إعدادات قاعدة البيانات"""
199
- return self.settings.get("database", {
200
- "type": "sqlite",
201
- "path": self.database_file
202
- })
203
-
204
- def get_notifications_config(self):
205
- """الحصول على إعدادات الإشعارات"""
206
- return self.settings.get("notifications", {
207
- "enabled": True,
208
- "email_enabled": True,
209
- "email_server": "smtp.example.com",
210
- "email_port": 587,
211
- "email_username": "",
212
- "email_password": ""
213
- })
214
-
215
- def get_reports_config(self):
216
- """الحصول على إعدادات التقارير"""
217
- return self.settings.get("reports", {
218
- "default_format": "pdf",
219
- "default_path": os.path.join(self.data_dir, "reports")
220
- })
221
-
222
- def get_backup_config(self):
223
- """الحصول على إعدادات النسخ الاحتياطي"""
224
- return self.settings.get("backup", {
225
- "auto_backup": True,
226
- "backup_frequency": "weekly",
227
- "backup_path": os.path.join(self.data_dir, "backups"),
228
- "max_backups": 10
229
- })
 
1
+ """
2
+ ملف إعدادات النظام
3
+ """
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ # مسارات النظام
9
+ ROOT_DIR = Path(__file__).parent
10
+ STATIC_DIR = os.path.join(ROOT_DIR, 'static')
11
+ MODELS_DIR = os.path.join(ROOT_DIR, 'models')
12
+ DATA_DIR = os.path.join(ROOT_DIR, 'database', 'data')
13
+
14
+ # عنوان التطبيق
15
+ APP_TITLE = "النظام الشامل لتحليل العقود والمناقصات - شركة شبه الجزيرة للمقاولات"
16
+ APP_ICON = "📋"
17
+
18
+ # إعدادات قاعدة البيانات
19
+ DB_TYPE = "sqlite" # يمكن استبدالها بـ 'mysql' أو 'postgresql'
20
+ DB_PATH = os.path.join(DATA_DIR, "tender_db.sqlite")
21
+
22
+ # إعدادات أخرى
23
+ DEBUG_MODE = True
24
+ LOG_LEVEL = "INFO"
25
+ LOCALE = "ar_SA"
26
+
27
+ # مسارات النماذج المدربة
28
+ NLP_ARABIC_MODEL = os.path.join(MODELS_DIR, "trained", "arabic_nlp_model.h5")
29
+ RISK_ANALYSIS_MODEL = os.path.join(MODELS_DIR, "trained", "risk_analysis_model.pkl")
30
+ PRICE_PREDICTION_MODEL = os.path.join(MODELS_DIR, "trained", "price_prediction_model.pkl")
31
+
32
+ # تكوين واجهة المستخدم
33
+ UI_THEME = "light" # 'light' أو 'dark'
34
+ ENABLE_ANIMATIONS = True
35
+ DEFAULT_MODULE = "الرئيسية"
36
+
37
+ # تكوين المحتوى المحلي
38
+ LOCAL_CONTENT_CATEGORIES = ["القوى العاملة", "المنتجات", "الخدمات"]
39
+ LOCAL_CONTENT_TARGETS = {
40
+ "القوى العاملة": 0.8, # 80%
41
+ "المنتجات": 0.7, # 70%
42
+ "الخدمات": 0.6 # 60%
43
+ }
44
+
45
+ # تكوين التسعير
46
+ PRICING_METHODS = [
47
+ "التسعير القياسي",
48
+ "التسعير غير المتزن",
49
+ "التسعير التنافسي",
50
+ "التسعير الموجه بالربحية"
51
+ ]
52
+
53
+ DEFAULT_OVERHEAD_PERCENTAGE = 15 # النسبة الافتراضية للمصاريف العامة والأرباح
54
+
55
+ # إعدادات تحليل المستندات
56
+ SUPPORTED_DOCUMENT_TYPES = ["pdf", "docx", "xlsx", "dwg", "jpg", "png"]
57
+ MAX_UPLOAD_SIZE_MB = 20
58
+
59
+ # إعدادات API الذكاء الاصطناعي
60
+ AI_API_ENABLED = True
61
+ AI_API_ENDPOINT = "http://localhost:8000/api/v1"
62
+ AI_API_KEY = "YOUR_API_KEY_HERE" # يجب استبدالها في بيئة الإنتاج
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
data/achievements/user_1_achievements.json ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "user_id": 1,
3
+ "total_points": 450,
4
+ "level": 1,
5
+ "unlocked_achievements": [
6
+ {
7
+ "id": "first_project",
8
+ "name": "بداية الرحلة",
9
+ "description": "قم بإنشاء مشروعك الأول",
10
+ "icon": "🏆",
11
+ "points": 100,
12
+ "category": "مشاريع",
13
+ "difficulty": "سهل",
14
+ "unlocked_date": "2025-03-15 14:32:05"
15
+ },
16
+ {
17
+ "id": "first_document_analysis",
18
+ "name": "المحلل الأول",
19
+ "description": "قم بتحليل مستند للمرة الأولى",
20
+ "icon": "📊",
21
+ "points": 150,
22
+ "category": "تحليل",
23
+ "difficulty": "سهل",
24
+ "unlocked_date": "2025-03-17 09:45:22"
25
+ },
26
+ {
27
+ "id": "voice_narration",
28
+ "name": "مترجم صوتي",
29
+ "description": "استخدم ميزة الترجمة الصوتية لأول مرة",
30
+ "icon": "🎙️",
31
+ "points": 200,
32
+ "category": "ترجمة",
33
+ "difficulty": "سهل",
34
+ "unlocked_date": "2025-03-30 12:15:30"
35
+ }
36
+ ],
37
+ "in_progress_achievements": [
38
+ {
39
+ "id": "five_projects",
40
+ "name": "محترف المشاريع",
41
+ "description": "قم بإنشاء خمسة مشاريع",
42
+ "icon": "🏅",
43
+ "points": 500,
44
+ "category": "مشاريع",
45
+ "difficulty": "متوسط",
46
+ "progress": 2,
47
+ "total": 5,
48
+ "percentage": 40,
49
+ "start_date": "2025-03-15 14:32:05",
50
+ "last_updated": "2025-03-28 16:10:45"
51
+ },
52
+ {
53
+ "id": "five_document_analysis",
54
+ "name": "محلل متمرس",
55
+ "description": "قم بتحليل خمسة مستندات",
56
+ "icon": "📈",
57
+ "points": 600,
58
+ "category": "تحليل",
59
+ "difficulty": "متوسط",
60
+ "progress": 3,
61
+ "total": 5,
62
+ "percentage": 60,
63
+ "start_date": "2025-03-17 09:45:22",
64
+ "last_updated": "2025-03-29 11:20:18"
65
+ },
66
+ {
67
+ "id": "multilingual_expert",
68
+ "name": "خبير متعدد اللغات",
69
+ "description": "استخدم الترجمة الصوتية بخمس لغات مختلفة",
70
+ "icon": "🌍",
71
+ "points": 800,
72
+ "category": "ترجمة",
73
+ "difficulty": "صعب",
74
+ "progress": 1,
75
+ "total": 5,
76
+ "percentage": 20,
77
+ "start_date": "2025-03-30 12:15:30",
78
+ "last_updated": "2025-03-30 12:15:30"
79
+ }
80
+ ],
81
+ "last_updated": "2025-03-30 12:15:30"
82
+ }
data/achievements/user_1_languages.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "languages": ["العربية"]
3
+ }
data/achievements/user_1_risks.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "total_risks": 4
3
+ }
data/project_tracker/project_1_kpis.json ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "spi": 1.05,
3
+ "cpi": 0.98,
4
+ "quality_score": 92,
5
+ "safety_incidents": 0,
6
+ "resource_utilization": 85,
7
+ "risk_score": 15,
8
+ "customer_satisfaction": 90,
9
+ "environmental_compliance": 95,
10
+ "trends": {
11
+ "spi": [
12
+ 0.95,
13
+ 0.98,
14
+ 1.02,
15
+ 1.05
16
+ ],
17
+ "cpi": [
18
+ 1.02,
19
+ 1.0,
20
+ 0.99,
21
+ 0.98
22
+ ],
23
+ "quality_score": [
24
+ 85,
25
+ 88,
26
+ 90,
27
+ 92
28
+ ],
29
+ "risk_score": [
30
+ 25,
31
+ 22,
32
+ 18,
33
+ 15
34
+ ],
35
+ "dates": [
36
+ "2025-03-09",
37
+ "2025-03-16",
38
+ "2025-03-23",
39
+ "2025-03-30"
40
+ ]
41
+ },
42
+ "issues": [
43
+ {
44
+ "id": 1,
45
+ "description": "تأخر في توريد المواد",
46
+ "severity": "متوسط",
47
+ "status": "قيد المعالجة",
48
+ "created_date": "2025-03-20",
49
+ "responsible": "قسم المشتريات",
50
+ "resolution": "التنسيق مع المورد البديل"
51
+ },
52
+ {
53
+ "id": 2,
54
+ "description": "نقص في فريق العمل",
55
+ "severity": "منخفض",
56
+ "status": "تم الحل",
57
+ "created_date": "2025-03-15",
58
+ "responsible": "قسم الموارد البشرية",
59
+ "resolution": "تم توظيف فريق إضافي"
60
+ }
61
+ ],
62
+ "last_updated": "2025-03-30 21:18:10"
63
+ }
data/project_tracker/project_1_status.json ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "project_id": 1,
3
+ "project_name": "مشروع إنشاء مبنى إداري",
4
+ "project_code": "PC-2025-001",
5
+ "client": "وزارة الإسكان",
6
+ "location": "الرياض، المملكة العربية السعودية",
7
+ "start_date": "2025-02-28",
8
+ "end_date": "2025-10-16",
9
+ "budget": 10000000,
10
+ "duration": 230,
11
+ "elapsed_days": 30,
12
+ "overall_progress": 25,
13
+ "status": "في التقدم",
14
+ "phases": [
15
+ {
16
+ "id": "planning",
17
+ "name": "التخطيط",
18
+ "description": "مرحلة التخطيط وإعداد الجدول الزمني",
19
+ "order": 1,
20
+ "progress": 100,
21
+ "status": "completed",
22
+ "start_date": "2025-02-28",
23
+ "end_date": "2025-03-10",
24
+ "actual_end_date": "2025-03-12",
25
+ "deliverables": [
26
+ "خطة المشروع",
27
+ "الجدول الزمني",
28
+ "خطة الموارد"
29
+ ],
30
+ "responsible": "فريق التخطيط",
31
+ "notes": "تم الانتهاء من مرحلة التخطيط بنجاح قبل الموعد المحدد",
32
+ "critical": true
33
+ },
34
+ {
35
+ "id": "pricing",
36
+ "name": "التسعير",
37
+ "description": "تسعير المشروع وتحليل التكاليف",
38
+ "order": 2,
39
+ "progress": 100,
40
+ "status": "completed",
41
+ "start_date": "2025-03-10",
42
+ "end_date": "2025-03-20",
43
+ "actual_end_date": "2025-03-22",
44
+ "deliverables": [
45
+ "جدول الكميات المسعر",
46
+ "تحليل التكاليف",
47
+ "خطة التدفق النقدي"
48
+ ],
49
+ "responsible": "قسم التسعير",
50
+ "notes": "تم تحقيق وفر في تكاليف المشروع بنسبة 5%",
51
+ "critical": true
52
+ },
53
+ {
54
+ "id": "bidding",
55
+ "name": "تقديم العطاء",
56
+ "description": "إعداد وتقديم وثائق العطاء",
57
+ "order": 3,
58
+ "progress": 100,
59
+ "status": "completed",
60
+ "start_date": "2025-03-20",
61
+ "end_date": "2025-03-25",
62
+ "actual_end_date": "2025-03-25",
63
+ "deliverables": [
64
+ "وثائق العطاء",
65
+ "خطاب التقديم",
66
+ "الضمان البنكي الابتدائي"
67
+ ],
68
+ "responsible": "مدير المشروع",
69
+ "notes": "تم تقديم العطاء في الموعد المحدد",
70
+ "critical": true
71
+ },
72
+ {
73
+ "id": "evaluation",
74
+ "name": "تقييم العطاء",
75
+ "description": "مرحلة تقييم العطاء من قبل العميل",
76
+ "order": 4,
77
+ "progress": 75,
78
+ "status": "in_progress",
79
+ "start_date": "2025-03-25",
80
+ "end_date": "2025-04-04",
81
+ "actual_end_date": null,
82
+ "deliverables": [
83
+ "الرد على استفسارات العميل",
84
+ "العرض التقديمي",
85
+ "تقديم المستندات الإضافية"
86
+ ],
87
+ "responsible": "العميل / مدير المشروع",
88
+ "notes": "مرحلة التقييم جارية، تم الرد على جميع استفسارات العميل",
89
+ "critical": true
90
+ },
91
+ {
92
+ "id": "awarding",
93
+ "name": "ترسية العطاء",
94
+ "description": "مرحلة ترسية العطاء وتوقيع العقد",
95
+ "order": 5,
96
+ "progress": 0,
97
+ "status": "not_started",
98
+ "start_date": "2025-04-04",
99
+ "end_date": "2025-04-14",
100
+ "actual_end_date": null,
101
+ "deliverables": [
102
+ "خطاب الترسية",
103
+ "العقد الموقع",
104
+ "الضمان البنكي النهائي"
105
+ ],
106
+ "responsible": "الإدارة القانونية / مدير المشروع",
107
+ "notes": "ننتظر نتيجة الترسية",
108
+ "critical": true
109
+ },
110
+ {
111
+ "id": "mobilization",
112
+ "name": "التجهيز",
113
+ "description": "تجهيز الموقع وتوفير الموارد",
114
+ "order": 6,
115
+ "progress": 0,
116
+ "status": "not_started",
117
+ "start_date": "2025-04-14",
118
+ "end_date": "2025-04-29",
119
+ "actual_end_date": null,
120
+ "deliverables": [
121
+ "تقرير التجهيز",
122
+ "قائمة الموارد",
123
+ "خطة التنفيذ التفصيلية"
124
+ ],
125
+ "responsible": "قسم العمليات",
126
+ "notes": "التجهيز سيبدأ بعد توقيع العقد",
127
+ "critical": false
128
+ },
129
+ {
130
+ "id": "execution",
131
+ "name": "التنفيذ",
132
+ "description": "تنفيذ أعمال المشروع",
133
+ "order": 7,
134
+ "progress": 0,
135
+ "status": "not_started",
136
+ "start_date": "2025-04-29",
137
+ "end_date": "2025-09-26",
138
+ "actual_end_date": null,
139
+ "deliverables": [
140
+ "تقارير التقدم الدورية",
141
+ "محاضر الاجتماعات",
142
+ "الفواتير"
143
+ ],
144
+ "responsible": "فريق التنفيذ",
145
+ "notes": "التنفيذ سيستمر لمدة 6 أشهر",
146
+ "critical": true
147
+ },
148
+ {
149
+ "id": "handover",
150
+ "name": "التسليم",
151
+ "description": "تسليم المشروع للعميل",
152
+ "order": 8,
153
+ "progress": 0,
154
+ "status": "not_started",
155
+ "start_date": "2025-09-26",
156
+ "end_date": "2025-10-11",
157
+ "actual_end_date": null,
158
+ "deliverables": [
159
+ "محضر الاستلام",
160
+ "وثائق الضمان",
161
+ "دليل التشغيل والصيانة"
162
+ ],
163
+ "responsible": "مدير المشروع / العميل",
164
+ "notes": "التسليم يشمل فترة الاختبار والتدريب",
165
+ "critical": true
166
+ }
167
+ ],
168
+ "last_updated": "2025-03-30 21:18:10"
169
+ }
data/projects/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+ # مجلد لحفظ ملفات المشاريع
database/db_connector.py CHANGED
@@ -1,323 +1,61 @@
 
 
 
1
  """
2
- موصل قاعدة البيانات لنظام إدارة المناقصات
3
  """
4
 
5
  import os
6
- import sqlite3
7
- import logging
 
8
 
9
- logger = logging.getLogger('tender_system.database')
 
10
 
11
- class DatabaseConnector:
12
- """فئة موصل قاعدة البيانات"""
13
-
14
- def __init__(self, config):
15
- """تهيئة موصل قاعدة البيانات"""
16
- self.config = config
17
- self.db_config = config.get_database_config()
18
- self.db_path = self.db_config.get('path')
19
- self.connection = None
20
- self.cursor = None
21
-
22
- # إنشاء قاعدة البيانات إذا لم تكن موجودة
23
- self._initialize_database()
24
-
25
- def _initialize_database(self):
26
- """تهيئة قاعدة البيانات"""
27
- try:
28
- # التأكد من وجود المجلد
29
- os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
30
-
31
- # إنشاء الاتصال
32
- self.connection = sqlite3.connect(self.db_path)
33
- self.cursor = self.connection.cursor()
34
-
35
- # إنشاء الجداول إذا لم تكن موجودة
36
- self._create_tables()
37
-
38
- # إضافة بيانات افتراضية إذا كانت قاعدة البيانات فارغة
39
- self._add_default_data()
40
-
41
- logger.info(f"تم تهيئة قاعدة البيانات بنجاح: {self.db_path}")
42
- except Exception as e:
43
- logger.error(f"خطأ في تهيئة قاعدة البيانات: {str(e)}")
44
- raise
45
-
46
- def _create_tables(self):
47
- """إنشاء جداول قاعدة البيانات"""
48
- # جدول المستخدمين
49
- self.cursor.execute('''
50
- CREATE TABLE IF NOT EXISTS users (
51
- id INTEGER PRIMARY KEY AUTOINCREMENT,
52
- username TEXT UNIQUE NOT NULL,
53
- password TEXT NOT NULL,
54
- full_name TEXT NOT NULL,
55
- email TEXT UNIQUE NOT NULL,
56
- role TEXT NOT NULL,
57
- status TEXT NOT NULL,
58
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
59
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
60
- )
61
- ''')
62
-
63
- # جدول المشاريع
64
- self.cursor.execute('''
65
- CREATE TABLE IF NOT EXISTS projects (
66
- id INTEGER PRIMARY KEY AUTOINCREMENT,
67
- name TEXT NOT NULL,
68
- client TEXT NOT NULL,
69
- description TEXT,
70
- start_date TEXT,
71
- end_date TEXT,
72
- status TEXT NOT NULL,
73
- created_by INTEGER,
74
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
75
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
76
- FOREIGN KEY (created_by) REFERENCES users (id)
77
- )
78
- ''')
79
-
80
- # جدول المستندات
81
- self.cursor.execute('''
82
- CREATE TABLE IF NOT EXISTS documents (
83
- id INTEGER PRIMARY KEY AUTOINCREMENT,
84
- project_id INTEGER,
85
- name TEXT NOT NULL,
86
- file_path TEXT NOT NULL,
87
- document_type TEXT NOT NULL,
88
- description TEXT,
89
- uploaded_by INTEGER,
90
- uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
91
- FOREIGN KEY (project_id) REFERENCES projects (id),
92
- FOREIGN KEY (uploaded_by) REFERENCES users (id)
93
- )
94
- ''')
95
-
96
- # جدول بنود التسعير
97
- self.cursor.execute('''
98
- CREATE TABLE IF NOT EXISTS pricing_items (
99
- id INTEGER PRIMARY KEY AUTOINCREMENT,
100
- project_id INTEGER,
101
- item_number TEXT NOT NULL,
102
- description TEXT NOT NULL,
103
- unit TEXT NOT NULL,
104
- quantity REAL NOT NULL,
105
- unit_price REAL NOT NULL,
106
- total_price REAL NOT NULL,
107
- created_by INTEGER,
108
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
109
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
110
- FOREIGN KEY (project_id) REFERENCES projects (id),
111
- FOREIGN KEY (created_by) REFERENCES users (id)
112
- )
113
- ''')
114
-
115
- # جدول الموارد البشرية
116
- self.cursor.execute('''
117
- CREATE TABLE IF NOT EXISTS human_resources (
118
- id INTEGER PRIMARY KEY AUTOINCREMENT,
119
- name TEXT NOT NULL,
120
- position TEXT NOT NULL,
121
- daily_cost REAL NOT NULL,
122
- skills TEXT,
123
- status TEXT NOT NULL,
124
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
125
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
126
- )
127
- ''')
128
-
129
- # جدول المعدات
130
- self.cursor.execute('''
131
- CREATE TABLE IF NOT EXISTS equipment (
132
- id INTEGER PRIMARY KEY AUTOINCREMENT,
133
- name TEXT NOT NULL,
134
- type TEXT NOT NULL,
135
- daily_cost REAL NOT NULL,
136
- status TEXT NOT NULL,
137
- location TEXT,
138
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
139
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
140
- )
141
- ''')
142
-
143
- # جدول المواد
144
- self.cursor.execute('''
145
- CREATE TABLE IF NOT EXISTS materials (
146
- id INTEGER PRIMARY KEY AUTOINCREMENT,
147
- name TEXT NOT NULL,
148
- unit TEXT NOT NULL,
149
- quantity REAL NOT NULL,
150
- unit_price REAL NOT NULL,
151
- supplier TEXT,
152
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
153
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
154
- )
155
- ''')
156
-
157
- # جدول المخاطر
158
- self.cursor.execute('''
159
- CREATE TABLE IF NOT EXISTS risks (
160
- id INTEGER PRIMARY KEY AUTOINCREMENT,
161
- project_id INTEGER,
162
- name TEXT NOT NULL,
163
- category TEXT NOT NULL,
164
- probability TEXT NOT NULL,
165
- impact TEXT NOT NULL,
166
- risk_level TEXT NOT NULL,
167
- mitigation_strategy TEXT,
168
- created_by INTEGER,
169
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
170
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
171
- FOREIGN KEY (project_id) REFERENCES projects (id),
172
- FOREIGN KEY (created_by) REFERENCES users (id)
173
- )
174
- ''')
175
-
176
- # جدول التقارير
177
- self.cursor.execute('''
178
- CREATE TABLE IF NOT EXISTS reports (
179
- id INTEGER PRIMARY KEY AUTOINCREMENT,
180
- name TEXT NOT NULL,
181
- project_id INTEGER,
182
- report_type TEXT NOT NULL,
183
- period TEXT,
184
- file_path TEXT,
185
- created_by INTEGER,
186
- status TEXT NOT NULL,
187
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
188
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
189
- FOREIGN KEY (project_id) REFERENCES projects (id),
190
- FOREIGN KEY (created_by) REFERENCES users (id)
191
- )
192
- ''')
193
-
194
- # حفظ التغييرات
195
- self.connection.commit()
196
-
197
- def _add_default_data(self):
198
- """إضافة بيانات افتراضية"""
199
- # التحقق من وجود مستخدمين
200
- self.cursor.execute("SELECT COUNT(*) FROM users")
201
- user_count = self.cursor.fetchone()[0]
202
-
203
- if user_count == 0:
204
- # إضافة مستخدم افتراضي (admin/admin)
205
- self.cursor.execute('''
206
- INSERT INTO users (username, password, full_name, email, role, status)
207
- VALUES (?, ?, ?, ?, ?, ?)
208
- ''', ('admin', 'admin', 'مدير النظام', '[email protected]', 'مدير', 'نشط'))
209
-
210
- # إضافة مستخدمين إضافيين
211
- self.cursor.execute('''
212
- INSERT INTO users (username, password, full_name, email, role, status)
213
- VALUES (?, ?, ?, ?, ?, ?)
214
- ''', ('user1', 'password', 'أحمد محمد', '[email protected]', 'مستخدم', 'نشط'))
215
-
216
- self.cursor.execute('''
217
- INSERT INTO users (username, password, full_name, email, role, status)
218
- VALUES (?, ?, ?, ?, ?, ?)
219
- ''', ('user2', 'password', 'سارة أحمد', '[email protected]', 'مستخدم', 'نشط'))
220
-
221
- # حفظ التغييرات
222
- self.connection.commit()
223
-
224
- logger.info("تم إضافة بيانات المستخدمين الافتراضية")
225
-
226
- # التحقق من وجود مشاريع
227
- self.cursor.execute("SELECT COUNT(*) FROM projects")
228
- project_count = self.cursor.fetchone()[0]
229
-
230
- if project_count == 0:
231
- # إضافة مشاريع افتراضية
232
- self.cursor.execute('''
233
- INSERT INTO projects (name, client, description, start_date, end_date, status, created_by)
234
- VALUES (?, ?, ?, ?, ?, ?, ?)
235
- ''', ('مشروع تطوير الطريق السريع', 'وزارة النقل', 'مشروع تطوير وتوسعة الطريق السريع', '2025-01-15', '2025-12-31', 'نشط', 1))
236
-
237
- self.cursor.execute('''
238
- INSERT INTO projects (name, client, description, start_date, end_date, status, created_by)
239
- VALUES (?, ?, ?, ?, ?, ?, ?)
240
- ''', ('مشروع بناء المدرسة الثانوية', 'وزارة التعليم', 'مشروع بناء مدرسة ثانوية جديدة', '2025-02-01', '2025-08-30', 'نشط', 1))
241
-
242
- self.cursor.execute('''
243
- INSERT INTO projects (name, client, description, start_date, end_date, status, created_by)
244
- VALUES (?, ?, ?, ?, ?, ?, ?)
245
- ''', ('مشروع تجديد المستشفى', 'وزارة الصحة', 'مشروع تجديد وتطوير المستشفى', '2024-10-15', '2025-03-15', 'مكتمل', 1))
246
-
247
- # حفظ التغييرات
248
- self.connection.commit()
249
-
250
- logger.info("تم إضافة بيانات المشاريع الافتراضية")
251
-
252
- def execute_query(self, query, params=None):
253
- """تنفيذ استعلام"""
254
- try:
255
- if params:
256
- self.cursor.execute(query, params)
257
- else:
258
- self.cursor.execute(query)
259
-
260
- self.connection.commit()
261
- return self.cursor
262
- except Exception as e:
263
- logger.error(f"خطأ في تنفيذ الاستعلام: {str(e)}")
264
- self.connection.rollback()
265
- raise
266
-
267
- def fetch_one(self, query, params=None):
268
- """جلب صف واحد"""
269
- cursor = self.execute_query(query, params)
270
- return cursor.fetchone()
271
-
272
- def fetch_all(self, query, params=None):
273
- """جلب جميع الصفوف"""
274
- cursor = self.execute_query(query, params)
275
- return cursor.fetchall()
276
-
277
- def insert(self, table, data):
278
- """إدراج بيانات"""
279
- columns = ', '.join(data.keys())
280
- placeholders = ', '.join(['?' for _ in data])
281
- query = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
282
-
283
- try:
284
- self.cursor.execute(query, list(data.values()))
285
- self.connection.commit()
286
- return self.cursor.lastrowid
287
- except Exception as e:
288
- logger.error(f"خطأ في إدراج البيانات: {str(e)}")
289
- self.connection.rollback()
290
- raise
291
-
292
- def update(self, table, data, condition):
293
- """تحديث بيانات"""
294
- set_clause = ', '.join([f"{column} = ?" for column in data.keys()])
295
- query = f"UPDATE {table} SET {set_clause} WHERE {condition}"
296
-
297
- try:
298
- self.cursor.execute(query, list(data.values()))
299
- self.connection.commit()
300
- return self.cursor.rowcount
301
- except Exception as e:
302
- logger.error(f"خطأ في تحديث البيانات: {str(e)}")
303
- self.connection.rollback()
304
- raise
305
-
306
- def delete(self, table, condition):
307
- """حذف بيانات"""
308
- query = f"DELETE FROM {table} WHERE {condition}"
309
-
310
- try:
311
- self.cursor.execute(query)
312
- self.connection.commit()
313
- return self.cursor.rowcount
314
- except Exception as e:
315
- logger.error(f"خطأ في حذف البيانات: {str(e)}")
316
- self.connection.rollback()
317
- raise
318
-
319
- def close(self):
320
- """إغلاق الاتصال"""
321
- if self.connection:
322
- self.connection.close()
323
- logger.info("تم إغلاق الاتصال بقاعدة البيانات")
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
  """
5
+ وحدة الاتصال بقاعدة البيانات
6
  """
7
 
8
  import os
9
+ import sys
10
+ import psycopg2
11
+ from dotenv import load_dotenv
12
 
13
+ # إضافة مسار النظام للوصول للملفات المشتركة
14
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
15
 
16
+ # تحميل متغيرات البيئة
17
+ load_dotenv()
18
+
19
+ def get_connection():
20
+ """
21
+ إنشاء اتصال بقاعدة البيانات
22
+
23
+ الإرجاع:
24
+ اتصال بقاعدة البيانات
25
+ """
26
+ try:
27
+ # محاولة الاتصال بقاعدة البيانات
28
+ conn = psycopg2.connect(
29
+ dbname=os.getenv("PGDATABASE"),
30
+ user=os.getenv("PGUSER"),
31
+ password=os.getenv("PGPASSWORD"),
32
+ host=os.getenv("PGHOST"),
33
+ port=os.getenv("PGPORT")
34
+ )
35
+ return conn
36
+ except Exception as e:
37
+ print(f"خطأ في الاتصال بقاعدة البيانات: {e}")
38
+
39
+ # إذا فشل الاتصال، استخدم اتصال قاعدة بيانات SQLite محلية
40
+ import os
41
+ import sqlite3
42
+
43
+ # إنشاء مجلد البيانات إذا لم يكن موجوداً
44
+ data_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data')
45
+ os.makedirs(data_dir, exist_ok=True)
46
+
47
+ # إنشاء اتصال قاعدة بيانات SQLite محلية
48
+ db_path = os.path.join(data_dir, 'local_db.sqlite')
49
+ conn = sqlite3.connect(db_path)
50
+
51
+ # إعادة محاكاة سلوك اتصال PostgreSQL
52
+ conn.execute = conn.cursor().execute
53
+
54
+ # إضافة وظيفة وهمية للاقتطاع (commit) والإغلاق
55
+ original_close = conn.close
56
+ def enhanced_close():
57
+ conn.commit()
58
+ original_close()
59
+ conn.close = enhanced_close
60
+
61
+ return conn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/models.py CHANGED
@@ -1,626 +1,279 @@
1
- """
2
- نماذج البيانات لنظام إدارة المناقصات
3
- """
4
-
5
- import sqlite3
6
- import logging
7
- from datetime import datetime
8
-
9
- logger = logging.getLogger('tender_system.models')
10
-
11
- class User:
12
- """نموذج المستخدم"""
13
-
14
- def __init__(self, id=None, username=None, password=None, full_name=None, email=None, role=None, status=None):
15
- """تهيئة نموذج المستخدم"""
16
- self.id = id
17
- self.username = username
18
- self.password = password
19
- self.full_name = full_name
20
- self.email = email
21
- self.role = role
22
- self.status = status
23
- self.created_at = None
24
- self.updated_at = None
25
-
26
- @staticmethod
27
- def authenticate(username, password, db):
28
- """مصادقة المستخدم"""
29
- try:
30
- query = "SELECT * FROM users WHERE username = ? AND password = ? AND status = 'نشط'"
31
- result = db.fetch_one(query, (username, password))
32
-
33
- if result:
34
- user = User()
35
- user.id = result[0]
36
- user.username = result[1]
37
- user.password = result[2]
38
- user.full_name = result[3]
39
- user.email = result[4]
40
- user.role = result[5]
41
- user.status = result[6]
42
- user.created_at = result[7]
43
- user.updated_at = result[8]
44
-
45
- return user
46
-
47
- return None
48
- except Exception as e:
49
- logger.error(f"خطأ في مصادقة المستخدم: {str(e)}")
50
- return None
51
-
52
- @staticmethod
53
- def get_by_id(user_id, db):
54
- """الحصول على المستخدم بواسطة المعرف"""
55
- try:
56
- query = "SELECT * FROM users WHERE id = ?"
57
- result = db.fetch_one(query, (user_id,))
58
-
59
- if result:
60
- user = User()
61
- user.id = result[0]
62
- user.username = result[1]
63
- user.password = result[2]
64
- user.full_name = result[3]
65
- user.email = result[4]
66
- user.role = result[5]
67
- user.status = result[6]
68
- user.created_at = result[7]
69
- user.updated_at = result[8]
70
-
71
- return user
72
-
73
- return None
74
- except Exception as e:
75
- logger.error(f"خطأ في الحصول على المستخدم: {str(e)}")
76
- return None
77
-
78
- @staticmethod
79
- def get_all(db):
80
- """الحصول على جميع المستخدمين"""
81
- try:
82
- query = "SELECT * FROM users"
83
- results = db.fetch_all(query)
84
-
85
- users = []
86
- for result in results:
87
- user = User()
88
- user.id = result[0]
89
- user.username = result[1]
90
- user.password = result[2]
91
- user.full_name = result[3]
92
- user.email = result[4]
93
- user.role = result[5]
94
- user.status = result[6]
95
- user.created_at = result[7]
96
- user.updated_at = result[8]
97
-
98
- users.append(user)
99
-
100
- return users
101
- except Exception as e:
102
- logger.error(f"خطأ في الحصول على المستخدمين: {str(e)}")
103
- return []
104
-
105
- def save(self, db):
106
- """حفظ المستخدم"""
107
- try:
108
- if self.id:
109
- # تحديث مستخدم موجود
110
- data = {
111
- 'username': self.username,
112
- 'password': self.password,
113
- 'full_name': self.full_name,
114
- 'email': self.email,
115
- 'role': self.role,
116
- 'status': self.status,
117
- 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
118
- }
119
-
120
- db.update('users', data, f"id = {self.id}")
121
- return self.id
122
- else:
123
- # إنشاء مستخدم جديد
124
- data = {
125
- 'username': self.username,
126
- 'password': self.password,
127
- 'full_name': self.full_name,
128
- 'email': self.email,
129
- 'role': self.role,
130
- 'status': self.status
131
- }
132
-
133
- self.id = db.insert('users', data)
134
- return self.id
135
- except Exception as e:
136
- logger.error(f"خطأ في حفظ المستخدم: {str(e)}")
137
- return None
138
-
139
- def delete(self, db):
140
- """حذف المستخدم"""
141
- try:
142
- if self.id:
143
- db.delete('users', f"id = {self.id}")
144
- return True
145
-
146
- return False
147
- except Exception as e:
148
- logger.error(f"خطأ في حذف المستخدم: {str(e)}")
149
- return False
150
-
151
-
152
- class Project:
153
- """نموذج المشروع"""
154
-
155
- def __init__(self, id=None, name=None, client=None, description=None, start_date=None, end_date=None, status=None, created_by=None):
156
- """تهيئة نموذج المشروع"""
157
- self.id = id
158
- self.name = name
159
- self.client = client
160
- self.description = description
161
- self.start_date = start_date
162
- self.end_date = end_date
163
- self.status = status
164
- self.created_by = created_by
165
- self.created_at = None
166
- self.updated_at = None
167
-
168
- @staticmethod
169
- def get_by_id(project_id, db):
170
- """الحصول على المشروع بواسطة المعرف"""
171
- try:
172
- query = "SELECT * FROM projects WHERE id = ?"
173
- result = db.fetch_one(query, (project_id,))
174
-
175
- if result:
176
- project = Project()
177
- project.id = result[0]
178
- project.name = result[1]
179
- project.client = result[2]
180
- project.description = result[3]
181
- project.start_date = result[4]
182
- project.end_date = result[5]
183
- project.status = result[6]
184
- project.created_by = result[7]
185
- project.created_at = result[8]
186
- project.updated_at = result[9]
187
-
188
- return project
189
-
190
- return None
191
- except Exception as e:
192
- logger.error(f"خطأ في الحصول على المشروع: {str(e)}")
193
- return None
194
-
195
- @staticmethod
196
- def get_all(db):
197
- """الحصول على جميع المشاريع"""
198
- try:
199
- query = "SELECT * FROM projects"
200
- results = db.fetch_all(query)
201
-
202
- projects = []
203
- for result in results:
204
- project = Project()
205
- project.id = result[0]
206
- project.name = result[1]
207
- project.client = result[2]
208
- project.description = result[3]
209
- project.start_date = result[4]
210
- project.end_date = result[5]
211
- project.status = result[6]
212
- project.created_by = result[7]
213
- project.created_at = result[8]
214
- project.updated_at = result[9]
215
-
216
- projects.append(project)
217
-
218
- return projects
219
- except Exception as e:
220
- logger.error(f"خطأ في الحصول على المشاريع: {str(e)}")
221
- return []
222
-
223
- def save(self, db):
224
- """حفظ المشروع"""
225
- try:
226
- if self.id:
227
- # تحديث مشروع موجود
228
- data = {
229
- 'name': self.name,
230
- 'client': self.client,
231
- 'description': self.description,
232
- 'start_date': self.start_date,
233
- 'end_date': self.end_date,
234
- 'status': self.status,
235
- 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
236
- }
237
-
238
- db.update('projects', data, f"id = {self.id}")
239
- return self.id
240
- else:
241
- # إنشاء مشروع جديد
242
- data = {
243
- 'name': self.name,
244
- 'client': self.client,
245
- 'description': self.description,
246
- 'start_date': self.start_date,
247
- 'end_date': self.end_date,
248
- 'status': self.status,
249
- 'created_by': self.created_by
250
- }
251
-
252
- self.id = db.insert('projects', data)
253
- return self.id
254
- except Exception as e:
255
- logger.error(f"خطأ في حفظ المشروع: {str(e)}")
256
- return None
257
-
258
- def delete(self, db):
259
- """حذف المشروع"""
260
- try:
261
- if self.id:
262
- db.delete('projects', f"id = {self.id}")
263
- return True
264
-
265
- return False
266
- except Exception as e:
267
- logger.error(f"خطأ في حذف المشروع: {str(e)}")
268
- return False
269
-
270
-
271
- class Document:
272
- """نموذج المستند"""
273
-
274
- def __init__(self, id=None, project_id=None, name=None, file_path=None, document_type=None, description=None, uploaded_by=None):
275
- """تهيئة نموذج المستند"""
276
- self.id = id
277
- self.project_id = project_id
278
- self.name = name
279
- self.file_path = file_path
280
- self.document_type = document_type
281
- self.description = description
282
- self.uploaded_by = uploaded_by
283
- self.uploaded_at = None
284
-
285
- @staticmethod
286
- def get_by_id(document_id, db):
287
- """الحصول على المستند بواس��ة المعرف"""
288
- try:
289
- query = "SELECT * FROM documents WHERE id = ?"
290
- result = db.fetch_one(query, (document_id,))
291
-
292
- if result:
293
- document = Document()
294
- document.id = result[0]
295
- document.project_id = result[1]
296
- document.name = result[2]
297
- document.file_path = result[3]
298
- document.document_type = result[4]
299
- document.description = result[5]
300
- document.uploaded_by = result[6]
301
- document.uploaded_at = result[7]
302
-
303
- return document
304
-
305
- return None
306
- except Exception as e:
307
- logger.error(f"خطأ في الحصول على المستند: {str(e)}")
308
- return None
309
-
310
- @staticmethod
311
- def get_by_project(project_id, db):
312
- """الحصول على المستندات بواسطة معرف المشروع"""
313
- try:
314
- query = "SELECT * FROM documents WHERE project_id = ?"
315
- results = db.fetch_all(query, (project_id,))
316
-
317
- documents = []
318
- for result in results:
319
- document = Document()
320
- document.id = result[0]
321
- document.project_id = result[1]
322
- document.name = result[2]
323
- document.file_path = result[3]
324
- document.document_type = result[4]
325
- document.description = result[5]
326
- document.uploaded_by = result[6]
327
- document.uploaded_at = result[7]
328
-
329
- documents.append(document)
330
-
331
- return documents
332
- except Exception as e:
333
- logger.error(f"خطأ في الحصول على المستندات: {str(e)}")
334
- return []
335
-
336
- def save(self, db):
337
- """حفظ المستند"""
338
- try:
339
- if self.id:
340
- # تحديث مستند موجود
341
- data = {
342
- 'project_id': self.project_id,
343
- 'name': self.name,
344
- 'file_path': self.file_path,
345
- 'document_type': self.document_type,
346
- 'description': self.description
347
- }
348
-
349
- db.update('documents', data, f"id = {self.id}")
350
- return self.id
351
- else:
352
- # إنشاء مستند جديد
353
- data = {
354
- 'project_id': self.project_id,
355
- 'name': self.name,
356
- 'file_path': self.file_path,
357
- 'document_type': self.document_type,
358
- 'description': self.description,
359
- 'uploaded_by': self.uploaded_by
360
- }
361
-
362
- self.id = db.insert('documents', data)
363
- return self.id
364
- except Exception as e:
365
- logger.error(f"خطأ في حفظ المستند: {str(e)}")
366
- return None
367
-
368
- def delete(self, db):
369
- """حذف المستند"""
370
- try:
371
- if self.id:
372
- db.delete('documents', f"id = {self.id}")
373
- return True
374
-
375
- return False
376
- except Exception as e:
377
- logger.error(f"خطأ في حذف المستند: {str(e)}")
378
- return False
379
-
380
-
381
- class PricingItem:
382
- """نموذج بند التسعير"""
383
-
384
- def __init__(self, id=None, project_id=None, item_number=None, description=None, unit=None, quantity=None, unit_price=None, total_price=None, created_by=None):
385
- """تهيئة نموذج بند التسعير"""
386
- self.id = id
387
- self.project_id = project_id
388
- self.item_number = item_number
389
- self.description = description
390
- self.unit = unit
391
- self.quantity = quantity
392
- self.unit_price = unit_price
393
- self.total_price = total_price
394
- self.created_by = created_by
395
- self.created_at = None
396
- self.updated_at = None
397
-
398
- @staticmethod
399
- def get_by_project(project_id, db):
400
- """الحصول على بنود التسعير بواسطة معرف المشروع"""
401
- try:
402
- query = "SELECT * FROM pricing_items WHERE project_id = ?"
403
- results = db.fetch_all(query, (project_id,))
404
-
405
- items = []
406
- for result in results:
407
- item = PricingItem()
408
- item.id = result[0]
409
- item.project_id = result[1]
410
- item.item_number = result[2]
411
- item.description = result[3]
412
- item.unit = result[4]
413
- item.quantity = result[5]
414
- item.unit_price = result[6]
415
- item.total_price = result[7]
416
- item.created_by = result[8]
417
- item.created_at = result[9]
418
- item.updated_at = result[10]
419
-
420
- items.append(item)
421
-
422
- return items
423
- except Exception as e:
424
- logger.error(f"خطأ في الحصول على بنود التسعير: {str(e)}")
425
- return []
426
-
427
- def save(self, db):
428
- """حفظ بند التسعير"""
429
- try:
430
- if self.id:
431
- # تحديث بند موجود
432
- data = {
433
- 'project_id': self.project_id,
434
- 'item_number': self.item_number,
435
- 'description': self.description,
436
- 'unit': self.unit,
437
- 'quantity': self.quantity,
438
- 'unit_price': self.unit_price,
439
- 'total_price': self.total_price,
440
- 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
441
- }
442
-
443
- db.update('pricing_items', data, f"id = {self.id}")
444
- return self.id
445
- else:
446
- # إنشاء بند جديد
447
- data = {
448
- 'project_id': self.project_id,
449
- 'item_number': self.item_number,
450
- 'description': self.description,
451
- 'unit': self.unit,
452
- 'quantity': self.quantity,
453
- 'unit_price': self.unit_price,
454
- 'total_price': self.total_price,
455
- 'created_by': self.created_by
456
- }
457
-
458
- self.id = db.insert('pricing_items', data)
459
- return self.id
460
- except Exception as e:
461
- logger.error(f"خطأ في حفظ بند التسعير: {str(e)}")
462
- return None
463
-
464
-
465
- class Risk:
466
- """نموذج المخاطرة"""
467
-
468
- def __init__(self, id=None, project_id=None, name=None, category=None, probability=None, impact=None, risk_level=None, mitigation_strategy=None, created_by=None):
469
- """تهيئة نموذج المخاطرة"""
470
- self.id = id
471
- self.project_id = project_id
472
- self.name = name
473
- self.category = category
474
- self.probability = probability
475
- self.impact = impact
476
- self.risk_level = risk_level
477
- self.mitigation_strategy = mitigation_strategy
478
- self.created_by = created_by
479
- self.created_at = None
480
- self.updated_at = None
481
-
482
- @staticmethod
483
- def get_by_project(project_id, db):
484
- """الحصول على المخاطر بواسطة معرف المشروع"""
485
- try:
486
- query = "SELECT * FROM risks WHERE project_id = ?"
487
- results = db.fetch_all(query, (project_id,))
488
-
489
- risks = []
490
- for result in results:
491
- risk = Risk()
492
- risk.id = result[0]
493
- risk.project_id = result[1]
494
- risk.name = result[2]
495
- risk.category = result[3]
496
- risk.probability = result[4]
497
- risk.impact = result[5]
498
- risk.risk_level = result[6]
499
- risk.mitigation_strategy = result[7]
500
- risk.created_by = result[8]
501
- risk.created_at = result[9]
502
- risk.updated_at = result[10]
503
-
504
- risks.append(risk)
505
-
506
- return risks
507
- except Exception as e:
508
- logger.error(f"خطأ في الحصول على المخاطر: {str(e)}")
509
- return []
510
-
511
- def save(self, db):
512
- """حفظ المخاطرة"""
513
- try:
514
- if self.id:
515
- # تحديث مخاطرة موجودة
516
- data = {
517
- 'project_id': self.project_id,
518
- 'name': self.name,
519
- 'category': self.category,
520
- 'probability': self.probability,
521
- 'impact': self.impact,
522
- 'risk_level': self.risk_level,
523
- 'mitigation_strategy': self.mitigation_strategy,
524
- 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
525
- }
526
-
527
- db.update('risks', data, f"id = {self.id}")
528
- return self.id
529
- else:
530
- # إنشاء مخاطرة جديدة
531
- data = {
532
- 'project_id': self.project_id,
533
- 'name': self.name,
534
- 'category': self.category,
535
- 'probability': self.probability,
536
- 'impact': self.impact,
537
- 'risk_level': self.risk_level,
538
- 'mitigation_strategy': self.mitigation_strategy,
539
- 'created_by': self.created_by
540
- }
541
-
542
- self.id = db.insert('risks', data)
543
- return self.id
544
- except Exception as e:
545
- logger.error(f"خطأ في حفظ المخاطرة: {str(e)}")
546
- return None
547
-
548
-
549
- class Report:
550
- """نموذج التقرير"""
551
-
552
- def __init__(self, id=None, name=None, project_id=None, report_type=None, period=None, file_path=None, created_by=None, status=None):
553
- """تهيئة نموذج التقرير"""
554
- self.id = id
555
- self.name = name
556
- self.project_id = project_id
557
- self.report_type = report_type
558
- self.period = period
559
- self.file_path = file_path
560
- self.created_by = created_by
561
- self.status = status
562
- self.created_at = None
563
- self.updated_at = None
564
-
565
- @staticmethod
566
- def get_by_project(project_id, db):
567
- """الحصول على التقارير بواسطة معرف المشروع"""
568
- try:
569
- query = "SELECT * FROM reports WHERE project_id = ?"
570
- results = db.fetch_all(query, (project_id,))
571
-
572
- reports = []
573
- for result in results:
574
- report = Report()
575
- report.id = result[0]
576
- report.name = result[1]
577
- report.project_id = result[2]
578
- report.report_type = result[3]
579
- report.period = result[4]
580
- report.file_path = result[5]
581
- report.created_by = result[6]
582
- report.status = result[7]
583
- report.created_at = result[8]
584
- report.updated_at = result[9]
585
-
586
- reports.append(report)
587
-
588
- return reports
589
- except Exception as e:
590
- logger.error(f"خطأ في الحصول على التقارير: {str(e)}")
591
- return []
592
-
593
- def save(self, db):
594
- """حفظ التقرير"""
595
- try:
596
- if self.id:
597
- # تحديث تقرير موجود
598
- data = {
599
- 'name': self.name,
600
- 'project_id': self.project_id,
601
- 'report_type': self.report_type,
602
- 'period': self.period,
603
- 'file_path': self.file_path,
604
- 'status': self.status,
605
- 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
606
- }
607
-
608
- db.update('reports', data, f"id = {self.id}")
609
- return self.id
610
- else:
611
- # إنشاء تقرير جديد
612
- data = {
613
- 'name': self.name,
614
- 'project_id': self.project_id,
615
- 'report_type': self.report_type,
616
- 'period': self.period,
617
- 'file_path': self.file_path,
618
- 'created_by': self.created_by,
619
- 'status': self.status
620
- }
621
-
622
- self.id = db.insert('reports', data)
623
- return self.id
624
- except Exception as e:
625
- logger.error(f"خطأ في حفظ التقرير: {str(e)}")
626
- return None
 
1
+ """
2
+ نماذج بيانات النظام
3
+ """
4
+
5
+ from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Boolean, Text, Table, Enum
6
+ from sqlalchemy.orm import relationship
7
+ from datetime import datetime
8
+ import enum
9
+
10
+ from database.db_connector import Base
11
+
12
+
13
+ # جدول العلاقة متعددة القيم بين المشاريع والملفات
14
+ project_files = Table(
15
+ 'project_files',
16
+ Base.metadata,
17
+ Column('project_id', Integer, ForeignKey('projects.id'), primary_key=True),
18
+ Column('file_id', Integer, ForeignKey('files.id'), primary_key=True)
19
+ )
20
+
21
+ # تعريف الأنواع المدرجة
22
+ class ProjectStatus(enum.Enum):
23
+ """حالة المشروع"""
24
+ NEW = "جديد"
25
+ PRICING = "قيد التسعير"
26
+ SUBMITTED = "تم التقديم"
27
+ AWARDED = "تمت الترسية"
28
+ EXECUTION = "قيد التنفيذ"
29
+ COMPLETED = "منتهي"
30
+ CANCELLED = "ملغي"
31
+
32
+ class TenderType(enum.Enum):
33
+ """نوع المناقصة"""
34
+ PUBLIC = "عامة"
35
+ PRIVATE = "خاصة"
36
+ DIRECT = "أمر مباشر"
37
+
38
+ class PricingMethod(enum.Enum):
39
+ """طريقة التسعير"""
40
+ STANDARD = "قياسي"
41
+ UNBALANCED = "غير متزن"
42
+ COMPETITIVE = "تنافسي"
43
+ PROFITABILITY = "موجه بالربحية"
44
+
45
+ # نموذج المستخدم
46
+ class User(Base):
47
+ """نموذج بيانات المستخدم"""
48
+ __tablename__ = 'users'
49
+
50
+ id = Column(Integer, primary_key=True)
51
+ username = Column(String(50), unique=True, nullable=False)
52
+ password_hash = Column(String(128), nullable=False)
53
+ full_name = Column(String(100), nullable=False)
54
+ email = Column(String(100), unique=True, nullable=False)
55
+ phone = Column(String(20))
56
+ role = Column(String(20), nullable=False)
57
+ department = Column(String(50))
58
+ is_active = Column(Boolean, default=True)
59
+ created_at = Column(DateTime, default=datetime.now)
60
+ last_login = Column(DateTime)
61
+
62
+ # العلاقات
63
+ projects = relationship("Project", back_populates="created_by")
64
+ pricing_items = relationship("PricingItem", back_populates="created_by")
65
+
66
+ def __repr__(self):
67
+ return f"<User {self.username}>"
68
+
69
+ # نموذج المشروع
70
+ class Project(Base):
71
+ """نموذج بيانات المشروع"""
72
+ __tablename__ = 'projects'
73
+
74
+ id = Column(Integer, primary_key=True)
75
+ name = Column(String(100), nullable=False)
76
+ tender_number = Column(String(50))
77
+ client = Column(String(100), nullable=False)
78
+ location = Column(String(100))
79
+ description = Column(Text)
80
+ status = Column(Enum(ProjectStatus), default=ProjectStatus.NEW)
81
+ tender_type = Column(Enum(TenderType), default=TenderType.PUBLIC)
82
+ pricing_method = Column(Enum(PricingMethod), default=PricingMethod.STANDARD)
83
+ submission_date = Column(DateTime)
84
+ created_at = Column(DateTime, default=datetime.now)
85
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
86
+ created_by_id = Column(Integer, ForeignKey('users.id'))
87
+
88
+ # العلاقات
89
+ created_by = relationship("User", back_populates="projects")
90
+ pricing_sections = relationship("PricingSection", back_populates="project", cascade="all, delete-orphan")
91
+ pricing_items = relationship("PricingItem", back_populates="project", cascade="all, delete-orphan")
92
+ local_content_items = relationship("LocalContentItem", back_populates="project", cascade="all, delete-orphan")
93
+ risk_items = relationship("RiskItem", back_populates="project", cascade="all, delete-orphan")
94
+ files = relationship("File", secondary=project_files, back_populates="projects")
95
+
96
+ def __repr__(self):
97
+ return f"<Project {self.name}>"
98
+
99
+ # نموذج قسم التسعير
100
+ class PricingSection(Base):
101
+ """نموذج بيانات قسم التسعير"""
102
+ __tablename__ = 'pricing_sections'
103
+
104
+ id = Column(Integer, primary_key=True)
105
+ project_id = Column(Integer, ForeignKey('projects.id'), nullable=False)
106
+ name = Column(String(100), nullable=False)
107
+ description = Column(Text)
108
+ section_order = Column(Integer, default=0)
109
+ created_at = Column(DateTime, default=datetime.now)
110
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
111
+
112
+ # العلاقات
113
+ project = relationship("Project", back_populates="pricing_sections")
114
+ pricing_items = relationship("PricingItem", back_populates="section", cascade="all, delete-orphan")
115
+
116
+ def __repr__(self):
117
+ return f"<PricingSection {self.name}>"
118
+
119
+ # نموذج بند التسعير
120
+ class PricingItem(Base):
121
+ """نموذج بيانات بند التسعير"""
122
+ __tablename__ = 'pricing_items'
123
+
124
+ id = Column(Integer, primary_key=True)
125
+ project_id = Column(Integer, ForeignKey('projects.id'), nullable=False)
126
+ section_id = Column(Integer, ForeignKey('pricing_sections.id'))
127
+ item_code = Column(String(20))
128
+ description = Column(Text, nullable=False)
129
+ unit = Column(String(20), nullable=False)
130
+ quantity = Column(Float, nullable=False)
131
+ unit_price = Column(Float, default=0)
132
+ unbalanced_price = Column(Float)
133
+ final_price = Column(Float)
134
+ pricing_strategy = Column(String(20), default="متوازن")
135
+ notes = Column(Text)
136
+ created_by_id = Column(Integer, ForeignKey('users.id'))
137
+ created_at = Column(DateTime, default=datetime.now)
138
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
139
+
140
+ # العلاقات
141
+ project = relationship("Project", back_populates="pricing_items")
142
+ section = relationship("PricingSection", back_populates="pricing_items")
143
+ created_by = relationship("User", back_populates="pricing_items")
144
+ resource_usages = relationship("ResourceUsage", back_populates="pricing_item")
145
+
146
+ def __repr__(self):
147
+ return f"<PricingItem {self.item_code}: {self.description[:30]}>"
148
+
149
+ # نموذج بند المحتوى المحلي
150
+ class LocalContentItem(Base):
151
+ """نموذج بيانات بند المحتوى المحلي"""
152
+ __tablename__ = 'local_content_items'
153
+
154
+ id = Column(Integer, primary_key=True)
155
+ project_id = Column(Integer, ForeignKey('projects.id'), nullable=False)
156
+ category = Column(String(50), nullable=False)
157
+ item_name = Column(String(100), nullable=False)
158
+ supplier_id = Column(Integer, ForeignKey('suppliers.id'))
159
+ total_cost = Column(Float, default=0)
160
+ local_percentage = Column(Float, default=0)
161
+ notes = Column(Text)
162
+ created_at = Column(DateTime, default=datetime.now)
163
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
164
+
165
+ # العلاقات
166
+ project = relationship("Project", back_populates="local_content_items")
167
+ supplier = relationship("Supplier", back_populates="local_content_items")
168
+
169
+ def __repr__(self):
170
+ return f"<LocalContentItem {self.item_name}>"
171
+
172
+ # نموذج المورد
173
+ class Supplier(Base):
174
+ """نموذج بيانات المور��"""
175
+ __tablename__ = 'suppliers'
176
+
177
+ id = Column(Integer, primary_key=True)
178
+ name = Column(String(100), nullable=False)
179
+ contact_person = Column(String(100))
180
+ phone = Column(String(20))
181
+ email = Column(String(100))
182
+ address = Column(String(200))
183
+ category = Column(String(50))
184
+ is_local = Column(Boolean, default=False)
185
+ local_content_percentage = Column(Float, default=0)
186
+ created_at = Column(DateTime, default=datetime.now)
187
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
188
+
189
+ # العلاقات
190
+ local_content_items = relationship("LocalContentItem", back_populates="supplier")
191
+ resources = relationship("Resource", back_populates="supplier")
192
+
193
+ def __repr__(self):
194
+ return f"<Supplier {self.name}>"
195
+
196
+ # نموذج المخاطرة
197
+ class RiskItem(Base):
198
+ """نموذج بيانات المخاطرة"""
199
+ __tablename__ = 'risk_items'
200
+
201
+ id = Column(Integer, primary_key=True)
202
+ project_id = Column(Integer, ForeignKey('projects.id'), nullable=False)
203
+ risk_code = Column(String(20))
204
+ description = Column(Text, nullable=False)
205
+ category = Column(String(50), nullable=False)
206
+ impact = Column(String(20), nullable=False)
207
+ probability = Column(String(20), nullable=False)
208
+ mitigation_strategy = Column(Text)
209
+ created_at = Column(DateTime, default=datetime.now)
210
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
211
+
212
+ # العلاقات
213
+ project = relationship("Project", back_populates="risk_items")
214
+
215
+ def __repr__(self):
216
+ return f"<RiskItem {self.risk_code}: {self.description[:30]}>"
217
+
218
+ # نموذج المورد
219
+ class Resource(Base):
220
+ """نموذج بيانات المورد"""
221
+ __tablename__ = 'resources'
222
+
223
+ id = Column(Integer, primary_key=True)
224
+ code = Column(String(20), unique=True)
225
+ name = Column(String(100), nullable=False)
226
+ description = Column(Text)
227
+ category = Column(String(50), nullable=False)
228
+ unit = Column(String(20), nullable=False)
229
+ unit_price = Column(Float, default=0)
230
+ supplier_id = Column(Integer, ForeignKey('suppliers.id'))
231
+ is_local = Column(Boolean, default=False)
232
+ local_content_percentage = Column(Float, default=0)
233
+ created_at = Column(DateTime, default=datetime.now)
234
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
235
+
236
+ # العلاقات
237
+ supplier = relationship("Supplier", back_populates="resources")
238
+ resource_usages = relationship("ResourceUsage", back_populates="resource")
239
+
240
+ def __repr__(self):
241
+ return f"<Resource {self.code}: {self.name}>"
242
+
243
+ # نموذج استخدام المورد
244
+ class ResourceUsage(Base):
245
+ """نموذج بيانات استخدام المورد"""
246
+ __tablename__ = 'resource_usages'
247
+
248
+ id = Column(Integer, primary_key=True)
249
+ pricing_item_id = Column(Integer, ForeignKey('pricing_items.id'), nullable=False)
250
+ resource_id = Column(Integer, ForeignKey('resources.id'), nullable=False)
251
+ quantity = Column(Float, nullable=False)
252
+ created_at = Column(DateTime, default=datetime.now)
253
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
254
+
255
+ # العلاقات
256
+ pricing_item = relationship("PricingItem", back_populates="resource_usages")
257
+ resource = relationship("Resource", back_populates="resource_usages")
258
+
259
+ def __repr__(self):
260
+ return f"<ResourceUsage {self.pricing_item_id} - {self.resource_id}>"
261
+
262
+ # نموذج الملف
263
+ class File(Base):
264
+ """نموذج بيانات الملف"""
265
+ __tablename__ = 'files'
266
+
267
+ id = Column(Integer, primary_key=True)
268
+ filename = Column(String(100), nullable=False)
269
+ original_filename = Column(String(100), nullable=False)
270
+ file_type = Column(String(20), nullable=False)
271
+ file_size = Column(Integer, nullable=False)
272
+ file_path = Column(String(255), nullable=False)
273
+ upload_date = Column(DateTime, default=datetime.now)
274
+
275
+ # العلاقات
276
+ projects = relationship("Project", secondary=project_files, back_populates="files")
277
+
278
+ def __repr__(self):
279
+ return f"<File {self.original_filename}>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demo_pricing.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import plotly.express as px
6
+ import plotly.graph_objects as go
7
+
8
+ # ملاحظة: تم نقل إعداد الصفحة إلى ملف app.py الرئيسي
9
+ # لتجنب أخطاء set_page_config يجب أن يكون في ملف واحد فقط
10
+
11
+ st.title("عرض تحسينات واجهة المستخدم")
12
+
13
+ # بيانات تجريبية للعرض
14
+ @st.cache_data
15
+ def get_sample_data():
16
+ items = pd.DataFrame({
17
+ 'رقم البند': ['UB1', 'UB2', 'UB3', 'UB4', 'UB5'],
18
+ 'وصف البند': ['حفر أساسات', 'صب خرسانة مسلحة', 'أعمال طوب', 'أعمال تشطيبات', 'أعمال كهرباء'],
19
+ 'الوحدة': ['م3', 'م3', 'م2', 'م2', 'نقطة'],
20
+ 'الكمية': [350.0, 120.0, 500.0, 800.0, 150.0],
21
+ 'سعر الوحدة': [80.0, 950.0, 45.0, 120.0, 90.0],
22
+ 'الإجمالي': [28000.0, 114000.0, 22500.0, 96000.0, 13500.0],
23
+ 'إستراتيجية التسعير': ['نقص', 'زيادة', 'متوازن', 'زيادة', 'نقص']
24
+ })
25
+ return items
26
+
27
+ items = get_sample_data()
28
+
29
+ # 1. عرض الجدول مع تنسيق محسن
30
+ st.markdown("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>بنود التسعير غير المتوازن</h3>", unsafe_allow_html=True)
31
+
32
+ # تعيين ألوان للإستراتيجيات وتنسيق الجدول بشكل متقدم
33
+ def highlight_row(row):
34
+ strategy = row['إستراتيجية التسعير']
35
+ styles = [''] * len(row)
36
+
37
+ # تطبيق لون خلفية لكل صف حسب الإستراتيجية
38
+ if strategy == 'زيادة':
39
+ background = 'linear-gradient(90deg, rgba(168, 230, 207, 0.3), rgba(168, 230, 207, 0.1))'
40
+ text_color = '#1F7A8C'
41
+ elif strategy == 'نقص':
42
+ background = 'linear-gradient(90deg, rgba(255, 154, 162, 0.3), rgba(255, 154, 162, 0.1))'
43
+ text_color = '#9D2A45'
44
+ else:
45
+ background = 'linear-gradient(90deg, rgba(220, 237, 255, 0.3), rgba(220, 237, 255, 0.1))'
46
+ text_color = '#555555'
47
+
48
+ # تطبيق النمط على جميع الخلايا في الصف
49
+ for i in range(len(styles)):
50
+ styles[i] = f'background: {background}; color: {text_color}; border-bottom: 1px solid #ddd;'
51
+
52
+ # تطبيق نمط خاص على خلية الإستراتيجية
53
+ if strategy == 'زيادة':
54
+ styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #a8e6cf; color: #007263; font-weight: bold; border-radius: 5px; text-align: center;'
55
+ elif strategy == 'نقص':
56
+ styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #ff9aa2; color: #9D2A45; font-weight: bold; border-radius: 5px; text-align: center;'
57
+ else:
58
+ styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #dceeff; color: #555555; font-weight: bold; border-radius: 5px; text-align: center;'
59
+
60
+ # تنسيق عمود السعر
61
+ price_idx = list(row.index).index('سعر الوحدة')
62
+ styles[price_idx] = styles[price_idx] + 'font-weight: bold;'
63
+
64
+ # تنسيق عمود الإجمالي
65
+ total_idx = list(row.index).index('الإجمالي')
66
+ styles[total_idx] = styles[total_idx] + 'font-weight: bold;'
67
+
68
+ return styles
69
+
70
+ # تطبيق التنسيق على الجدول
71
+ styled_items = items.style.apply(highlight_row, axis=1)
72
+
73
+ # تنسيق تنسيق الأرقام
74
+ styled_items = styled_items.format({
75
+ 'الكمية': '{:,.2f}',
76
+ 'سعر الوحدة': '{:,.2f}',
77
+ 'الإجمالي': '{:,.2f}'
78
+ })
79
+
80
+ st.dataframe(styled_items, use_container_width=True, height=None)
81
+
82
+ # 2. عرض المقارنة مع تصميم محسن
83
+ st.markdown("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>مقارنة التسعير المتوازن وغير المتوازن</h3>", unsafe_allow_html=True)
84
+
85
+ # بيانات المقارنة
86
+ original_items = items.copy()
87
+ original_items['سعر الوحدة'] = [70.0, 820.0, 45.0, 100.0, 110.0]
88
+ original_items['الإجمالي'] = original_items['الكمية'] * original_items['سعر الوحدة']
89
+
90
+ original_total = original_items['الإجمالي'].sum()
91
+ unbalanced_total = items['الإجمالي'].sum()
92
+
93
+ # عرض بطاقات المقارنة بتصميم متقدم
94
+ st.markdown("""
95
+ <style>
96
+ .metric-container {
97
+ background: linear-gradient(to right, #f1f8ff, #ffffff);
98
+ border-radius: 10px;
99
+ padding: 15px;
100
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
101
+ text-align: center;
102
+ border: 1px solid #e6f2ff;
103
+ }
104
+ .metric-title {
105
+ color: #555;
106
+ font-size: 0.9em;
107
+ margin-bottom: 5px;
108
+ }
109
+ .metric-value {
110
+ color: #1F7A8C;
111
+ font-size: 1.8em;
112
+ font-weight: bold;
113
+ margin: 5px 0;
114
+ }
115
+ .metric-delta {
116
+ font-size: 0.9em;
117
+ font-weight: bold;
118
+ padding: 3px 8px;
119
+ border-radius: 10px;
120
+ display: inline-block;
121
+ margin-top: 5px;
122
+ }
123
+ .positive-delta {
124
+ background-color: rgba(40, 167, 69, 0.1);
125
+ color: #28a745;
126
+ }
127
+ .negative-delta {
128
+ background-color: rgba(220, 53, 69, 0.1);
129
+ color: #dc3545;
130
+ }
131
+ .neutral-delta {
132
+ background-color: rgba(108, 117, 125, 0.1);
133
+ color: #6c757d;
134
+ }
135
+ </style>
136
+ """, unsafe_allow_html=True)
137
+
138
+ col1, col2, col3 = st.columns(3)
139
+
140
+ with col1:
141
+ st.markdown("""
142
+ <div class="metric-container">
143
+ <div class="metric-title">إجمالي التسعير المتوازن</div>
144
+ <div class="metric-value">{:,.2f} ريال</div>
145
+ <div class="metric-delta neutral-delta">التسعير الأصلي</div>
146
+ </div>
147
+ """.format(original_total), unsafe_allow_html=True)
148
+
149
+ with col2:
150
+ st.markdown("""
151
+ <div class="metric-container">
152
+ <div class="metric-title">إجمالي التسعير غير المتوازن</div>
153
+ <div class="metric-value">{:,.2f} ريال</div>
154
+ <div class="metric-delta {}">بعد إعادة توزيع الأسعار</div>
155
+ </div>
156
+ """.format(
157
+ unbalanced_total,
158
+ "positive-delta" if unbalanced_total > original_total else "negative-delta" if unbalanced_total < original_total else "neutral-delta"
159
+ ), unsafe_allow_html=True)
160
+
161
+ with col3:
162
+ diff = unbalanced_total - original_total
163
+ delta_percent = diff/original_total*100 if original_total > 0 else 0
164
+
165
+ st.markdown("""
166
+ <div class="metric-container">
167
+ <div class="metric-title">الفرق بين التسعيرين</div>
168
+ <div class="metric-value">{:,.2f} ريال</div>
169
+ <div class="metric-delta {}">نسبة الفرق: {:+.1f}%</div>
170
+ </div>
171
+ """.format(
172
+ diff,
173
+ "positive-delta" if diff > 0 else "negative-delta" if diff < 0 else "neutral-delta",
174
+ delta_percent
175
+ ), unsafe_allow_html=True)
176
+
177
+ # 3. رسم بياني للمقارنة
178
+ st.markdown("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>تحليل بصري للتسعير غير المتوازن</h3>", unsafe_allow_html=True)
179
+
180
+ # إعداد البيانات للرسم البياني
181
+ chart_data = pd.DataFrame({
182
+ 'وصف البند': original_items['وصف البند'],
183
+ 'التسعير المتوازن': original_items['الإجمالي'],
184
+ 'التسعير غير المتوازن': items['الإجمالي']
185
+ })
186
+
187
+ # إضافة عمود للنسبة المئوية للتغيير
188
+ chart_data['نسبة التغيير'] = (chart_data['التسعير غير المتوازن'] - chart_data['التسعير المتوازن']) / chart_data['التسعير المتوازن'] * 100
189
+
190
+ # تحديد لون الأعمدة بناءً على نسبة التغيير
191
+ bar_colors = []
192
+ for change in chart_data['نسبة التغيير']:
193
+ if change > 5: # زيادة كبيرة
194
+ bar_colors.append('#1F7A8C') # أزرق مخضر
195
+ elif change > 0: # زيادة صغيرة
196
+ bar_colors.append('#81B29A') # أخضر فاتح
197
+ elif change > -5: # نقص صغير
198
+ bar_colors.append('#F2CC8F') # أصفر
199
+ else: # نقص كبير
200
+ bar_colors.append('#E07A5F') # أحمر
201
+
202
+ # التبويب بين مخططات مختلفة للمقارنة
203
+ chart_tabs = st.tabs(["مخطط شريطي", "مخطط مقارنة", "مخطط نسبة التغيير"])
204
+
205
+ with chart_tabs[0]: # رسم بياني شريطي
206
+ # رسم بياني شريطي للمقارنة
207
+ fig = go.Figure()
208
+
209
+ fig.add_trace(go.Bar(
210
+ x=chart_data['وصف البند'],
211
+ y=chart_data['التسعير المتوازن'],
212
+ name='التسعير المتوازن',
213
+ marker_color='rgba(55, 83, 109, 0.7)'
214
+ ))
215
+
216
+ fig.add_trace(go.Bar(
217
+ x=chart_data['وصف البند'],
218
+ y=chart_data['التسعير غير المتوازن'],
219
+ name='التسعير غير المتوازن',
220
+ marker_color=bar_colors
221
+ ))
222
+
223
+ fig.update_layout(
224
+ title='مقارنة بين التسعير المتوازن وغير المتوازن',
225
+ xaxis_tickfont_size=14,
226
+ yaxis=dict(
227
+ title='الإجمالي (ريال)',
228
+ titlefont_size=16,
229
+ tickfont_size=14,
230
+ ),
231
+ legend=dict(
232
+ x=0.01,
233
+ y=0.99,
234
+ bgcolor='rgba(255, 255, 255, 0.8)',
235
+ bordercolor='rgba(0, 0, 0, 0.1)',
236
+ borderwidth=1
237
+ ),
238
+ barmode='group',
239
+ bargap=0.15,
240
+ bargroupgap=0.1,
241
+ plot_bgcolor='rgba(240, 249, 255, 0.5)',
242
+ margin=dict(t=50, b=50, l=20, r=20)
243
+ )
244
+
245
+ st.plotly_chart(fig, use_container_width=True)
246
+
247
+ with chart_tabs[1]: # رسم مقارنة
248
+ # رسم مقارنة بين التسعيرين
249
+ fig = go.Figure()
250
+
251
+ # إضافة خط للتسعير المتوازن
252
+ fig.add_trace(go.Scatter(
253
+ x=chart_data['و��ف البند'],
254
+ y=chart_data['التسعير المتوازن'],
255
+ name='التسعير المتوازن',
256
+ mode='lines+markers',
257
+ line=dict(color='rgb(55, 83, 109)', width=3),
258
+ marker=dict(size=10, color='rgb(55, 83, 109)')
259
+ ))
260
+
261
+ # إضافة نقاط للتسعير غير المتوازن
262
+ fig.add_trace(go.Scatter(
263
+ x=chart_data['وصف البند'],
264
+ y=chart_data['التسعير غير المتوازن'],
265
+ name='التسعير غير المتوازن',
266
+ mode='lines+markers',
267
+ line=dict(color='rgb(26, 118, 255)', width=3),
268
+ marker=dict(
269
+ size=12,
270
+ color=bar_colors,
271
+ line=dict(width=2, color='white')
272
+ )
273
+ ))
274
+
275
+ # تحديثات التخطيط
276
+ fig.update_layout(
277
+ title='مقارنة مرئية بين استراتيجيات التسعير',
278
+ xaxis_tickfont_size=14,
279
+ yaxis=dict(
280
+ title='القيمة الإجمالية (ريال)',
281
+ titlefont_size=16,
282
+ tickfont_size=14,
283
+ gridcolor='rgba(200, 200, 200, 0.2)'
284
+ ),
285
+ legend=dict(
286
+ x=0.01,
287
+ y=0.99,
288
+ bgcolor='rgba(255, 255, 255, 0.8)',
289
+ bordercolor='rgba(0, 0, 0, 0.1)',
290
+ borderwidth=1
291
+ ),
292
+ plot_bgcolor='rgba(240, 249, 255, 0.5)',
293
+ margin=dict(t=50, b=50, l=20, r=20)
294
+ )
295
+
296
+ st.plotly_chart(fig, use_container_width=True)
297
+
298
+ with chart_tabs[2]: # مخطط نسبة التغيير
299
+ # مخطط للنسبة المئوية للتغيير
300
+ fig = go.Figure()
301
+
302
+ # إضافة أعمدة لنسبة التغيير مع ألوان مختلفة حسب القيمة
303
+ fig.add_trace(go.Bar(
304
+ x=chart_data['وصف البند'],
305
+ y=chart_data['نسبة التغيير'],
306
+ name='نسبة التغيير',
307
+ marker_color=bar_colors,
308
+ text=[f"{val:.1f}%" for val in chart_data['نسبة التغيير']],
309
+ textposition='auto'
310
+ ))
311
+
312
+ # إضافة خط أفقي عند الصفر
313
+ fig.add_shape(
314
+ type="line",
315
+ x0=-0.5,
316
+ y0=0,
317
+ x1=len(chart_data['وصف البند'])-0.5,
318
+ y1=0,
319
+ line=dict(
320
+ color="black",
321
+ width=2,
322
+ dash="dash",
323
+ )
324
+ )
325
+
326
+ # تحديثات التخطيط
327
+ fig.update_layout(
328
+ title='نسبة التغيير في أسعار البنود (%)',
329
+ xaxis_tickfont_size=14,
330
+ yaxis=dict(
331
+ title='نسبة التغيير (%)',
332
+ titlefont_size=16,
333
+ tickfont_size=14,
334
+ gridcolor='rgba(200, 200, 200, 0.2)',
335
+ zeroline=True,
336
+ zerolinecolor='black',
337
+ zerolinewidth=2
338
+ ),
339
+ plot_bgcolor='rgba(240, 249, 255, 0.5)',
340
+ margin=dict(t=50, b=50, l=20, r=20)
341
+ )
342
+
343
+ st.plotly_chart(fig, use_container_width=True)
344
+
345
+ # إضافة جدول مع نسب التغيير
346
+ st.markdown("#### جدول مفصل بنسب التغيير")
347
+
348
+ # إعداد بيانات الجدول
349
+ table_data = chart_data[['وصف البند', 'التسعير المتوازن', 'التسعير غير المتوازن', 'نسبة التغيير']]
350
+
351
+ # تنسيق الجدول
352
+ def highlight_change(row):
353
+ change = row['نسبة التغيير']
354
+ if change > 5:
355
+ return ['', '', '', 'background-color: rgba(31, 122, 140, 0.3); color: #1F7A8C; font-weight: bold;']
356
+ elif change > 0:
357
+ return ['', '', '', 'background-color: rgba(129, 178, 154, 0.3); color: #2A9D8F; font-weight: bold;']
358
+ elif change > -5:
359
+ return ['', '', '', 'background-color: rgba(242, 204, 143, 0.3); color: #BC6C25; font-weight: bold;']
360
+ else:
361
+ return ['', '', '', 'background-color: rgba(224, 122, 95, 0.3); color: #AE2012; font-weight: bold;']
362
+
363
+ # تطبيق التنسيق
364
+ styled_table = table_data.style.apply(highlight_change, axis=1).format({
365
+ 'التسعير المتوازن': '{:,.2f} ريال',
366
+ 'التسعير غير المتوازن': '{:,.2f} ريال',
367
+ 'نسبة التغيير': '{:+.1f}%'
368
+ })
369
+
370
+ st.dataframe(styled_table, use_container_width=True)
371
+
372
+ # 4. أزرار الحفظ والتصدير مع تصميم محسن
373
+ st.markdown("<hr style='margin-top: 30px; margin-bottom: 20px; border-top: 1px solid #ddd;'>", unsafe_allow_html=True)
374
+ st.markdown("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>حفظ وتصدير البيانات</h3>", unsafe_allow_html=True)
375
+
376
+ st.markdown("""
377
+ <style>
378
+ .action-card {
379
+ background: linear-gradient(135deg, #f8f9fa, #e9ecef);
380
+ border-radius: 10px;
381
+ padding: 20px;
382
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
383
+ border-left: 5px solid #1F7A8C;
384
+ transition: all 0.3s ease;
385
+ }
386
+ .action-card:hover {
387
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
388
+ transform: translateY(-2px);
389
+ }
390
+ .action-icon {
391
+ color: #1F7A8C;
392
+ font-size: 24px;
393
+ margin-bottom: 10px;
394
+ }
395
+ .action-title {
396
+ color: #333;
397
+ font-size: 18px;
398
+ font-weight: bold;
399
+ margin-bottom: 10px;
400
+ }
401
+ .action-desc {
402
+ color: #666;
403
+ font-size: 14px;
404
+ margin-bottom: 15px;
405
+ }
406
+ </style>
407
+ """, unsafe_allow_html=True)
408
+
409
+ col1, col2 = st.columns(2)
410
+
411
+ with col1:
412
+ # بطاقة حفظ التسعير
413
+ st.markdown("""
414
+ <div class="action-card">
415
+ <div class="action-icon">💾</div>
416
+ <div class="action-title">حفظ التسعير غير المتوازن</div>
417
+ <div class="action-desc">قم بحفظ التسعير الحالي في المشروع لاستخدامه لاحقاً في التقارير وفي إجمالي التسعير.</div>
418
+ </div>
419
+ """, unsafe_allow_html=True)
420
+
421
+ # زر حفظ التسعير غير المتوازن
422
+ if st.button("حفظ التسعير غير المتوازن", type="primary", use_container_width=True):
423
+ st.success("تم حفظ التسعير غير المتوازن بنجاح!")
424
+ st.balloons() # إضافة تأثير احتفالي عند الحفظ
425
+
426
+ with col2:
427
+ # بطاقة تصدير التسعير
428
+ st.markdown("""
429
+ <div class="action-card">
430
+ <div class="action-icon">📊</div>
431
+ <div class="action-title">تصدير البيانات</div>
432
+ <div class="action-desc">قم بتصدير جدول التسعير الحالي بصيغة CSV لاستخدامه في برامج أخرى مثل Excel.</div>
433
+ </div>
434
+ """, unsafe_allow_html=True)
435
+
436
+ # زر تصدير التسعير
437
+ export_button = st.button("تجهيز ملف للتصدير", use_container_width=True)
438
+ if export_button:
439
+ # تحويل البيانات إلى CSV
440
+ csv = items.to_csv(index=False)
441
+ st.success("تم تجهيز ملف التصدير بنجاح! يمكنك تنزيله الآن.")
442
+ # تقديم البيانات للتنزيل
443
+ st.download_button(
444
+ label="تنزيل ملف CSV",
445
+ data=csv,
446
+ file_name="unbalanced_pricing.csv",
447
+ mime="text/csv",
448
+ use_container_width=True
449
+ )
docs/technical_docs.md ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # التوثيق التقني
2
+ ## نظام تحليل العقود والمناقصات بالذكاء الاصطناعي - شركة شبه الجزيرة للمقاولات
3
+
4
+ <p align="center">
5
+ <img src="../static/images/logo.png" alt="شعار النظام" width="200"/>
6
+ <br>
7
+ <em>إصدار التوثيق: 1.0.2 - تاريخ التحديث: 2025/03/01</em>
8
+ </p>
9
+
10
+ ## جدول المحتويات
11
+
12
+ 1. [نظرة عامة](#نظرة-عامة)
13
+ 2. [المعمارية التقنية](#المعمارية-التقنية)
14
+ 3. [متطلبات النظام](#متطلبات-النظام)
15
+ 4. [الإعداد والتثبيت](#الإعداد-والتثبيت)
16
+ 5. [بيئة Hybrid Face](#بيئة-hybrid-face)
17
+ 6. [هيكل قاعدة البيانات](#هيكل-قاعدة-البيانات)
18
+ 7. [وحدات النظام](#وحدات-النظام)
19
+ 8. [واجهات برمجة التطبيقات (APIs)](#واجهات-برمجة-التطبيقات-apis)
20
+ 9. [الأمان والمصادقة](#الأمان-والمصادقة)
21
+ 10. [الأداء وقابلية التوسع](#الأداء-وقابلية-التوسع)
22
+ 11. [استراتيجية النسخ الاحتياطي واستعادة البيانات](#استراتيجية-النسخ-الاحتياطي-واستعادة-البيانات)
23
+ 12. [إرشادات التطوير](#إرشادات-التطوير)
24
+ 13. [اختبار النظام](#اختبار-النظام)
25
+ 14. [التكامل مع الأنظمة الخارجية](#التكامل-مع-الأنظمة-الخارجية)
26
+ 15. [سجل التغييرات](#سجل-التغييرات)
27
+
28
+ ## نظرة عامة
29
+
30
+ ### عن النظام
31
+
32
+ نظام تحليل العقود والمناقصات بالذكاء الاصطناعي هو منصة متكاملة تعتمد على تقنيات الذكاء الاصطناعي ومعالجة اللغة العربية الطبيعية لمساعدة شركة شبه الجزيرة للمقاولات في تحليل وتسعير المناقصات وإدارة المشاريع.
33
+
34
+ ### المكونات الرئيسية
35
+
36
+ 1. **واجهة المستخدم (Frontend)**: تطبيق ويب تفاعلي مبني بواسطة Streamlit
37
+ 2. **خدمات الخلفية (Backend)**: مجموعة من الخدمات والوحدات البرمجية بلغة Python
38
+ 3. **قاعدة البيانات**: SQLite للتطوير والنشر المحلي، MySQL للنشر المؤسسي
39
+ 4. **محركات الذكاء الاصطناعي**: نماذج معالجة اللغة الطبيعية والتعلم الآلي
40
+ 5. **خدمات التكامل**: واجهات برمجة للتكامل مع الأنظمة الخارجية
41
+
42
+ ## المعمارية التقنية
43
+
44
+ ### المخطط العام للنظام
45
+
46
+ ```mermaid
47
+ graph TD
48
+ User[المستخدم] --> UI[واجهة المستخدم Streamlit]
49
+ UI --> API[طبقة API]
50
+ API --> Core[النواة]
51
+ Core --> DB[(قاعدة البيانات)]
52
+ Core --> NLP[معالجة اللغة العربية]
53
+ Core --> ML[نماذج التعلم الآلي]
54
+ Core --> FS[نظام الملفات]
55
+ Core --> External[أنظمة خارجية]
56
+
57
+ subgraph Core Modules
58
+ NLP
59
+ ML
60
+ Doc[تحليل المستندات]
61
+ Pricing[التسعير]
62
+ Risk[تحليل المخاطر]
63
+ Res[إدارة الموارد]
64
+ Proj[إدارة المشاريع]
65
+ Rep[التقارير]
66
+ end
67
+ ```
68
+
69
+ ### نمط المعمارية
70
+
71
+ النظام يعتمد على نمط المعمارية طبقية (Layered Architecture) ونمط وحدات الخدمة (Service Modules):
72
+
73
+ 1. **طبقة العرض**: واجهة المستخدم Streamlit
74
+ 2. **طبقة الخدمات**: واجهات برمجة التطبيقات RESTful
75
+ 3. **طبقة الأعمال**: وحدات المعالجة المنطقية
76
+ 4. **طبقة البيانات**: الوصول إلى قاعدة البيانات وتخزين الملفات
77
+
78
+ ## متطلبات النظام
79
+
80
+ ### متطلبات الأجهزة
81
+
82
+ | المكون | الحد الأدنى | الموصى به |
83
+ |--------|-------------|-----------|
84
+ | المعالج | Intel Core i5 (8 أنوية) | Intel Core i7 (12 أنوية) أو أعلى |
85
+ | الذاكرة | 16GB RAM | 32GB RAM أو أكثر |
86
+ | التخزين | 10GB + مساحة للمستندات | SSD بسعة 50GB أو أكثر |
87
+ | الشبكة | اتصال إنترنت 10Mbps | اتصال إنترنت 50Mbps أو أسرع |
88
+ | الشاشة | دقة 1080p | دقة 1440p أو أعلى |
89
+
90
+ ### متطلبات البرمجيات
91
+
92
+ | البرمجيات | الإصدار المطلوب |
93
+ |-----------|-----------------|
94
+ | نظام التشغيل | Windows 10/11، MacOS 12+، Ubuntu 20.04+ |
95
+ | Python | 3.9 أو أحدث |
96
+ | بيئة Hybrid Face | 2.5 أو أحدث |
97
+ | متصفح | Chrome 90+، Firefox 88+، Edge 90+ |
98
+ | MySQL (اختياري) | 8.0 أو أحدث |
99
+
100
+ ### المكتبات الأس��سية
101
+
102
+ ```python
103
+ # المكتبات الأساسية المستخدمة
104
+ streamlit==1.10.0
105
+ pandas==1.5.0
106
+ numpy==1.23.0
107
+ scikit-learn==1.1.0
108
+ nltk==3.7.0
109
+ spacy==3.4.0
110
+ transformers==4.20.0
111
+ pyarabic==0.6.15
112
+ sqlalchemy==1.4.40
113
+ plotly==5.9.0
114
+ pymysql==1.0.2
115
+ pdfplumber==0.7.0
116
+ python-docx==0.8.11
117
+ openpyxl==3.0.10
118
+ ezdxf==0.17.2
119
+ ```
120
+
121
+ ## الإعداد والتثبيت
122
+
123
+ ### إعداد بيئة التطوير
124
+
125
+ ```bash
126
+ # إنشاء بيئة Python افتراضية
127
+ python -m venv venv
128
+ source venv/bin/activate # Linux/MacOS
129
+ venv\Scripts\activate # Windows
130
+
131
+ # تثبيت المكتبات المطلوبة
132
+ pip install -r requirements.txt
133
+ pip install -r arabic_support_requirements.txt
134
+ ```
135
+
136
+ ### تثبيت نماذج معالجة اللغة العربية
137
+
138
+ ```bash
139
+ # تثبيت نموذج اللغة العربية لـ SpaCy
140
+ python -m spacy download ar_core_news_lg
141
+
142
+ # تحميل موارد NLTK للغة العربية
143
+ python -m nltk.downloader stopwords
144
+ python -m nltk.downloader punkt
145
+ python -m nltk.downloader wordnet
146
+ ```
147
+
148
+ ### إعداد قاعدة البيانات
149
+
150
+ #### SQLite (للتطوير المحلي)
151
+
152
+ ```bash
153
+ # إنشاء قاعدة بيانات SQLite
154
+ python setup_db.py --mode=local
155
+ ```
156
+
157
+ #### MySQL (للنشر المؤسسي)
158
+
159
+ ```bash
160
+ # إعداد قاعدة بيانات MySQL
161
+ python setup_db.py --mode=enterprise \
162
+ --db-host=YOUR_DB_HOST \
163
+ --db-user=YOUR_DB_USER \
164
+ --db-pass=YOUR_DB_PASS \
165
+ --db-name=tender_analysis_system
docs/user_manual.md ADDED
@@ -0,0 +1,594 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # دليل المستخدم
2
+ ## نظام تحليل العقود والمناقصات بالذكاء الاصطناعي - شركة شبه الجزيرة للمقاولات
3
+
4
+ <p align="center">
5
+ <img src="../static/images/logo.png" alt="شعار النظام" width="200"/>
6
+ <br>
7
+ <em>الإصدار 2.0.0</em>
8
+ </p>
9
+
10
+ ## جدول المحتويات
11
+
12
+ 1. [مقدمة](#مقدمة)
13
+ 2. [بدء الاستخدام](#بدء-الاستخدام)
14
+ 3. [الواجهة الرئيسية](#الواجهة-الرئيسية)
15
+ 4. [إدارة المناقصات والعقود](#إدارة-المناقصات-والعقود)
16
+ 5. [تحليل المستندات](#تحليل-المستندات)
17
+ 6. [نظام التسعير الشامل](#نظام-التسعير-الشامل)
18
+ 7. [حاسبة تكاليف البناء](#حاسبة-تكاليف-البناء)
19
+ 8. [إدارة الموارد والتكاليف](#إدارة-الموارد-والتكاليف)
20
+ 9. [تحليل المخاطر](#تحليل-المخاطر)
21
+ 10. [إدارة المشاريع المرساة](#إدارة-المشاريع-المرساة)
22
+ 11. [الخرائط والمواقع](#الخرائط-والمواقع)
23
+ 12. [الإشعارات الذكية](#الإشعارات-الذكية)
24
+ 13. [الجدول الزمني التفاعلي](#الجدول-الزمني-التفاعلي)
25
+ 14. [مساعد الذكاء الاصطناعي](#مساعد-الذكاء-الاصطناعي)
26
+ 15. [مقارنة المستندات](#مقارنة-المستندات)
27
+ 16. [التقارير والتحليلات](#التقارير-والتحليلات)
28
+ 17. [إعدادات النظام](#إعدادات-النظام)
29
+ 18. [الأسئلة الشائعة](#الأسئلة-الشائعة)
30
+ 19. [استكشاف الأخطاء وإصلاحها](#استكشاف-الأخطاء-وإصلاحها)
31
+
32
+ ## مقدمة
33
+
34
+ ### حول النظام
35
+
36
+ نظام تحليل العقود والمناقصات بالذكاء الاصطناعي هو منصة متكاملة تهدف إلى مساعدة شركة شبه الجزيرة للمقاولات في تحليل وتسعير المناقصات بكفاءة عالية. يعتمد النظام على تقنيات الذكاء الاصطناعي ومعالجة اللغة العربية الطبيعية لتحليل المستندات والمساعدة في عملية التسعير واتخاذ القرارات.
37
+
38
+ ### مزايا النظام
39
+
40
+ - تحليل متقدم لكراسات الشروط والعقود باللغة العربية
41
+ - تسعير دقيق ومنهجي للمناقصات
42
+ - حاسبة تكاليف بناء شاملة مع مكونات متعددة
43
+ - تحديد المخاطر وتقييمها بشكل آلي
44
+ - إدارة الموارد والتكاليف بكفاءة
45
+ - دعم المحتوى المحلي السعودي
46
+ - جدول زمني تفاعلي مع تتبع المراحل
47
+ - مساعد ذكاء اصطناعي متطور
48
+ - متابعة شاملة للمناقصات والمشاريع
49
+ - تقارير وتحليلات متقدمة لدعم اتخاذ القرار
50
+ - خرائط تفاعلية لمواقع المشاريع
51
+ - نظام إشعارات ذكي
52
+
53
+ ## بدء الاستخدام
54
+
55
+ ### تسجيل الدخول
56
+
57
+ 1. افتح تطبيق نظام تحليل العقود والمناقصات
58
+ 2. أدخل اسم المستخدم وكلمة المرور
59
+ 3. انقر على زر "تسجيل الدخول"
60
+
61
+ ![شاشة تسجيل الدخول](../static/images/screenshots/login.png)
62
+
63
+ ### الصلاحيات ومستويات الوصول
64
+
65
+ النظام يدعم عدة مستويات من الصلاحيات:
66
+
67
+ | المستوى | الوصف | الصلاحيات |
68
+ |---------|-------|-----------|
69
+ | مدير النظام | المسؤول الرئيسي عن النظام | كامل الصلاحيات |
70
+ | مدير المناقصات | مسؤول عن إدارة المناقصات | إضافة وتعديل وحذف المناقصات، التسعير |
71
+ | محلل عقود | مختص بتحليل العقود والمستندات | قراءة وتحليل المستندات |
72
+ | محاسب | مسؤول عن الجوانب المالية | الوصول للتكاليف والتسعير |
73
+ | مستخدم عادي | مستخدم بصلاحيات محدودة | عرض المناقصات والتقارير فقط |
74
+
75
+ ## الواجهة الرئيسية
76
+
77
+ ### مكونات الواجهة
78
+
79
+ ![الواجهة الرئيسية](../static/images/screenshots/dashboard.png)
80
+
81
+ 1. **شريط القوائم**: للوصول إلى الوظائف الرئيسية
82
+ 2. **لوحة المعلومات**: عرض ملخص للمناقصات والمشاريع
83
+ 3. **المناقصات النشطة**: قائمة بالمناقصات قيد الدراسة
84
+ 4. **المواعيد الهامة**: تنبيهات بالمواعيد النهائية
85
+ 5. **المؤشرات الرئيسية**: إحصائيات ومؤشرات أداء رئيسية
86
+ 6. **معلومات الشركة**: بيان "هذا النظام يعمل لصالح شركة شبه الجزيرة للمقاولات، جميع الحقوق محفوظة 2025"
87
+
88
+ ### التنقل في النظام
89
+
90
+ تم تصميم شريط القوائم للوصول السريع إلى جميع وظائف النظام:
91
+
92
+ - **لوحة المعلومات**: الصفحة الرئيسية
93
+ - **المناقصات والعقود**: إدارة المناقصات وتحليل العقود
94
+ - **التسعير**: نظام التسعير الشامل
95
+ - **حاسبة تكاليف البناء**: حساب تكاليف البناء بالتفصيل
96
+ - **الموارد والتكاليف**: إدارة المواد والمعدات والعمالة
97
+ - **تحليل المخاطر**: تقييم وإدارة المخاطر
98
+ - **المشاريع**: إدارة المشاريع المرساة
99
+ - **الجدول الزمني**: الجدول الزمني التفاعلي للمشاريع
100
+ - **الخرائط**: خرائط مواقع المشاريع
101
+ - **الإشعارات**: نظام الإشعارات الذكي
102
+ - **المساعد الذكي**: مساعد الذكاء الاصطناعي التفاعلي
103
+ - **مقارنة المستندات**: أدوات مقارنة المستندات المتطورة
104
+ - **التقارير**: التقارير والتحليلات
105
+ - **الإعدادات**: إعدادات النظام والمستخدمين
106
+
107
+ ## إدارة المناقصات والعقود
108
+
109
+ ### إضافة مناقصة جديدة
110
+
111
+ 1. انقر على "المناقصات والعقود" من شريط القوائم
112
+ 2. اختر "إضافة مناقصة جديدة"
113
+ 3. املأ النموذج بالمعلومات المطلوبة:
114
+ - اسم المناقصة
115
+ - الجهة المالكة
116
+ - رقم المناقصة
117
+ - تاريخ الطرح
118
+ - تاريخ الإقفال
119
+ - موقع المشروع
120
+ - نوع المشروع
121
+
122
+ ![إضافة مناقصة](../static/images/screenshots/add_tender.png)
123
+
124
+ ### رفع المستندات
125
+
126
+ 1. من صفحة تفاصيل المناقصة، انقر على "رفع مستند"
127
+ 2. اختر نوع المستند:
128
+ - كراسة شروط
129
+ - جدول كميات
130
+ - مخططات
131
+ - عقد
132
+ - ملحق
133
+ 3. انقر على "استعراض" واختر الملف من جهازك
134
+ 4. يدعم النظام صيغ المستندات التالية: PDF, DOCX, XLSX, DWG
135
+ 5. **جديد**: يمكنك الآن رفع صور موقع المشروع ومقاطع الفيديو ومعلومات المزايا/المخاطر واستفسارات المالك
136
+
137
+ ### متابعة حالة المناقصات
138
+
139
+ يوفر النظام لوحة متابعة للمناقصات تعرض:
140
+
141
+ - المناقصات قيد الدراسة
142
+ - المناقصات المقدمة
143
+ - المناقصات المرساة
144
+ - المناقصات المستبعدة
145
+
146
+ لكل مناقصة، يعرض النظام:
147
+ - الحالة الحالية
148
+ - نسبة الإنجاز
149
+ - المواعيد النهائية
150
+ - المهام المتبقية
151
+ - **جديد**: مؤقت لبدء الدراسة ومواعيد التسليم النهائية
152
+
153
+ ### معلومات الموقع وسهولة الوصول
154
+
155
+ **جديد**: يمكنك الآن إضافة معلومات مفصلة عن الموقع وتفاصيل الوصول إليه بجانب زر موقع المشروع، مما يساعد فرق العمل الميدانية.
156
+
157
+ ## تحليل المستندات
158
+
159
+ ### كيفية تحليل المستندات
160
+
161
+ 1. من صفحة تفاصيل المناقصة، اختر المستند المراد تحليله
162
+ 2. انقر على زر "تحليل المستند"
163
+ 3. اختر نوع التحليل:
164
+ - تحليل كامل
165
+ - استخراج البنود والشروط
166
+ - تحديد المخاطر
167
+ - استخراج معلومات التسعير
168
+
169
+ ![تحليل المستندات](../static/images/screenshots/document_analysis.png)
170
+
171
+ ### مراجعة نتائج التحليل
172
+
173
+ بعد اكتمال التحليل، يعرض النظام:
174
+
175
+ 1. **البنود المستخرجة**: قائمة بالبنود والشروط المهمة مرتبة حسب أهميتها
176
+ 2. **المخاطر المحددة**: المخاطر المحتملة مصنفة حسب نوعها وأهميتها
177
+ 3. **المتطلبات الرئيسية**: قائمة بالمتطلبات الأساسية للمناقصة
178
+ 4. **الكلمات المفتاحية**: الكلمات والمصطلحات المهمة في المستند
179
+ 5. **جديد**: متطلبات المحتوى المحلي في المشاريع السعودية
180
+
181
+ يمكنك النقر على أي بند لعرض النص الأصلي في المستند وسياقه.
182
+
183
+ ## نظام التسعير الشامل
184
+
185
+ ### بدء عملية التسعير
186
+
187
+ 1. من صفحة تفاصيل المناقصة، انقر على "بدء التسعير"
188
+ 2. اختر جدول الكميات المراد تسعيره
189
+ 3. حدد نوع التسعير:
190
+ - تسعير قياسي
191
+ - تسعير غير متزن
192
+ - تسعير مختلط
193
+
194
+ ![بدء التسعير](../static/images/screenshots/pricing_start.png)
195
+
196
+ ### تسعير البنود
197
+
198
+ 1. لكل بند في جدول الكميات، يعرض النظام:
199
+ - وصف البند
200
+ - الوحدة
201
+ - الكمية
202
+ - التكاليف المقدرة (المواد، العمالة، المعدات)
203
+ 2. يمكنك تعديل التكاليف يدوياً أو الاعتماد على التقديرات الآلية
204
+ 3. النظام يحسب تلقائياً:
205
+ - المصاريف العامة
206
+ - هامش الربح
207
+ - السعر الإجمالي
208
+
209
+ ### التسعير غير المتزن
210
+
211
+ لتطبيق استراتيجية التسعير غير المتزن:
212
+
213
+ 1. انقر على "التسعير غير المتزن" من صفحة التسعير
214
+ 2. اختر نوع الاستراتيجية:
215
+ - التحميل الأمامي
216
+ - التحميل الخلفي
217
+ - التسعير الاستراتيجي
218
+ - التسعير القائم على المخاطر
219
+ 3. عدل المعلمات حسب الحاجة
220
+ 4. راجع التغييرات في توزيع التكاليف والأسعار
221
+
222
+ ![التسعير غير المتزن](../static/images/screenshots/unbalanced_pricing.png)
223
+
224
+ ### المحتوى المحلي
225
+
226
+ لحساب وتحسين نسبة المحتوى المحلي:
227
+
228
+ 1. انقر على "المحتوى المحلي" من صفحة التسعير
229
+ 2. قم بتقييم المعايير المختلفة:
230
+ - نسبة الموظفين السعوديين
231
+ - نسبة المواد المحلية
232
+ - نسبة المعدات المحلية
233
+ - نسبة المقاولين من الباطن المحليين
234
+ 3. راجع الدرجة الإجمالية للمحتوى المحلي والأفضلية السعرية المقابلة
235
+
236
+ ## حاسبة تكاليف البناء
237
+
238
+ ### نظرة عامة
239
+
240
+ **جديد**: حاسبة تكاليف البناء المتكاملة تتيح لك حساب تكاليف المشاريع بالتفصيل، مع تقسيم واضح للعناصر المختلفة:
241
+
242
+ - المواد الخام
243
+ - المعدات
244
+ - العمالة
245
+ - المصاريف الإدارية
246
+ - هوامش الربح
247
+
248
+ ### استخدام الحاسبة
249
+
250
+ 1. انقر على "حاسبة تكاليف البناء" من شريط القوائم
251
+ 2. اختر نوع المشروع من القائمة
252
+ 3. أدخل مواصفات المشروع الأساسية (المساحة، الموقع، نوع البناء)
253
+ 4. استعرض التكاليف المقدرة لكل مكون
254
+ 5. عدل البنود حسب الحاجة
255
+ 6. راجع تفصيل الأسعار والتكلفة الإجمالية
256
+
257
+ ### كتالوج القوالب الإنشائية
258
+
259
+ **جديد**: يتضمن النظام الآن كتالوجًا شاملاً للقوالب الإنشائية لمختلف أنواع المشاريع:
260
+
261
+ 1. مباني سكنية
262
+ 2. مباني تجارية
263
+ 3. مشاريع بنية تحتية
264
+ 4. منشآت صناعية
265
+ 5. مرافق عامة
266
+
267
+ استخدم هذه القوالب لبدء حسابات التكلفة بسرعة، ثم قم بتخصيصها حسب احتياجات مشروعك.
268
+
269
+ ## إدارة الموارد والتكاليف
270
+
271
+ ### إدارة المواد
272
+
273
+ 1. انقر على "الموارد والتكاليف" من شريط القوائم
274
+ 2. اختر "المواد"
275
+ 3. يمكنك:
276
+ - استعراض قائمة المواد
277
+ - إضافة مواد جديدة
278
+ - تحديث أسعار المواد
279
+ - ربط المواد بالموردين
280
+ - **جديد**: تقديم طلبات أسعار للمواد الخام
281
+
282
+ ![إدارة المواد](../static/images/screenshots/materials.png)
283
+
284
+ ### إدارة المعدات
285
+
286
+ 1. انقر على "الموارد والتكاليف" من شريط القوائم
287
+ 2. اختر "المعدات"
288
+ 3. يمكنك:
289
+ - استعراض قائمة المعدات
290
+ - تسجيل معدلات الأداء
291
+ - تحديث أسعار التأجير
292
+ - تسجيل تكاليف التشغيل
293
+ - **جديد**: إدارة المعدات الخاصة والمستأجرة
294
+
295
+ ### إدارة العمالة
296
+
297
+ 1. انقر على "الموارد والتكاليف" من شريط القوائم
298
+ 2. اختر "العمالة"
299
+ 3. يمكنك:
300
+ - استعراض فئات العمالة
301
+ - تسجيل معدلات الإنتاجية
302
+ - تحديث أسعار العمالة
303
+ - تكوين فرق العمل النموذجية
304
+
305
+ ## تحليل المخاطر
306
+
307
+ ### تقييم المخاطر
308
+
309
+ 1. انقر على "تحليل المخاطر" من شريط القوائم
310
+ 2. اختر المناقصة المراد تقييم مخاطرها
311
+ 3. يعرض النظام المخاطر المحددة مصنفة إلى:
312
+ - مخاطر تعاقدية
313
+ - مخاطر مالية
314
+ - مخاطر فنية
315
+ - مخاطر لوجستية
316
+
317
+ ![تحليل المخاطر](../static/images/screenshots/risk_analysis.png)
318
+
319
+ ### إدارة المخاطر
320
+
321
+ لكل خطر محدد، يمكنك:
322
+
323
+ 1. مراجعة تفاصيل الخطر
324
+ 2. تعديل تقييم احتمالية الحدوث والتأثير
325
+ 3. إضافة إجراءات التخفيف
326
+ 4. تعيين مسؤول المتابعة
327
+ 5. تحديد تكلفة التخفيف
328
+
329
+ ## إدارة المشاريع المرساة
330
+
331
+ ### متابعة المشاريع
332
+
333
+ 1. انقر على "المشاريع" من شريط القوائم
334
+ 2. اختر المشروع المراد متابعته
335
+ 3. يعرض النظام:
336
+ - ملخص المشروع
337
+ - حالة التنفيذ
338
+ - المستخلصات
339
+ - المراسلات
340
+
341
+ ![متابعة المشاريع](../static/images/screenshots/project_management.png)
342
+
343
+ ### إدارة المستخلصات
344
+
345
+ 1. من صفحة تفاصيل المشروع، انقر على "المستخلصات"
346
+ 2. يمكنك:
347
+ - إنشاء مستخلص جديد
348
+ - متابعة حالة المستخلصات
349
+ - الاطلاع على المدفوعات
350
+
351
+ ## الخرائط والمواقع
352
+
353
+ ### نظرة عامة
354
+
355
+ **جديد**: نظام الخرائط التفاعلي يتيح لك:
356
+
357
+ 1. عرض مواقع جميع المشاريع على خريطة واحدة
358
+ 2. تصفية المشاريع حسب الحالة والنوع والمنطقة
359
+ 3. عرض معلومات تفصيلية عن كل موقع
360
+ 4. تحليل التوزيع الجغرافي للمشاريع
361
+ 5. حساب المسافات واتجاهات السير إلى المواقع
362
+
363
+ ### استخدام الخرائط
364
+
365
+ 1. انقر على "الخرائط" من شريط القوائم
366
+ 2. استخدم أدوات التصفية لعرض المشاريع المطلوبة
367
+ 3. انقر على أي علامة موقع لعرض تفاصيل المشروع
368
+ 4. استخدم خيار "تفاصيل الوصول" لعرض معلومات الوصول إلى الموقع
369
+ 5. يمكنك تصدير معلومات الموقع أو مشاركتها مع فريق العمل
370
+
371
+ ## الإشعارات الذكية
372
+
373
+ ### نظرة عامة
374
+
375
+ **جديد**: نظام الإشعارات الذكية يقوم بتنبيهك تلقائيًا بشأن:
376
+
377
+ 1. المواعيد النهائية للمناقصات
378
+ 2. تحديثات حالة المناقصات والمشاريع
379
+ 3. المهام المستحقة
380
+ 4. تغييرات الأسعار في المواد الرئيسية
381
+ 5. الفرص الجديدة المحتملة
382
+
383
+ ### إعدادات الإشعارات
384
+
385
+ 1. انقر على "الإعدادات" ثم "إعدادات الإشعارات"
386
+ 2. خصص أنواع الإشعارات التي ترغب في تلقيها
387
+ 3. حدد طريقة التنبيه (داخل النظام، بريد إلكتروني، رسائل نصية)
388
+ 4. ضبط مستوى الأهمية والتكرار
389
+
390
+ ## الجدول الزمني التفاعلي
391
+
392
+ ### نظرة عامة
393
+
394
+ **جديد**: الجدول الزمني التفاعلي يتيح لك:
395
+
396
+ 1. عرض مراحل المشروع بتنسيق رسومي سهل الفهم
397
+ 2. تتبع المراحل والإنجازات الرئيسية
398
+ 3. تحديث حالة المهام في الوقت الفعلي
399
+ 4. توقع المشكلات المحتملة قبل حدوثها
400
+ 5. مشاهدة تأثير التأخيرات على الخطة الزمنية الكلية
401
+
402
+ ### استخدام الجدول الزمني
403
+
404
+ 1. انقر على "الجدول الزمني" من شريط القوائم
405
+ 2. اختر المشروع المراد عرض جدوله الزمني
406
+ 3. استعرض المراحل والمهام
407
+ 4. انقر على أي مرحلة لعرض التفاصيل أو تحديث الحالة
408
+ 5. استخدم ميزة "ماذا لو" لتقييم تأثير التغييرات المحتملة
409
+
410
+ ## مساعد الذكاء الاصطناعي
411
+
412
+ ### نظرة عامة
413
+
414
+ **جديد**: مساعد الذكاء الاصطناعي التفاعلي يمكنه:
415
+
416
+ 1. الإجابة على الأسئلة حول المناقصات والعقود
417
+ 2. توفير تحليلات سريعة للمستندات
418
+ 3. اقتراح حلول للمشكلات الشائعة
419
+ 4. مساعدتك في فهم البنود القانونية المعقدة
420
+ 5. توفير ملخصات دقيقة للمستندات الطويلة
421
+
422
+ ### استخدام المساعد
423
+
424
+ 1. انقر على رمز المساعد في أي صفحة من صفحات النظام
425
+ 2. اكتب سؤالك أو طلبك بلغة طبيعية
426
+ 3. يمكنك تحميل مستند للتحليل أو الإشارة إلى مستند موجود
427
+ 4. راجع الإجابة واطرح أسئلة متابعة إذا لزم الأمر
428
+
429
+ ## مقارنة المستندات
430
+
431
+ ### نظرة عامة
432
+
433
+ **جديد**: أدوات مقارنة المستندات المتطورة تتيح لك:
434
+
435
+ 1. مقارنة نسخ مختلفة من العقود أو المناقصات
436
+ 2. تحديد التغييرات بين المستندات بدقة
437
+ 3. تقييم تأثير التعديلات على المخاطر والتكاليف
438
+ 4. اكتشاف التناقضات بين البنود المختلفة
439
+ 5. مقارنة العقود بالنماذج القياسية
440
+
441
+ ### استخدام أدوات المقارنة
442
+
443
+ 1. انقر على "مقارنة المستندات" من شريط القو��ئم
444
+ 2. حدد المستندين المراد مقارنتهما
445
+ 3. اختر نوع المقارنة (نصية، هيكلية، دلالية)
446
+ 4. راجع نتائج المقارنة مع تمييز الاختلافات
447
+ 5. يمكنك تصدير تقرير المقارنة
448
+
449
+ ## التقارير والتحليلات
450
+
451
+ ### إنشاء التقارير
452
+
453
+ 1. انقر على "التقارير" من شريط القوائم
454
+ 2. اختر نوع التقرير:
455
+ - تقرير المناقصات
456
+ - تقرير المشاريع
457
+ - تقرير مالي
458
+ - تقرير المخاطر
459
+ - **جديد**: تقرير المحتوى المحلي
460
+ - **جديد**: تقرير توقعات الفرص المستقبلية
461
+ 3. حدد معايير التقرير
462
+ 4. انقر على "إنشاء التقرير"
463
+
464
+ ![التقارير](../static/images/screenshots/reports.png)
465
+
466
+ ### تصدير التقارير
467
+
468
+ يمكن تصدير التقارير بصيغ متعددة:
469
+ - PDF
470
+ - Excel
471
+ - Word
472
+ - PowerPoint
473
+ - **جديد**: صيغة JSON للتكامل مع الأنظمة الأخرى
474
+
475
+ ## إعدادات النظام
476
+
477
+ ### نظرة عامة
478
+
479
+ **جديد**: صفحة الإعدادات المحسنة تتيح لك:
480
+
481
+ 1. تخصيص واجهة المستخدم
482
+ 2. تغيير لغة النظام
483
+ 3. إدارة إعدادات الإشعارات
484
+ 4. تكوين عمليات النسخ الاحتياطي التلقائية
485
+ 5. إدارة حسابات المستخدمين والصلاحيات
486
+
487
+ ### الإعدادات الشخصية
488
+
489
+ 1. انقر على "الإعدادات" ثم "الإعدادات الشخصية"
490
+ 2. اختر لغة النظام (العربية، الإنجليزية)
491
+ 3. خصص الواجهة (الألوان، الخط، ترتيب العناصر)
492
+ 4. ضبط إعدادات الإشعارات الشخصية
493
+
494
+ ### إدارة المستخدمين
495
+
496
+ 1. انقر على "الإعدادات" ثم "إدارة المستخدمين" (للمديرين فقط)
497
+ 2. استعرض قائمة المستخدمين
498
+ 3. أضف مستخدمًا جديدًا أو عدل بيانات مستخدم موجود
499
+ 4. حدد صلاحيات الوصول والأدوار
500
+
501
+ ## الأسئلة الشائعة
502
+
503
+ ### أسئلة عامة
504
+
505
+ **س: كيف يمكنني الحصول على حساب للنظام؟**
506
+ ج: يرجى التواصل مع مدير النظام في شركتك.
507
+
508
+ **س: هل يمكن استخدام النظام عبر الأجهزة المحمولة؟**
509
+ ج: نعم، النظام متوافق مع جميع الأجهزة بما فيها الهواتف الذكية والأجهزة اللوحية.
510
+
511
+ **س: هل يمكنني استخدام النظام دون اتصال بالإنترنت؟**
512
+ ج: بعض الوظائف متاحة دون اتصال، لكن معظم الميزات تتطلب اتصالًا بالإنترنت.
513
+
514
+ ### أسئلة عن تحليل المستندات
515
+
516
+ **س: ما هي أنواع المستندات التي يدعمها النظام؟**
517
+ ج: يدعم النظام مستندات PDF وWord وExcel والمخططات DWG.
518
+
519
+ **س: هل يستطيع النظام تحليل المستندات الممسوحة ضوئياً؟**
520
+ ج: نعم، يمكن للنظام تحليل المستندات الممسوحة ضوئياً، لكن دقة التحليل تعتمد على جودة المسح.
521
+
522
+ **س: كم من الوقت يستغرق تحليل مستند كبير؟**
523
+ ج: يعتمد على حجم وتعقيد المستند، لكن معظم المستندات تحلل في غضون دقائق.
524
+
525
+ ### أسئلة عن التسعير وحاسبة التكاليف
526
+
527
+ **س: كيف يحدد النظام تكاليف المواد والعمالة؟**
528
+ ج: يعتمد النظام على قاعدة بيانات الأسعار المتاحة ومعدلات الأداء المسجلة.
529
+
530
+ **س: ما هو التسعير غير المتزن؟**
531
+ ج: هو استراتيجية لتوزيع التكاليف بشكل غير متساوٍ على بنود المناقصة لتحقيق ميزة تنافسية أو تحسين التدفق النقدي.
532
+
533
+ **س: هل يمكن إضافة عناصر مخصصة لحاسبة تكاليف البناء؟**
534
+ ج: نعم، يمكنك إضافة عناصر مخصصة وتعديل المعلمات حسب متطلبات المشروع.
535
+
536
+ ## استكشاف الأخطاء وإصلاحها
537
+
538
+ ### مشاكل تسجيل الدخول
539
+
540
+ **المشكلة: لا يمكن تسجيل الدخول**
541
+ الحل:
542
+ 1. تأكد من صحة اسم المستخدم وكلمة المرور
543
+ 2. تأكد من اتصالك بالإنترنت
544
+ 3. امسح ذاكرة التخزين المؤقت للمتصفح
545
+ 4. إذا استمرت المشكلة، تواصل مع الدعم الفني
546
+
547
+ ### مشاكل تحليل المستندات
548
+
549
+ **المشكلة: فشل تحليل المستند**
550
+ الحل:
551
+ 1. تأكد من أن المستند بتنسيق مدعوم
552
+ 2. تحقق من جودة المسح إذا كان المستند ممسوحاً ضوئياً
553
+ 3. قلل حجم الملف إذا كان كبيراً جداً
554
+ 4. جرب تقسيم المستند إلى أجزاء أصغر
555
+
556
+ ### مشاكل التسعير
557
+
558
+ **المشكلة: عدم ظهور التكاليف المقدرة**
559
+ الحل:
560
+ 1. تأكد من تحديث قاعدة بيانات الأسعار
561
+ 2. تحقق من صحة وحدات البنود
562
+ 3. تأكد من ربط البنود بالمواد والعمالة المناسبة
563
+ 4. أعد تشغيل عملية التسعير
564
+
565
+ ### مشاكل الجدول الزمني
566
+
567
+ **المشكلة: عدم ظهور بعض المراحل في الجدول الزمني**
568
+ الحل:
569
+ 1. تأكد من إضافة جميع المراحل في تفاصيل المشروع
570
+ 2. تحقق من تواريخ البدء والانتهاء
571
+ 3. تأكد من تسلسل المراحل المنطقي
572
+ 4. حاول تحديث الصفحة أو إعادة تحميلها
573
+
574
+ ---
575
+
576
+ ## حول النظام
577
+
578
+ نظام تحليل العقود والمناقصات بالذكاء الاصطناعي هو منتج متطور تم تصميمه وتطويره خصيصًا لشركة شبه الجزيرة للمقاولات. يعمل النظام على تحسين كفاءة دراسة المناقصات وإدارة المشاريع من خلال الاستفادة من تقنيات الذكاء الاصطناعي وتحليل البيانات المتقدمة.
579
+
580
+ **مزايا النظام الرئيسية:**
581
+ - تحليل متعمق للمناقصات والعقود باللغة العربية
582
+ - حاسبة تكاليف متكاملة مع تفاصيل دقيقة
583
+ - أدوات متقدمة لإدارة المشاريع
584
+ - تحليل المخاطر الآلي
585
+ - الجدول الزمني التفاعلي
586
+ - نظام الخرائط والمواقع
587
+ - مساعد الذكاء الاصطناعي
588
+
589
+ ---
590
+
591
+ لمزيد من المساعدة، يرجى التواصل مع:
592
+ - البريد الإلكتروني: [email protected]
593
+ - رقم الهاتف: +966 123456789
594
+ - نظام التذاكر: https://support.peninsula-contracting.com
fonts/Amiri-Bold.ttf ADDED
The diff for this file is too large to render. See raw diff
 
fonts/Amiri-Regular.ttf ADDED
The diff for this file is too large to render. See raw diff
 
huggingface_app.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import streamlit as st
4
+
5
+ # إضافة المسارات للعثور على الوحدات
6
+ current_dir = os.path.dirname(os.path.abspath(__file__))
7
+ sys.path.append(current_dir)
8
+
9
+ # استيراد التطبيق الرئيسي
10
+ try:
11
+ from app import main
12
+ except ImportError:
13
+ # محاولة استيراد بطريقة بديلة إذا فشلت الطريقة الأولى
14
+ try:
15
+ from tender_analysis_system.app import main
16
+ except ImportError:
17
+ st.error("❌ فشل استيراد التطبيق الرئيسي. تأكد من هيكل المجلدات وتثبيت المكتبات.")
18
+ st.info("ℹ️ قم بالتحقق من ملف requirements.txt وتأكد من تثبيت جميع المكتبات المطلوبة.")
19
+
20
+ # عرض تعليمات حول كيفية إصلاح المشكلة
21
+ with st.expander("🛠️ كيفية إصلاح المشكلة"):
22
+ st.markdown("""
23
+ ## خطوات إصلاح مشكلة الاستيراد
24
+
25
+ 1. تأكد من تثبيت جميع المكتبات المطلوبة:
26
+ ```bash
27
+ pip install -r requirements.txt
28
+ ```
29
+
30
+ 2. تأكد من هيكل المجلدات:
31
+ ```
32
+ /
33
+ ├── huggingface_app.py # هذا الملف الحالي
34
+ ├── app.py # التطبيق الرئيسي
35
+ ├── config.py # ملف الإعدادات
36
+ └── modules/ # وحدات التطبيق
37
+ ├── pricing/
38
+ ├── document_analysis/
39
+ └── ...
40
+ ```
41
+
42
+ 3. قم بفحص سجل الأخطاء أدناه:
43
+ """)
44
+ st.code(str(sys.path), language="python")
45
+
46
+ # إظهار واجهة بديلة بسيطة
47
+ st.header("🚧 نظام تحليل المناقصات والعقود")
48
+ st.subheader("لم يتم تحميل التطبيق بنجاح")
49
+ st.write("هناك مشكلة في تحميل تطبيق تحليل المناقصات. يرجى مراجعة الإعدادات وإعادة المحاولة.")
50
+
51
+ # الخروج من السكريبت
52
+ sys.exit(1)
53
+
54
+ # ملاحظة: تم نقل إعداد الصفحة إلى ملف app.py الرئيسي
55
+ # لتجنب أخطاء set_page_config يجب أن يكون في ملف واحد فقط
56
+ # إعدادات الصفحة المطلوبة:
57
+ # page_title="نظام تحليل المناقصات والعقود"
58
+ # page_icon="📊"
59
+ # layout="wide"
60
+ # initial_sidebar_state="expanded"
61
+
62
+ # تهيئة متغيرات البيئة
63
+ def setup_environment():
64
+ """تهيئة متغيرات البيئة اللازمة للتطبيق"""
65
+ # التحقق من وجود مفاتيح API
66
+ if os.environ.get("ANTHROPIC_API_KEY") is None:
67
+ st.warning("⚠️ مفتاح API لـ Anthropic غير موجود. بعض الميزات قد لا تعمل.")
68
+ api_key = st.text_input("أدخل مفتاح Anthropic API الخاص بك:", type="password")
69
+ if api_key:
70
+ os.environ["ANTHROPIC_API_KEY"] = api_key
71
+ st.success("✅ تم تعيين مفتاح Anthropic API!")
72
+
73
+ if os.environ.get("HUGGINGFACE_API_KEY") is None:
74
+ st.warning("⚠️ مفتاح API لـ Hugging Face غير موجود. بعض الميزات قد لا تعمل.")
75
+ api_key = st.text_input("أدخل مفتاح Hugging Face API الخاص بك:", type="password")
76
+ if api_key:
77
+ os.environ["HUGGINGFACE_API_KEY"] = api_key
78
+ st.success("✅ تم تعيين مفتاح Hugging Face API!")
79
+
80
+ # تشغيل التطبيق
81
+ if __name__ == "__main__":
82
+ setup_environment()
83
+ main()
models/README.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # نماذج التعلم الآلي
2
+
3
+ يحتوي هذا المجلد على نماذج التعلم الآلي المستخدمة في نظام تسعير المناقصات.
4
+
5
+ ## هيكل المجلد
6
+
7
+ - `trained/`: يحتوي على النماذج المدربة جاهزة للاستخدام
8
+ - `datasets/`: يحتوي على مجموعات البيانات المستخدمة في تدريب النماذج
9
+
10
+ ## النماذج المستخدمة
11
+
12
+ يستخدم النظام مجموعة من نماذج التعلم الآلي تشمل:
13
+
14
+ 1. **نموذج التنبؤ بالتكاليف**: يستخدم لتقدير تكاليف المشاريع بناءً على خصائص المشروع
15
+ 2. **نموذج تقييم المخاطر**: يقيم المخاطر المحتملة للمشروع ويقدر تأثيرها
16
+ 3. **نموذج التنبؤ بالمحتوى المحلي**: يتنبأ بنسبة المحتوى المحلي المتوقعة للمشروع
17
+ 4. **نموذج التصنيف الذكي للمستندات**: يصنف مستندات المناقصة تلقائيًا
18
+ 5. **نموذج التعرف على الكيانات**: يستخرج الكيانات المهمة من مستندات المناقصة
19
+
20
+ ## كيفية استخدام النماذج
21
+
22
+ لاستخدام النماذج في التطبيق:
23
+
24
+ ```python
25
+ from models.inference import load_cost_prediction_model, predict_cost
26
+
27
+ # تحميل النموذج
28
+ model = load_cost_prediction_model()
29
+
30
+ # التنبؤ
31
+ features = {
32
+ 'project_type': 'construction',
33
+ 'area': 5000,
34
+ 'location': 'Riyadh',
35
+ 'duration_months': 18
36
+ }
37
+
38
+ predicted_cost = predict_cost(model, features)
39
+ print(f"التكلفة المتوقعة: {predicted_cost} ريال")
40
+ ```
41
+
42
+ ## تدريب النماذج
43
+
44
+ يمكن إعادة تدريب النماذج باستخدام البيانات الجديدة من خلال:
45
+
46
+ ```python
47
+ from models.training import train_cost_prediction_model
48
+
49
+ # تدريب النموذج
50
+ train_cost_prediction_model(new_data_path="datasets/new_cost_data.csv",
51
+ output_model_path="trained/cost_prediction_v2.pkl")
52
+ ```
models/datasets/README.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # مجموعات البيانات
2
+
3
+ يحتوي هذا المجلد على مجموعات البيانات المستخدمة لتدريب نماذج التعلم الآلي في نظام تسعير المناقصات.
4
+
5
+ ## المجموعات المتوفرة
6
+
7
+ - `cost_data.csv`: بيانات تكاليف المشاريع السابقة
8
+ - `risk_data.csv`: بيانات المخاطر وتأثيراتها
9
+ - `local_content_data.csv`: بيانات المحتوى المحلي
10
+ - `documents_data.csv`: بيانات المستندات المصنفة
11
+ - `entities_data.csv`: بيانات الكيانات المستخرجة
12
+
13
+ ## هيكل مجموعات البيانات
14
+
15
+ ### cost_data.csv
16
+
17
+ بيانات تكاليف المشاريع السابقة مع خصائص كل مشروع:
18
+
19
+ | العمود | الوصف | النوع |
20
+ |--------|-------|------|
21
+ | project_id | رقم المشروع | نص |
22
+ | project_type | نوع المشروع | نص |
23
+ | location | الموقع | نص |
24
+ | area | المساحة (م²) | رقم |
25
+ | floors | عدد الطوابق | رقم |
26
+ | duration_months | مدة التنفيذ (شهور) | رقم |
27
+ | tender_type | نوع المناقصة | نص |
28
+ | client_type | نوع العميل | نص |
29
+ | total_cost | إجمالي التكلفة | رقم |
30
+ | cost_per_sqm | تكلفة المتر المربع | رقم |
31
+ | material_cost | تكلفة المواد | رقم |
32
+ | labor_cost | تكلفة العمالة | رقم |
33
+ | equipment_cost | تكلفة المعدات | رقم |
34
+ | overhead_percentage | نسبة المصاريف العامة | رقم |
35
+
36
+ ### risk_data.csv
37
+
38
+ بيانات المخاطر وتأثيراتها:
39
+
40
+ | العمود | الوصف | النوع |
41
+ |--------|-------|------|
42
+ | risk_id | رقم المخاطرة | نص |
43
+ | project_id | رقم المشروع | نص |
44
+ | risk_category | فئة المخاطرة | نص |
45
+ | risk_description | وصف المخاطرة | نص |
46
+ | impact | التأثير | نص |
47
+ | probability | الاحتمالية | نص |
48
+ | risk_score | درجة المخاطرة | رقم |
49
+ | response_strategy | استراتيجية الاستجابة | نص |
50
+ | actual_impact | التأثير الفعلي | نص |
51
+ | actual_cost | التكلفة الفعلية | رقم |
52
+
53
+ ## الإحصاءات
54
+
55
+ - عدد المشاريع: 500+
56
+ - الفترة الزمنية: 2018-2024
57
+ - التوزيع الجغرافي: جميع مناطق المملكة العربية السعودية
58
+
59
+ ## الترخيص والقيود
60
+
61
+ هذه البيانات للاستخدام الداخلي فقط ولا يجوز مشاركتها خارج الشركة.
models/trained/README.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # النماذج المدربة
2
+
3
+ يحتوي هذا المجلد على النماذج المدربة الجاهزة للاستخدام في نظام تسعير المناقصات.
4
+
5
+ ## النماذج المتوفرة
6
+
7
+ - `cost_prediction.pkl`: نموذج التنبؤ بالتكاليف (Random Forest)
8
+ - `risk_assessment.pkl`: نموذج تقييم المخاطر (Gradient Boosting)
9
+ - `local_content_prediction.pkl`: نموذج التنبؤ بالمحتوى المحلي (XGBoost)
10
+ - `document_classifier.pkl`: نموذج تصنيف المستندات (BERT فائق)
11
+ - `entity_recognition.pkl`: نموذج التعرف على الكيانات (BiLSTM-CRF)
12
+
13
+ ## إصدارات النماذج
14
+
15
+ | النموذج | الإصدار | تاريخ التدريب | المؤشرات الرئيسية | مجموعة التدريب |
16
+ |---------|---------|----------------|-------------------|----------------|
17
+ | cost_prediction.pkl | v1.2 | 2024-02-15 | MAE: 45,000 ريال | 500 مشروع |
18
+ | risk_assessment.pkl | v1.1 | 2024-02-10 | Accuracy: 87% | 350 مشروع |
19
+ | local_content_prediction.pkl | v1.0 | 2024-01-25 | RMSE: 3.2% | 280 مشروع |
20
+ | document_classifier.pkl | v2.1 | 2024-03-01 | F1: 0.92 | 1200 مستند |
21
+ | entity_recognition.pkl | v1.3 | 2024-03-05 | F1: 0.88 | 800 مستند |
22
+
23
+ ## ملاحظات الاستخدام
24
+
25
+ - تم تدريب النماذج على بيانات مشاريع البناء والإنشاءات في المملكة العربية السعودية
26
+ - يتم تحديث النماذج بشكل دوري كل 3 أشهر لضمان دقتها
27
+ - للحصول على أفضل النتائج، استخدم البيانات بنفس التنسيق المستخدم في التدريب
modules/achievements/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ وحدة نظام الإنجازات المحفز لمراحل المشروع
4
+ """
modules/achievements/achievement_system.py ADDED
@@ -0,0 +1,1033 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ نظام الإنجازات المحفز لمراحل المشروع
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import json
11
+ import streamlit as st
12
+ import pandas as pd
13
+ import numpy as np
14
+ import time
15
+ from datetime import datetime, timedelta
16
+ import random
17
+
18
+ # إضافة مسار النظام للوصول للملفات المشتركة
19
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
20
+
21
+ # استيراد مكونات قاعدة البيانات
22
+ try:
23
+ from database.db_connector import get_connection
24
+ except ImportError:
25
+ from utils.helpers import get_connection
26
+
27
+ from utils.helpers import format_time, get_user_info, load_icons
28
+
29
+
30
+ class AchievementSystem:
31
+ """نظام الإنجازات المحفز لمراحل المشروع"""
32
+
33
+ def __init__(self, user_id=None):
34
+ """تهيئة نظام الإنجازات المحفز"""
35
+ self.user_id = user_id or 1 # استخدام المستخدم الافتراضي إذا لم يتم توفير معرف المستخدم
36
+ self.conn = get_connection()
37
+ self.achievements_path = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'achievements')
38
+ os.makedirs(self.achievements_path, exist_ok=True)
39
+ self.user_data_file = os.path.join(self.achievements_path, f'user_{self.user_id}_achievements.json')
40
+ self.icons = load_icons()
41
+
42
+ # تحميل بيانات المستخدم
43
+ self.load_user_data()
44
+
45
+ # تعريف قائمة الإنجازات
46
+ self.define_achievements()
47
+
48
+ def load_user_data(self):
49
+ """تحميل بيانات إنجازات المستخدم"""
50
+ try:
51
+ if os.path.exists(self.user_data_file):
52
+ with open(self.user_data_file, 'r', encoding='utf-8') as f:
53
+ self.user_data = json.load(f)
54
+ else:
55
+ # بيانات افتراضية عند عدم وجود ملف
56
+ self.user_data = {
57
+ 'user_id': self.user_id,
58
+ 'total_points': 0,
59
+ 'level': 1,
60
+ 'unlocked_achievements': [],
61
+ 'in_progress_achievements': [],
62
+ 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
63
+ }
64
+ self.save_user_data()
65
+ except Exception as e:
66
+ st.error(f"خطأ في تحميل بيانات المستخدم: {e}")
67
+ self.user_data = {
68
+ 'user_id': self.user_id,
69
+ 'total_points': 0,
70
+ 'level': 1,
71
+ 'unlocked_achievements': [],
72
+ 'in_progress_achievements': [],
73
+ 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
74
+ }
75
+
76
+ def save_user_data(self):
77
+ """حفظ بيانات إنجازات المستخدم"""
78
+ try:
79
+ with open(self.user_data_file, 'w', encoding='utf-8') as f:
80
+ json.dump(self.user_data, f, ensure_ascii=False, indent=2)
81
+ except Exception as e:
82
+ st.error(f"خطأ في حفظ بيانات المستخدم: {e}")
83
+
84
+ def define_achievements(self):
85
+ """تعريف قائمة الإنجازات المتاحة"""
86
+ self.achievements = [
87
+ {
88
+ 'id': 'first_project',
89
+ 'name': 'بداية الرحلة',
90
+ 'description': 'قم بإنشاء مشروعك الأول',
91
+ 'icon': '🏆',
92
+ 'points': 100,
93
+ 'category': 'مشاريع',
94
+ 'difficulty': 'سهل'
95
+ },
96
+ {
97
+ 'id': 'five_projects',
98
+ 'name': 'محترف المشاريع',
99
+ 'description': 'قم بإنشاء خمسة مشاريع',
100
+ 'icon': '🏅',
101
+ 'points': 500,
102
+ 'category': 'مشاريع',
103
+ 'difficulty': 'متوسط'
104
+ },
105
+ {
106
+ 'id': 'ten_projects',
107
+ 'name': 'خبير المشاريع',
108
+ 'description': 'قم بإنشاء عشرة مشاريع',
109
+ 'icon': '🎖️',
110
+ 'points': 1000,
111
+ 'category': 'مشاريع',
112
+ 'difficulty': 'صعب'
113
+ },
114
+ {
115
+ 'id': 'first_document_analysis',
116
+ 'name': 'المحلل الأول',
117
+ 'description': 'قم بتحليل مستند للمرة الأولى',
118
+ 'icon': '📊',
119
+ 'points': 150,
120
+ 'category': 'تحليل',
121
+ 'difficulty': 'سهل'
122
+ },
123
+ {
124
+ 'id': 'five_document_analysis',
125
+ 'name': 'محلل متمرس',
126
+ 'description': 'قم بتحليل خمسة مستندات',
127
+ 'icon': '📈',
128
+ 'points': 600,
129
+ 'category': 'تحليل',
130
+ 'difficulty': 'متوسط'
131
+ },
132
+ {
133
+ 'id': 'complete_boq',
134
+ 'name': 'خبير جداول الكميات',
135
+ 'description': 'أكمل تحليل جدول كميات كامل',
136
+ 'icon': '📋',
137
+ 'points': 300,
138
+ 'category': 'تحليل',
139
+ 'difficulty': 'متوسط'
140
+ },
141
+ {
142
+ 'id': 'risk_analysis',
143
+ 'name': 'محلل المخاطر',
144
+ 'description': 'أكمل تحليل مخاطر متقدم',
145
+ 'icon': '⚠️',
146
+ 'points': 400,
147
+ 'category': 'مخاطر',
148
+ 'difficulty': 'متوسط'
149
+ },
150
+ {
151
+ 'id': 'ten_risk_identified',
152
+ 'name': 'متنبئ المخاطر',
153
+ 'description': 'تعرف على عشرة مخاطر في المشاريع',
154
+ 'icon': '🔍',
155
+ 'points': 700,
156
+ 'category': 'مخاطر',
157
+ 'difficulty': 'صعب'
158
+ },
159
+ {
160
+ 'id': 'first_terms_analysis',
161
+ 'name': 'محلل الشروط',
162
+ 'description': 'قم بتحليل بنود الشروط والأحكام',
163
+ 'icon': '📝',
164
+ 'points': 250,
165
+ 'category': 'تحليل',
166
+ 'difficulty': 'متوسط'
167
+ },
168
+ {
169
+ 'id': 'quick_analysis',
170
+ 'name': 'محلل سريع',
171
+ 'description': 'أكمل تحليل مستند في أقل من 5 دقائق',
172
+ 'icon': '⚡',
173
+ 'points': 500,
174
+ 'category': 'كفاءة',
175
+ 'difficulty': 'صعب'
176
+ },
177
+ {
178
+ 'id': 'voice_narration',
179
+ 'name': 'مترجم صوتي',
180
+ 'description': 'استخدم ميزة الترجمة الصوتية لأول مرة',
181
+ 'icon': '🎙️',
182
+ 'points': 200,
183
+ 'category': 'ترجمة',
184
+ 'difficulty': 'سهل'
185
+ },
186
+ {
187
+ 'id': 'multilingual_expert',
188
+ 'name': 'خبير متعدد اللغات',
189
+ 'description': 'استخدم الترجمة الصوتية بخمس لغات مختلفة',
190
+ 'icon': '🌍',
191
+ 'points': 800,
192
+ 'category': 'ترجمة',
193
+ 'difficulty': 'صعب'
194
+ },
195
+ {
196
+ 'id': 'first_map',
197
+ 'name': 'مستكشف الخرائط',
198
+ 'description': 'استخدم ميزة الخريطة التفاعلية لأول مرة',
199
+ 'icon': '🗺️',
200
+ 'points': 200,
201
+ 'category': 'خرائط',
202
+ 'difficulty': 'سهل'
203
+ },
204
+ {
205
+ 'id': 'ai_fine_tuning',
206
+ 'name': 'مدرب الذكاء',
207
+ 'description': 'قم بتدريب نموذج ذكاء اصطناعي مخصص',
208
+ 'icon': '🧠',
209
+ 'points': 1000,
210
+ 'category': 'ذكاء اصطناعي',
211
+ 'difficulty': 'خبير'
212
+ },
213
+ {
214
+ 'id': 'pricing_master',
215
+ 'name': 'سيد التسعير',
216
+ 'description': 'أكمل حساب تكلفة مشروع بالكامل',
217
+ 'icon': '💰',
218
+ 'points': 500,
219
+ 'category': 'تسعير',
220
+ 'difficulty': 'متوسط'
221
+ }
222
+ ]
223
+
224
+ def calculate_level(self, points):
225
+ """حساب مستوى المستخدم بناءً على النقاط"""
226
+ # صيغة بسيطة لحساب المستوى: كل 1000 نقطة = مستوى واحد
227
+ level = 1 + int(points / 1000)
228
+ return level
229
+
230
+ def unlock_achievement(self, achievement_id):
231
+ """إلغاء قفل إنجاز جديد"""
232
+ # التحقق من وجود الإنجاز في القائمة
233
+ achievement = next((a for a in self.achievements if a['id'] == achievement_id), None)
234
+ if not achievement:
235
+ return False
236
+
237
+ # التحقق من عدم وجود الإنجاز مسبقاً
238
+ if achievement_id in [a['id'] for a in self.user_data['unlocked_achievements']]:
239
+ return False
240
+
241
+ # إزالة الإنجاز من قائمة "قيد التقدم" إذا كان موجوداً
242
+ self.user_data['in_progress_achievements'] = [
243
+ a for a in self.user_data['in_progress_achievements']
244
+ if a['id'] != achievement_id
245
+ ]
246
+
247
+ # إضافة الإنجاز إلى القائمة
248
+ achievement['unlocked_date'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
249
+ self.user_data['unlocked_achievements'].append(achievement)
250
+
251
+ # تحديث النقاط والمستوى
252
+ self.user_data['total_points'] += achievement['points']
253
+ self.user_data['level'] = self.calculate_level(self.user_data['total_points'])
254
+
255
+ # حفظ البيانات
256
+ self.save_user_data()
257
+
258
+ return achievement
259
+
260
+ def update_achievement_progress(self, achievement_id, progress, total):
261
+ """تحديث تقدم إنجاز معين"""
262
+ # التحقق من وجود الإنجاز في القائمة
263
+ achievement = next((a for a in self.achievements if a['id'] == achievement_id), None)
264
+ if not achievement:
265
+ return False
266
+
267
+ # التحقق من عدم وجود الإنجاز في قائمة "تم إلغاء قفله"
268
+ if achievement_id in [a['id'] for a in self.user_data['unlocked_achievements']]:
269
+ return False
270
+
271
+ # البحث عن الإنجاز في قائمة "قيد التقدم"
272
+ in_progress_achievement = next(
273
+ (a for a in self.user_data['in_progress_achievements'] if a['id'] == achievement_id),
274
+ None
275
+ )
276
+
277
+ if in_progress_achievement:
278
+ # تحديث التقدم
279
+ in_progress_achievement['progress'] = progress
280
+ in_progress_achievement['total'] = total
281
+ in_progress_achievement['percentage'] = min(100, int((progress / total) * 100))
282
+ in_progress_achievement['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
283
+ else:
284
+ # إضافة الإنجاز إلى قائمة "قيد التقدم"
285
+ progress_data = achievement.copy()
286
+ progress_data['progress'] = progress
287
+ progress_data['total'] = total
288
+ progress_data['percentage'] = min(100, int((progress / total) * 100))
289
+ progress_data['start_date'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
290
+ progress_data['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
291
+ self.user_data['in_progress_achievements'].append(progress_data)
292
+
293
+ # إذا اكتمل التقدم، قم بإلغاء قفل الإنجاز
294
+ if progress >= total:
295
+ return self.unlock_achievement(achievement_id)
296
+
297
+ # حفظ البيانات
298
+ self.save_user_data()
299
+
300
+ return True
301
+
302
+ def check_and_award_achievements(self, action_type, data=None):
303
+ """التحقق من ومنح الإنجازات بناءً على إجراءات المستخدم"""
304
+ try:
305
+ if action_type == 'create_project':
306
+ # حساب عدد المشاريع
307
+ projects_count = self._get_projects_count()
308
+
309
+ # منح إنجازات المشاريع
310
+ if projects_count == 1:
311
+ self.unlock_achievement('first_project')
312
+ elif projects_count == 5:
313
+ self.unlock_achievement('five_projects')
314
+ elif projects_count == 10:
315
+ self.unlock_achievement('ten_projects')
316
+
317
+ # تحديث تقدم الإنجاز
318
+ self.update_achievement_progress('five_projects', min(projects_count, 5), 5)
319
+ self.update_achievement_progress('ten_projects', min(projects_count, 10), 10)
320
+
321
+ elif action_type == 'analyze_document':
322
+ # حساب عدد تحليلات المستندات
323
+ analysis_count = self._get_document_analysis_count()
324
+
325
+ # منح إنجازات تحليل المستندات
326
+ if analysis_count == 1:
327
+ self.unlock_achievement('first_document_analysis')
328
+ elif analysis_count == 5:
329
+ self.unlock_achievement('five_document_analysis')
330
+
331
+ # تحديث تقدم الإنجاز
332
+ self.update_achievement_progress('five_document_analysis', min(analysis_count, 5), 5)
333
+
334
+ # التحقق من الوقت المستغرق للتحليل
335
+ if data and 'duration_seconds' in data and data['duration_seconds'] < 300: # أقل من 5 دقائق
336
+ self.unlock_achievement('quick_analysis')
337
+
338
+ elif action_type == 'analyze_boq':
339
+ self.unlock_achievement('complete_boq')
340
+
341
+ elif action_type == 'analyze_terms':
342
+ self.unlock_achievement('first_terms_analysis')
343
+
344
+ elif action_type == 'analyze_risks':
345
+ self.unlock_achievement('risk_analysis')
346
+
347
+ # حساب عدد المخاطر المحددة
348
+ if data and 'risks_count' in data:
349
+ risks_count = data['risks_count']
350
+ risk_total = self._get_total_risks_identified()
351
+ new_total = risk_total + risks_count
352
+
353
+ # تحديث تقدم إنجاز "متنبئ المخاطر"
354
+ self.update_achievement_progress('ten_risk_identified', min(new_total, 10), 10)
355
+
356
+ if new_total >= 10 and risk_total < 10:
357
+ self.unlock_achievement('ten_risk_identified')
358
+
359
+ elif action_type == 'use_voice_narration':
360
+ self.unlock_achievement('voice_narration')
361
+
362
+ # حساب عدد اللغات المستخدمة
363
+ if data and 'language' in data:
364
+ languages_used = self._get_languages_used()
365
+ if data['language'] not in languages_used:
366
+ languages_used.append(data['language'])
367
+ self._save_languages_used(languages_used)
368
+
369
+ # تحديث تقدم إنجاز "خبير متعدد اللغات"
370
+ self.update_achievement_progress('multilingual_expert', len(languages_used), 5)
371
+
372
+ if len(languages_used) >= 5:
373
+ self.unlock_achievement('multilingual_expert')
374
+
375
+ elif action_type == 'use_map':
376
+ self.unlock_achievement('first_map')
377
+
378
+ elif action_type == 'train_ai_model':
379
+ self.unlock_achievement('ai_fine_tuning')
380
+
381
+ elif action_type == 'complete_pricing':
382
+ self.unlock_achievement('pricing_master')
383
+
384
+ except Exception as e:
385
+ st.error(f"خطأ في التحقق من الإنجازات: {e}")
386
+
387
+ def _get_projects_count(self):
388
+ """الحصول على عدد المشاريع"""
389
+ try:
390
+ cursor = self.conn.cursor()
391
+ cursor.execute("SELECT COUNT(*) FROM documents WHERE user_id = %s AND type = 'project'", (self.user_id,))
392
+ count = cursor.fetchone()[0]
393
+ cursor.close()
394
+ return count
395
+ except Exception:
396
+ # في حالة عدم وجود جدول أو خطأ في الاتصال، استخدم بيانات تقديرية
397
+ projects_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'projects')
398
+ if os.path.exists(projects_dir):
399
+ return len([f for f in os.listdir(projects_dir) if os.path.isdir(os.path.join(projects_dir, f))])
400
+ return 0
401
+
402
+ def _get_document_analysis_count(self):
403
+ """الحصول على عدد تحليلات المستندات"""
404
+ try:
405
+ cursor = self.conn.cursor()
406
+ cursor.execute("SELECT COUNT(*) FROM document_analysis WHERE document_id IN (SELECT id FROM documents WHERE user_id = %s)", (self.user_id,))
407
+ count = cursor.fetchone()[0]
408
+ cursor.close()
409
+ return count
410
+ except Exception:
411
+ # في حالة عدم وجود جدول أو خطأ في الاتصال، استخدم بيانات تقديرية
412
+ return len(self.user_data['unlocked_achievements'])
413
+
414
+ def _get_total_risks_identified(self):
415
+ """الحصول على إجمالي عدد المخاطر المحددة"""
416
+ risks_file = os.path.join(self.achievements_path, f'user_{self.user_id}_risks.json')
417
+ if os.path.exists(risks_file):
418
+ try:
419
+ with open(risks_file, 'r', encoding='utf-8') as f:
420
+ risks_data = json.load(f)
421
+ return risks_data.get('total_risks', 0)
422
+ except Exception:
423
+ return 0
424
+ return 0
425
+
426
+ def _save_total_risks_identified(self, total):
427
+ """حفظ إجمالي عدد المخاطر المحددة"""
428
+ risks_file = os.path.join(self.achievements_path, f'user_{self.user_id}_risks.json')
429
+ try:
430
+ with open(risks_file, 'w', encoding='utf-8') as f:
431
+ json.dump({'total_risks': total}, f, ensure_ascii=False, indent=2)
432
+ except Exception:
433
+ pass
434
+
435
+ def _get_languages_used(self):
436
+ """الحصول على قائمة اللغات المستخدمة"""
437
+ languages_file = os.path.join(self.achievements_path, f'user_{self.user_id}_languages.json')
438
+ if os.path.exists(languages_file):
439
+ try:
440
+ with open(languages_file, 'r', encoding='utf-8') as f:
441
+ languages_data = json.load(f)
442
+ return languages_data.get('languages', [])
443
+ except Exception:
444
+ return []
445
+ return []
446
+
447
+ def _save_languages_used(self, languages):
448
+ """حفظ قائمة اللغات المستخدمة"""
449
+ languages_file = os.path.join(self.achievements_path, f'user_{self.user_id}_languages.json')
450
+ try:
451
+ with open(languages_file, 'w', encoding='utf-8') as f:
452
+ json.dump({'languages': languages}, f, ensure_ascii=False, indent=2)
453
+ except Exception:
454
+ pass
455
+
456
+ def render_achievements_tab(self):
457
+ """عرض علامة تبويب الإنجازات"""
458
+ st.markdown("<h3 class='achievement-title'>إنجازاتك</h3>", unsafe_allow_html=True)
459
+
460
+ # عرض مستوى المستخدم والنقاط
461
+ col1, col2 = st.columns([1, 3])
462
+ with col1:
463
+ st.markdown(f"<div class='level-badge'>المستوى {self.user_data['level']}</div>", unsafe_allow_html=True)
464
+ with col2:
465
+ # حساب النقاط المطلوبة للمستوى التالي
466
+ next_level_points = (self.user_data['level']) * 1000
467
+ current_level_points = (self.user_data['level'] - 1) * 1000
468
+ progress = (self.user_data['total_points'] - current_level_points) / (next_level_points - current_level_points)
469
+
470
+ st.markdown(f"<div class='points-text'>{self.user_data['total_points']} نقطة</div>", unsafe_allow_html=True)
471
+ st.progress(progress, text=f"المستوى التالي: {next_level_points} نقطة")
472
+
473
+ # تقسيم الإنجازات إلى مجموعات
474
+ st.markdown("<h4 class='achievement-subtitle'>الإنجازات المفتوحة</h4>", unsafe_allow_html=True)
475
+
476
+ if not self.user_data['unlocked_achievements']:
477
+ st.info("لم تقم بفتح أي إنجازات حتى الآن. أكمل المهام للحصول على الإنجازات!")
478
+ else:
479
+ # عرض الإنجازات المفتوحة بتنسيق الشبكة
480
+ cols = st.columns(3)
481
+ for i, achievement in enumerate(self.user_data['unlocked_achievements']):
482
+ with cols[i % 3]:
483
+ self._render_achievement_card(achievement, is_unlocked=True)
484
+
485
+ # عرض الإنجازات قيد التقدم
486
+ st.markdown("<h4 class='achievement-subtitle'>الإنجازات قيد التقدم</h4>", unsafe_allow_html=True)
487
+
488
+ if not self.user_data['in_progress_achievements']:
489
+ st.info("ليس لديك أي إنجازات قيد التقدم حالياً.")
490
+ else:
491
+ # عرض الإنجازات قيد التقدم
492
+ for achievement in self.user_data['in_progress_achievements']:
493
+ self._render_progress_achievement(achievement)
494
+
495
+ # عرض الإنجازات المتاحة
496
+ st.markdown("<h4 class='achievement-subtitle'>الإنجازات المتاحة</h4>", unsafe_allow_html=True)
497
+
498
+ # فلترة الإنجازات غير المفتوحة وغير قيد التقدم
499
+ unlocked_ids = [a['id'] for a in self.user_data['unlocked_achievements']]
500
+ in_progress_ids = [a['id'] for a in self.user_data['in_progress_achievements']]
501
+ available_achievements = [a for a in self.achievements if a['id'] not in unlocked_ids and a['id'] not in in_progress_ids]
502
+
503
+ if not available_achievements:
504
+ st.success("رائع! لقد حققت جميع الإنجازات المتاحة.")
505
+ else:
506
+ # تقسيم الإنجازات المتاحة حسب الفئات
507
+ categories = sorted(set(a['category'] for a in available_achievements))
508
+ for category in categories:
509
+ st.markdown(f"<h5 class='achievement-category'>{category}</h5>", unsafe_allow_html=True)
510
+
511
+ category_achievements = [a for a in available_achievements if a['category'] == category]
512
+ cols = st.columns(3)
513
+ for i, achievement in enumerate(category_achievements):
514
+ with cols[i % 3]:
515
+ self._render_achievement_card(achievement, is_unlocked=False)
516
+
517
+ def _render_achievement_card(self, achievement, is_unlocked):
518
+ """عرض بطاقة إنجاز"""
519
+ if is_unlocked:
520
+ card_class = "achievement-card unlocked"
521
+ icon_class = "achievement-icon unlocked"
522
+ title_class = "achievement-name unlocked"
523
+ points_display = f"{achievement['points']} نقطة"
524
+ date_display = f"تم الفتح: {achievement.get('unlocked_date', 'غير معروف')}"
525
+ else:
526
+ card_class = "achievement-card locked"
527
+ icon_class = "achievement-icon locked"
528
+ title_class = "achievement-name locked"
529
+ points_display = f"{achievement['points']} نقطة"
530
+ date_display = f"صعوبة: {achievement['difficulty']}"
531
+
532
+ html = f"""
533
+ <div class="{card_class}">
534
+ <div class="{icon_class}">{achievement['icon']}</div>
535
+ <div class="{title_class}">{achievement['name']}</div>
536
+ <div class="achievement-description">{achievement['description']}</div>
537
+ <div class="achievement-footer">
538
+ <span class="achievement-points">{points_display}</span>
539
+ <span class="achievement-date">{date_display}</span>
540
+ </div>
541
+ </div>
542
+ """
543
+ st.markdown(html, unsafe_allow_html=True)
544
+
545
+ def _render_progress_achievement(self, achievement):
546
+ """عرض إنجاز قيد التقدم"""
547
+ progress = achievement.get('percentage', 0)
548
+
549
+ html = f"""
550
+ <div class="progress-achievement">
551
+ <div class="progress-achievement-header">
552
+ <div class="progress-achievement-icon">{achievement['icon']}</div>
553
+ <div class="progress-achievement-info">
554
+ <div class="progress-achievement-name">{achievement['name']}</div>
555
+ <div class="progress-achievement-description">{achievement['description']}</div>
556
+ </div>
557
+ <div class="progress-achievement-points">{achievement['points']} نقطة</div>
558
+ </div>
559
+ </div>
560
+ """
561
+ st.markdown(html, unsafe_allow_html=True)
562
+
563
+ st.progress(progress / 100, text=f"{progress}% ({achievement.get('progress', 0)}/{achievement.get('total', 1)})")
564
+
565
+ def render_achievements_summary(self):
566
+ """عرض ملخص الإنجازات في لوحة التحكم"""
567
+ # حساب الإحصائيات
568
+ total_achievements = len(self.achievements)
569
+ unlocked_count = len(self.user_data['unlocked_achievements'])
570
+ in_progress_count = len(self.user_data['in_progress_achievements'])
571
+
572
+ st.markdown(f"""
573
+ <div class="achievements-summary">
574
+ <div class="achievements-summary-header">
575
+ <div class="achievements-summary-title">الإنجازات</div>
576
+ <div class="achievements-summary-level">المستوى {self.user_data['level']}</div>
577
+ </div>
578
+ <div class="achievements-summary-progress">
579
+ <div class="achievements-summary-percentage">{int((unlocked_count / total_achievements) * 100)}%</div>
580
+ <div class="achievements-summary-counts">{unlocked_count} / {total_achievements}</div>
581
+ </div>
582
+ <div class="achievements-summary-footer">
583
+ <div class="achievements-summary-stat">
584
+ <div class="achievements-summary-stat-value">{unlocked_count}</div>
585
+ <div class="achievements-summary-stat-label">مفتوحة</div>
586
+ </div>
587
+ <div class="achievements-summary-stat">
588
+ <div class="achievements-summary-stat-value">{in_progress_count}</div>
589
+ <div class="achievements-summary-stat-label">قيد التقدم</div>
590
+ </div>
591
+ <div class="achievements-summary-stat">
592
+ <div class="achievements-summary-stat-value">{self.user_data['total_points']}</div>
593
+ <div class="achievements-summary-stat-label">نقطة</div>
594
+ </div>
595
+ </div>
596
+ </div>
597
+ """, unsafe_allow_html=True)
598
+
599
+ # عرض آخر 3 إنجازات تم فتحها
600
+ if self.user_data['unlocked_achievements']:
601
+ st.markdown("<div class='achievements-recent-title'>آخر الإنجازات</div>", unsafe_allow_html=True)
602
+
603
+ recent_achievements = sorted(
604
+ self.user_data['unlocked_achievements'],
605
+ key=lambda x: x.get('unlocked_date', ''),
606
+ reverse=True
607
+ )[:3]
608
+
609
+ for achievement in recent_achievements:
610
+ st.markdown(f"""
611
+ <div class="achievement-recent-item">
612
+ <div class="achievement-recent-icon">{achievement['icon']}</div>
613
+ <div class="achievement-recent-info">
614
+ <div class="achievement-recent-name">{achievement['name']}</div>
615
+ <div class="achievement-recent-date">{achievement.get('unlocked_date', '')}</div>
616
+ </div>
617
+ <div class="achievement-recent-points">+{achievement['points']}</div>
618
+ </div>
619
+ """, unsafe_allow_html=True)
620
+
621
+ def render(self):
622
+ """عرض واجهة نظام الإنجازات"""
623
+ st.markdown("<h2 class='module-title'>نظام الإنجازات المحفز لمراحل المشروع</h2>", unsafe_allow_html=True)
624
+
625
+ st.markdown("""
626
+ <div class="module-description">
627
+ نظام الإنجازات يحفزك على إكمال المهام وتحقيق أهداف المشروع من خلال مكافآت
628
+ وإنجازات قابلة للفتح. اكتسب النقاط وارتقِ بمستواك وافتح إنجازات جديدة كلما تقدمت في استخدام نظام تحليل المناقصات.
629
+ </div>
630
+ """, unsafe_allow_html=True)
631
+
632
+ # عرض صندوق معلومات عند تشغيل الوحدة لأول مرة
633
+ if not self.user_data['unlocked_achievements'] and not self.user_data['in_progress_achievements']:
634
+ st.info("""
635
+ 👋 مرحباً بك في نظام الإنجازات!
636
+
637
+ استكشف الإنجازات المتاحة وابدأ في تحقيقها عن طريق إكمال المهام في أنحاء النظام المختلفة.
638
+ كلما حققت المزيد من الإنجازات، حصلت على نقاط أكثر وارتقيت في المستويات.
639
+
640
+ ابدأ الآن بإنشاء مشروع جديد أو تحليل مستند!
641
+ """)
642
+
643
+ # إنشاء علامات تبويب لعرض محتوى مختلف
644
+ tab1, tab2, tab3 = st.tabs(["الإنجازات", "المستويات والمكافآت", "الإحصائيات"])
645
+
646
+ with tab1:
647
+ self.render_achievements_tab()
648
+
649
+ with tab2:
650
+ st.markdown("<h3 class='achievement-title'>المستويات والمكافآت</h3>", unsafe_allow_html=True)
651
+
652
+ # عرض معلومات عن نظام المستويات
653
+ st.markdown("""
654
+ <div class="levels-info">
655
+ <p>نظام المستويات يعتمد على النقاط التي تكتسبها من إنجاز المهام وفتح الإنجازات:</p>
656
+ <ul>
657
+ <li>المستوى 1: 0 - 999 نقطة</li>
658
+ <li>المستوى 2: 1000 - 1999 نقطة</li>
659
+ <li>المستوى 3: 2000 - 2999 نقطة</li>
660
+ <li>وهكذا...</li>
661
+ </ul>
662
+ <p>كلما ارتقيت في المستويات، تفتح مكافآت وميزات جديدة في النظام!</p>
663
+ </div>
664
+ """, unsafe_allow_html=True)
665
+
666
+ # عرض قائمة المكافآت
667
+ st.markdown("<h4 class='achievement-subtitle'>المكافآت المتاحة</h4>", unsafe_allow_html=True)
668
+
669
+ rewards = [
670
+ {"level": 2, "name": "قوالب مخصصة", "description": "الوصول إلى قوالب مخصصة للتقارير والتحليلات"},
671
+ {"level": 3, "name": "تنبيهات متقدمة", "description": "إعدادات إشعارات متقدمة للمشاريع والمواعيد النهائية"},
672
+ {"level": 5, "name": "تحليل معزز", "description": "خيارات إضافية لتحليل المستندات والعقود"},
673
+ {"level": 7, "name": "تخصيص متقدم", "description": "خيارات إضافية لتخصيص واجهة النظام والتقارير"},
674
+ {"level": 10, "name": "وضع الخبراء", "description": "وضع متقدم مع ميزات خاصة متاحة فقط للمستخدمين المخضرمين"}
675
+ ]
676
+
677
+ for reward in rewards:
678
+ status = "متاح" if self.user_data['level'] >= reward['level'] else "مقفل"
679
+ status_class = "available" if self.user_data['level'] >= reward['level'] else "locked"
680
+
681
+ st.markdown(f"""
682
+ <div class="reward-item">
683
+ <div class="reward-level">المستوى {reward['level']}</div>
684
+ <div class="reward-info">
685
+ <div class="reward-name">{reward['name']}</div>
686
+ <div class="reward-description">{reward['description']}</div>
687
+ </div>
688
+ <div class="reward-status {status_class}">{status}</div>
689
+ </div>
690
+ """, unsafe_allow_html=True)
691
+
692
+ with tab3:
693
+ st.markdown("<h3 class='achievement-title'>إحصائيات الإنجازات</h3>", unsafe_allow_html=True)
694
+
695
+ # إعداد بيانات للرسم البياني
696
+ categories = {}
697
+ for achievement in self.achievements:
698
+ category = achievement['category']
699
+ if category not in categories:
700
+ categories[category] = {"total": 0, "unlocked": 0}
701
+ categories[category]["total"] += 1
702
+
703
+ # حساب الإنجازات المفتوحة لكل فئة
704
+ for achievement in self.user_data['unlocked_achievements']:
705
+ category = achievement['category']
706
+ if category in categories:
707
+ categories[category]["unlocked"] += 1
708
+
709
+ # تحويل البيانات إلى DataFrame
710
+ df = pd.DataFrame([
711
+ {
712
+ "الفئة": category,
713
+ "المفتوحة": data["unlocked"],
714
+ "الإجمالي": data["total"],
715
+ "النسبة": round((data["unlocked"] / data["total"]) * 100 if data["total"] > 0 else 0)
716
+ }
717
+ for category, data in categories.items()
718
+ ])
719
+
720
+ # عرض البيانات في جدول
721
+ st.dataframe(
722
+ df,
723
+ column_config={
724
+ "النسبة": st.column_config.ProgressColumn(
725
+ "نسبة الإنجاز",
726
+ format="%d%%",
727
+ min_value=0,
728
+ max_value=100
729
+ )
730
+ },
731
+ hide_index=True
732
+ )
733
+
734
+ # عرض معلومات إضافية
735
+ col1, col2, col3 = st.columns(3)
736
+ with col1:
737
+ total_points_possible = sum(a['points'] for a in self.achievements)
738
+ st.metric(
739
+ "إجمالي النقاط المحتملة",
740
+ f"{total_points_possible}",
741
+ f"{int((self.user_data['total_points'] / total_points_possible) * 100)}%"
742
+ )
743
+
744
+ with col2:
745
+ days_since_first = 0
746
+ if self.user_data['unlocked_achievements']:
747
+ first_date = min([
748
+ datetime.strptime(a.get('unlocked_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')), '%Y-%m-%d %H:%M:%S')
749
+ for a in self.user_data['unlocked_achievements']
750
+ ])
751
+ days_since_first = (datetime.now() - first_date).days
752
+
753
+ st.metric("أيام النشاط", f"{days_since_first}")
754
+
755
+ with col3:
756
+ if self.user_data['unlocked_achievements']:
757
+ achievements_per_day = round(len(self.user_data['unlocked_achievements']) / max(1, days_since_first), 2)
758
+ st.metric("معدل الإنجازات اليومي", f"{achievements_per_day}")
759
+ else:
760
+ st.metric("معدل الإنجازات اليومي", "0")
761
+
762
+ # إضافة CSS مخصص للصفحة
763
+ st.markdown("""
764
+ <style>
765
+ .achievement-title {
766
+ color: #1E88E5;
767
+ font-size: 1.5rem;
768
+ margin-bottom: 1rem;
769
+ text-align: right;
770
+ }
771
+ .achievement-subtitle {
772
+ color: #424242;
773
+ font-size: 1.2rem;
774
+ margin: 1.5rem 0 1rem 0;
775
+ text-align: right;
776
+ }
777
+ .achievement-category {
778
+ color: #616161;
779
+ font-size: 1rem;
780
+ margin: 1rem 0 0.5rem 0;
781
+ text-align: right;
782
+ border-bottom: 1px solid #e0e0e0;
783
+ padding-bottom: 0.3rem;
784
+ }
785
+ .level-badge {
786
+ background-color: #1E88E5;
787
+ color: white;
788
+ padding: 0.5rem 1rem;
789
+ border-radius: 1rem;
790
+ font-weight: bold;
791
+ text-align: center;
792
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
793
+ }
794
+ .points-text {
795
+ font-size: 1.2rem;
796
+ font-weight: bold;
797
+ color: #424242;
798
+ margin-bottom: 0.5rem;
799
+ text-align: right;
800
+ }
801
+ .achievement-card {
802
+ border-radius: 10px;
803
+ padding: 1rem;
804
+ margin-bottom: 1rem;
805
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
806
+ text-align: center;
807
+ transition: transform 0.2s;
808
+ }
809
+ .achievement-card:hover {
810
+ transform: translateY(-5px);
811
+ }
812
+ .achievement-card.unlocked {
813
+ background-color: #E3F2FD;
814
+ border: 1px solid #BBDEFB;
815
+ }
816
+ .achievement-card.locked {
817
+ background-color: #F5F5F5;
818
+ border: 1px solid #E0E0E0;
819
+ opacity: 0.7;
820
+ }
821
+ .achievement-icon {
822
+ font-size: 2rem;
823
+ margin-bottom: 0.5rem;
824
+ }
825
+ .achievement-icon.unlocked {
826
+ color: #1E88E5;
827
+ }
828
+ .achievement-icon.locked {
829
+ color: #9E9E9E;
830
+ }
831
+ .achievement-name {
832
+ font-weight: bold;
833
+ margin-bottom: 0.5rem;
834
+ }
835
+ .achievement-name.unlocked {
836
+ color: #1565C0;
837
+ }
838
+ .achievement-name.locked {
839
+ color: #616161;
840
+ }
841
+ .achievement-description {
842
+ font-size: 0.85rem;
843
+ color: #757575;
844
+ margin-bottom: 0.7rem;
845
+ min-height: 2.5rem;
846
+ }
847
+ .achievement-footer {
848
+ display: flex;
849
+ justify-content: space-between;
850
+ font-size: 0.8rem;
851
+ color: #9E9E9E;
852
+ border-top: 1px solid #E0E0E0;
853
+ padding-top: 0.5rem;
854
+ }
855
+ .progress-achievement {
856
+ background-color: #F5F5F5;
857
+ border-radius: 10px;
858
+ padding: 1rem;
859
+ margin-bottom: 0.5rem;
860
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
861
+ }
862
+ .progress-achievement-header {
863
+ display: flex;
864
+ align-items: center;
865
+ margin-bottom: 0.5rem;
866
+ }
867
+ .progress-achievement-icon {
868
+ font-size: 1.5rem;
869
+ color: #1E88E5;
870
+ margin-left: 1rem;
871
+ }
872
+ .progress-achievement-info {
873
+ flex: 1;
874
+ }
875
+ .progress-achievement-name {
876
+ font-weight: bold;
877
+ color: #424242;
878
+ }
879
+ .progress-achievement-description {
880
+ font-size: 0.85rem;
881
+ color: #757575;
882
+ }
883
+ .progress-achievement-points {
884
+ color: #1E88E5;
885
+ font-weight: bold;
886
+ }
887
+ .levels-info {
888
+ background-color: #F5F5F5;
889
+ border-radius: 10px;
890
+ padding: 1rem;
891
+ margin-bottom: 1.5rem;
892
+ text-align: right;
893
+ }
894
+ .levels-info ul {
895
+ list-style-position: inside;
896
+ margin: 0.5rem 1rem;
897
+ padding: 0;
898
+ }
899
+ .reward-item {
900
+ display: flex;
901
+ align-items: center;
902
+ background-color: #F5F5F5;
903
+ border-radius: 10px;
904
+ padding: 1rem;
905
+ margin-bottom: 0.5rem;
906
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
907
+ }
908
+ .reward-level {
909
+ background-color: #1E88E5;
910
+ color: white;
911
+ padding: 0.3rem 0.7rem;
912
+ border-radius: 1rem;
913
+ font-size: 0.8rem;
914
+ font-weight: bold;
915
+ margin-left: 1rem;
916
+ white-space: nowrap;
917
+ }
918
+ .reward-info {
919
+ flex: 1;
920
+ }
921
+ .reward-name {
922
+ font-weight: bold;
923
+ color: #424242;
924
+ }
925
+ .reward-description {
926
+ font-size: 0.85rem;
927
+ color: #757575;
928
+ }
929
+ .reward-status {
930
+ font-weight: bold;
931
+ padding: 0.3rem 0.7rem;
932
+ border-radius: 1rem;
933
+ font-size: 0.8rem;
934
+ white-space: nowrap;
935
+ }
936
+ .reward-status.available {
937
+ background-color: #C8E6C9;
938
+ color: #2E7D32;
939
+ }
940
+ .reward-status.locked {
941
+ background-color: #FFCDD2;
942
+ color: #C62828;
943
+ }
944
+ .achievements-summary {
945
+ background-color: #F5F5F5;
946
+ border-radius: 10px;
947
+ padding: 1rem;
948
+ margin-bottom: 1rem;
949
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
950
+ }
951
+ .achievements-summary-header {
952
+ display: flex;
953
+ justify-content: space-between;
954
+ align-items: center;
955
+ margin-bottom: 0.5rem;
956
+ }
957
+ .achievements-summary-title {
958
+ font-weight: bold;
959
+ color: #424242;
960
+ }
961
+ .achievements-summary-level {
962
+ background-color: #1E88E5;
963
+ color: white;
964
+ padding: 0.3rem 0.7rem;
965
+ border-radius: 1rem;
966
+ font-size: 0.8rem;
967
+ font-weight: bold;
968
+ }
969
+ .achievements-summary-progress {
970
+ display: flex;
971
+ justify-content: space-between;
972
+ align-items: center;
973
+ margin-bottom: 1rem;
974
+ }
975
+ .achievements-summary-percentage {
976
+ font-size: 1.2rem;
977
+ font-weight: bold;
978
+ color: #1E88E5;
979
+ }
980
+ .achievements-summary-counts {
981
+ color: #757575;
982
+ }
983
+ .achievements-summary-footer {
984
+ display: flex;
985
+ justify-content: space-between;
986
+ text-align: center;
987
+ }
988
+ .achievements-summary-stat-value {
989
+ font-weight: bold;
990
+ color: #424242;
991
+ font-size: 1.1rem;
992
+ }
993
+ .achievements-summary-stat-label {
994
+ color: #757575;
995
+ font-size: 0.8rem;
996
+ }
997
+ .achievements-recent-title {
998
+ font-weight: bold;
999
+ color: #424242;
1000
+ margin: 1rem 0 0.5rem 0;
1001
+ }
1002
+ .achievement-recent-item {
1003
+ display: flex;
1004
+ align-items: center;
1005
+ background-color: #E3F2FD;
1006
+ border-radius: 10px;
1007
+ padding: 0.7rem;
1008
+ margin-bottom: 0.5rem;
1009
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1010
+ }
1011
+ .achievement-recent-icon {
1012
+ font-size: 1.2rem;
1013
+ color: #1E88E5;
1014
+ margin-left: 0.7rem;
1015
+ }
1016
+ .achievement-recent-info {
1017
+ flex: 1;
1018
+ }
1019
+ .achievement-recent-name {
1020
+ font-weight: bold;
1021
+ color: #424242;
1022
+ font-size: 0.9rem;
1023
+ }
1024
+ .achievement-recent-date {
1025
+ font-size: 0.75rem;
1026
+ color: #757575;
1027
+ }
1028
+ .achievement-recent-points {
1029
+ color: #1E88E5;
1030
+ font-weight: bold;
1031
+ }
1032
+ </style>
1033
+ """, unsafe_allow_html=True)
modules/achievements/achievements_app.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ وحدة تطبيق نظام الإنجازات المحفز لمراحل المشروع
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import streamlit as st
11
+ import pandas as pd
12
+ import numpy as np
13
+ import time
14
+ from datetime import datetime, timedelta
15
+
16
+ # إضافة مسار النظام للوصول للملفات المشتركة
17
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
18
+
19
+ # استيراد مكونات نظام الإنجازات
20
+ from modules.achievements.achievement_system import AchievementSystem
21
+
22
+
23
+ class AchievementsApp:
24
+ """وحدة تطبيق نظام الإنجازات المحفز لمراحل المشروع"""
25
+
26
+ def __init__(self, user_id=None):
27
+ """تهيئة وحدة تطبيق نظام الإنجازات المحفز"""
28
+ self.achievement_system = AchievementSystem(user_id)
29
+
30
+ def render(self):
31
+ """عرض واجهة وحدة تطبيق نظام الإنجازات المحفز"""
32
+ self.achievement_system.render()
33
+
34
+ def render_dashboard_summary(self):
35
+ """عرض ملخص الإنجازات في لوحة التحكم"""
36
+ self.achievement_system.render_achievements_summary()
37
+
38
+
39
+ # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
40
+ if __name__ == "__main__":
41
+ st.set_page_config(
42
+ page_title="نظام الإنجازات المحفز | WAHBi AI",
43
+ page_icon="🏆",
44
+ layout="wide",
45
+ initial_sidebar_state="expanded"
46
+ )
47
+
48
+ app = AchievementsApp()
49
+ app.render()
modules/ai_assistant/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """
2
+ وحدة المساعد الذكي
3
+ """
4
+
5
+ __version__ = '1.0.0'
modules/ai_assistant/ai_assistant.py ADDED
@@ -0,0 +1,773 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ وحدة المساعد الذكي التفاعلية
6
+ تتيح للمستخدمين التفاعل مع نماذج الذكاء الاصطناعي المتقدمة للحصول على مساعدة في تحليل العقود والمناقصات
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import json
12
+ import re
13
+ import time
14
+ import base64
15
+ import tempfile
16
+ import logging
17
+ from datetime import datetime
18
+ import streamlit as st
19
+ import pandas as pd
20
+ import numpy as np
21
+ import requests
22
+ from io import BytesIO
23
+ from PIL import Image
24
+ import openai
25
+ import plotly.express as px
26
+ import plotly.graph_objects as go
27
+
28
+ # إضافة مسار النظام للوصول للملفات المشتركة
29
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
30
+
31
+ # استيراد المكونات المساعدة
32
+ from utils.helpers import create_directory_if_not_exists, format_time, get_user_info, render_credits, load_css
33
+
34
+
35
+ class AIAssistant:
36
+ """فئة المساعد الذكي التفاعلية"""
37
+
38
+ def __init__(self):
39
+ """تهيئة المساعد الذكي"""
40
+ self.conversations_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'assistant_conversations')
41
+ create_directory_if_not_exists(self.conversations_dir)
42
+
43
+ # تهيئة مفتاح OpenAI API
44
+ self.openai_api_key = os.environ.get("OPENAI_API_KEY")
45
+ if self.openai_api_key:
46
+ openai.api_key = self.openai_api_key
47
+ self.is_api_available = True
48
+ else:
49
+ self.is_api_available = False
50
+
51
+ # نموذج OpenAI المستخدم
52
+ self.model = "gpt-4o" # النموذج الأحدث من OpenAI
53
+
54
+ # تهيئة حالة المحادثة في الجلسة
55
+ if "assistant_messages" not in st.session_state:
56
+ st.session_state.assistant_messages = []
57
+
58
+ if "assistant_mode" not in st.session_state:
59
+ st.session_state.assistant_mode = "general"
60
+
61
+ if "document_context" not in st.session_state:
62
+ st.session_state.document_context = None
63
+
64
+ # الأنماط المتاحة للمساعد
65
+ self.assistant_modes = {
66
+ "general": "مساعد عام",
67
+ "contract_analysis": "تحليل العقود",
68
+ "cost_estimation": "تقدير التكاليف",
69
+ "risk_assessment": "تقييم المخاطر",
70
+ "project_planning": "تخطيط المشاريع"
71
+ }
72
+
73
+ # توجيهات النظام للمساعد
74
+ self.system_prompts = {
75
+ "general": """
76
+ أنت مساعد ذكي متخصص في شركة شبه الجزيرة للمقاولات، وتعمل ضمن نظام WAHBi لتحليل العقود والمناقصات.
77
+ دورك هو مساعدة المستخدمين في:
78
+ 1. تحليل المستندات والعقود، وتوضيح بنود العقود وفهم الالتزامات والشروط.
79
+ 2. المساعدة في تسعير المشاريع وحساب التكاليف والموارد.
80
+ 3. تقييم مخاطر العقود والمشاريع والمساعدة في اتخاذ القرارات.
81
+ 4. المساعدة في إدارة المشاريع ومتابعة الإنجاز.
82
+
83
+ استخدم لغة مهنية واضحة ومباشرة. قدم إجابات دقيقة ومختصرة.
84
+ عند قيام المستخدم بسؤال عن كيفية استخدام النظام، قم بإرشاده إلى الوحدة المناسبة في النظام.
85
+
86
+ معلومات هامة عن وحدات النظام:
87
+ - وحدة تحليل المستندات: لتحليل العقود والمناقصات باستخدام الذكاء الاصطناعي.
88
+ - وحدة مقارنة المستندات: لمقارنة نسخ مختلفة من المستندات وتحديد التغييرات.
89
+ - وحدة التسعير المتكاملة: لحساب تكاليف المشاريع بناءً على الموارد والمواد والعمالة.
90
+ - وحدة تقييم مخاطر العقود: لتحليل وتقييم المخاطر المحتملة في العقود والمشاريع.
91
+ - وحدة متتبع حالة المشروع: لمتابعة تقدم المشاريع وعرض مؤشرات الأداء.
92
+ - وحدة خريطة المشاريع: لعرض مواقع المشاريع على الخريطة بشكل تفاعلي.
93
+ - وحدة الإشعارات الذكية: لإرسال تنبيهات وإشعارات للمستخدمين حول المشاريع.
94
+
95
+ تذكر أن تكون مفيداً ودقيقاً ومهنياً في جميع إجاباتك.
96
+ """,
97
+
98
+ "contract_analysis": """
99
+ أنت محلل عقود متخ��ص في تحليل العقود والمناقصات لشركات المقاولات.
100
+ مهمتك هي تحليل العقود وتحديد:
101
+ - الالتزامات الرئيسية
102
+ - المواعيد النهائية والتسليمات
103
+ - الشروط الجزائية والغرامات
104
+ - آلية الدفع والمستحقات المالية
105
+ - الشروط الخاصة والاستثناءات
106
+ - المخاطر المحتملة وكيفية التخفيف منها
107
+
108
+ عند تحليل عقد، قم بتوضيح البنود غير المواتية التي قد تسبب مشاكل مستقبلية.
109
+ استخدم لغة قانونية دقيقة مع شرح المصطلحات القانونية بلغة مبسطة.
110
+ قدم توصيات عملية لكيفية التعامل مع بنود العقد وتجنب المخاطر.
111
+ """,
112
+
113
+ "cost_estimation": """
114
+ أنت خبير في تقدير تكاليف مشاريع البناء والمقاولات.
115
+ مهمتك هي مساعدة المستخدم في:
116
+ - تقدير تكاليف المشاريع بناءً على وصف المشروع ومتطلباته
117
+ - حساب تكاليف المواد والعمالة والمعدات والنفقات العامة
118
+ - توضيح كيفية تخصيص الميزانية بين مختلف عناصر المشروع
119
+ - تحديد التكاليف غير المباشرة التي قد يغفل عنها المستخدم
120
+ - اقتراح طرق لتقليل التكاليف دون التأثير على جودة المشروع
121
+
122
+ استخدم أسلوب منهجي في تقدير التكاليف واشرح افتراضاتك بوضوح.
123
+ قدم نطاقات تقديرية بدلاً من أرقام دقيقة للتكاليف حيثما كان ذلك مناسباً.
124
+ عند الإشارة إلى تكاليف، وضح ما إذا كانت التكاليف تشمل ضريبة القيمة المضافة أم لا.
125
+ """,
126
+
127
+ "risk_assessment": """
128
+ أنت خبير في تقييم مخاطر مشاريع البناء والمقاولات.
129
+ مهمتك هي مساعدة المستخدم في:
130
+ - تحديد المخاطر المحتملة في المشاريع والعقود
131
+ - تقييم احتمالية وتأثير كل خطر
132
+ - اقتراح استراتيجيات للتخفيف من المخاطر
133
+ - تحليل السيناريوهات المحتملة وخطط الطوارئ
134
+ - تقديم أفضل الممارسات لإدارة المخاطر في مشاريع المقاولات
135
+
136
+ صنف المخاطر إلى فئات (عالية، متوسطة، منخفضة) بناءً على احتماليتها وتأثيرها.
137
+ اشرح كيف يمكن للشركة أن تحول بعض المخاطر إلى فرص.
138
+ قدم أمثلة عملية من مشاريع مماثلة لتوضيح كيفية إدارة المخاطر المحددة.
139
+ """,
140
+
141
+ "project_planning": """
142
+ أنت خبير في تخطيط وإدارة مشاريع البناء والمقاولات.
143
+ مهمتك هي مساعدة المستخدم في:
144
+ - تخطيط المشاريع وتقسيمها إلى مراحل ومهام
145
+ - تحديد الموارد اللازمة والجداول الزمنية
146
+ - إنشاء مخطط جانت وتحديد المسار الحرج
147
+ - التخطيط للموارد البشرية والمعدات والمواد
148
+ - متابعة تقدم المشروع ومؤشرات الأداء
149
+
150
+ قدم نصائح عملية لإدارة المشاريع بكفاءة وتجنب التأخيرات.
151
+ اشرح كيفية التعامل مع التغييرات والمطالبات خلال تنفيذ المشروع.
152
+ قدم أفضل الممارسات للتواصل مع أصحاب المصلحة وإدارة التوقعات.
153
+ """
154
+ }
155
+
156
+ def _call_openai_api(self, messages, model=None, max_tokens=2000):
157
+ """استدعاء OpenAI API للحصول على استجابة"""
158
+ if not self.is_api_available:
159
+ return {
160
+ "choices": [{"message": {"content": "عذراً، مفتاح OpenAI API غير متوفر. يرجى التواصل مع مسؤول النظام."}}]
161
+ }
162
+
163
+ try:
164
+ if model is None:
165
+ model = self.model
166
+
167
+ response = openai.ChatCompletion.create(
168
+ model=model,
169
+ messages=messages,
170
+ max_tokens=max_tokens,
171
+ temperature=0.7,
172
+ top_p=0.9,
173
+ frequency_penalty=0,
174
+ presence_penalty=0
175
+ )
176
+
177
+ return response
178
+ except Exception as e:
179
+ logging.error(f"خطأ في استدعاء OpenAI API: {e}")
180
+ return {
181
+ "choices": [{"message": {"content": f"عذراً، حدث خطأ في الاتصال بـ OpenAI API: {str(e)}"}}]
182
+ }
183
+
184
+ def _call_backend_api(self, endpoint, data):
185
+ """استدعاء واجهة API الخلفية للنظام"""
186
+ try:
187
+ response = requests.post(
188
+ f"http://localhost:5000/api/{endpoint}",
189
+ json=data,
190
+ timeout=60
191
+ )
192
+
193
+ if response.status_code == 200:
194
+ return response.json()
195
+ else:
196
+ logging.error(f"خطأ في استدعاء واجهة API الخلفية: {response.status_code} - {response.text}")
197
+ return {"error": f"خطأ في استدعاء واجهة API الخلفية: {response.status_code}"}
198
+ except Exception as e:
199
+ logging.error(f"خطأ في الاتصال بواجهة API الخلفية: {e}")
200
+ return {"error": f"خطأ في الاتصال بواجهة API الخلفية: {str(e)}"}
201
+
202
+ def _process_user_message(self, user_message, mode=None):
203
+ """معالجة رسالة المستخدم والحصول على رد من المساعد الذكي"""
204
+ if mode is None:
205
+ mode = st.session_state.assistant_mode
206
+
207
+ # إنشاء قائمة الرسائل للمحادثة
208
+ messages = [
209
+ {"role": "system", "content": self.system_prompts[mode]}
210
+ ]
211
+
212
+ # إضافة سياق المستند إذا كان متاحاً
213
+ if st.session_state.document_context:
214
+ messages.append({
215
+ "role": "system",
216
+ "content": f"معلومات سياقية عن المستند: {st.session_state.document_context}"
217
+ })
218
+
219
+ # إضافة المحادثة السابقة
220
+ for msg in st.session_state.assistant_messages:
221
+ messages.append({
222
+ "role": msg["role"],
223
+ "content": msg["content"]
224
+ })
225
+
226
+ # إضافة رسالة المستخدم الحالية
227
+ messages.append({
228
+ "role": "user",
229
+ "content": user_message
230
+ })
231
+
232
+ # استدعاء API
233
+ response = self._call_openai_api(messages)
234
+
235
+ # استخراج الرد
236
+ assistant_response = response["choices"][0]["message"]["content"]
237
+
238
+ # تحديث سجل المحادثة
239
+ st.session_state.assistant_messages.append({"role": "user", "content": user_message})
240
+ st.session_state.assistant_messages.append({"role": "assistant", "content": assistant_response})
241
+
242
+ return assistant_response
243
+
244
+ def _clear_chat(self):
245
+ """مسح المحادثة الحالية"""
246
+ st.session_state.assistant_messages = []
247
+ st.session_state.document_context = None
248
+
249
+ def _save_conversation(self):
250
+ """حفظ المحادثة الحالية"""
251
+ if not st.session_state.assistant_messages:
252
+ st.warning("لا توجد محادثة لحفظها.")
253
+ return False
254
+
255
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
256
+ user_info = get_user_info()
257
+
258
+ conversation_data = {
259
+ "timestamp": timestamp,
260
+ "user": user_info["username"],
261
+ "mode": st.session_state.assistant_mode,
262
+ "messages": st.session_state.assistant_messages,
263
+ "document_context": st.session_state.document_context
264
+ }
265
+
266
+ filename = f"conversation_{user_info['username']}_{timestamp}.json"
267
+ file_path = os.path.join(self.conversations_dir, filename)
268
+
269
+ try:
270
+ with open(file_path, 'w', encoding='utf-8') as f:
271
+ json.dump(conversation_data, f, ensure_ascii=False, indent=2)
272
+
273
+ return True
274
+ except Exception as e:
275
+ logging.error(f"خطأ في حفظ المحادثة: {e}")
276
+ return False
277
+
278
+ def _load_conversation(self, filename):
279
+ """تحميل محادثة محفوظة"""
280
+ file_path = os.path.join(self.conversations_dir, filename)
281
+
282
+ try:
283
+ with open(file_path, 'r', encoding='utf-8') as f:
284
+ conversation_data = json.load(f)
285
+
286
+ st.session_state.assistant_messages = conversation_data["messages"]
287
+ st.session_state.assistant_mode = conversation_data["mode"]
288
+ st.session_state.document_context = conversation_data.get("document_context")
289
+
290
+ return True
291
+ except Exception as e:
292
+ logging.error(f"خطأ في تحميل المحادثة: {e}")
293
+ return False
294
+
295
+ def _get_saved_conversations(self):
296
+ """الحصول على قائمة المحادثات المحفوظة"""
297
+ conversations = []
298
+
299
+ try:
300
+ for filename in os.listdir(self.conversations_dir):
301
+ if filename.endswith(".json") and filename.startswith("conversation_"):
302
+ file_path = os.path.join(self.conversations_dir, filename)
303
+
304
+ with open(file_path, 'r', encoding='utf-8') as f:
305
+ data = json.load(f)
306
+
307
+ conversations.append({
308
+ "filename": filename,
309
+ "timestamp": data.get("timestamp", ""),
310
+ "user": data.get("user", ""),
311
+ "mode": data.get("mode", "general"),
312
+ "message_count": len(data.get("messages", []))
313
+ })
314
+ except Exception as e:
315
+ logging.error(f"خطأ في قراءة المحادثات المحفوظة: {e}")
316
+
317
+ # ترتيب المحادثات حسب التاريخ (الأحدث أولاً)
318
+ conversations.sort(key=lambda x: x["timestamp"], reverse=True)
319
+
320
+ return conversations
321
+
322
+ def render_chat_interface(self):
323
+ """عرض واجهة المحادثة الرئيسية"""
324
+ st.markdown("<h2 class='module-title'>المساعد الذكي</h2>", unsafe_allow_html=True)
325
+
326
+ # التحقق من توفر OpenAI API
327
+ if not self.is_api_available:
328
+ st.warning("⚠️ مفتاح OpenAI API غير متوفر. لن يكون المساعد الذكي قادراً على الرد. يرجى التواصل مع مسؤول النظام.")
329
+
330
+ # إضافة CSS
331
+ st.markdown("""
332
+ <style>
333
+ .chat-container {
334
+ background-color: #f8f9fa;
335
+ border-radius: 10px;
336
+ padding: 20px;
337
+ margin-bottom: 20px;
338
+ max-height: 500px;
339
+ overflow-y: auto;
340
+ }
341
+
342
+ .chat-message {
343
+ margin-bottom: 15px;
344
+ display: flex;
345
+ flex-direction: row;
346
+ }
347
+
348
+ .user-message {
349
+ justify-content: flex-end;
350
+ }
351
+
352
+ .assistant-message {
353
+ justify-content: flex-start;
354
+ }
355
+
356
+ .message-bubble {
357
+ padding: 10px 15px;
358
+ border-radius: 15px;
359
+ max-width: 75%;
360
+ }
361
+
362
+ .user-bubble {
363
+ background-color: #1E88E5;
364
+ color: white;
365
+ border-top-left-radius: 15px;
366
+ border-top-right-radius: 15px;
367
+ border-bottom-left-radius: 15px;
368
+ border-bottom-right-radius: 0;
369
+ }
370
+
371
+ .assistant-bubble {
372
+ background-color: #f0f0f0;
373
+ color: #333;
374
+ border-top-left-radius: 15px;
375
+ border-top-right-radius: 15px;
376
+ border-bottom-left-radius: 0;
377
+ border-bottom-right-radius: 15px;
378
+ }
379
+
380
+ .message-avatar {
381
+ width: 40px;
382
+ height: 40px;
383
+ border-radius: 50%;
384
+ background-color: #ccc;
385
+ display: flex;
386
+ align-items: center;
387
+ justify-content: center;
388
+ margin: 0 10px;
389
+ font-weight: bold;
390
+ color: white;
391
+ }
392
+
393
+ .user-avatar {
394
+ background-color: #78909C;
395
+ }
396
+
397
+ .assistant-avatar {
398
+ background-color: #1E88E5;
399
+ }
400
+
401
+ .message-content {
402
+ white-space: pre-wrap;
403
+ }
404
+
405
+ .message-time {
406
+ font-size: 0.8em;
407
+ color: #888;
408
+ margin-top: 5px;
409
+ text-align: right;
410
+ }
411
+
412
+ .chat-input {
413
+ background-color: #f8f9fa;
414
+ border-radius: 10px;
415
+ padding: 20px;
416
+ }
417
+
418
+ .suggestions-container {
419
+ display: flex;
420
+ flex-wrap: wrap;
421
+ gap: 10px;
422
+ margin-top: 10px;
423
+ }
424
+
425
+ .suggestion-chip {
426
+ background-color: #e9ecef;
427
+ border-radius: 20px;
428
+ padding: 5px 15px;
429
+ cursor: pointer;
430
+ text-align: center;
431
+ transition: background-color 0.3s;
432
+ }
433
+
434
+ .suggestion-chip:hover {
435
+ background-color: #dee2e6;
436
+ }
437
+ </style>
438
+ """, unsafe_allow_html=True)
439
+
440
+ # عرض أوضاع المساعد
441
+ st.markdown("#### اختر وضع المساعد الذكي")
442
+
443
+ col1, col2, col3, col4, col5 = st.columns(5)
444
+
445
+ with col1:
446
+ if st.button("مساعد عام", key="mode_general",
447
+ help="مساعد عام للإجابة على الأسئلة المتعلقة بالعقود والمناقصات"):
448
+ st.session_state.assistant_mode = "general"
449
+ st.rerun()
450
+
451
+ with col2:
452
+ if st.button("تحليل العقود", key="mode_contract_analysis",
453
+ help="متخصص في تحليل العقود وتحديد البنود والشروط والمخاطر"):
454
+ st.session_state.assistant_mode = "contract_analysis"
455
+ st.rerun()
456
+
457
+ with col3:
458
+ if st.button("تقدير التكاليف", key="mode_cost_estimation",
459
+ help="متخصص في تقدير تكاليف المشاريع والبنود"):
460
+ st.session_state.assistant_mode = "cost_estimation"
461
+ st.rerun()
462
+
463
+ with col4:
464
+ if st.button("تقييم المخاطر", key="mode_risk_assessment",
465
+ help="متخصص في تحديد وتقييم المخاطر المحتملة في المشاريع والعقود"):
466
+ st.session_state.assistant_mode = "risk_assessment"
467
+ st.rerun()
468
+
469
+ with col5:
470
+ if st.button("تخطيط المشاريع", key="mode_project_planning",
471
+ help="متخصص في تخطيط وإدارة المشاريع وتحديد المراحل والموارد"):
472
+ st.session_state.assistant_mode = "project_planning"
473
+ st.rerun()
474
+
475
+ st.markdown(f"**الوضع الحالي:** {self.assistant_modes[st.session_state.assistant_mode]}")
476
+
477
+ # تحميل سياق من مستند (اختياري)
478
+ st.markdown("---")
479
+ with st.expander("إضافة سياق من مستند", expanded=False):
480
+ context_text = st.text_area(
481
+ "نص المستند (اختياري)",
482
+ value=st.session_state.document_context if st.session_state.document_context else "",
483
+ height=150,
484
+ help="أضف نص المستند هنا ليتم استخدامه كسياق للمحادثة"
485
+ )
486
+
487
+ uploaded_file = st.file_uploader(
488
+ "أو قم بتحميل ملف نصي أو PDF",
489
+ type=["txt", "pdf"],
490
+ help="يمكنك تحميل ملف نصي أو PDF ليتم استخدامه كسياق للمحادثة"
491
+ )
492
+
493
+ doc_col1, doc_col2 = st.columns(2)
494
+
495
+ with doc_col1:
496
+ if st.button("إضافة السياق", disabled=not context_text and not uploaded_file):
497
+ if uploaded_file:
498
+ try:
499
+ # قراءة الملف المرفوع
500
+ if uploaded_file.name.endswith(".pdf"):
501
+ import PyPDF2
502
+ reader = PyPDF2.PdfReader(uploaded_file)
503
+ context = ""
504
+ for page in reader.pages:
505
+ context += page.extract_text() + "\n"
506
+ else:
507
+ context = uploaded_file.read().decode("utf-8")
508
+
509
+ st.session_state.document_context = context
510
+ st.success("تم إضافة نص المستند كسياق للمحادثة بنجاح.")
511
+ except Exception as e:
512
+ st.error(f"حدث خطأ أثناء قراءة الملف: {str(e)}")
513
+ elif context_text:
514
+ st.session_state.document_context = context_text
515
+ st.success("تم إضافة نص المستند كسياق للمحادثة بنجاح.")
516
+
517
+ with doc_col2:
518
+ if st.button("مسح السياق", disabled=not st.session_state.document_context):
519
+ st.session_state.document_context = None
520
+ st.success("تم مسح سياق المستند بنجاح.")
521
+
522
+ # عرض المحادثة
523
+ st.markdown("---")
524
+ st.markdown("#### المحادثة مع المساعد الذكي")
525
+
526
+ # عرض رسائل المحادثة
527
+ chat_container = st.container()
528
+
529
+ with chat_container:
530
+ with st.container():
531
+ if not st.session_state.assistant_messages:
532
+ st.markdown("""
533
+ <div style="text-align: center; padding: 30px; color: #666;">
534
+ <p>مرحباً بك في المساعد الذكي!</p>
535
+ <p>يمكنك البدء بطرح سؤال أو طلب مساعدة.</p>
536
+ </div>
537
+ """, unsafe_allow_html=True)
538
+ else:
539
+ message_html = ""
540
+
541
+ for msg in st.session_state.assistant_messages:
542
+ if msg["role"] == "user":
543
+ message_html += f"""
544
+ <div class="chat-message user-message">
545
+ <div class="message-bubble user-bubble">
546
+ <div class="message-content">{msg["content"]}</div>
547
+ </div>
548
+ <div class="message-avatar user-avatar">أ</div>
549
+ </div>
550
+ """
551
+ else:
552
+ message_html += f"""
553
+ <div class="chat-message assistant-message">
554
+ <div class="message-avatar assistant-avatar">W</div>
555
+ <div class="message-bubble assistant-bubble">
556
+ <div class="message-content">{msg["content"]}</div>
557
+ </div>
558
+ </div>
559
+ """
560
+
561
+ st.markdown(f"""
562
+ <div class="chat-container">
563
+ {message_html}
564
+ </div>
565
+ """, unsafe_allow_html=True)
566
+
567
+ # ادخال الرسالة
568
+ st.markdown("#### أدخل رسالتك")
569
+
570
+ with st.container():
571
+ with st.form(key="chat_form"):
572
+ user_message = st.text_area("رسالتك", height=100, placeholder="اكتب سؤالك أو طلبك هنا...")
573
+
574
+ col1, col2, col3 = st.columns([2, 2, 1])
575
+
576
+ with col1:
577
+ send_button = st.form_submit_button(
578
+ "إرسال",
579
+ help="إرسال الرسالة إلى المساعد الذكي"
580
+ )
581
+
582
+ with col2:
583
+ suggested_questions = [
584
+ "كيف يمكنني تحليل بنود الدفع في العقد؟",
585
+ "ما هي أفضل طريقة لتقدير تكاليف مشروع بناء؟",
586
+ "كيف أحدد المخاطر المحتملة في مشروع جديد؟",
587
+ "كيف يمكنني إنشاء جدول زمني فعال للمشروع؟",
588
+ "ما هي أهم البنود التي يجب الانتباه إليها في عقود المقاولات؟"
589
+ ]
590
+
591
+ if st.session_state.assistant_mode == "contract_analysis":
592
+ suggested_questions = [
593
+ "كيف أحدد البنود غير المواتية في العقد؟",
594
+ "ما هي العناصر الأساسية التي يجب أن يتضمنها عقد المقاولة؟",
595
+ "كيف أتعامل مع بنود الغرامات والتعويضات؟",
596
+ "كيف يمكنني التفاوض على تحسين شروط الدفع؟",
597
+ "ما هي الفروق الرئيسية بين عقد الثمن الثابت وعقد التكلفة زائد أتعاب؟"
598
+ ]
599
+ elif st.session_state.assistant_mode == "cost_estimation":
600
+ suggested_questions = [
601
+ "كيف أقدر تكلفة المواد في مشروع بناء؟",
602
+ "ما هي نسبة النفقات العامة المعقولة لمشروع مقاولات؟",
603
+ "كيف أحسب تكلفة العمالة بدقة؟",
604
+ "ما هي العوامل التي تؤثر على تكلفة المعدات؟",
605
+ "كيف أقدر هامش الربح المناسب للمشروع؟"
606
+ ]
607
+
608
+ selected_question = st.selectbox(
609
+ "أو اختر سؤال مقترح",
610
+ [""] + suggested_questions,
611
+ index=0
612
+ )
613
+
614
+ with col3:
615
+ clear_button = st.form_submit_button(
616
+ "مسح المحادثة",
617
+ help="مسح جميع الرسائل في المحادثة الحالية"
618
+ )
619
+
620
+ if send_button and user_message:
621
+ # معالجة رسالة المستخدم
622
+ with st.spinner("جاري معالجة الرسالة..."):
623
+ self._process_user_message(user_message)
624
+ st.rerun()
625
+
626
+ if send_button and selected_question and not user_message:
627
+ # استخدام السؤال المقترح
628
+ with st.spinner("جاري معالجة الرسالة..."):
629
+ self._process_user_message(selected_question)
630
+ st.rerun()
631
+
632
+ if clear_button:
633
+ self._clear_chat()
634
+ st.rerun()
635
+
636
+ # زر لحفظ المحادثة
637
+ col1, col2, col3 = st.columns([1, 1, 2])
638
+
639
+ with col1:
640
+ if st.button("حفظ المحادثة", key="save_conversation", disabled=not st.session_state.assistant_messages):
641
+ if self._save_conversation():
642
+ st.success("تم حفظ المحادثة بنجاح.")
643
+ else:
644
+ st.error("حدث خطأ أثناء حفظ المحادثة.")
645
+
646
+ with col2:
647
+ if st.button("تحميل محادثة سابقة", key="show_load_conversation"):
648
+ st.session_state.show_conversations = True
649
+ st.rerun()
650
+
651
+ # عرض المحادثات المحفوظة
652
+ if "show_conversations" in st.session_state and st.session_state.show_conversations:
653
+ st.markdown("---")
654
+ st.markdown("#### المحادثات المحفوظة")
655
+
656
+ conversations = self._get_saved_conversations()
657
+
658
+ if not conversations:
659
+ st.info("لا توجد محادثات محفوظة.")
660
+ else:
661
+ # عرض المحادثات في جدول
662
+ conversation_data = []
663
+ for conv in conversations:
664
+ timestamp = datetime.strptime(conv["timestamp"], "%Y%m%d%H%M%S") if conv["timestamp"] else ""
665
+ formatted_time = timestamp.strftime("%Y-%m-%d %H:%M:%S") if timestamp else ""
666
+
667
+ conversation_data.append({
668
+ "التاريخ": formatted_time,
669
+ "المستخدم": conv["user"],
670
+ "وضع المساعد": self.assistant_modes.get(conv["mode"], "غير معروف"),
671
+ "عدد الرسائل": conv["message_count"],
672
+ "الملف": conv["filename"]
673
+ })
674
+
675
+ df = pd.DataFrame(conversation_data)
676
+ st.dataframe(df, height=300)
677
+
678
+ # اختيار محادثة لتحميلها
679
+ selected_filename = st.selectbox(
680
+ "اختر محادثة لتحميلها",
681
+ options=[""] + [conv["filename"] for conv in conversations],
682
+ format_func=lambda x: next((f"{c['user']} - {datetime.strptime(c['timestamp'], '%Y%m%d%H%M%S').strftime('%Y-%m-%d %H:%M:%S')}" for c in conversations if c["filename"] == x), x),
683
+ index=0
684
+ )
685
+
686
+ col1, col2 = st.columns(2)
687
+
688
+ with col1:
689
+ if st.button("تحميل المحادثة المختارة", disabled=not selected_filename):
690
+ if self._load_conversation(selected_filename):
691
+ st.success("تم تحميل المحادثة بنجاح.")
692
+ st.session_state.show_conversations = False
693
+ st.rerun()
694
+ else:
695
+ st.error("حدث خطأ أثناء تحميل المحادثة.")
696
+
697
+ with col2:
698
+ if st.button("إلغاء", key="cancel_load_conversation"):
699
+ st.session_state.show_conversations = False
700
+ st.rerun()
701
+
702
+ # عرض المعلومات عن وضع المساعد الحالي
703
+ st.markdown("---")
704
+ st.markdown(f"#### معلومات عن وضع المساعد: {self.assistant_modes[st.session_state.assistant_mode]}")
705
+
706
+ if st.session_state.assistant_mode == "general":
707
+ st.markdown("""
708
+ المساعد العام يمكنه مساعدتك في مجموعة متنوعة من المهام المتعلقة بالعقود والمناقصات وإدارة المشاريع. يمكنه:
709
+ - الإجابة على الأسئلة العامة حول العقود والمناقصات
710
+ - توجيهك إلى الوحدات المناسبة في النظام
711
+ - تقديم معلومات عامة عن إدارة المشاريع وأفضل الممارسات
712
+ - المساعدة في فهم المصطلحات والمفاهيم المتعلقة بمجال المقاولات
713
+ """)
714
+ elif st.session_state.assistant_mode == "contract_analysis":
715
+ st.markdown("""
716
+ مساعد تحليل العقود متخصص في:
717
+ - تحليل بنود العقود وتوضيح معانيها
718
+ - تحديد الالتزامات والحقوق لكل طرف
719
+ - تسليط الضوء على البنود غير المواتية أو الغامضة
720
+ - تقديم توصيات للتفاوض على تحسين شروط العقد
721
+ - مقارنة العقد مع أفضل الممارسات في الق��اع
722
+ """)
723
+ elif st.session_state.assistant_mode == "cost_estimation":
724
+ st.markdown("""
725
+ مساعد تقدير التكاليف متخصص في:
726
+ - حساب تكاليف المشاريع بناءً على المتطلبات والمواصفات
727
+ - تقدير تكاليف المواد والعمالة والمعدات
728
+ - تحليل التكاليف المباشرة وغير المباشرة
729
+ - تقديم نصائح لتقليل التكاليف وزيادة الكفاءة
730
+ - تحديد العوامل التي قد تؤثر على التكلفة الإجمالية
731
+ """)
732
+ elif st.session_state.assistant_mode == "risk_assessment":
733
+ st.markdown("""
734
+ مساعد تقييم المخاطر متخصص في:
735
+ - تحديد المخاطر المحتملة في المشاريع والعقود
736
+ - تقييم احتمالية وتأثير كل خطر
737
+ - اقتراح استراتيجيات للتخفيف من المخاطر
738
+ - إنشاء خطط للطوارئ والاستجابة للمخاطر
739
+ - تحليل تأثير المخاطر على الجدول الزمني والتكلفة
740
+ """)
741
+ elif st.session_state.assistant_mode == "project_planning":
742
+ st.markdown("""
743
+ مساعد تخطيط المشاريع متخصص في:
744
+ - تقسيم المشروع إلى مراحل ومهام وأنشطة
745
+ - تحديد الموارد اللازمة لكل نشاط
746
+ - إنشاء الجداول الزمنية والمسار الحرج
747
+ - التخطيط للموارد البشرية والمعدات والمواد
748
+ - مراقبة تقدم المشروع وإدارة التغييرات
749
+ """)
750
+
751
+ # عرض معلومات حقوق الملكية
752
+ render_credits()
753
+
754
+ def render(self):
755
+ """عرض واجهة المساعد الذكي الرئيسية"""
756
+ # تحميل CSS المخصص
757
+ load_css()
758
+
759
+ # عرض واجهة المحادثة
760
+ self.render_chat_interface()
761
+
762
+
763
+ # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
764
+ if __name__ == "__main__":
765
+ st.set_page_config(
766
+ page_title="المساعد الذكي | WAHBi AI",
767
+ page_icon="🤖",
768
+ layout="wide",
769
+ initial_sidebar_state="expanded"
770
+ )
771
+
772
+ assistant = AIAssistant()
773
+ assistant.render()
modules/ai_assistant/ai_assistant_app.py ADDED
The diff for this file is too large to render. See raw diff
 
modules/ai_assistant/assistant_app.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ تطبيق المساعد الذكي التفاعلي
6
+ يتيح للمستخدمين التفاعل مع نماذج الذكاء الاصطناعي المتقدمة للحصول على مساعدة في تحليل العقود والمناقصات
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import streamlit as st
12
+ import pandas as pd
13
+ import numpy as np
14
+
15
+ # إضافة مسار النظام للوصول للملفات المشتركة
16
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
17
+
18
+ # استيراد مكونات المساعد الذكي
19
+ from modules.ai_assistant.ai_assistant import AIAssistant
20
+
21
+
22
+ class AssistantApp:
23
+ """تطبيق المساعد الذكي التفاعلي"""
24
+
25
+ def __init__(self):
26
+ """تهيئة تطبيق المساعد الذكي"""
27
+ self.assistant = AIAssistant()
28
+
29
+ def render(self):
30
+ """عرض واجهة المستخدم الرئيسية للتطبيق"""
31
+ self.assistant.render()
32
+
33
+
34
+ # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
35
+ if __name__ == "__main__":
36
+ st.set_page_config(
37
+ page_title="المساعد الذكي | WAHBi AI",
38
+ page_icon="🤖",
39
+ layout="wide",
40
+ initial_sidebar_state="expanded"
41
+ )
42
+
43
+ app = AssistantApp()
44
+ app.render()
modules/ai_finetuning/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # ملف تهيئة حزمة ضبط نماذج الذكاء الاصطناعي
modules/ai_finetuning/finetuning_app.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي للمصطلحات التعاقدية المتخصصة
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import streamlit as st
11
+ import pandas as pd
12
+ import numpy as np
13
+
14
+ # إضافة مسار النظام للوصول للملفات المشتركة
15
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
16
+
17
+ # استيراد مكونات تخصيص وضبط نماذج الذكاء الاصطناعي
18
+ from modules.ai_finetuning.model_finetuning import ModelFinetuning
19
+
20
+
21
+ class FinetuningApp:
22
+ """وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي"""
23
+
24
+ def __init__(self):
25
+ """تهيئة وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي"""
26
+ self.model_finetuning = ModelFinetuning()
27
+
28
+ def render(self):
29
+ """عرض واجهة وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي"""
30
+ st.markdown("<h2 class='module-title'>وحدة تخصيص وضبط نماذج الذكاء الاصطناعي</h2>", unsafe_allow_html=True)
31
+
32
+ st.markdown("""
33
+ <div class="module-description">
34
+ تمكنك هذه الوحدة من تخصيص وضبط نماذج الذكاء الاصطناعي للتعرف بدقة على المصطلحات التعاقدية والهندسية المتخصصة باللغة العربية.
35
+ يمكنك إنشاء قاموس للمصطلحات، وإعداد أمثلة التدريب، وتدريب النماذج واختبارها.
36
+ </div>
37
+ """, unsafe_allow_html=True)
38
+
39
+ # عرض نموذج تخصيص وضبط نماذج الذكاء الاصطناعي
40
+ self.model_finetuning.render()
41
+
42
+
43
+ # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
44
+ if __name__ == "__main__":
45
+ st.set_page_config(
46
+ page_title="تخصيص وضبط نماذج الذكاء الاصطناعي | WAHBi AI",
47
+ page_icon="🧠",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded"
50
+ )
51
+
52
+ app = FinetuningApp()
53
+ app.render()
modules/ai_finetuning/model_finetuning.py ADDED
The diff for this file is too large to render. See raw diff
 
modules/document_analysis/document_analysis_app.py ADDED
@@ -0,0 +1,1114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ وحدة تطبيق تحليل المستندات
4
+
5
+ هذا الملف يحتوي على الفئة الرئيسية لتطبيق تحليل المستندات.
6
+ """
7
+
8
+ # استيراد المكتبات القياسية
9
+ import os
10
+ import sys
11
+ import logging
12
+ import base64
13
+ import json
14
+ import time
15
+ from io import BytesIO
16
+ from pathlib import Path
17
+ from urllib.parse import urlparse
18
+ from tempfile import NamedTemporaryFile
19
+
20
+ # استيراد مكتبة Streamlit
21
+ import streamlit as st
22
+
23
+ # استيراد المكتبات الإضافية
24
+ import requests
25
+ from PIL import Image
26
+
27
+ try:
28
+ # استيراد مكتبات Docling و MLX VLM
29
+ from docling_core.types.doc import ImageRefMode
30
+ from docling_core.types.doc.document import DocTagsDocument, DoclingDocument
31
+ from mlx_vlm import load, generate
32
+ from mlx_vlm.prompt_utils import apply_chat_template
33
+ from mlx_vlm.utils import load_config, stream_generate
34
+ docling_available = True
35
+ except ImportError:
36
+ docling_available = False
37
+ logging.warning("لم يتم العثور على مكتبات Docling و MLX VLM. بعض الوظائف قد لا تعمل.")
38
+
39
+ try:
40
+ # استيراد مكتبة pdf2image للتعامل مع ملفات PDF
41
+ from pdf2image import convert_from_path
42
+ pdf_conversion_available = True
43
+ except ImportError:
44
+ pdf_conversion_available = False
45
+ logging.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.")
46
+
47
+ # إعداد المسار للوحدات النمطية
48
+ current_dir = os.path.dirname(os.path.abspath(__file__))
49
+ parent_dir = os.path.dirname(os.path.dirname(current_dir))
50
+ if parent_dir not in sys.path:
51
+ sys.path.append(parent_dir)
52
+
53
+ # استيراد الخدمات باستخدام المسار النسبي
54
+ try:
55
+ # الطريقة 1: استيراد نسبي مباشر
56
+ from .services.text_extractor import TextExtractor
57
+ from .services.item_extractor import ItemExtractor
58
+ from .services.document_parser import DocumentParser
59
+ except ImportError:
60
+ try:
61
+ # الطريقة 2: استيراد مطلق
62
+ from modules.document_analysis.services.text_extractor import TextExtractor
63
+ from modules.document_analysis.services.item_extractor import ItemExtractor
64
+ from modules.document_analysis.services.document_parser import DocumentParser
65
+ except ImportError:
66
+ # الطريقة 3: تعريف الفئات مباشرة كحل مؤقت
67
+ logging.warning("لا يمكن استيراد خدمات تحليل المستندات. استخدام التعريفات المؤقتة.")
68
+
69
+ class TextExtractor:
70
+ def __init__(self, config=None):
71
+ self.config = config or {}
72
+
73
+ def extract_from_pdf(self, file_path):
74
+ return "نص مستخرج مؤقت من PDF"
75
+
76
+ def extract_from_docx(self, file_path):
77
+ return "نص مستخرج مؤقت من DOCX"
78
+
79
+ def extract_from_image(self, file_path):
80
+ return "نص مستخرج مؤقت من صورة"
81
+
82
+ def extract(self, file_path):
83
+ _, ext = os.path.splitext(file_path)
84
+ ext = ext.lower()
85
+
86
+ if ext == '.pdf':
87
+ return self.extract_from_pdf(file_path)
88
+ elif ext in ('.doc', '.docx'):
89
+ return self.extract_from_docx(file_path)
90
+ elif ext in ('.jpg', '.jpeg', '.png'):
91
+ return self.extract_from_image(file_path)
92
+ else:
93
+ return "نوع ملف غير مدعوم"
94
+
95
+ class ItemExtractor:
96
+ def __init__(self, config=None):
97
+ self.config = config or {}
98
+
99
+ def extract_tables(self, document):
100
+ return [{"عنوان": "جدول مؤقت", "بيانات": []}]
101
+
102
+ def extract(self, file_path):
103
+ return [
104
+ {"بند": "بند مؤقت 1", "قيمة": 1000},
105
+ {"بند": "بند مؤقت 2", "قيمة": 2000},
106
+ {"بند": "بند مؤقت 3", "قيمة": 3000}
107
+ ]
108
+
109
+ class DocumentParser:
110
+ def __init__(self, config=None):
111
+ self.config = config or {}
112
+
113
+ def parse_document(self, file_path):
114
+ return {"نوع": "مستند مؤقت", "محتوى": "محتوى مؤقت"}
115
+
116
+ def parse(self, file_path):
117
+ return {
118
+ "نوع المستند": "مستند مؤقت",
119
+ "عدد الصفحات": 5,
120
+ "تاريخ التحليل": "2025-03-24",
121
+ "درجة الثقة": "80%",
122
+ "ملاحظات": "تحليل مؤقت للمستند"
123
+ }
124
+
125
+
126
+ class DoclingAnalyzer:
127
+ """
128
+ فئة لتحليل المستندات باستخدام نماذج Docling و MLX VLM
129
+ """
130
+ def __init__(self):
131
+ self.model = None
132
+ self.processor = None
133
+ self.config = None
134
+ self.docling_available = False
135
+
136
+ try:
137
+ # تحميل النموذج
138
+ import os
139
+ from mlx_vlm import load, generate
140
+ from mlx_vlm.utils import load_config
141
+
142
+ model_path = "ds4sd/SmolDocling-256M-preview-mlx-bf16"
143
+ self.model, self.processor = load(model_path)
144
+ self.config = load_config(model_path)
145
+ self.docling_available = True
146
+ except Exception as e:
147
+ print(f"خطأ في تحميل نموذج Docling: {str(e)}")
148
+ self.docling_available = False
149
+
150
+ def is_available(self):
151
+ """التحقق من توفر نماذج Docling"""
152
+ return self.docling_available and self.model is not None
153
+
154
+ def analyze_image(self, image_path=None, image_url=None, image_bytes=None, prompt="Convert this page to docling."):
155
+ """
156
+ تحليل صورة باستخدام نموذج Docling
157
+
158
+ المعلمات:
159
+ image_path (str): مسار الصورة المحلية (اختياري)
160
+ image_url (str): رابط الصورة (اختياري)
161
+ image_bytes (bytes): بيانات الصورة (اختياري)
162
+ prompt (str): التوجيه للنموذج
163
+
164
+ العوائد:
165
+ dict: نتائج التحليل متضمنة النص والعلامات والمستند
166
+ """
167
+ if not self.is_available():
168
+ return {
169
+ "error": "Docling غير متوفر. يرجى تثبيت المكتبات المطلوبة."
170
+ }
171
+
172
+ try:
173
+ from io import BytesIO
174
+ from pathlib import Path
175
+ from urllib.parse import urlparse
176
+ import requests
177
+ from PIL import Image
178
+ from docling_core.types.doc import ImageRefMode
179
+ from docling_core.types.doc.document import DocTagsDocument, DoclingDocument
180
+ from mlx_vlm.prompt_utils import apply_chat_template
181
+ from mlx_vlm.utils import stream_generate, load_image
182
+
183
+ # تحميل الصورة
184
+ pil_image = None
185
+ image_source = None
186
+
187
+ if image_url:
188
+ try:
189
+ response = requests.get(image_url, stream=True, timeout=10)
190
+ response.raise_for_status()
191
+ pil_image = Image.open(BytesIO(response.content))
192
+ image_source = image_url
193
+ except Exception as e:
194
+ return {"error": f"فشل في تحميل الصورة من الرابط: {str(e)}"}
195
+ elif image_path:
196
+ try:
197
+ # التأكد من وجود الملف
198
+ if not Path(image_path).exists():
199
+ return {"error": f"ملف الصورة غير موجود: {image_path}"}
200
+ pil_image = Image.open(image_path)
201
+ image_source = image_path
202
+ except Exception as e:
203
+ return {"error": f"فشل في فتح ملف الصورة: {str(e)}"}
204
+ elif image_bytes:
205
+ try:
206
+ pil_image = Image.open(BytesIO(image_bytes))
207
+ # حفظ الصورة مؤقتا للتحليل
208
+ temp_path = "/tmp/temp_image.jpg"
209
+ pil_image.save(temp_path)
210
+ image_source = temp_path
211
+ except Exception as e:
212
+ return {"error": f"فشل في معالجة بيانات الصورة: {str(e)}"}
213
+ else:
214
+ return {"error": "يجب توفير مصدر للصورة (مسار، رابط، أو بيانات)"}
215
+
216
+ # تطبيق قالب المحادثة
217
+ formatted_prompt = apply_chat_template(self.processor, self.config, prompt, num_images=1)
218
+
219
+ # إنشاء النتيجة
220
+ output = ""
221
+
222
+ # تمرير مسار الصورة أو عنوان URL الفعلي
223
+ try:
224
+ for token in stream_generate(
225
+ self.model, self.processor, formatted_prompt, [image_source],
226
+ max_tokens=4096, verbose=False
227
+ ):
228
+ output += token.text
229
+ if "</doctag>" in token.text:
230
+ break
231
+ except Exception as e:
232
+ return {"error": f"فشل في تحليل الصورة: {str(e)}"}
233
+
234
+ # إنشاء مستند Docling
235
+ try:
236
+ doctags_doc = DocTagsDocument.from_doctags_and_image_pairs([output], [pil_image])
237
+ doc = DoclingDocument(name="AnalyzedDocument")
238
+ doc.load_from_doctags(doctags_doc)
239
+
240
+ # إرجاع النتائج
241
+ return {
242
+ "doctags": output,
243
+ "markdown": doc.export_to_markdown(),
244
+ "document": doc,
245
+ "image": pil_image
246
+ }
247
+ except Exception as e:
248
+ return {"error": f"فشل في إنشاء مستند Docling: {str(e)}"}
249
+
250
+ except Exception as e:
251
+ return {"error": f"حدث خطأ غير متوقع: {str(e)}"}
252
+
253
+ def export_to_html(self, doc, output_path="./output.html", show_in_browser=False):
254
+ """
255
+ تصدير المستند إلى HTML
256
+
257
+ المعلمات:
258
+ doc (DoclingDocument): مستند Docling
259
+ output_path (str): مسار ملف الإخراج
260
+ show_in_browser (bool): عرض الملف في المتصفح
261
+
262
+ العوائد:
263
+ str: مسار ملف HTML المولد
264
+ """
265
+ if not self.is_available():
266
+ return None
267
+
268
+ try:
269
+ from pathlib import Path
270
+ from docling_core.types.doc import ImageRefMode
271
+
272
+ # إنشاء مسار الإخراج
273
+ out_path = Path(output_path)
274
+ # التأكد من وجود المجلد
275
+ out_path.parent.mkdir(exist_ok=True, parents=True)
276
+
277
+ doc.save_as_html(out_path, image_mode=ImageRefMode.EMBEDDED)
278
+
279
+ # فتح في المتصفح إذا تم طلب ذلك
280
+ if show_in_browser:
281
+ import webbrowser
282
+ webbrowser.open(f"file:///{str(out_path.resolve())}")
283
+
284
+ return str(out_path)
285
+ except Exception as e:
286
+ print(f"خطأ في تصدير المستند إلى HTML: {str(e)}")
287
+ return None
288
+
289
+
290
+ class ClaudeAnalyzer:
291
+ """
292
+ فئة لتحليل المستندات باستخدام Claude.ai API
293
+ """
294
+ def __init__(self):
295
+ """تهيئة محلل Claude"""
296
+ self.api_url = "https://api.anthropic.com/v1/messages"
297
+
298
+ def get_api_key(self):
299
+ """الحصول على مفتاح API من متغيرات البيئة"""
300
+ api_key = os.environ.get("anthropic")
301
+ if not api_key:
302
+ raise ValueError("مفتاح API لـ Claude غير موجود في متغيرات البيئة")
303
+ return api_key
304
+
305
+ def analyze_document(self, file_path, model_name="claude-3-7-sonnet", prompt=None):
306
+ """
307
+ تحليل مستند باستخدام Claude AI
308
+
309
+ المعلمات:
310
+ file_path: مسار الملف المراد تحليله
311
+ model_name: اسم نموذج Claude المراد استخدامه
312
+ prompt: التوجيه المخصص للتحليل (اختياري)
313
+
314
+ العوائد:
315
+ dict: نتائج التحليل
316
+ """
317
+ try:
318
+ # الحصول على مفتاح API
319
+ api_key = self.get_api_key()
320
+
321
+ # تحديد التوجيه المناسب إذا لم يتم توفيره
322
+ if prompt is None:
323
+ _, ext = os.path.splitext(file_path)
324
+ ext = ext.lower()
325
+
326
+ if ext == '.pdf':
327
+ prompt = "قم بتحليل هذه الصورة المستخرجة من مستند PDF واستخراج المعلومات الرئيسية مثل العناوين، الفقرات، الجداول، والنقاط المهمة."
328
+ elif ext in ('.doc', '.docx'):
329
+ prompt = "قم بتحليل هذه الصورة المستخرجة من مستند Word واستخراج المعلومات الرئيسية والخلاصة."
330
+ elif ext in ('.jpg', '.jpeg', '.png', '.gif', '.webp'):
331
+ prompt = "قم بوصف وتحليل محتوى هذه الصورة بالتفصيل، مع ذكر العناصر المهمة والنصوص والبيانات الموجودة فيها."
332
+ else:
333
+ prompt = "قم بتحليل محتوى هذا الملف واستخراج المعلومات المفيدة منه."
334
+
335
+ # التحقق من نوع الملف وتحويله إذا لزم الأمر
336
+ _, ext = os.path.splitext(file_path)
337
+ ext = ext.lower()
338
+
339
+ processed_file_path = file_path
340
+ temp_files = [] # قائمة للملفات المؤقتة لحذفها لاحقاً
341
+
342
+ # للملفات غير المدعومة مباشرة (مثل PDF)
343
+ if ext not in ('.jpg', '.jpeg', '.png', '.gif', '.webp'):
344
+ # إذا كان الملف PDF، حاول تحويله إلى صورة
345
+ if ext == '.pdf':
346
+ if not pdf_conversion_available:
347
+ return {"error": "لا يمكن تحويل ملف PDF إلى صورة. يرجى تثبيت مكتبة pdf2image."}
348
+
349
+ try:
350
+ # تحويل الصفحة الأولى فقط
351
+ images = convert_from_path(file_path, first_page=1, last_page=1)
352
+ if images:
353
+ # حفظ الصورة بشكل مؤقت
354
+ temp_image_path = "/tmp/temp_pdf_image.jpg"
355
+ images[0].save(temp_image_path, 'JPEG')
356
+ processed_file_path = temp_image_path # استخدام مسار الصورة الجديد
357
+ temp_files.append(temp_image_path)
358
+ else:
359
+ return {"error": "فشل في تحويل ملف PDF إلى صورة"}
360
+ except Exception as e:
361
+ return {"error": f"فشل في تحويل ملف PDF إلى صورة: {str(e)}"}
362
+ else:
363
+ return {"error": f"نوع الملف {ext} غير مدعوم. Claude API يدعم فقط الصور (JPEG, PNG, GIF, WebP) أو PDF (يتم تحويله تلقائياً)."}
364
+
365
+ # ضغط الصورة إذا كان حجمها كبيراً
366
+ try:
367
+ img = Image.open(processed_file_path)
368
+
369
+ # تحقق من حجم الصورة وضغطها إذا كانت كبيرة
370
+ img_width, img_height = img.size
371
+ if img_width > 1500 or img_height > 1500:
372
+ # تحويل الصورة إلى حجم أصغر (1500×1500 بكسل كحد أقصى)
373
+ img.thumbnail((1500, 1500))
374
+
375
+ # حفظ الصورة المضغوطة في ملف مؤقت
376
+ compressed_image_path = "/tmp/compressed_image.jpg"
377
+ img.save(compressed_image_path, format="JPEG", quality=85)
378
+
379
+ # إضافة الملف المؤقت إلى القائمة
380
+ if processed_file_path not in temp_files:
381
+ temp_files.append(compressed_image_path)
382
+
383
+ processed_file_path = compressed_image_path
384
+ except Exception as e:
385
+ logging.warning(f"فشل في ضغط الصورة: {str(e)}. سيتم استخدام الصورة الأصلية.")
386
+
387
+ # قراءة محتوى الملف المعالج
388
+ with open(processed_file_path, 'rb') as f:
389
+ file_content = f.read()
390
+
391
+ # التحقق من حجم الملف (يجب أن يكون أقل من 20 ميجابايت)
392
+ file_size_mb = len(file_content) / (1024 * 1024)
393
+ if file_size_mb > 20:
394
+ # محاولة ضغط الصورة أكثر إذا كان حجمها أكبر من 20 ميجابايت
395
+ try:
396
+ img = Image.open(processed_file_path)
397
+
398
+ # ضغط أكبر - حجم أصغر وجودة أقل
399
+ compressed_image_path = "/tmp/extra_compressed_image.jpg"
400
+ img.thumbnail((1000, 1000))
401
+ img.save(compressed_image_path, format="JPEG", quality=70)
402
+
403
+ # إضافة الملف المؤقت إلى القائمة
404
+ temp_files.append(compressed_image_path)
405
+ processed_file_path = compressed_image_path
406
+
407
+ # قراءة الملف المضغوط
408
+ with open(processed_file_path, 'rb') as f:
409
+ file_content = f.read()
410
+
411
+ # التحقق من الحجم مرة أخرى
412
+ file_size_mb = len(file_content) / (1024 * 1024)
413
+ if file_size_mb > 20:
414
+ # لا يزال الحجم كبيراً
415
+ for temp_file in temp_files:
416
+ try:
417
+ os.unlink(temp_file)
418
+ except:
419
+ pass
420
+ return {"error": f"حجم الملف كبير جدًا ({file_size_mb:.2f} ميجابايت) حتى بعد الضغط. يجب أن يكون أقل من 20 ميجابايت."}
421
+ except Exception as e:
422
+ for temp_file in temp_files:
423
+ try:
424
+ os.unlink(temp_file)
425
+ except:
426
+ pass
427
+ return {"error": f"الملف كبير جدًا ({file_size_mb:.2f} ميجابايت) ولا يمكن ضغطه. يجب أن يكون أقل من 20 ميجابايت."}
428
+
429
+ # تحديد نوع الملف المعالج (بعد التحويل إذا تم)
430
+ file_type = self._get_file_type(processed_file_path)
431
+
432
+ # تحويل المحتوى إلى Base64
433
+ file_base64 = base64.b64encode(file_content).decode('utf-8')
434
+
435
+ # إعداد البيانات للطلب
436
+ headers = {
437
+ "Content-Type": "application/json",
438
+ "x-api-key": api_key,
439
+ "anthropic-version": "2023-06-01"
440
+ }
441
+
442
+ # التحقق من اسم النموذج وتصحيحه إذا لزم الأمر
443
+ valid_models = {
444
+ "claude-3-7-sonnet": "claude-3-7-sonnet-20250219",
445
+ "claude-3-5-haiku": "claude-3-5-haiku-20240307"
446
+ }
447
+
448
+ if model_name in valid_models:
449
+ model_name = valid_models[model_name]
450
+
451
+ # طباعة معلومات التصحيح
452
+ logging.debug(f"إرسال طلب إلى Claude API: {model_name}, نوع الملف: {file_type}")
453
+
454
+ # تحضير payload للـ API
455
+ payload = {
456
+ "model": model_name,
457
+ "max_tokens": 4096,
458
+ "messages": [
459
+ {
460
+ "role": "user",
461
+ "content": [
462
+ {"type": "text", "text": prompt},
463
+ {
464
+ "type": "image",
465
+ "source": {
466
+ "type": "base64",
467
+ "media_type": file_type,
468
+ "data": file_base64
469
+ }
470
+ }
471
+ ]
472
+ }
473
+ ]
474
+ }
475
+
476
+ # إرسال الطلب إلى API مع محاولات إعادة
477
+ for attempt in range(3): # ثلاث محاولات كحد أقصى
478
+ try:
479
+ response = requests.post(
480
+ self.api_url,
481
+ headers=headers,
482
+ json=payload,
483
+ timeout=120 # زيادة مهلة الانتظار إلى دقيقتين
484
+ )
485
+
486
+ # إذا نجح الطلب، نخرج من الحلقة
487
+ if response.status_code == 200:
488
+ break
489
+
490
+ # إذا كان الخطأ 502، ننتظر ونحاول مرة أخرى
491
+ if response.status_code == 502:
492
+ wait_time = (attempt + 1) * 5 # انتظار 5، 10، 15 ثانية
493
+ logging.warning(f"تم استلام خطأ 502. الانتظار {wait_time} ثانية قبل إعادة المحاولة.")
494
+ time.sleep(wait_time)
495
+ else:
496
+ # إذا كان الخطأ ليس 502، نخرج من الحلقة
497
+ break
498
+
499
+ except requests.exceptions.RequestException as e:
500
+ logging.warning(f"فشل الطلب في المحاولة {attempt+1}: {str(e)}")
501
+ if attempt == 2: # آخر محاولة
502
+ # حذف الملفات المؤقتة
503
+ for temp_file in temp_files:
504
+ try:
505
+ os.unlink(temp_file)
506
+ except:
507
+ pass
508
+ return {"error": f"فشل الاتصال بعد عدة محاولات: {str(e)}"}
509
+ time.sleep((attempt + 1) * 5) # انتظار قبل إعادة المحاولة
510
+
511
+ # حذف الملفات المؤقتة
512
+ for temp_file in temp_files:
513
+ try:
514
+ os.unlink(temp_file)
515
+ except:
516
+ pass
517
+
518
+ # التحقق من نجاح الطلب
519
+ if response.status_code != 200:
520
+ error_message = f"فشل طلب API: {response.status_code}"
521
+ try:
522
+ error_details = response.json()
523
+ error_message += f"\nتفاصيل: {error_details}"
524
+ except:
525
+ error_message += f"\nتفاصيل: {response.text}"
526
+
527
+ return {
528
+ "error": error_message
529
+ }
530
+
531
+ # معالجة الاستجابة
532
+ result = response.json()
533
+
534
+ return {
535
+ "success": True,
536
+ "content": result["content"][0]["text"],
537
+ "model": result["model"],
538
+ "usage": result.get("usage", {})
539
+ }
540
+
541
+ except Exception as e:
542
+ # حذف الملفات المؤقتة في حالة حدوث خطأ
543
+ for temp_file in temp_files:
544
+ try:
545
+ os.unlink(temp_file)
546
+ except:
547
+ pass
548
+
549
+ logging.error(f"خطأ أثناء تحليل المستند: {str(e)}")
550
+ import traceback
551
+ stack_trace = traceback.format_exc()
552
+ return {"error": f"فشل في تحليل المستند: {str(e)}\n{stack_trace}"}
553
+
554
+ def _get_file_type(self, file_path):
555
+ """تحديد نوع الملف من امتداده"""
556
+ _, ext = os.path.splitext(file_path)
557
+ ext = ext.lower()
558
+
559
+ # Claude API يدعم فقط أنواع الصور التالية
560
+ if ext in ('.jpg', '.jpeg'):
561
+ return "image/jpeg"
562
+ elif ext == '.png':
563
+ return "image/png"
564
+ elif ext == '.gif':
565
+ return "image/gif"
566
+ elif ext == '.webp':
567
+ return "image/webp"
568
+ else:
569
+ # للملفات الأخرى، نعيد نوع صورة افتراضي
570
+ # هذا سيستخدم فقط إذا تم تحويل الملف إلى صورة أولاً
571
+ return "image/jpeg"
572
+
573
+ def get_available_models(self):
574
+ """
575
+ الحصول على قائمة بالنماذج المتاحة
576
+
577
+ العوائد:
578
+ dict: قائمة بالنماذج مع وصفها
579
+ """
580
+ return {
581
+ "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة",
582
+ "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية"
583
+ }
584
+
585
+ def get_model_full_name(self, short_name):
586
+ """
587
+ تحويل الاسم المختصر للنموذج إلى الاسم الكامل
588
+
589
+ المعلمات:
590
+ short_name: الاسم المختصر للنموذج
591
+
592
+ العوائد:
593
+ str: الاسم الكامل للنموذج
594
+ """
595
+ valid_models = {
596
+ "claude-3-7-sonnet": "claude-3-7-sonnet-20250219",
597
+ "claude-3-5-haiku": "claude-3-5-haiku-20240307"
598
+ }
599
+
600
+ return valid_models.get(short_name, short_name)
601
+
602
+
603
+ class DocumentAnalysisApp:
604
+ def __init__(self):
605
+ # إنشاء كائنات الخدمات
606
+ self.text_extractor = TextExtractor()
607
+ self.item_extractor = ItemExtractor()
608
+ self.document_parser = DocumentParser()
609
+
610
+ # إنشاء محلل Docling
611
+ self.docling_analyzer = DoclingAnalyzer()
612
+
613
+ # إنشاء محلل Claude
614
+ self.claude_analyzer = ClaudeAnalyzer()
615
+
616
+ def render(self):
617
+ """العرض الرئيسي للتطبيق"""
618
+ st.title("تحليل المستندات")
619
+ st.write("اختر ملفًا لتحليله واستخرج البيانات المطلوبة.")
620
+
621
+ # إنشاء علامات تبويب للأنواع المختلفة من التحليل
622
+ tabs = st.tabs(["تحليل عام", "تحليل Docling", "تحليل Claude AI"])
623
+
624
+ with tabs[0]:
625
+ self._render_general_analysis()
626
+
627
+ with tabs[1]:
628
+ self._render_docling_analysis()
629
+
630
+ with tabs[2]:
631
+ self._render_claude_analysis()
632
+
633
+ def _render_general_analysis(self):
634
+ """عرض واجهة التحليل العام"""
635
+ uploaded_file = st.file_uploader("ارفع ملف PDF أو DOCX", type=["pdf", "docx"], key="general_uploader")
636
+
637
+ if uploaded_file:
638
+ with st.spinner("جاري تحليل المستند..."):
639
+ file_path = f"/tmp/{uploaded_file.name}"
640
+ with open(file_path, "wb") as f:
641
+ f.write(uploaded_file.read())
642
+
643
+ # تحديد نوع الملف من امتداده
644
+ _, ext = os.path.splitext(file_path)
645
+ ext = ext.lower()
646
+
647
+ # استخراج النص حسب نوع الملف
648
+ if ext == '.pdf':
649
+ extracted_text = self.text_extractor.extract_from_pdf(file_path)
650
+ elif ext in ('.doc', '.docx'):
651
+ extracted_text = self.text_extractor.extract_from_docx(file_path)
652
+ else:
653
+ extracted_text = "نوع ملف غير مدعوم للنص"
654
+
655
+ # عرض النص المستخرج
656
+ st.subheader("النص المستخرج:")
657
+ st.text_area("النص", extracted_text, height=300)
658
+
659
+ # استخراج البنود
660
+ extracted_items = self.item_extractor.extract(file_path)
661
+ if extracted_items:
662
+ st.subheader("البنود المستخرجة:")
663
+ st.dataframe(extracted_items)
664
+
665
+ # تحليل المستند
666
+ parsed_data = self.document_parser.parse(file_path)
667
+ st.subheader("تحليل المستند:")
668
+ st.json(parsed_data)
669
+
670
+ def _render_docling_analysis(self):
671
+ """عرض واجهة تحليل Docling"""
672
+ import streamlit as st
673
+ from tempfile import NamedTemporaryFile
674
+
675
+ if not self.docling_analyzer.is_available():
676
+ st.warning("مكتبات Docling و MLX VLM غير متوفرة. يرجى تثبيت الحزم المطلوبة.")
677
+ st.code("""
678
+ # يرجى تثبيت الحزم التالية:
679
+ pip install docling-core mlx-vlm pillow>=10.3.0 transformers>=4.49.0 tqdm>=4.66.2
680
+ """)
681
+ return
682
+
683
+ st.subheader("تحليل الصور والمستندات باستخدام Docling")
684
+
685
+ # اختيار مصدر الصورة
686
+ source_option = st.radio("اختر مصدر الصورة:", ["رفع صورة", "رابط صورة"])
687
+
688
+ image_path = None
689
+ image_url = None
690
+ image_data = None
691
+
692
+ if source_option == "رفع صورة":
693
+ uploaded_image = st.file_uploader("ارفع صورة", type=["jpg", "jpeg", "png"], key="docling_uploader")
694
+ if uploaded_image:
695
+ # حفظ الصورة المرفوعة إلى ملف مؤقت
696
+ image_data = uploaded_image.read()
697
+
698
+ # عرض الصورة المرفوعة
699
+ st.image(image_data, caption="الصورة المرفوعة", width=400)
700
+
701
+ # إنشاء ملف مؤقت لحفظ الصورة
702
+ with NamedTemporaryFile(delete=False, suffix=f".{uploaded_image.name.split('.')[-1]}") as temp_file:
703
+ temp_file.write(image_data)
704
+ image_path = temp_file.name
705
+ else:
706
+ image_url = st.text_input("أدخل رابط الصورة:")
707
+ if image_url:
708
+ try:
709
+ # عرض الصورة من الرابط
710
+ st.image(image_url, caption="الصورة من الرابط", width=400)
711
+ except Exception as e:
712
+ st.error(f"خطأ في تحميل الصورة: {str(e)}")
713
+
714
+ # توجيه للنموذج
715
+ prompt = st.text_input("توجيه للنموذج:", value="Convert this page to docling.")
716
+
717
+ # زر التحليل
718
+ if st.button("تحليل الصورة"):
719
+ if image_path or image_url:
720
+ with st.spinner("جاري تحليل الصورة..."):
721
+ # تحليل الصورة
722
+ results = self.docling_analyzer.analyze_image(
723
+ image_path=image_path,
724
+ image_url=image_url,
725
+ image_bytes=None, # نستخدم الملف المؤقت بدلاً من البيانات المباشرة
726
+ prompt=prompt
727
+ )
728
+
729
+ if "error" in results:
730
+ st.error(results["error"])
731
+ else:
732
+ # عرض النتائج
733
+ with st.expander("علامات DocTags", expanded=True):
734
+ st.code(results["doctags"], language="xml")
735
+
736
+ with st.expander("Markdown", expanded=True):
737
+ st.code(results["markdown"], language="markdown")
738
+
739
+ # تصدير إلى HTML
740
+ if st.button("تصدير إلى HTML"):
741
+ html_path = self.docling_analyzer.export_to_html(
742
+ results["document"],
743
+ show_in_browser=True
744
+ )
745
+ if html_path:
746
+ st.success(f"تم تصدير المستند إلى: {html_path}")
747
+ else:
748
+ st.error("فشل تصدير المستند إلى HTML")
749
+
750
+ # حذف الملف المؤقت بعد الانتهاء
751
+ if image_path and os.path.exists(image_path) and image_data:
752
+ try:
753
+ os.unlink(image_path)
754
+ except:
755
+ pass
756
+ else:
757
+ st.warning("يرجى اختيار صورة للتحليل أولاً.")
758
+
759
+ def _render_claude_analysis(self):
760
+ """عرض واجهة تحليل Claude AI مع توسعة البيانات المعروضة"""
761
+ import time
762
+
763
+ st.subheader("تحليل المستندات باستخدام Claude AI")
764
+
765
+ col1, col2 = st.columns([2, 1])
766
+
767
+ with col1:
768
+ # إضافة اختيار النموذج
769
+ claude_models = {
770
+ "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة",
771
+ "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية"
772
+ }
773
+
774
+ selected_model = st.radio(
775
+ "اختر نموذج Claude",
776
+ options=list(claude_models.keys()),
777
+ format_func=lambda x: claude_models[x],
778
+ horizontal=True
779
+ )
780
+
781
+ with col2:
782
+ # إضافة شرح بسيط للنموذج
783
+ if selected_model == "claude-3-7-sonnet":
784
+ st.info("نموذج Claude 3.7 Sonnet هو أحدث نموذج ذكي يقدم تحليلاً متعمقاً للمستندات مع دقة عالية")
785
+ else:
786
+ st.info("نموذج Claude 3.5 Haiku أسرع في التحليل ومناسب للمهام البسيطة والاستخدام اليومي")
787
+
788
+ # تخصيص التوجيه مع اقتراحات للتوجيهات المخصصة
789
+ st.subheader("تخصيص التحليل")
790
+
791
+ prompt_templates = {
792
+ "تحليل عام": "قم بتحليل هذا المستند واستخراج جميع المعلومات المهمة.",
793
+ "استخراج البيانات الأساسية": "استخرج كافة البيانات الأساسية من هذا المستند بما في ذلك الأسماء والتواريخ والأرقام والمبالغ المالية.",
794
+ "تلخيص المستند": "قم بتلخيص هذا المستند بشكل مفصل مع التركيز على النقاط الرئيسية.",
795
+ "تحليل العقود": "حلل هذا العقد واستخرج الأطراف والالتزامات والشروط والتواريخ المهمة.",
796
+ "تحليل فواتير": "استخرج كافة المعلومات من هذه الفاتورة بما في ذلك المورد والعميل وتفاصيل المنتجات والأسعار والمبالغ الإجمالية."
797
+ }
798
+
799
+ prompt_type = st.selectbox(
800
+ "اختر نوع التوجيه",
801
+ options=list(prompt_templates.keys()),
802
+ index=0
803
+ )
804
+
805
+ default_prompt = prompt_templates[prompt_type]
806
+
807
+ custom_prompt = st.text_area(
808
+ "تخصيص التوجيه للتحليل",
809
+ value=default_prompt,
810
+ height=100
811
+ )
812
+
813
+ # خيارات متقدمة
814
+ with st.expander("خيارات متقدمة"):
815
+ extraction_format = st.selectbox(
816
+ "تنسيق استخراج البيانات",
817
+ ["عام", "جداول", "قائمة", "هيكل منظم"],
818
+ index=0
819
+ )
820
+
821
+ detail_level = st.slider(
822
+ "مستوى التفاصيل",
823
+ min_value=1,
824
+ max_value=5,
825
+ value=3,
826
+ help="1: ملخص موجز، 5: تحليل تفصيلي كامل"
827
+ )
828
+
829
+ # تحديث التوجيه بناء على الخيارات المتقدمة
830
+ if extraction_format != "عام" or detail_level != 3:
831
+ custom_prompt += f"\n\nاستخدم تنسيق {extraction_format} مع مستوى تفاصيل {detail_level}/5."
832
+
833
+ # رفع الملف
834
+ uploaded_file = st.file_uploader(
835
+ "ارفع ملفًا للتحليل",
836
+ type=["pdf", "jpg", "jpeg", "png"],
837
+ key="claude_uploader",
838
+ help="يدعم ملفات PDF والصور. سيتم تحويل PDF إلى صور لمعالجتها."
839
+ )
840
+
841
+ # التحقق من وجود مفتاح API
842
+ api_available = True
843
+ try:
844
+ self.claude_analyzer.get_api_key()
845
+ except ValueError:
846
+ api_available = False
847
+ st.warning("مفتاح API لـ Claude غير متوفر. يرجى التأكد من تعيين متغير البيئة 'anthropic'.")
848
+
849
+ # زر التحليل
850
+ analyze_col1, analyze_col2 = st.columns([1, 3])
851
+
852
+ with analyze_col1:
853
+ analyze_button = st.button(
854
+ "تحليل المستند",
855
+ key="analyze_claude_btn",
856
+ use_container_width=True,
857
+ disabled=not (uploaded_file and api_available)
858
+ )
859
+
860
+ with analyze_col2:
861
+ if not uploaded_file:
862
+ st.info("يرجى رفع ملف للتحليل")
863
+
864
+ # إجراء التحليل
865
+ if uploaded_file and api_available and analyze_button:
866
+ # عرض شريط التقدم
867
+ progress_bar = st.progress(0, text="جاري تجهيز الملف...")
868
+
869
+ with st.spinner(f"جاري التحليل باستخدام {claude_models[selected_model].split('-')[0]}..."):
870
+ # حفظ الملف المرفوع إلى ملف مؤقت
871
+ temp_path = f"/tmp/{uploaded_file.name}"
872
+ with open(temp_path, "wb") as f:
873
+ f.write(uploaded_file.getbuffer())
874
+
875
+ # تحديث شريط التقدم
876
+ progress_bar.progress(25, text="جاري معالجة الملف...")
877
+
878
+ try:
879
+ # تحليل المستند
880
+ progress_bar.progress(40, text="جاري إرسال الطلب إلى Claude AI...")
881
+
882
+ results = self.claude_analyzer.analyze_document(
883
+ temp_path,
884
+ model_name=selected_model,
885
+ prompt=custom_prompt
886
+ )
887
+
888
+ progress_bar.progress(90, text="جاري معالجة النتائج...")
889
+
890
+ if "error" in results:
891
+ st.error(results["error"])
892
+ else:
893
+ progress_bar.progress(100, text="اكتمل التحليل!")
894
+
895
+ # عرض النتائج بشكل منظم
896
+ st.success(f"تم التحليل بنجاح باستخدام {results.get('model', selected_model)}!")
897
+
898
+ # إضافة علامات تبويب فرعية للنتائج
899
+ result_tabs = st.tabs(["التحليل الكامل", "بيانات مستخرجة", "معلومات إضافية"])
900
+
901
+ with result_tabs[0]:
902
+ # عرض النتائج الكاملة
903
+ st.markdown("## نتائج التحليل")
904
+ st.markdown(results["content"])
905
+
906
+ with result_tabs[1]:
907
+ # محاولة استخراج بيانات منظمة من النتائج
908
+ st.markdown("## البيانات المستخرجة")
909
+
910
+ # تقسيم النتائج إلى أقسام
911
+ content_parts = results["content"].split("\n\n")
912
+
913
+ # استخراج العناوين والبيانات الهامة
914
+ headings = []
915
+ key_values = {}
916
+
917
+ for part in content_parts:
918
+ # تحديد العناوين
919
+ if part.startswith("#") or part.startswith("##") or part.startswith("###"):
920
+ headings.append(part.strip())
921
+ continue
922
+
923
+ # محاولة استخراج أزواج المفتاح/القيمة
924
+ if ":" in part and len(part.split(":")) == 2:
925
+ key, value = part.split(":")
926
+ key_values[key.strip()] = value.strip()
927
+
928
+ # عرض العناوين
929
+ if headings:
930
+ st.markdown("### العناوين الرئيسية")
931
+ for heading in headings[:5]: # عرض أهم 5 عناوين
932
+ st.markdown(f"- {heading}")
933
+
934
+ if len(headings) > 5:
935
+ with st.expander(f"عرض {len(headings) - 5} عناوين إضافية"):
936
+ for heading in headings[5:]:
937
+ st.markdown(f"- {heading}")
938
+
939
+ # عرض البيانات الهامة
940
+ if key_values:
941
+ st.markdown("### بيانات هامة")
942
+
943
+ # تحويل البيانات إلى DataFrame
944
+ import pandas as pd
945
+ df = pd.DataFrame([key_values.values()], columns=key_values.keys())
946
+ st.dataframe(df.T)
947
+
948
+ # البحث عن الجداول في النص
949
+ if "| ------ |" in results["content"] or "\n|" in results["content"]:
950
+ st.markdown("### جداول مستخرجة")
951
+ # استخراج الجداول من النص Markdown
952
+ table_parts = []
953
+ in_table = False
954
+ current_table = []
955
+
956
+ for line in results["content"].split("\n"):
957
+ if line.startswith("|") and "-|-" in line.replace(" ", ""):
958
+ in_table = True
959
+ current_table.append(line)
960
+ elif in_table and line.startswith("|"):
961
+ current_table.append(line)
962
+ elif in_table and not line.startswith("|") and line.strip():
963
+ in_table = False
964
+ table_parts.append("\n".join(current_table))
965
+ current_table = []
966
+
967
+ # إضافة الجدول الأخير إذا كان هناك
968
+ if current_table:
969
+ table_parts.append("\n".join(current_table))
970
+
971
+ # عرض الجداول
972
+ for i, table in enumerate(table_parts):
973
+ st.markdown(f"#### جدول {i+1}")
974
+ st.markdown(table)
975
+
976
+ # إذا لم يتم العثور على أي بيانات منظمة
977
+ if not headings and not key_values and not ("| ------ |" in results["content"] or "\n|" in results["content"]):
978
+ st.info("لم يتم العثور على بيانات منظمة في النتائج. يمكنك تعديل التوجيه لطلب تنسيق أكثر هيكلية.")
979
+
980
+ with result_tabs[2]:
981
+ # عرض معلومات إضافية
982
+ st.markdown("## معلومات عن التحليل")
983
+
984
+ # عرض معلومات الاستخدام
985
+ col1, col2 = st.columns(2)
986
+
987
+ with col1:
988
+ st.markdown("### معلومات النموذج")
989
+ st.markdown(f"**النموذج المستخدم**: {results.get('model', selected_model)}")
990
+ st.markdown(f"**تاريخ التحليل**: {time.strftime('%Y-%m-%d %H:%M:%S')}")
991
+
992
+ with col2:
993
+ st.markdown("### إحصائيات الاستخدام")
994
+
995
+ if "usage" in results:
996
+ usage = results["usage"]
997
+ st.markdown(f"**توكنز المدخلات**: {usage.get('input_tokens', 'غير متوفر')}")
998
+ st.markdown(f"**توكنز الإخراج**: {usage.get('output_tokens', 'غير متوفر')}")
999
+ st.markdown(f"**إجمالي التوكنز**: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0)}")
1000
+ else:
1001
+ st.info("معلومات الاستخدام غير متوفرة")
1002
+
1003
+ # إضافة خيارات التصدير
1004
+ st.markdown("### تصدير النتائج")
1005
+
1006
+ export_col1, export_col2 = st.columns(2)
1007
+
1008
+ with export_col1:
1009
+ # تصدير كنص
1010
+ st.download_button(
1011
+ label="تحميل النتائج كملف نصي",
1012
+ data=results["content"],
1013
+ file_name=f"claude_analysis_{uploaded_file.name.split('.')[0]}.txt",
1014
+ mime="text/plain"
1015
+ )
1016
+
1017
+ with export_col2:
1018
+ # تصدير كـ Markdown
1019
+ st.download_button(
1020
+ label="تحميل النتائج كملف Markdown",
1021
+ data=results["content"],
1022
+ file_name=f"claude_analysis_{uploaded_file.name.split('.')[0]}.md",
1023
+ mime="text/markdown"
1024
+ )
1025
+ finally:
1026
+ # حذف الملف المؤقت
1027
+ try:
1028
+ os.unlink(temp_path)
1029
+ except:
1030
+ pass
1031
+
1032
+ def analyze_document(self, file_path):
1033
+ """
1034
+ تحليل مستند وإرجاع نتائج التحليل
1035
+
1036
+ المعلمات:
1037
+ file_path (str): مسار المستند المراد تحليله
1038
+
1039
+ العوائد:
1040
+ dict: نتائج تحليل المستند
1041
+ """
1042
+ # تحديد نوع المستند من امتداد الملف
1043
+ _, ext = os.path.splitext(file_path)
1044
+ ext = ext.lower()
1045
+
1046
+ # تحليل المستند حسب نوعه
1047
+ if ext == '.pdf':
1048
+ text = self.text_extractor.extract_from_pdf(file_path)
1049
+ elif ext in ('.doc', '.docx'):
1050
+ text = self.text_extractor.extract_from_docx(file_path)
1051
+ elif ext in ('.jpg', '.jpeg', '.png'):
1052
+ # استخدام محلل Docling للصور إذا كان متاحًا
1053
+ if self.docling_analyzer.is_available():
1054
+ docling_results = self.docling_analyzer.analyze_image(image_path=file_path)
1055
+ if "error" not in docling_results:
1056
+ return {
1057
+ "نص": docling_results["markdown"],
1058
+ "doctags": docling_results["doctags"],
1059
+ "معلومات": {
1060
+ "نوع المستند": "صورة",
1061
+ "تحليل": "تم تحليله باستخدام Docling"
1062
+ }
1063
+ }
1064
+
1065
+ # استخدام المحلل العادي إذا كان Docling غير متاح
1066
+ text = self.text_extractor.extract_from_image(file_path)
1067
+ else:
1068
+ raise ValueError(f"نوع المستند غير مدعوم: {ext}")
1069
+
1070
+ # تحليل المستند
1071
+ document = self.document_parser.parse_document(file_path)
1072
+
1073
+ # استخراج العناصر المنظمة
1074
+ tables = self.item_extractor.extract_tables(document)
1075
+
1076
+ # إرجاع نتائج التحليل
1077
+ return {
1078
+ "نص": text,
1079
+ "جداول": tables,
1080
+ "معلومات": document
1081
+ }
1082
+
1083
+ def analyze_with_claude(self, file_path, model_name="claude-3-7-sonnet", prompt=None):
1084
+ """
1085
+ تحليل مستند باستخدام Claude AI
1086
+
1087
+ المعلمات:
1088
+ file_path (str): مسار المستند المراد تحليله
1089
+ model_name (str): اسم نموذج Claude المراد استخدامه
1090
+ prompt (str): التوجيه المخصص للتحليل (اختياري)
1091
+
1092
+ العوائد:
1093
+ dict: نتائج التحليل
1094
+ """
1095
+ # محاولة تحليل المستند باستخدام Claude
1096
+ try:
1097
+ # التحقق من وجود المفتاح
1098
+ self.claude_analyzer.get_api_key()
1099
+
1100
+ # تحليل المستند باستخدام Claude
1101
+ return self.claude_analyzer.analyze_document(
1102
+ file_path,
1103
+ model_name=model_name,
1104
+ prompt=prompt
1105
+ )
1106
+ except Exception as e:
1107
+ logging.error(f"خطأ في تحليل المستند باستخدام Claude: {str(e)}")
1108
+ return {"error": f"فشل في تحليل المستند باستخدام Claude: {str(e)}"}
1109
+
1110
+
1111
+ # تشغيل التطبيق
1112
+ if __name__ == "__main__":
1113
+ app = DocumentAnalysisApp()
1114
+ app.render()
modules/document_analysis/services/__init__.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ حزمة خدمات تحليل المستندات
3
+
4
+ توفر هذه الحزمة الأدوات والخدمات اللازمة لتحليل المستندات بمختلف أنواعها
5
+ واستخراج النصوص والبيانات المنظمة منها.
6
+ """
7
+
8
+ # استيراد الفئات الرئيسية
9
+ from .text_extractor import TextExtractor
10
+ from .item_extractor import ItemExtractor
11
+ from .document_parser import DocumentParser
12
+
13
+ # تحديد الفئات التي يمكن استيرادها عند استخدام from services import *
14
+ __all__ = [
15
+ 'TextExtractor',
16
+ 'ItemExtractor',
17
+ 'DocumentParser',
18
+ ]
19
+
20
+ # معلومات الإصدار
21
+ __version__ = '0.1.0'
22
+ __author__ = 'فريق تطوير تحليل المستندات'
modules/document_analysis/services/document_parser.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ خدمة تحليل المستندات
4
+
5
+ هذا الملف يحتوي على الفئة المسؤولة عن تحليل المستندات واستخراج المعلومات الهيكلية منها.
6
+ """
7
+
8
+ import os
9
+ import logging
10
+ import datetime
11
+
12
+ class DocumentParser:
13
+ """فئة تحليل المستندات واستخراج المعلومات منها"""
14
+
15
+ def __init__(self, config=None):
16
+ """
17
+ تهيئة محلل المستندات
18
+
19
+ المعلمات:
20
+ config (dict): إعدادات محلل المستندات
21
+ """
22
+ self.config = config or {}
23
+ self.logger = logging.getLogger(__name__)
24
+
25
+ def parse(self, file_path):
26
+ """
27
+ تحليل المستند واستخراج المعلومات منه
28
+
29
+ المعلمات:
30
+ file_path (str): مسار الملف
31
+
32
+ العوائد:
33
+ dict: معلومات المستند المستخرجة
34
+ """
35
+ self.logger.info(f"جاري تحليل المستند: {file_path}")
36
+
37
+ try:
38
+ # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
39
+ # محاكاة التحليل للعرض
40
+ file_name = os.path.basename(file_path)
41
+ file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
42
+
43
+ # تحديد نوع الملف
44
+ _, ext = os.path.splitext(file_path)
45
+ ext = ext.lower()
46
+
47
+ # تحديد نوع المستند
48
+ document_type = self._get_document_type(ext)
49
+
50
+ # محاكاة معلومات المستند
51
+ current_date = datetime.datetime.now().strftime("%Y-%m-%d")
52
+
53
+ result = {
54
+ "اسم الملف": file_name,
55
+ "حجم الملف": f"{file_size / 1024:.2f} كيلوبايت",
56
+ "نوع الملف": document_type,
57
+ "تاريخ التحليل": current_date,
58
+ "تقدير عدد الصفحات": self._estimate_pages(file_size),
59
+ "نتائج التحليل": {
60
+ "نوع المستند": self._classify_document(file_name),
61
+ "درجة الثقة": "85%",
62
+ "الأقسام الرئيسية": self._get_main_sections(),
63
+ "الكلمات الرئيسية": self._get_main_keywords(),
64
+ "الشروط الهامة": self._get_important_terms()
65
+ }
66
+ }
67
+
68
+ return result
69
+ except Exception as e:
70
+ self.logger.error(f"خطأ في تحليل المستند: {str(e)}")
71
+ return {"خطأ": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
72
+
73
+ def parse_document(self, file_path):
74
+ """
75
+ تحليل المستند واستخراج المعلومات الأساسية منه
76
+
77
+ المعلمات:
78
+ file_path (str): مسار الملف
79
+
80
+ العوائد:
81
+ dict: معلومات المستند الأساسية
82
+ """
83
+ self.logger.info(f"جاري تحليل المستند الأساسي: {file_path}")
84
+
85
+ # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
86
+ # محاكاة التحليل للعرض
87
+ file_name = os.path.basename(file_path)
88
+
89
+ return {
90
+ "نوع": self._classify_document(file_name),
91
+ "محتوى": "محتوى المستند...",
92
+ "هيكل": {
93
+ "عنوان": "عنوان المستند",
94
+ "أقسام": ["قسم 1", "قسم 2", "قسم 3"]
95
+ }
96
+ }
97
+
98
+ def _get_document_type(self, ext):
99
+ """
100
+ تحديد نوع المستند من امتداد الملف
101
+
102
+ المعلمات:
103
+ ext (str): امتداد الملف
104
+
105
+ العوائد:
106
+ str: نوع المستند
107
+ """
108
+ document_types = {
109
+ '.pdf': 'مستند PDF',
110
+ '.doc': 'مستند Word',
111
+ '.docx': 'مستند Word',
112
+ '.jpg': 'صورة JPEG',
113
+ '.jpeg': 'صورة JPEG',
114
+ '.png': 'صورة PNG',
115
+ '.xlsx': 'جدول Excel',
116
+ '.xls': 'جدول Excel',
117
+ '.txt': 'ملف نصي'
118
+ }
119
+
120
+ return document_types.get(ext, 'نوع ملف غير معروف')
121
+
122
+ def _estimate_pages(self, file_size):
123
+ """
124
+ تقدير عدد صفحات المستند بناءً على حجمه
125
+
126
+ المعلمات:
127
+ file_size (int): حجم الملف بالبايت
128
+
129
+ العوائد:
130
+ int: تقدير عدد الصفحات
131
+ """
132
+ # تقدير بسيط: كل 50 كيلوبايت تقريباً صفحة واحدة
133
+ # هذا تقدير بسيط جداً ويختلف حسب نوع المستند ومحتواه
134
+ return max(1, int(file_size / (50 * 1024)))
135
+
136
+ def _classify_document(self, file_name):
137
+ """
138
+ تصنيف نوع المستند بناءً على اسمه
139
+
140
+ المعلمات:
141
+ file_name (str): اسم الملف
142
+
143
+ العوائد:
144
+ str: تصنيف المستند
145
+ """
146
+ file_name_lower = file_name.lower()
147
+
148
+ if 'عقد' in file_name_lower or 'contract' in file_name_lower:
149
+ return "عقد"
150
+ elif 'مناقصة' in file_name_lower or 'tender' in file_name_lower:
151
+ return "مستند مناقصة"
152
+ elif 'تقرير' in file_name_lower or 'report' in file_name_lower:
153
+ return "تقرير"
154
+ elif 'فاتورة' in file_name_lower or 'invoice' in file_name_lower:
155
+ return "فاتورة"
156
+ elif 'عرض' in file_name_lower or 'proposal' in file_name_lower:
157
+ return "عرض سعر"
158
+ elif 'مواصفات' in file_name_lower or 'spec' in file_name_lower:
159
+ return "مواصفات فنية"
160
+ elif 'كراسة' in file_name_lower or 'شروط' in file_name_lower:
161
+ return "كراسة شروط"
162
+ else:
163
+ return "مستند عام"
164
+
165
+ def _get_main_sections(self):
166
+ """
167
+ الحصول على قائمة الأقسام الرئيسية التقديرية للمستند
168
+
169
+ العوائد:
170
+ list: قائمة الأقسام الرئيسية
171
+ """
172
+ # محاكاة قائمة الأقسام
173
+ return [
174
+ "مقدمة",
175
+ "نطاق العمل",
176
+ "المواصفات الفنية",
177
+ "جدول الكميات",
178
+ "الشروط والأحكام",
179
+ "الجدول الزمني",
180
+ "المتطلبات الخاصة"
181
+ ]
182
+
183
+ def _get_main_keywords(self):
184
+ """
185
+ الحصول على قائمة الكلمات الرئيسية التقديرية للمستند
186
+
187
+ العوائد:
188
+ list: قائمة الكلمات الرئيسية
189
+ """
190
+ # محاكاة قائمة الكلمات الرئيسية
191
+ return [
192
+ "مناقصة",
193
+ "بناء",
194
+ "تشييد",
195
+ "تسليم مفتاح",
196
+ "مواصفات فنية",
197
+ "جدول كميات",
198
+ "ضمان",
199
+ "غرامة تأخير",
200
+ "دفعة مقدمة",
201
+ "محتوى محلي"
202
+ ]
203
+
204
+ def _get_important_terms(self):
205
+ """
206
+ الحصول على قائمة الشروط الهامة التقديرية للمستند
207
+
208
+ العوائد:
209
+ list: قائمة الشروط الهامة
210
+ """
211
+ # محاكاة قائمة الشروط الهامة
212
+ return [
213
+ "مدة تنفيذ المشروع: 18 شهر",
214
+ "غرامة التأخير: 0.5% أسبوعياً بحد أقصى 10%",
215
+ "الدفعة المقدمة: 10%",
216
+ "الضمان النهائي: 5% لمدة سنة",
217
+ "شروط الدفع: دفعات شهرية حسب نسبة الإنجاز",
218
+ "المحتوى المحلي: 70% كحد أدنى"
219
+ ]
modules/document_analysis/services/item_extractor.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ خدمة استخراج البنود من المستندات
4
+
5
+ هذا الملف يحتوي على الفئة المسؤولة عن استخراج البنود والجداول من المستندات.
6
+ """
7
+
8
+ import os
9
+ import logging
10
+
11
+ class ItemExtractor:
12
+ """فئة استخراج البنود من المستندات"""
13
+
14
+ def __init__(self, config=None):
15
+ """
16
+ تهيئة مستخرج البنود
17
+
18
+ المعلمات:
19
+ config (dict): إعدادات مستخرج البنود
20
+ """
21
+ self.config = config or {}
22
+ self.logger = logging.getLogger(__name__)
23
+
24
+ def extract(self, file_path):
25
+ """
26
+ استخراج البنود من ملف
27
+
28
+ المعلمات:
29
+ file_path (str): مسار الملف
30
+
31
+ العوائد:
32
+ list: قائمة البنود المستخرجة
33
+ """
34
+ self.logger.info(f"جاري استخراج البنود من الملف: {file_path}")
35
+
36
+ try:
37
+ # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
38
+ # محاكاة الاستخراج للعرض
39
+ file_name = os.path.basename(file_path)
40
+
41
+ # تحديد نوع الملف
42
+ _, ext = os.path.splitext(file_path)
43
+ ext = ext.lower()
44
+
45
+ if ext == '.pdf':
46
+ return self._extract_items_from_pdf(file_path)
47
+ elif ext in ('.doc', '.docx'):
48
+ return self._extract_items_from_docx(file_path)
49
+ else:
50
+ return [{"بند": "نوع الملف غير مدعوم", "قيمة": 0}]
51
+ except Exception as e:
52
+ self.logger.error(f"خطأ في استخراج البنود: {str(e)}")
53
+ return [{"بند": "حدث خطأ أثناء الاستخراج", "قيمة": 0, "خطأ": str(e)}]
54
+
55
+ def _extract_items_from_pdf(self, file_path):
56
+ """
57
+ استخراج البنود من ملف PDF
58
+
59
+ المعلمات:
60
+ file_path (str): مسار ملف PDF
61
+
62
+ العوائد:
63
+ list: قائمة البنود المستخرجة
64
+ """
65
+ # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
66
+ # محاكاة الاستخراج للعرض
67
+ return [
68
+ {"بند": "أعمال الخرسانة", "وحدة": "م3", "كمية": 250, "سعر الوحدة": 1200, "الإجمالي": 300000},
69
+ {"بند": "أعمال التشطيبات", "وحدة": "م2", "كمية": 1500, "سعر الوحدة": 350, "الإجمالي": 525000},
70
+ {"بند": "أعمال الكهرباء", "وحدة": "نقطة", "كمية": 120, "سعر الوحدة": 200, "الإجمالي": 24000},
71
+ {"بند": "أعمال السباكة", "وحدة": "نقطة", "كمية": 75, "سعر الوحدة": 300, "الإجمالي": 22500},
72
+ {"بند": "أعمال الألمنيوم", "وحدة": "م2", "كمية": 85, "سعر الوحدة": 1500, "الإجمالي": 127500}
73
+ ]
74
+
75
+ def _extract_items_from_docx(self, file_path):
76
+ """
77
+ استخراج البنود من ملف Word
78
+
79
+ المعلمات:
80
+ file_path (str): مسار ملف Word
81
+
82
+ العوائد:
83
+ list: قائمة البنود المستخرجة
84
+ """
85
+ # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
86
+ # محاكاة الاستخراج للعرض
87
+ return [
88
+ {"بند": "استشارات هندسية", "وحدة": "ساعة", "كمية": 120, "سعر الوحدة": 500, "الإجمالي": 60000},
89
+ {"بند": "تصميم معماري", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 100, "الإجمالي": 180000},
90
+ {"بند": "تصميم إنشائي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 80, "الإجمالي": 144000},
91
+ {"بند": "تصميم كهربائي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 40, "الإجمالي": 72000},
92
+ {"بند": "تصميم ميكانيكي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 40, "الإجمالي": 72000}
93
+ ]
94
+
95
+ def extract_tables(self, document):
96
+ """
97
+ استخراج الجداول من مستند
98
+
99
+ المعلمات:
100
+ document (dict): المستند المحلل
101
+
102
+ العوائد:
103
+ list: قائمة الجداول المستخرجة
104
+ """
105
+ self.logger.info("جاري استخراج الجداول من المستند")
106
+
107
+ try:
108
+ # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
109
+ # محاكاة الاستخراج للعرض
110
+ return [
111
+ {
112
+ "عنوان": "جدول البنود والتكاليف",
113
+ "بيانات": [
114
+ {"بند": "أعمال الخرسانة", "وحدة": "م3", "كمية": 250, "سعر الوحدة": 1200, "الإجمالي": 300000},
115
+ {"بند": "أعمال التشطيبات", "وحدة": "م2", "كمية": 1500, "سعر الوحدة": 350, "الإجمالي": 525000},
116
+ {"بند": "أعمال الكهرباء", "وحدة": "نقطة", "كمية": 120, "سعر الوحدة": 200, "الإجمالي": 24000},
117
+ {"بند": "أعمال السباكة", "وحدة": "نقطة", "كمية": 75, "سعر الوحدة": 300, "الإجمالي": 22500},
118
+ {"بند": "أعمال الألمنيوم", "وحدة": "م2", "كمية": 85, "سعر الوحدة": 1500, "الإجمالي": 127500}
119
+ ]
120
+ },
121
+ {
122
+ "عنوان": "جدول المعلومات العامة",
123
+ "بيانات": [
124
+ {"اسم المشروع": "مبنى سكني", "المالك": "شركة الإسكان", "الموقع": "الرياض", "المساحة": "2500 م2"},
125
+ {"اسم المشروع": "مبنى تجاري", "المالك": "شركة التطوير", "الموقع": "جدة", "المساحة": "3500 م2"}
126
+ ]
127
+ }
128
+ ]
129
+ except Exception as e:
130
+ self.logger.error(f"خطأ في استخراج الجداول: {str(e)}")
131
+ return [{"عنوان": "حدث خطأ أثناء الاستخراج", "بيانات": []}]
modules/document_analysis/services/text_extractor.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ خدمة استخراج النص من المستندات
4
+
5
+ هذا الملف يحتوي على الفئة المسؤولة عن استخراج النص من أنواع مختلفة من المستندات.
6
+ """
7
+
8
+ import os
9
+ import logging
10
+
11
+ class TextExtractor:
12
+ """فئة استخراج النص من المستندات"""
13
+
14
+ def __init__(self, config=None):
15
+ """
16
+ تهيئة مستخرج النص
17
+
18
+ المعلمات:
19
+ config (dict): إعدادات مستخرج النص
20
+ """
21
+ self.config = config or {}
22
+ self.logger = logging.getLogger(__name__)
23
+
24
+ def extract(self, file_path):
25
+ """
26
+ استخراج النص من ملف بناءً على نوع الملف
27
+
28
+ المعلمات:
29
+ file_path (str): مسار الملف
30
+
31
+ العوائد:
32
+ str: النص المستخرج
33
+ """
34
+ _, ext = os.path.splitext(file_path)
35
+ ext = ext.lower()
36
+
37
+ if ext == '.pdf':
38
+ return self.extract_from_pdf(file_path)
39
+ elif ext in ('.doc', '.docx'):
40
+ return self.extract_from_docx(file_path)
41
+ elif ext in ('.jpg', '.jpeg', '.png'):
42
+ return self.extract_from_image(file_path)
43
+ else:
44
+ self.logger.warning(f"نوع ملف غير مدعوم: {ext}")
45
+ return f"نوع ملف غير مدعوم: {ext}"
46
+
47
+ def extract_from_pdf(self, file_path):
48
+ """
49
+ استخراج النص من ملف PDF
50
+
51
+ المعلمات:
52
+ file_path (str): مسار ملف PDF
53
+
54
+ العوائد:
55
+ str: النص المستخرج
56
+ """
57
+ self.logger.info(f"جاري استخراج النص من ملف PDF: {file_path}")
58
+
59
+ try:
60
+ # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل PyPDF2 أو pdfplumber
61
+ # محاكاة الاستخراج للعرض
62
+ return f"هذا نص مستخرج من ملف PDF: {os.path.basename(file_path)}\n\nيتم استخراج النص من الملف باستخدام مكتبة مناسبة في البيئة الحقيقية."
63
+ except Exception as e:
64
+ self.logger.error(f"خطأ في استخراج النص من PDF: {str(e)}")
65
+ return f"حدث خطأ أثناء استخراج النص: {str(e)}"
66
+
67
+ def extract_from_docx(self, file_path):
68
+ """
69
+ استخراج النص من ملف Word
70
+
71
+ المعلمات:
72
+ file_path (str): مسار ملف Word
73
+
74
+ العوائد:
75
+ str: النص المستخرج
76
+ """
77
+ self.logger.info(f"جاري استخراج النص من ملف Word: {file_path}")
78
+
79
+ try:
80
+ # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل python-docx
81
+ # محاكاة الاستخراج للعرض
82
+ return f"هذا نص مستخرج من ملف Word: {os.path.basename(file_path)}\n\nيتم استخراج النص من الملف باستخدام مكتبة مناسبة في البيئة الحقيقية."
83
+ except Exception as e:
84
+ self.logger.error(f"خطأ في استخراج النص من Word: {str(e)}")
85
+ return f"حدث خطأ أثناء استخراج النص: {str(e)}"
86
+
87
+ def extract_from_image(self, file_path):
88
+ """
89
+ استخراج النص من ملف صورة باستخدام OCR
90
+
91
+ المعلمات:
92
+ file_path (str): مسار ملف الصورة
93
+
94
+ العوائد:
95
+ str: النص المستخرج
96
+ """
97
+ self.logger.info(f"جاري استخراج النص من ملف صورة: {file_path}")
98
+
99
+ try:
100
+ # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل pytesseract
101
+ # محاكاة الاستخراج للعرض
102
+ return f"هذا نص مستخرج من ملف صورة: {os.path.basename(file_path)}\n\nيتم استخراج النص من الصورة باستخدام تقنية OCR في البيئة الحقيقية."
103
+ except Exception as e:
104
+ self.logger.error(f"خطأ في استخراج النص من الصورة: {str(e)}")
105
+ return f"حدث خطأ أثناء استخراج النص: {str(e)}"
modules/document_comparison/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ وحدة مقارنة المستندات المتقدمة
4
+ """
modules/document_comparison/comparison_app.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ تطبيق مقارنة المستندات المتقدمة
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import streamlit as st
11
+ import pandas as pd
12
+ import numpy as np
13
+
14
+ # إضافة مسار النظام للوصول للملفات المشتركة
15
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
16
+
17
+ # استيراد مكونات مقارنة المستندات
18
+ from modules.document_comparison.document_comparator import DocumentComparator
19
+
20
+
21
+ class DocumentComparisonApp:
22
+ """تطبيق مقارنة المستندات المتقدمة"""
23
+
24
+ def __init__(self):
25
+ """تهيئة تطبيق مقارنة المستندات"""
26
+ self.comparator = DocumentComparator()
27
+
28
+ def render(self):
29
+ """عرض واجهة المستخدم الرئيسية للتطبيق"""
30
+ self.comparator.render()
31
+
32
+
33
+ # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
34
+ if __name__ == "__main__":
35
+ st.set_page_config(
36
+ page_title="مقارنة المستندات المتقدمة | WAHBi AI",
37
+ page_icon="📄",
38
+ layout="wide",
39
+ initial_sidebar_state="expanded"
40
+ )
41
+
42
+ app = DocumentComparisonApp()
43
+ app.render()
modules/document_comparison/document_comparator.py ADDED
@@ -0,0 +1,1503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ وحدة مقارنة المستندات المتقدمة لتحليل الفروقات بين نسخ المستندات
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import json
11
+ import re
12
+ import difflib
13
+ import Levenshtein
14
+ from datetime import datetime
15
+ import numpy as np
16
+ import pandas as pd
17
+ import streamlit as st
18
+ import plotly.express as px
19
+ import plotly.graph_objects as go
20
+ from collections import Counter
21
+ from nltk.tokenize import sent_tokenize, word_tokenize
22
+ from rouge_score import rouge_scorer
23
+ from PyPDF2 import PdfReader
24
+ import io
25
+
26
+ # إضافة مسار النظام للوصول للملفات المشتركة
27
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
28
+
29
+ # استيراد المكونات المساعدة
30
+ from utils.helpers import create_directory_if_not_exists, format_time, get_user_info
31
+
32
+
33
+ class DocumentComparator:
34
+ """فئة مقارنة المستندات المتقدمة"""
35
+
36
+ def __init__(self):
37
+ """تهيئة مقارن المستندات"""
38
+ self.comparison_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'document_comparison')
39
+ create_directory_if_not_exists(self.comparison_dir)
40
+
41
+ # تهيئة NLTK وتنزيل حزمة punkt إذا لم تكن موجودة
42
+ self._initialize_nltk()
43
+
44
+ # إعداد مقيم ROUGE لمقارنة النصوص
45
+ self.rouge_scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=False)
46
+
47
+ def _initialize_nltk(self):
48
+ """تهيئة مكتبة NLTK وتنزيل الحزم المطلوبة"""
49
+ try:
50
+ # استيراد nltk
51
+ import nltk
52
+
53
+ # قائمة بالحزم المطلوبة
54
+ required_packages = ['punkt', 'stopwords', 'wordnet']
55
+ for package in required_packages:
56
+ try:
57
+ # محاولة استخدام الحزمة أولاً، وإذا فشلت يتم تنزيلها
58
+ nltk.data.find(f'tokenizers/{package}')
59
+ except LookupError:
60
+ print(f"تنزيل حزمة NLTK: {package}")
61
+ nltk.download(package, quiet=True)
62
+
63
+ # محاولة استخدام sent_tokenize للتحقق من وجود حزمة punkt
64
+ from nltk.tokenize import sent_tokenize
65
+ sent_tokenize("This is a test sentence.")
66
+ except LookupError:
67
+ # تنزيل حزمة punkt تلقائيًا إذا لم تكن موجودة
68
+ import nltk
69
+ nltk.download('punkt', quiet=True)
70
+ # طباعة رسالة تأكيد التنزيل
71
+ st.info("تم تنزيل حزمة NLTK punkt بنجاح للاستخدام في مقارنة المستندات.")
72
+
73
+ def _preprocess_text(self, text):
74
+ """معالجة النص قبل التحليل"""
75
+ # إزالة الأرقام والرموز الخاصة والمسافات الزائدة
76
+ text = re.sub(r'\s+', ' ', text)
77
+ text = text.strip()
78
+ return text
79
+
80
+ def _segment_text(self, text):
81
+ """تقسيم النص إلى فقرات وجمل"""
82
+ # تقسيم النص إلى فقرات
83
+ paragraphs = [p.strip() for p in text.split('\n') if p.strip()]
84
+
85
+ # تقسيم كل فقرة إلى جمل
86
+ sentences = []
87
+ for paragraph in paragraphs:
88
+ paragraph_sentences = sent_tokenize(paragraph)
89
+ sentences.extend(paragraph_sentences)
90
+
91
+ return paragraphs, sentences
92
+
93
+ def _calculate_similarity(self, text1, text2):
94
+ """حساب نسبة التشابه بين نصين"""
95
+ # حساب نسبة التشابه باستخدام مقياس Levenshtein
96
+ ratio = Levenshtein.ratio(text1, text2)
97
+
98
+ # حساب درجات ROUGE
99
+ rouge_scores = self.rouge_scorer.score(text1, text2)
100
+
101
+ # حساب متوسط نقاط Rouge
102
+ rouge1_f1 = rouge_scores['rouge1'].fmeasure
103
+ rouge2_f1 = rouge_scores['rouge2'].fmeasure
104
+ rougeL_f1 = rouge_scores['rougeL'].fmeasure
105
+ avg_rouge = (rouge1_f1 + rouge2_f1 + rougeL_f1) / 3
106
+
107
+ # دمج النقاط للحصول على نتيجة نهائية
108
+ combined_score = (ratio + avg_rouge) / 2
109
+
110
+ return {
111
+ 'levenshtein_ratio': ratio,
112
+ 'rouge1_f1': rouge1_f1,
113
+ 'rouge2_f1': rouge2_f1,
114
+ 'rougeL_f1': rougeL_f1,
115
+ 'avg_rouge': avg_rouge,
116
+ 'combined_score': combined_score
117
+ }
118
+
119
+ def _extract_text_from_pdf(self, pdf_file):
120
+ """استخراج النص من ملف PDF"""
121
+ text = ""
122
+ try:
123
+ # قراءة ملف PDF
124
+ pdf_reader = PdfReader(pdf_file)
125
+
126
+ # استخراج النص من كل صفحة
127
+ for page in pdf_reader.pages:
128
+ text += page.extract_text() + "\n"
129
+ except Exception as e:
130
+ st.error(f"خطأ في قراءة ملف PDF: {e}")
131
+
132
+ return text
133
+
134
+ def get_document_diff(self, text1, text2, title1="المستند الأول", title2="المستند الثاني"):
135
+ """حساب الفروقات بين نصين"""
136
+ if not text1 or not text2:
137
+ return {
138
+ "title1": title1,
139
+ "title2": title2,
140
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
141
+ "similarity": 0,
142
+ "similarity_score": 0,
143
+ "text_diffs": [],
144
+ "summary": "أحد المستندات فارغ، لا يمكن إجراء المقارنة."
145
+ }
146
+
147
+ # معالجة النصوص
148
+ preprocessed_text1 = self._preprocess_text(text1)
149
+ preprocessed_text2 = self._preprocess_text(text2)
150
+
151
+ # حساب نسبة التشابه الإجمالية
152
+ similarity_metrics = self._calculate_similarity(preprocessed_text1, preprocessed_text2)
153
+ similarity_score = similarity_metrics['combined_score']
154
+ similarity_percentage = int(similarity_score * 100)
155
+
156
+ # تقسيم النصوص إلى فقرات وجمل
157
+ paragraphs1, sentences1 = self._segment_text(text1)
158
+ paragraphs2, sentences2 = self._segment_text(text2)
159
+
160
+ # تحديد الفروقات بين الجمل باستخدام difflib
161
+ differ = difflib.Differ()
162
+ sentence_diffs = []
163
+
164
+ # مصفوفة التشابه بين الجمل
165
+ similarity_matrix = np.zeros((len(sentences1), len(sentences2)))
166
+ for i, s1 in enumerate(sentences1):
167
+ for j, s2 in enumerate(sentences2):
168
+ similarity_matrix[i, j] = Levenshtein.ratio(s1, s2)
169
+
170
+ # تحديد أفضل مطابقة لكل جملة
171
+ matched_sentences2 = set() # تتبع الجمل المطابقة في المستند الثاني
172
+
173
+ for i, s1 in enumerate(sentences1):
174
+ if len(s1.split()) < 3: # تجاهل الجمل القصيرة جداً
175
+ continue
176
+
177
+ best_match_idx = -1
178
+ best_match_score = 0.7 # عتبة التشابه
179
+
180
+ for j, s2 in enumerate(sentences2):
181
+ if j in matched_sentences2:
182
+ continue # تجاهل الجمل التي تم مطابقتها بالفعل
183
+
184
+ if len(s2.split()) < 3: # تجاهل الجمل القصيرة جداً
185
+ continue
186
+
187
+ score = similarity_matrix[i, j]
188
+ if score > best_match_score and score > 0.7:
189
+ best_match_score = score
190
+ best_match_idx = j
191
+
192
+ if best_match_idx != -1:
193
+ # وجدنا تطابق، تحديد الفروقات باستخدام difflib
194
+ s2 = sentences2[best_match_idx]
195
+ diff = list(differ.compare(s1.split(), s2.split()))
196
+
197
+ # تحويل مخرجات difflib إلى تنسيق أسهل للاستخدام
198
+ formatted_diff = []
199
+ for token in diff:
200
+ if token.startswith(' '): # متطابق
201
+ formatted_diff.append({'text': token[2:], 'status': 'same'})
202
+ elif token.startswith('- '): # حذف
203
+ formatted_diff.append({'text': token[2:], 'status': 'removed'})
204
+ elif token.startswith('+ '): # إضافة
205
+ formatted_diff.append({'text': token[2:], 'status': 'added'})
206
+
207
+ sentence_diffs.append({
208
+ 'doc1_idx': i,
209
+ 'doc2_idx': best_match_idx,
210
+ 'doc1_text': s1,
211
+ 'doc2_text': s2,
212
+ 'similarity': best_match_score,
213
+ 'diff': formatted_diff
214
+ })
215
+
216
+ matched_sentences2.add(best_match_idx)
217
+ else:
218
+ # لم نجد تطابق، هذه الجملة غير موجودة في المستند الثاني
219
+ sentence_diffs.append({
220
+ 'doc1_idx': i,
221
+ 'doc2_idx': -1,
222
+ 'doc1_text': s1,
223
+ 'doc2_text': "",
224
+ 'similarity': 0,
225
+ 'diff': [{'text': word, 'status': 'removed'} for word in s1.split()]
226
+ })
227
+
228
+ # تحديد الجمل الجديدة في المستند الثاني
229
+ for j, s2 in enumerate(sentences2):
230
+ if j not in matched_sentences2 and len(s2.split()) >= 3:
231
+ sentence_diffs.append({
232
+ 'doc1_idx': -1,
233
+ 'doc2_idx': j,
234
+ 'doc1_text': "",
235
+ 'doc2_text': s2,
236
+ 'similarity': 0,
237
+ 'diff': [{'text': word, 'status': 'added'} for word in s2.split()]
238
+ })
239
+
240
+ # ترتيب الفروقات حسب الموقع في المستند الأول
241
+ sentence_diffs.sort(key=lambda x: (x['doc1_idx'] if x['doc1_idx'] != -1 else float('inf'), x['doc2_idx'] if x['doc2_idx'] != -1 else float('inf')))
242
+
243
+ # تحديد الفقرات المضافة والمحذوفة
244
+ paragraph_diffs = []
245
+ matched_paragraphs2 = set()
246
+
247
+ for i, p1 in enumerate(paragraphs1):
248
+ if len(p1.split()) < 5: # تجاهل الفقرات القصيرة جداً
249
+ continue
250
+
251
+ best_match_idx = -1
252
+ best_match_score = 0.6 # عتبة التشابه
253
+
254
+ for j, p2 in enumerate(paragraphs2):
255
+ if j in matched_paragraphs2:
256
+ continue
257
+
258
+ if len(p2.split()) < 5:
259
+ continue
260
+
261
+ score = Levenshtein.ratio(p1, p2)
262
+ if score > best_match_score:
263
+ best_match_score = score
264
+ best_match_idx = j
265
+
266
+ if best_match_idx != -1:
267
+ # وجدنا تطابق
268
+ p2 = paragraphs2[best_match_idx]
269
+ paragraph_diffs.append({
270
+ 'doc1_idx': i,
271
+ 'doc2_idx': best_match_idx,
272
+ 'doc1_text': p1,
273
+ 'doc2_text': p2,
274
+ 'similarity': best_match_score,
275
+ 'status': 'modified' if best_match_score < 0.9 else 'same'
276
+ })
277
+
278
+ matched_paragraphs2.add(best_match_idx)
279
+ else:
280
+ # لم نجد تطابق، هذه الفقرة غير موجودة في المستند الثاني
281
+ paragraph_diffs.append({
282
+ 'doc1_idx': i,
283
+ 'doc2_idx': -1,
284
+ 'doc1_text': p1,
285
+ 'doc2_text': "",
286
+ 'similarity': 0,
287
+ 'status': 'removed'
288
+ })
289
+
290
+ # تحديد الفقرات الجديدة في المستند الثاني
291
+ for j, p2 in enumerate(paragraphs2):
292
+ if j not in matched_paragraphs2 and len(p2.split()) >= 5:
293
+ paragraph_diffs.append({
294
+ 'doc1_idx': -1,
295
+ 'doc2_idx': j,
296
+ 'doc1_text': "",
297
+ 'doc2_text': p2,
298
+ 'similarity': 0,
299
+ 'status': 'added'
300
+ })
301
+
302
+ # ترتيب الفروقات حسب الموقع
303
+ paragraph_diffs.sort(key=lambda x: (x['doc1_idx'] if x['doc1_idx'] != -1 else float('inf'), x['doc2_idx'] if x['doc2_idx'] != -1 else float('inf')))
304
+
305
+ # تحليل الفروقات للحصول على إحصائيات
306
+ total_paragraphs = len(paragraphs1) + len(paragraphs2)
307
+ removed_paragraphs = sum(1 for p in paragraph_diffs if p['status'] == 'removed')
308
+ added_paragraphs = sum(1 for p in paragraph_diffs if p['status'] == 'added')
309
+ modified_paragraphs = sum(1 for p in paragraph_diffs if p['status'] == 'modified')
310
+
311
+ # تحليل الكلمات المضافة، المحذوفة والمتغيرة
312
+ added_words = []
313
+ removed_words = []
314
+ modified_contexts = []
315
+
316
+ for diff in sentence_diffs:
317
+ for token in diff['diff']:
318
+ if token['status'] == 'added':
319
+ added_words.append(token['text'])
320
+ elif token['status'] == 'removed':
321
+ removed_words.append(token['text'])
322
+
323
+ # جمع السياقات المتغيرة للتحليل
324
+ if diff['doc1_idx'] != -1 and diff['doc2_idx'] != -1 and diff['similarity'] < 0.9:
325
+ modified_contexts.append({
326
+ 'doc1_text': diff['doc1_text'],
327
+ 'doc2_text': diff['doc2_text'],
328
+ 'similarity': diff['similarity']
329
+ })
330
+
331
+ # إنشاء التقرير النهائي
332
+ comparison_report = {
333
+ "title1": title1,
334
+ "title2": title2,
335
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
336
+ "similarity": similarity_percentage,
337
+ "similarity_metrics": similarity_metrics,
338
+ "sentence_diffs": sentence_diffs,
339
+ "paragraph_diffs": paragraph_diffs,
340
+ "statistics": {
341
+ "doc1_paragraphs": len(paragraphs1),
342
+ "doc2_paragraphs": len(paragraphs2),
343
+ "doc1_sentences": len(sentences1),
344
+ "doc2_sentences": len(sentences2),
345
+ "removed_paragraphs": removed_paragraphs,
346
+ "added_paragraphs": added_paragraphs,
347
+ "modified_paragraphs": modified_paragraphs,
348
+ "removed_words_count": len(removed_words),
349
+ "added_words_count": len(added_words),
350
+ "top_removed_words": Counter(removed_words).most_common(10),
351
+ "top_added_words": Counter(added_words).most_common(10)
352
+ },
353
+ "modified_contexts": modified_contexts[:10], # أهم 10 سياقات متغيرة
354
+ "summary": self._generate_comparison_summary(
355
+ similarity_percentage,
356
+ len(paragraphs1),
357
+ len(paragraphs2),
358
+ removed_paragraphs,
359
+ added_paragraphs,
360
+ modified_paragraphs,
361
+ len(removed_words),
362
+ len(added_words)
363
+ )
364
+ }
365
+
366
+ # حفظ تقرير المقارنة
367
+ self._save_comparison_report(comparison_report, title1, title2)
368
+
369
+ return comparison_report
370
+
371
+ def _generate_comparison_summary(self, similarity, p1_count, p2_count, removed_p, added_p, modified_p, removed_w, added_w):
372
+ """إنشاء ملخص للمقارنة بين المستندين"""
373
+ if similarity >= 90:
374
+ similarity_description = "متطابقة بشكل كبير"
375
+ elif similarity >= 70:
376
+ similarity_description = "متشابهة"
377
+ elif similarity >= 50:
378
+ similarity_description = "متشابهة جزئياً"
379
+ else:
380
+ similarity_description = "مختلفة"
381
+
382
+ summary = f"المستندان {similarity_description} بنسبة {similarity}%. "
383
+
384
+ # وصف التغييرات في الفقرات
385
+ if removed_p > 0 or added_p > 0 or modified_p > 0:
386
+ changes = []
387
+ if removed_p > 0:
388
+ changes.append(f"تم حذف {removed_p} فقرة")
389
+ if added_p > 0:
390
+ changes.append(f"تم إضافة {added_p} فقرة")
391
+ if modified_p > 0:
392
+ changes.append(f"تم تعديل {modified_p} فقرة")
393
+
394
+ summary += "التغييرات تشمل: " + "، ".join(changes) + ". "
395
+
396
+ # وصف التغييرات في الكلمات
397
+ if removed_w > 0 or added_w > 0:
398
+ word_changes = []
399
+ if removed_w > 0:
400
+ word_changes.append(f"تم حذف {removed_w} كلمة")
401
+ if added_w > 0:
402
+ word_changes.append(f"تم إضافة {added_w} كلمة")
403
+
404
+ summary += "على مستوى الكلمات: " + "، ".join(word_changes) + "."
405
+
406
+ return summary
407
+
408
+ def _save_comparison_report(self, report, title1, title2):
409
+ """حفظ تقرير المقارنة"""
410
+ # إنشاء اسم ملف فريد
411
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
412
+ filename = f"compare_{title1.replace(' ', '_')}_{title2.replace(' ', '_')}_{timestamp}.json"
413
+ file_path = os.path.join(self.comparison_dir, filename)
414
+
415
+ try:
416
+ with open(file_path, 'w', encoding='utf-8') as f:
417
+ json.dump(report, f, ensure_ascii=False, indent=2)
418
+ except Exception as e:
419
+ print(f"خطأ في حفظ تقرير المقارنة: {e}")
420
+
421
+ def load_comparison_report(self, filename):
422
+ """تحميل تقرير مقارنة محفوظ"""
423
+ file_path = os.path.join(self.comparison_dir, filename)
424
+
425
+ if not os.path.exists(file_path):
426
+ return None
427
+
428
+ try:
429
+ with open(file_path, 'r', encoding='utf-8') as f:
430
+ report = json.load(f)
431
+ return report
432
+ except Exception as e:
433
+ print(f"خطأ في تحميل تقرير المقارنة: {e}")
434
+ return None
435
+
436
+ def get_comparison_reports(self):
437
+ """الحصول على قائمة تقارير المقارنة المحفوظة"""
438
+ reports = []
439
+
440
+ for filename in os.listdir(self.comparison_dir):
441
+ if filename.startswith("compare_") and filename.endswith(".json"):
442
+ file_path = os.path.join(self.comparison_dir, filename)
443
+ try:
444
+ with open(file_path, 'r', encoding='utf-8') as f:
445
+ report = json.load(f)
446
+ reports.append({
447
+ "filename": filename,
448
+ "title1": report.get("title1", "مستند 1"),
449
+ "title2": report.get("title2", "مستند 2"),
450
+ "timestamp": report.get("timestamp", ""),
451
+ "similarity": report.get("similarity", 0)
452
+ })
453
+ except Exception as e:
454
+ print(f"خطأ في قراءة تقرير المقارنة {filename}: {e}")
455
+
456
+ # ترتيب التقارير حسب التاريخ (الأحدث أولاً)
457
+ reports.sort(key=lambda x: x["timestamp"], reverse=True)
458
+
459
+ return reports
460
+
461
+ def extract_key_differences(self, comparison_report):
462
+ """استخراج الاختلافات الرئيسية من تقرير المقارنة"""
463
+ if not comparison_report or "paragraph_diffs" not in comparison_report:
464
+ return []
465
+
466
+ key_differences = []
467
+
468
+ # استخراج الفقرات المضافة
469
+ added_paragraphs = [p for p in comparison_report["paragraph_diffs"] if p["status"] == "added"]
470
+ if added_paragraphs:
471
+ key_differences.append({
472
+ "type": "added_paragraphs",
473
+ "label": "فقرات مضافة",
474
+ "count": len(added_paragraphs),
475
+ "items": [p["doc2_text"] for p in added_paragraphs]
476
+ })
477
+
478
+ # استخراج الفقرات المحذوفة
479
+ removed_paragraphs = [p for p in comparison_report["paragraph_diffs"] if p["status"] == "removed"]
480
+ if removed_paragraphs:
481
+ key_differences.append({
482
+ "type": "removed_paragraphs",
483
+ "label": "فقرات محذوفة",
484
+ "count": len(removed_paragraphs),
485
+ "items": [p["doc1_text"] for p in removed_paragraphs]
486
+ })
487
+
488
+ # استخراج الفقرات المعدلة
489
+ modified_paragraphs = [p for p in comparison_report["paragraph_diffs"] if p["status"] == "modified"]
490
+ if modified_paragraphs:
491
+ modified_items = []
492
+ for p in modified_paragraphs:
493
+ modified_items.append({
494
+ "doc1_text": p["doc1_text"],
495
+ "doc2_text": p["doc2_text"],
496
+ "similarity": p["similarity"]
497
+ })
498
+
499
+ key_differences.append({
500
+ "type": "modified_paragraphs",
501
+ "label": "فقرات معدلة",
502
+ "count": len(modified_paragraphs),
503
+ "items": modified_items
504
+ })
505
+
506
+ # استخراج الكلمات الرئيسية المضافة والمحذوفة
507
+ if "statistics" in comparison_report:
508
+ stats = comparison_report["statistics"]
509
+
510
+ if "top_added_words" in stats and stats["top_added_words"]:
511
+ key_differences.append({
512
+ "type": "added_words",
513
+ "label": "الكلمات المضافة الأكثر تكراراً",
514
+ "count": stats["added_words_count"],
515
+ "items": stats["top_added_words"]
516
+ })
517
+
518
+ if "top_removed_words" in stats and stats["top_removed_words"]:
519
+ key_differences.append({
520
+ "type": "removed_words",
521
+ "label": "الكلمات المحذوفة الأكثر تكراراً",
522
+ "count": stats["removed_words_count"],
523
+ "items": stats["top_removed_words"]
524
+ })
525
+
526
+ return key_differences
527
+
528
+ def analyze_legal_changes(self, comparison_report):
529
+ """تحليل التغييرات القانونية في المستندات"""
530
+ if not comparison_report:
531
+ return []
532
+
533
+ # قائمة المصطلحات القانونية الهامة للبحث عنها
534
+ legal_terms = {
535
+ "payment": ["دفع", "سداد", "مستحقات", "مقابل", "رسوم", "تكلفة", "مبلغ", "أتعاب"],
536
+ "deadlines": ["ميعاد", "موعد", "تاريخ", "أجل", "مدة", "فترة", "مهلة"],
537
+ "liability": ["مسؤولية", "التزام", "تحمل", "تعويض", "ضمان", "كفالة"],
538
+ "termination": ["إنهاء", "فسخ", "إلغاء", "إيقاف", "إنهاء العلاقة"],
539
+ "dispute": ["نزاع", "خلاف", "منازعة", "اعتراض", "تحكيم", "قضاء", "محكمة"],
540
+ "penalties": ["غرامة", "عقوبة", "شرط جزائي", "جزاء", "تعويض"],
541
+ "conditions": ["شرط", "بند", "حالة", "اشتراط", "متطلب"],
542
+ "rights": ["حق", "صلاحية", "امتياز", "منفعة", "ملكية", "تصرف"],
543
+ "obligations": ["التزام", "واجب", "تعهد", "إلزام", "لازم"]
544
+ }
545
+
546
+ # البحث عن التغييرات المتعلقة بالمصطلحات القانونية
547
+ legal_changes = []
548
+
549
+ if "sentence_diffs" in comparison_report:
550
+ for category, terms in legal_terms.items():
551
+ category_changes = []
552
+
553
+ for diff in comparison_report["sentence_diffs"]:
554
+ # فحص فقط الجمل المعدلة (المتطابقة جزئياً)
555
+ if diff["doc1_idx"] != -1 and diff["doc2_idx"] != -1 and diff["similarity"] < 0.9:
556
+ # فحص ما إذا كانت الجمل�� تحتوي على أي من المصطلحات القانونية
557
+ contains_term = False
558
+ for term in terms:
559
+ if term in diff["doc1_text"].lower() or term in diff["doc2_text"].lower():
560
+ contains_term = True
561
+ break
562
+
563
+ if contains_term:
564
+ category_changes.append({
565
+ "doc1_text": diff["doc1_text"],
566
+ "doc2_text": diff["doc2_text"],
567
+ "similarity": diff["similarity"]
568
+ })
569
+
570
+ if category_changes:
571
+ legal_category_name = {
572
+ "payment": "الدفع والمستحقات المالية",
573
+ "deadlines": "المواعيد والفترات الزمنية",
574
+ "liability": "المسؤولية والالتزامات",
575
+ "termination": "إنهاء العقد أو فسخه",
576
+ "dispute": "النزاعات والخلافات",
577
+ "penalties": "الغرامات والعقوبات",
578
+ "conditions": "الشروط والبنود",
579
+ "rights": "الحقوق والصلاحيات",
580
+ "obligations": "الالتزامات والواجبات"
581
+ }
582
+
583
+ legal_changes.append({
584
+ "category": category,
585
+ "label": legal_category_name.get(category, category),
586
+ "count": len(category_changes),
587
+ "changes": category_changes
588
+ })
589
+
590
+ # ترتيب التغييرات حسب الأهمية (عدد التغييرات)
591
+ legal_changes.sort(key=lambda x: x["count"], reverse=True)
592
+
593
+ return legal_changes
594
+
595
+ def analyze_price_changes(self, text1, text2):
596
+ """تحليل التغييرات في الأسعار بين نسختي المستند"""
597
+ # البحث عن الأرقام متبوعة بعملة أو تعبيرات تدل على المبالغ
598
+ price_pattern = r'(\d{1,3}(?:,\d{3})*(?:\.\d+)?)\s*(?:ريال|دولار|يورو|جنيه|درهم|دينار|SAR|USD|EUR|SR|$|€|£)'
599
+ amount_pattern = r'مبلغ[\s\w]*?(\d{1,3}(?:,\d{3})*(?:\.\d+)?)'
600
+
601
+ # استخراج الأسعار من كل نص
602
+ prices1 = re.findall(price_pattern, text1)
603
+ prices1.extend(re.findall(amount_pattern, text1))
604
+ prices1 = [p.replace(',', '') for p in prices1]
605
+ prices1 = [float(p) for p in prices1 if p]
606
+
607
+ prices2 = re.findall(price_pattern, text2)
608
+ prices2.extend(re.findall(amount_pattern, text2))
609
+ prices2 = [p.replace(',', '') for p in prices2]
610
+ prices2 = [float(p) for p in prices2 if p]
611
+
612
+ # تحليل التغييرات
613
+ price_diff = {
614
+ "doc1_prices_count": len(prices1),
615
+ "doc2_prices_count": len(prices2),
616
+ "doc1_total": sum(prices1) if prices1 else 0,
617
+ "doc2_total": sum(prices2) if prices2 else 0,
618
+ "doc1_average": sum(prices1) / len(prices1) if prices1 else 0,
619
+ "doc2_average": sum(prices2) / len(prices2) if prices2 else 0,
620
+ "doc1_min": min(prices1) if prices1 else 0,
621
+ "doc2_min": min(prices2) if prices2 else 0,
622
+ "doc1_max": max(prices1) if prices1 else 0,
623
+ "doc2_max": max(prices2) if prices2 else 0
624
+ }
625
+
626
+ # حساب التغيير في إجمالي الأسعار
627
+ if price_diff["doc1_total"] > 0:
628
+ price_diff["total_change_percentage"] = ((price_diff["doc2_total"] - price_diff["doc1_total"]) / price_diff["doc1_total"]) * 100
629
+ else:
630
+ price_diff["total_change_percentage"] = 0
631
+
632
+ return price_diff
633
+
634
+ def analyze_date_changes(self, text1, text2):
635
+ """تحليل التغييرات في التواريخ بين نسختي المستند"""
636
+ # البحث عن التواريخ بالصيغ المختلفة
637
+ date_patterns = [
638
+ r'\d{1,2}/\d{1,2}/\d{2,4}', # DD/MM/YYYY or MM/DD/YYYY
639
+ r'\d{1,2}-\d{1,2}-\d{2,4}', # DD-MM-YYYY or MM-DD-YYYY
640
+ r'\d{2,4}/\d{1,2}/\d{1,2}', # YYYY/MM/DD
641
+ r'\d{2,4}-\d{1,2}-\d{1,2}', # YYYY-MM-DD
642
+ r'\d{1,2}\s+(?:يناير|فبراير|مارس|أبريل|مايو|يونيو|يوليو|أغسطس|سبتمبر|أكتوبر|نوفمبر|ديسمبر)\s+\d{2,4}' # DD شهر YYYY
643
+ ]
644
+
645
+ dates1 = []
646
+ dates2 = []
647
+
648
+ for pattern in date_patterns:
649
+ dates1.extend(re.findall(pattern, text1))
650
+ dates2.extend(re.findall(pattern, text2))
651
+
652
+ # إنشاء تقرير التغييرات في التواريخ
653
+ date_changes = {
654
+ "doc1_dates_count": len(dates1),
655
+ "doc2_dates_count": len(dates2),
656
+ "doc1_dates": dates1[:10], # أول 10 تواريخ فقط
657
+ "doc2_dates": dates2[:10],
658
+ "common_dates": list(set(dates1).intersection(set(dates2))),
659
+ "removed_dates": list(set(dates1) - set(dates2)),
660
+ "added_dates": list(set(dates2) - set(dates1))
661
+ }
662
+
663
+ return date_changes
664
+
665
+ def render_document_comparison(self, text1, text2, title1="المستند الأول", title2="المستند الثاني"):
666
+ """عرض مقارنة المستندات بالواجهة التفاعلية"""
667
+ st.markdown("<h2 class='module-title'>مقارنة المستندات المتقدمة</h2>", unsafe_allow_html=True)
668
+
669
+ if not text1 or not text2:
670
+ st.warning("يرجى توفير نصوص المستندين للمقارنة")
671
+ return
672
+
673
+ with st.spinner("جاري تحليل ومقارنة المستندين..."):
674
+ # إجراء المقارنة
675
+ comparison_report = self.get_document_diff(text1, text2, title1, title2)
676
+
677
+ # تحليل التغييرات القانونية
678
+ legal_changes = self.analyze_legal_changes(comparison_report)
679
+
680
+ # تحليل التغييرات في الأسعار والتواريخ
681
+ price_changes = self.analyze_price_changes(text1, text2)
682
+ date_changes = self.analyze_date_changes(text1, text2)
683
+
684
+ # عرض ملخص المقارنة
685
+ st.markdown("<h3>ملخص المقارنة</h3>", unsafe_allow_html=True)
686
+
687
+ col1, col2, col3 = st.columns([1, 1, 1])
688
+
689
+ with col1:
690
+ similarity = comparison_report["similarity"]
691
+ color = "#00b894" if similarity >= 80 else "#fdcb6e" if similarity >= 50 else "#d63031"
692
+
693
+ st.markdown(f"""
694
+ <div class="similarity-card">
695
+ <div class="similarity-title">نسبة التشابه الإجمالية</div>
696
+ <div class="similarity-score" style="color: {color};">{similarity}%</div>
697
+ <div class="similarity-info">تم تحليل {comparison_report["statistics"]["doc1_paragraphs"]} فقرة في {title1} و {comparison_report["statistics"]["doc2_paragraphs"]} فقرة في {title2}</div>
698
+ </div>
699
+ """, unsafe_allow_html=True)
700
+
701
+ with col2:
702
+ st.markdown(f"""
703
+ <div class="changes-card">
704
+ <div class="changes-title">ملخص التغييرات</div>
705
+ <div class="changes-list">
706
+ <div class="change-item">
707
+ <span class="change-label">فقرات محذوفة:</span>
708
+ <span class="change-value">{comparison_report["statistics"]["removed_paragraphs"]}</span>
709
+ </div>
710
+ <div class="change-item">
711
+ <span class="change-label">فقرات مضافة:</span>
712
+ <span class="change-value">{comparison_report["statistics"]["added_paragraphs"]}</span>
713
+ </div>
714
+ <div class="change-item">
715
+ <span class="change-label">فقرات معدلة:</span>
716
+ <span class="change-value">{comparison_report["statistics"]["modified_paragraphs"]}</span>
717
+ </div>
718
+ </div>
719
+ </div>
720
+ """, unsafe_allow_html=True)
721
+
722
+ with col3:
723
+ st.markdown(f"""
724
+ <div class="words-card">
725
+ <div class="words-title">تغييرات الكلمات</div>
726
+ <div class="words-list">
727
+ <div class="words-item">
728
+ <span class="words-label">كلمات محذوفة:</span>
729
+ <span class="words-value">{comparison_report["statistics"]["removed_words_count"]}</span>
730
+ </div>
731
+ <div class="words-item">
732
+ <span class="words-label">كلمات مضافة:</span>
733
+ <span class="words-value">{comparison_report["statistics"]["added_words_count"]}</span>
734
+ </div>
735
+ </div>
736
+ </div>
737
+ """, unsafe_allow_html=True)
738
+
739
+ # عرض ملخص نصي
740
+ st.markdown(f"""
741
+ <div class="text-summary">
742
+ {comparison_report["summary"]}
743
+ </div>
744
+ """, unsafe_allow_html=True)
745
+
746
+ # عرض تحليل التغييرات القانونية
747
+ st.markdown("<h3>تحليل التغييرات القانونية</h3>", unsafe_allow_html=True)
748
+
749
+ if legal_changes:
750
+ tabs = st.tabs([change["label"] for change in legal_changes])
751
+
752
+ for i, tab in enumerate(tabs):
753
+ with tab:
754
+ st.markdown(f"**عدد التغييرات: {legal_changes[i]['count']}**")
755
+
756
+ for j, change in enumerate(legal_changes[i]["changes"]):
757
+ col1, col2 = st.columns(2)
758
+ with col1:
759
+ st.markdown(f"**{title1}:**")
760
+ st.markdown(f"<div class='diff-text diff-old'>{change['doc1_text']}</div>", unsafe_allow_html=True)
761
+ with col2:
762
+ st.markdown(f"**{title2}:**")
763
+ st.markdown(f"<div class='diff-text diff-new'>{change['doc2_text']}</div>", unsafe_allow_html=True)
764
+
765
+ if j < len(legal_changes[i]["changes"]) - 1:
766
+ st.markdown("---")
767
+ else:
768
+ st.info("لم يتم اكتشاف تغييرات قانونية هامة بين المستندين.")
769
+
770
+ # عرض الرسوم البيانية للتغييرات
771
+ st.markdown("<h3>رسوم بيانية للتغييرات</h3>", unsafe_allow_html=True)
772
+
773
+ col1, col2 = st.columns(2)
774
+
775
+ with col1:
776
+ # رسم بياني لتوزيع أنواع التغييرات في الفقرات
777
+ stats = comparison_report["statistics"]
778
+ fig = px.pie(
779
+ names=["فقرات متطابقة", "فقرات معدلة", "فقرات محذوفة", "فقرات مضافة"],
780
+ values=[
781
+ stats["doc1_paragraphs"] - stats["removed_paragraphs"] - stats["modified_paragraphs"],
782
+ stats["modified_paragraphs"],
783
+ stats["removed_paragraphs"],
784
+ stats["added_paragraphs"]
785
+ ],
786
+ title="توزيع التغييرات في الفقرات",
787
+ color_discrete_sequence=["#00b894", "#fdcb6e", "#d63031", "#0984e3"]
788
+ )
789
+
790
+ fig.update_layout(
791
+ font=dict(family="Arial, sans-serif", size=14),
792
+ height=350
793
+ )
794
+
795
+ st.plotly_chart(fig, use_container_width=True)
796
+
797
+ with col2:
798
+ # رسم بياني للكلمات المضافة والمحذوفة الأكثر تكراراً
799
+ words_data = []
800
+
801
+ for word, count in comparison_report["statistics"]["top_removed_words"]:
802
+ if len(word) > 1: # تجاهل الأحرف المفردة
803
+ words_data.append({"word": word, "count": count, "type": "محذوفة"})
804
+
805
+ for word, count in comparison_report["statistics"]["top_added_words"]:
806
+ if len(word) > 1: # تجاهل الأحرف المفردة
807
+ words_data.append({"word": word, "count": count, "type": "مضافة"})
808
+
809
+ if words_data:
810
+ words_df = pd.DataFrame(words_data)
811
+
812
+ fig = px.bar(
813
+ words_df,
814
+ x="word",
815
+ y="count",
816
+ color="type",
817
+ title="الكلمات المضافة والمحذوفة الأكثر تكراراً",
818
+ labels={"word": "الكلمة", "count": "عدد المرات", "type": "النوع"},
819
+ color_discrete_map={"محذوفة": "#d63031", "مضافة": "#0984e3"}
820
+ )
821
+
822
+ fig.update_layout(
823
+ font=dict(family="Arial, sans-serif", size=14),
824
+ height=350
825
+ )
826
+
827
+ st.plotly_chart(fig, use_container_width=True)
828
+ else:
829
+ st.info("لا توجد بيانات كافية للكلمات المضافة والمحذوفة.")
830
+
831
+ # عرض تحليل الأسعار والتواريخ
832
+ col1, col2 = st.columns(2)
833
+
834
+ with col1:
835
+ st.markdown("<h3>تحليل التغييرات في الأسعار</h3>", unsafe_allow_html=True)
836
+
837
+ if price_changes["doc1_prices_count"] > 0 or price_changes["doc2_prices_count"] > 0:
838
+ price_change_direction = "زيادة" if price_changes["total_change_percentage"] > 0 else "نقص"
839
+ price_change_color = "#d63031" if price_changes["total_change_percentage"] > 0 else "#00b894"
840
+
841
+ st.markdown(f"""
842
+ <div class="price-analysis">
843
+ <div class="price-summary">تغيير في إجمالي الأسعار بنسبة <span style="color: {price_change_color}; font-weight: bold;">{abs(price_changes['total_change_percentage']):.2f}% ({price_change_direction})</span></div>
844
+ <div class="price-details">
845
+ <div class="price-row">
846
+ <div class="price-label"></div>
847
+ <div class="price-value-header">{title1}</div>
848
+ <div class="price-value-header">{title2}</div>
849
+ </div>
850
+ <div class="price-row">
851
+ <div class="price-label">عدد الأسعار:</div>
852
+ <div class="price-value">{price_changes['doc1_prices_count']}</div>
853
+ <div class="price-value">{price_changes['doc2_prices_count']}</div>
854
+ </div>
855
+ <div class="price-row">
856
+ <div class="price-label">الإجمالي:</div>
857
+ <div class="price-value">{price_changes['doc1_total']:,.2f}</div>
858
+ <div class="price-value">{price_changes['doc2_total']:,.2f}</div>
859
+ </div>
860
+ <div class="price-row">
861
+ <div class="price-label">المتوسط:</div>
862
+ <div class="price-value">{price_changes['doc1_average']:,.2f}</div>
863
+ <div class="price-value">{price_changes['doc2_average']:,.2f}</div>
864
+ </div>
865
+ <div class="price-row">
866
+ <div class="price-label">الحد الأدنى:</div>
867
+ <div class="price-value">{price_changes['doc1_min']:,.2f}</div>
868
+ <div class="price-value">{price_changes['doc2_min']:,.2f}</div>
869
+ </div>
870
+ <div class="price-row">
871
+ <div class="price-label">الحد الأقصى:</div>
872
+ <div class="price-value">{price_changes['doc1_max']:,.2f}</div>
873
+ <div class="price-value">{price_changes['doc2_max']:,.2f}</div>
874
+ </div>
875
+ </div>
876
+ </div>
877
+ """, unsafe_allow_html=True)
878
+
879
+ # رسم بياني للأسعار
880
+ if price_changes["doc1_prices_count"] > 0 and price_changes["doc2_prices_count"] > 0:
881
+ price_chart_data = [
882
+ {"document": title1, "metric": "الإجمالي", "value": price_changes["doc1_total"]},
883
+ {"document": title2, "metric": "الإجمالي", "value": price_changes["doc2_total"]},
884
+ {"document": title1, "metric": "المتوسط", "value": price_changes["doc1_average"]},
885
+ {"document": title2, "metric": "المتوسط", "value": price_changes["doc2_average"]},
886
+ {"document": title1, "metric": "الحد الأقصى", "value": price_changes["doc1_max"]},
887
+ {"document": title2, "metric": "الحد الأقصى", "value": price_changes["doc2_max"]}
888
+ ]
889
+
890
+ price_df = pd.DataFrame(price_chart_data)
891
+
892
+ fig = px.bar(
893
+ price_df,
894
+ x="metric",
895
+ y="value",
896
+ color="document",
897
+ barmode="group",
898
+ title="مقارنة الأسعار بين المستندين",
899
+ color_discrete_map={title1: "#0984e3", title2: "#00b894"}
900
+ )
901
+
902
+ fig.update_layout(
903
+ font=dict(family="Arial, sans-serif", size=14),
904
+ height=350
905
+ )
906
+
907
+ st.plotly_chart(fig, use_container_width=True)
908
+ else:
909
+ st.info("لم يتم اكتشاف أي أسعار في المستندين.")
910
+
911
+ with col2:
912
+ st.markdown("<h3>تحليل التغييرات في التواريخ</h3>", unsafe_allow_html=True)
913
+
914
+ if date_changes["doc1_dates_count"] > 0 or date_changes["doc2_dates_count"] > 0:
915
+ st.markdown(f"""
916
+ <div class="date-analysis">
917
+ <div class="date-summary">تم اكتشاف {date_changes['doc1_dates_count']} تاريخ في {title1} و {date_changes['doc2_dates_count']} تاريخ في {title2}</div>
918
+ <div class="date-stats">
919
+ <div class="date-stat">
920
+ <span class="date-label">تواريخ مشتركة:</span>
921
+ <span class="date-value">{len(date_changes['common_dates'])}</span>
922
+ </div>
923
+ <div class="date-stat">
924
+ <span class="date-label">تواريخ محذوفة:</span>
925
+ <span class="date-value">{len(date_changes['removed_dates'])}</span>
926
+ </div>
927
+ <div class="date-stat">
928
+ <span class="date-label">تواريخ مضافة:</span>
929
+ <span class="date-value">{len(date_changes['added_dates'])}</span>
930
+ </div>
931
+ </div>
932
+ </div>
933
+ """, unsafe_allow_html=True)
934
+
935
+ # عرض التواريخ المحذوفة والمضافة
936
+ if date_changes["removed_dates"]:
937
+ st.markdown("**التواريخ المحذوفة:**")
938
+ for date in date_changes["removed_dates"][:10]: # عرض أول 10 فقط إذا كان هناك الكثير
939
+ st.markdown(f"<div class='diff-text diff-old'>{date}</div>", unsafe_allow_html=True)
940
+
941
+ if date_changes["added_dates"]:
942
+ st.markdown("**التواريخ المضافة:**")
943
+ for date in date_changes["added_dates"][:10]: # عرض أول 10 فقط
944
+ st.markdown(f"<div class='diff-text diff-new'>{date}</div>", unsafe_allow_html=True)
945
+
946
+ # رسم بياني للتواريخ
947
+ date_chart_data = [
948
+ {"category": "تواريخ مشتركة", "count": len(date_changes["common_dates"])},
949
+ {"category": "تواريخ محذوفة", "count": len(date_changes["removed_dates"])},
950
+ {"category": "تواريخ مضافة", "count": len(date_changes["added_dates"])}
951
+ ]
952
+
953
+ date_df = pd.DataFrame(date_chart_data)
954
+
955
+ fig = px.bar(
956
+ date_df,
957
+ x="category",
958
+ y="count",
959
+ title="توزيع التغييرات في التواريخ",
960
+ color="category",
961
+ color_discrete_map={
962
+ "تواريخ مشتركة": "#00b894",
963
+ "تواريخ محذوفة": "#d63031",
964
+ "تواريخ مضافة": "#0984e3"
965
+ }
966
+ )
967
+
968
+ fig.update_layout(
969
+ font=dict(family="Arial, sans-serif", size=14),
970
+ height=350
971
+ )
972
+
973
+ st.plotly_chart(fig, use_container_width=True)
974
+ else:
975
+ st.info("لم يتم اكتشاف أي تواريخ في المستندين.")
976
+
977
+ # عرض العرض المرئي للتغييرات بين المستندين
978
+ st.markdown("<h3>العرض المرئي للتغييرات</h3>", unsafe_allow_html=True)
979
+
980
+ # إضافة خيار لتصفية الفروقات
981
+ st.markdown("#### تصفية الفروقات حسب النوع")
982
+ col1, col2, col3 = st.columns(3)
983
+
984
+ with col1:
985
+ show_added = st.checkbox("عرض الإضافات", value=True)
986
+ with col2:
987
+ show_removed = st.checkbox("عرض الحذف", value=True)
988
+ with col3:
989
+ show_modified = st.checkbox("عرض التعديلات", value=True)
990
+
991
+ # تحديد الفروقات للعرض
992
+ filtered_diffs = []
993
+
994
+ for diff in comparison_report["paragraph_diffs"]:
995
+ if diff["status"] == "added" and show_added:
996
+ filtered_diffs.append(diff)
997
+ elif diff["status"] == "removed" and show_removed:
998
+ filtered_diffs.append(diff)
999
+ elif diff["status"] == "modified" and show_modified:
1000
+ filtered_diffs.append(diff)
1001
+
1002
+ # عرض الفروقات
1003
+ if filtered_diffs:
1004
+ for diff in filtered_diffs:
1005
+ if diff["status"] == "added":
1006
+ st.markdown(f"""
1007
+ <div class="diff-block diff-added">
1008
+ <div class="diff-header">
1009
+ <div class="diff-title">فقرة مضافة في {title2}</div>
1010
+ </div>
1011
+ <div class="diff-content">
1012
+ {diff["doc2_text"]}
1013
+ </div>
1014
+ </div>
1015
+ """, unsafe_allow_html=True)
1016
+
1017
+ elif diff["status"] == "removed":
1018
+ st.markdown(f"""
1019
+ <div class="diff-block diff-removed">
1020
+ <div class="diff-header">
1021
+ <div class="diff-title">فقرة محذوفة من {title1}</div>
1022
+ </div>
1023
+ <div class="diff-content">
1024
+ {diff["doc1_text"]}
1025
+ </div>
1026
+ </div>
1027
+ """, unsafe_allow_html=True)
1028
+
1029
+ elif diff["status"] == "modified":
1030
+ similarity_percentage = int(diff["similarity"] * 100)
1031
+
1032
+ st.markdown(f"""
1033
+ <div class="diff-block diff-modified">
1034
+ <div class="diff-header">
1035
+ <div class="diff-title">فقرة معدلة (نسبة التشابه: {similarity_percentage}%)</div>
1036
+ </div>
1037
+ <div class="diff-content-container">
1038
+ <div class="diff-content-old">
1039
+ <div class="diff-subtitle">{title1}:</div>
1040
+ {diff["doc1_text"]}
1041
+ </div>
1042
+ <div class="diff-content-new">
1043
+ <div class="diff-subtitle">{title2}:</div>
1044
+ {diff["doc2_text"]}
1045
+ </div>
1046
+ </div>
1047
+ </div>
1048
+ """, unsafe_allow_html=True)
1049
+ else:
1050
+ st.info("لا توجد فروقات تطابق معايير التصفية المحددة.")
1051
+
1052
+ # إضافة CSS للتنسيق
1053
+ st.markdown("""
1054
+ <style>
1055
+ .module-title {
1056
+ color: #1E88E5;
1057
+ font-size: 1.8rem;
1058
+ font-weight: bold;
1059
+ margin-bottom: 1rem;
1060
+ text-align: center;
1061
+ }
1062
+
1063
+ .similarity-card, .changes-card, .words-card {
1064
+ background-color: #fff;
1065
+ border-radius: 8px;
1066
+ padding: 1rem;
1067
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
1068
+ height: 100%;
1069
+ text-align: center;
1070
+ }
1071
+
1072
+ .similarity-title, .changes-title, .words-title {
1073
+ font-weight: bold;
1074
+ font-size: 1rem;
1075
+ margin-bottom: 0.5rem;
1076
+ color: #333;
1077
+ }
1078
+
1079
+ .similarity-score {
1080
+ font-size: 2.5rem;
1081
+ font-weight: bold;
1082
+ margin-bottom: 0.25rem;
1083
+ }
1084
+
1085
+ .similarity-info {
1086
+ font-size: 0.8rem;
1087
+ color: #666;
1088
+ }
1089
+
1090
+ .changes-list, .words-list {
1091
+ text-align: right;
1092
+ }
1093
+
1094
+ .change-item, .words-item {
1095
+ display: flex;
1096
+ justify-content: space-between;
1097
+ margin-bottom: 0.5rem;
1098
+ }
1099
+
1100
+ .change-label, .words-label {
1101
+ color: #555;
1102
+ }
1103
+
1104
+ .change-value, .words-value {
1105
+ font-weight: bold;
1106
+ color: #333;
1107
+ }
1108
+
1109
+ .text-summary {
1110
+ background-color: #f8f9fa;
1111
+ border-right: 4px solid #1E88E5;
1112
+ padding: 1rem;
1113
+ margin: 1rem 0;
1114
+ color: #444;
1115
+ font-size: 1rem;
1116
+ text-align: right;
1117
+ }
1118
+
1119
+ .diff-text {
1120
+ padding: 0.5rem;
1121
+ border-radius: 4px;
1122
+ margin-bottom: 0.5rem;
1123
+ white-space: pre-wrap;
1124
+ }
1125
+
1126
+ .diff-old {
1127
+ background-color: rgba(214, 48, 49, 0.1);
1128
+ border-right: 3px solid #d63031;
1129
+ }
1130
+
1131
+ .diff-new {
1132
+ background-color: rgba(9, 132, 227, 0.1);
1133
+ border-right: 3px solid #0984e3;
1134
+ }
1135
+
1136
+ .price-analysis, .date-analysis {
1137
+ background-color: #f8f9fa;
1138
+ border-radius: 8px;
1139
+ padding: 1rem;
1140
+ margin-bottom: 1rem;
1141
+ }
1142
+
1143
+ .price-summary, .date-summary {
1144
+ font-size: 1rem;
1145
+ margin-bottom: 0.5rem;
1146
+ text-align: center;
1147
+ }
1148
+
1149
+ .price-details {
1150
+ margin-top: 1rem;
1151
+ }
1152
+
1153
+ .price-row {
1154
+ display: flex;
1155
+ justify-content: space-between;
1156
+ margin-bottom: 0.25rem;
1157
+ border-bottom: 1px solid #eee;
1158
+ padding-bottom: 0.25rem;
1159
+ }
1160
+
1161
+ .price-label {
1162
+ flex: 1;
1163
+ text-align: right;
1164
+ font-weight: bold;
1165
+ color: #555;
1166
+ }
1167
+
1168
+ .price-value-header {
1169
+ flex: 1;
1170
+ text-align: center;
1171
+ font-weight: bold;
1172
+ color: #333;
1173
+ }
1174
+
1175
+ .price-value {
1176
+ flex: 1;
1177
+ text-align: center;
1178
+ color: #333;
1179
+ }
1180
+
1181
+ .date-stats {
1182
+ display: flex;
1183
+ justify-content: space-around;
1184
+ margin-top: 0.5rem;
1185
+ }
1186
+
1187
+ .date-stat {
1188
+ text-align: center;
1189
+ }
1190
+
1191
+ .date-label {
1192
+ display: block;
1193
+ font-size: 0.9rem;
1194
+ color: #555;
1195
+ }
1196
+
1197
+ .date-value {
1198
+ display: block;
1199
+ font-size: 1.2rem;
1200
+ font-weight: bold;
1201
+ color: #333;
1202
+ }
1203
+
1204
+ .diff-block {
1205
+ background-color: #fff;
1206
+ border-radius: 8px;
1207
+ margin-bottom: 1rem;
1208
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1209
+ overflow: hidden;
1210
+ }
1211
+
1212
+ .diff-header {
1213
+ padding: 0.5rem 1rem;
1214
+ border-bottom: 1px solid #eee;
1215
+ }
1216
+
1217
+ .diff-title {
1218
+ font-weight: bold;
1219
+ color: #333;
1220
+ }
1221
+
1222
+ .diff-content {
1223
+ padding: 1rem;
1224
+ white-space: pre-wrap;
1225
+ direction: rtl;
1226
+ text-align: right;
1227
+ }
1228
+
1229
+ .diff-content-container {
1230
+ display: flex;
1231
+ flex-direction: column;
1232
+ }
1233
+
1234
+ .diff-content-old, .diff-content-new {
1235
+ padding: 1rem;
1236
+ white-space: pre-wrap;
1237
+ direction: rtl;
1238
+ text-align: right;
1239
+ }
1240
+
1241
+ .diff-content-old {
1242
+ background-color: rgba(214, 48, 49, 0.05);
1243
+ border-bottom: 1px solid #eee;
1244
+ }
1245
+
1246
+ .diff-content-new {
1247
+ background-color: rgba(9, 132, 227, 0.05);
1248
+ }
1249
+
1250
+ .diff-subtitle {
1251
+ font-weight: bold;
1252
+ margin-bottom: 0.5rem;
1253
+ color: #555;
1254
+ }
1255
+
1256
+ .diff-added {
1257
+ border-right: 4px solid #0984e3;
1258
+ }
1259
+
1260
+ .diff-removed {
1261
+ border-right: 4px solid #d63031;
1262
+ }
1263
+
1264
+ .diff-modified {
1265
+ border-right: 4px solid #fdcb6e;
1266
+ }
1267
+
1268
+ @media (min-width: 992px) {
1269
+ .diff-content-container {
1270
+ flex-direction: row;
1271
+ }
1272
+
1273
+ .diff-content-old, .diff-content-new {
1274
+ flex: 1;
1275
+ }
1276
+
1277
+ .diff-content-old {
1278
+ border-bottom: none;
1279
+ border-left: 1px solid #eee;
1280
+ }
1281
+ }
1282
+ </style>
1283
+ """, unsafe_allow_html=True)
1284
+
1285
+ def render_advanced_comparison_tools(self):
1286
+ """عرض أدوات المقارنة المتقدمة"""
1287
+ st.markdown("<h2 class='module-title'>أدوات مقارنة المستندات المتقدمة</h2>", unsafe_allow_html=True)
1288
+
1289
+ st.markdown("""
1290
+ <div class="module-description">
1291
+ استخدم هذه الأدوات لمقارنة مستندات العقود بشكل متقدم، واكتشاف التغييرات والفروقات بين نسخ المستندات المختلفة،
1292
+ مع تحليل التغييرات القانونية والمالية والتواريخ.
1293
+ </div>
1294
+ """, unsafe_allow_html=True)
1295
+
1296
+ # إنشاء علامات التبويب للأدوات المختلفة
1297
+ tabs = st.tabs([
1298
+ "مقارنة نصية مباشرة",
1299
+ "مقارنة ملفات PDF",
1300
+ "عرض تقارير المقارنة السابقة"
1301
+ ])
1302
+
1303
+ with tabs[0]:
1304
+ st.markdown("### مقارنة نصية مباشرة")
1305
+
1306
+ col1, col2 = st.columns(2)
1307
+
1308
+ with col1:
1309
+ title1 = st.text_input("عنوان المستند الأول", key="text_title1")
1310
+ text1 = st.text_area("نص المستند الأول", height=300, key="text_input1")
1311
+
1312
+ with col2:
1313
+ title2 = st.text_input("عنوان المستند الثاني", key="text_title2")
1314
+ text2 = st.text_area("نص المستند الثاني", height=300, key="text_input2")
1315
+
1316
+ if st.button("قارن النصوص", key="compare_text_btn"):
1317
+ if text1 and text2:
1318
+ self.render_document_comparison(
1319
+ text1,
1320
+ text2,
1321
+ title1 or "المستند الأول",
1322
+ title2 or "المستند الثاني"
1323
+ )
1324
+ else:
1325
+ st.warning("يرجى إدخال نص المستندين للمقارنة")
1326
+
1327
+ with tabs[1]:
1328
+ st.markdown("### مقارنة ملفات PDF")
1329
+
1330
+ col1, col2 = st.columns(2)
1331
+
1332
+ with col1:
1333
+ title1_pdf = st.text_input("عنوان المستند الأول", key="pdf_title1")
1334
+ uploaded_file1 = st.file_uploader("تحميل المستند الأول (PDF)", type=["pdf"], key="pdf_upload1")
1335
+
1336
+ with col2:
1337
+ title2_pdf = st.text_input("عنوان المستند الثاني", key="pdf_title2")
1338
+ uploaded_file2 = st.file_uploader("تحميل المستند الثاني (PDF)", type=["pdf"], key="pdf_upload2")
1339
+
1340
+ if st.button("قارن ملفات PDF", key="compare_pdf_btn"):
1341
+ if uploaded_file1 is not None and uploaded_file2 is not None:
1342
+ with st.spinner("جاري استخراج النصوص من ملفات PDF..."):
1343
+ text1_pdf = self._extract_text_from_pdf(uploaded_file1)
1344
+ text2_pdf = self._extract_text_from_pdf(uploaded_file2)
1345
+
1346
+ if text1_pdf and text2_pdf:
1347
+ self.render_document_comparison(
1348
+ text1_pdf,
1349
+ text2_pdf,
1350
+ title1_pdf or uploaded_file1.name,
1351
+ title2_pdf or uploaded_file2.name
1352
+ )
1353
+ else:
1354
+ st.error("تعذر استخراج النص من ملفات PDF. يرجى التأكد من أن الملفات تحتوي على نصوص قابلة للاستخراج.")
1355
+ else:
1356
+ st.warning("يرجى تحميل ملفي PDF للمقارنة")
1357
+
1358
+ with tabs[2]:
1359
+ st.markdown("### تقارير المقارنة السابقة")
1360
+
1361
+ # الحصول على تقارير المقارنة المحفوظة
1362
+ reports = self.get_comparison_reports()
1363
+
1364
+ if reports:
1365
+ # عرض التقارير في جدول
1366
+ report_data = []
1367
+ for report in reports:
1368
+ report_data.append({
1369
+ "التاريخ": report["timestamp"],
1370
+ "المستند الأول": report["title1"],
1371
+ "المستند الثاني": report["title2"],
1372
+ "نسبة التشابه": f"{report['similarity']}%",
1373
+ "الملف": report["filename"]
1374
+ })
1375
+
1376
+ report_df = pd.DataFrame(report_data)
1377
+ st.dataframe(report_df)
1378
+
1379
+ # اختيار تقرير لعرضه
1380
+ selected_report = st.selectbox(
1381
+ "اختر تقريراً لعرضه",
1382
+ options=[f"{r['title1']} و {r['title2']} ({r['timestamp']})" for r in reports],
1383
+ format_func=lambda x: x
1384
+ )
1385
+
1386
+ report_index = next((i for i, r in enumerate(reports) if f"{r['title1']} و {r['title2']} ({r['timestamp']})" == selected_report), None)
1387
+
1388
+ if report_index is not None and st.button("عرض التقرير المحدد"):
1389
+ selected_filename = reports[report_index]["filename"]
1390
+ report_data = self.load_comparison_report(selected_filename)
1391
+
1392
+ if report_data:
1393
+ st.success(f"تم تحميل تقرير المقارنة بنجاح")
1394
+
1395
+ # عرض ملخص التقرير
1396
+ st.markdown(f"### ملخص تقرير المقارنة")
1397
+ st.markdown(f"**نسبة التشابه:** {report_data['similarity']}%")
1398
+ st.markdown(f"**تاريخ المقارنة:** {report_data['timestamp']}")
1399
+ st.markdown(f"**ملخص التغييرات:** {report_data['summary']}")
1400
+
1401
+ # استخراج الاختلافات الرئيسية
1402
+ key_differences = self.extract_key_differences(report_data)
1403
+
1404
+ if key_differences:
1405
+ st.markdown("### الاختلافات الرئيسية")
1406
+
1407
+ for diff in key_differences:
1408
+ st.markdown(f"#### {diff['label']} ({diff['count']})")
1409
+
1410
+ if diff["type"] == "added_paragraphs":
1411
+ for item in diff["items"][:5]: # عرض أول 5 فقط
1412
+ st.markdown(f"<div class='diff-text diff-new'>{item}</div>", unsafe_allow_html=True)
1413
+
1414
+ elif diff["type"] == "removed_paragraphs":
1415
+ for item in diff["items"][:5]:
1416
+ st.markdown(f"<div class='diff-text diff-old'>{item}</div>", unsafe_allow_html=True)
1417
+
1418
+ elif diff["type"] == "modified_paragraphs":
1419
+ for item in diff["items"][:3]:
1420
+ col1, col2 = st.columns(2)
1421
+ with col1:
1422
+ st.markdown(f"**{report_data['title1']}:**")
1423
+ st.markdown(f"<div class='diff-text diff-old'>{item['doc1_text']}</div>", unsafe_allow_html=True)
1424
+ with col2:
1425
+ st.markdown(f"**{report_data['title2']}:**")
1426
+ st.markdown(f"<div class='diff-text diff-new'>{item['doc2_text']}</div>", unsafe_allow_html=True)
1427
+
1428
+ elif diff["type"] in ["added_words", "removed_words"]:
1429
+ # عرض الكلمات في شكل جدول
1430
+ word_data = []
1431
+ for word, count in diff["items"]:
1432
+ if len(word) > 1: # تجاهل الأحرف المفردة
1433
+ word_data.append({"الكلمة": word, "عدد المرات": count})
1434
+
1435
+ if word_data:
1436
+ word_df = pd.DataFrame(word_data)
1437
+ st.dataframe(word_df)
1438
+
1439
+ # تحليل التغييرات القانونية
1440
+ legal_changes = self.analyze_legal_changes(report_data)
1441
+
1442
+ if legal_changes:
1443
+ st.markdown("### تحليل التغييرات القانونية")
1444
+
1445
+ for change in legal_changes[:3]: # عرض أهم 3 فئات فقط
1446
+ st.markdown(f"#### {change['label']} ({change['count']})")
1447
+
1448
+ for item in change["changes"][:2]: # عرض أول مثالين فقط
1449
+ col1, col2 = st.columns(2)
1450
+ with col1:
1451
+ st.markdown(f"**{report_data['title1']}:**")
1452
+ st.markdown(f"<div class='diff-text diff-old'>{item['doc1_text']}</div>", unsafe_allow_html=True)
1453
+ with col2:
1454
+ st.markdown(f"**{report_data['title2']}:**")
1455
+ st.markdown(f"<div class='diff-text diff-new'>{item['doc2_text']}</div>", unsafe_allow_html=True)
1456
+ else:
1457
+ st.error("تعذر تحميل تقرير المقارنة")
1458
+ else:
1459
+ st.info("لا توجد تقارير مقارنة محفوظة")
1460
+
1461
+ # إضافة CSS للتنسيق
1462
+ st.markdown("""
1463
+ <style>
1464
+ .module-title {
1465
+ color: #1E88E5;
1466
+ font-size: 1.8rem;
1467
+ font-weight: bold;
1468
+ margin-bottom: 1rem;
1469
+ text-align: center;
1470
+ }
1471
+
1472
+ .module-description {
1473
+ background-color: #f8f9fa;
1474
+ border-right: 4px solid #1E88E5;
1475
+ padding: 1rem;
1476
+ margin-bottom: 1.5rem;
1477
+ color: #444;
1478
+ font-size: 1rem;
1479
+ text-align: right;
1480
+ }
1481
+
1482
+ .diff-text {
1483
+ padding: 0.5rem;
1484
+ border-radius: 4px;
1485
+ margin-bottom: 0.5rem;
1486
+ white-space: pre-wrap;
1487
+ }
1488
+
1489
+ .diff-old {
1490
+ background-color: rgba(214, 48, 49, 0.1);
1491
+ border-right: 3px solid #d63031;
1492
+ }
1493
+
1494
+ .diff-new {
1495
+ background-color: rgba(9, 132, 227, 0.1);
1496
+ border-right: 3px solid #0984e3;
1497
+ }
1498
+ </style>
1499
+ """, unsafe_allow_html=True)
1500
+
1501
+ def render(self):
1502
+ """عرض واجهة المستخدم الرئيسية للتطبيق"""
1503
+ self.render_advanced_comparison_tools()
modules/maps/README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
2
+
3
+ ## نظرة عامة
4
+ تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد، مما يساعد في تقييم طبيعة الموقع بشكل أفضل قبل البدء في العمل.
5
+
6
+ ## الميزات الرئيسية
7
+
8
+ ### الخريطة التفاعلية
9
+ - عرض جميع مواقع المشاريع على خريطة تفاعلية
10
+ - إمكانية البحث عن المواقع وتصفيتها
11
+ - تجميع المواقع القريبة (Clustering)
12
+ - عرض خرائط حرارية لتوزيع المشاريع
13
+ - أدوات قياس المسافة والمساحة
14
+
15
+ ### عرض التضاريس ثلاثي الأبعاد
16
+ - عرض تضاريس موقع المشروع بشكل ثلاثي الأبعاد
17
+ - التحكم في نطاق العرض ومقياس الارتفاع
18
+ - تحليل الارتفاعات وعرض المقطع الجانبي
19
+ - إمكانية تدوير وتكبير العرض للرؤية من زوايا مختلفة
20
+
21
+ ### تحليل المواقع
22
+ - عرض توزيع المشاريع حسب المدينة والحالة
23
+ - تحليل المسافات بين المشاريع
24
+ - عرض المشاريع القريبة من مشروع محدد
25
+ - رسوم بيانية توضيحية للتوزيع الجغرافي
26
+
27
+ ### إدارة المواقع
28
+ - إضافة مواقع جديدة
29
+ - تحرير وحذف المواقع الموجودة
30
+ - استيراد وتصدير بيانات المواقع بصيغ متعددة (CSV, JSON, GeoJSON)
31
+
32
+ ## المتطلبات الفنية
33
+ - Streamlit
34
+ - Folium
35
+ - PyDeck
36
+ - Pandas
37
+ - NumPy
38
+ - Plotly
39
+ - streamlit-folium
40
+
41
+ ## المطورون
42
+ فريق تطوير نظام WAHBI AI لتحليل العقود والمناقصات - شركة شبه الجزيرة للمقاولات
43
+
44
+ ## تاريخ الإصدار
45
+ مارس 2025
modules/maps/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # ملف تهيئة وحدة الخرائط
modules/maps/interactive_map.py ADDED
@@ -0,0 +1,1671 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
6
+ تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import streamlit as st
12
+ import pandas as pd
13
+ import numpy as np
14
+ import pydeck as pdk
15
+ import folium
16
+ from folium.plugins import MarkerCluster, HeatMap, MeasureControl
17
+ from streamlit_folium import folium_static
18
+ import requests
19
+ import json
20
+ import random
21
+ from typing import List, Dict, Any, Tuple, Optional
22
+ import tempfile
23
+ import base64
24
+ from PIL import Image
25
+ from io import BytesIO
26
+
27
+ # إضافة مسار النظام للوصول للملفات المشتركة
28
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
29
+
30
+ # استيراد مكونات واجهة المستخدم
31
+ from utils.components.header import render_header
32
+ from utils.components.credits import render_credits
33
+ from utils.helpers import format_number, format_currency, styled_button
34
+
35
+
36
+ class InteractiveMap:
37
+ """فئة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد"""
38
+
39
+ def __init__(self):
40
+ """تهيئة وحدة الخريطة التفاعلية"""
41
+ # تهيئة مجلدات حفظ البيانات
42
+ self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/maps"))
43
+ os.makedirs(self.data_dir, exist_ok=True)
44
+
45
+ # مفاتيح API لخدمات الخرائط
46
+ self.mapbox_token = os.environ.get("MAPBOX_TOKEN", "")
47
+ self.opentopodata_api = "https://api.opentopodata.org/v1/srtm30m"
48
+
49
+ # تهيئة حالة الجلسة
50
+ if 'project_locations' not in st.session_state:
51
+ st.session_state.project_locations = []
52
+
53
+ if 'selected_location' not in st.session_state:
54
+ st.session_state.selected_location = None
55
+
56
+ if 'terrain_data' not in st.session_state:
57
+ st.session_state.terrain_data = None
58
+
59
+ # بيانات اختبارية للمشاريع (سيتم استبدالها بالبيانات الفعلية من قاعدة البيانات)
60
+ self._initialize_sample_projects()
61
+
62
+ def render(self):
63
+ """عرض واجهة وحدة الخريطة التفاعلية"""
64
+ # عرض الشعار والعنوان الرئيسي
65
+ render_header("خريطة مواقع المشاريع التفاعلية")
66
+
67
+ # تبويبات الوحدة
68
+ tabs = st.tabs([
69
+ "الخريطة التفاعلية",
70
+ "عرض التضاريس ثلاثي الأبعاد",
71
+ "تحليل المواقع",
72
+ "إدارة المواقع"
73
+ ])
74
+
75
+ # تبويب الخريطة التفاعلية
76
+ with tabs[0]:
77
+ self._render_interactive_map()
78
+
79
+ # تبويب عرض التضاريس ثلاثي الأبعاد
80
+ with tabs[1]:
81
+ self._render_3d_terrain()
82
+
83
+ # تبويب تحليل المواقع
84
+ with tabs[2]:
85
+ self._render_location_analysis()
86
+
87
+ # تبويب إدارة المواقع
88
+ with tabs[3]:
89
+ self._render_location_management()
90
+
91
+ # عرض حقوق النشر
92
+ render_credits()
93
+
94
+ def _render_interactive_map(self):
95
+ """عرض الخريطة التفاعلية"""
96
+ st.markdown("""
97
+ <div class='custom-box info-box'>
98
+ <h3>🗺️ الخريطة التفاعلية لمواقع المشاريع</h3>
99
+ <p>خريطة تفاعلية تعرض مواقع جميع المشاريع مع إمكانية التكبير والتصغير والتحرك.</p>
100
+ <p>يمكنك النقر على أي موقع للحصول على تفاصيل المشروع.</p>
101
+ </div>
102
+ """, unsafe_allow_html=True)
103
+
104
+ # مربع البحث
105
+ search_query = st.text_input("البحث عن موقع أو مشروع", key="map_search")
106
+
107
+ # أزرار تحكم للخريطة
108
+ col1, col2, col3, col4 = st.columns(4)
109
+
110
+ with col1:
111
+ map_style = st.selectbox(
112
+ "نمط الخريطة",
113
+ options=["OpenStreetMap", "Stamen Terrain", "Stamen Toner", "CartoDB Positron"],
114
+ key="map_style"
115
+ )
116
+
117
+ with col2:
118
+ cluster_markers = st.checkbox("تجميع المواقع القريبة", value=True, key="cluster_markers")
119
+
120
+ with col3:
121
+ show_heatmap = st.checkbox("عرض خريطة حرارية للمواقع", value=False, key="show_heatmap")
122
+
123
+ with col4:
124
+ show_measurements = st.checkbox("أدوات القياس", value=False, key="show_measurements")
125
+
126
+ # إنشاء الخريطة
127
+ if len(st.session_state.project_locations) > 0:
128
+ # بيانات النقاط على الخريطة
129
+ locations = []
130
+
131
+ # تصفية المشاريع حسب البحث
132
+ filtered_projects = st.session_state.project_locations
133
+ if search_query:
134
+ filtered_projects = [
135
+ p for p in filtered_projects
136
+ if search_query.lower() in p.get("name", "").lower() or
137
+ search_query.lower() in p.get("description", "").lower() or
138
+ search_query.lower() in p.get("city", "").lower()
139
+ ]
140
+
141
+ # عرض عدد النتائج
142
+ if search_query:
143
+ st.markdown(f"عدد النتائج: {len(filtered_projects)}")
144
+
145
+ # تحضير البيانات للخريطة
146
+ heat_data = []
147
+ for project in filtered_projects:
148
+ locations.append({
149
+ "lat": project.get("latitude"),
150
+ "lon": project.get("longitude"),
151
+ "name": project.get("name"),
152
+ "description": project.get("description"),
153
+ "city": project.get("city"),
154
+ "status": project.get("status"),
155
+ "project_id": project.get("project_id")
156
+ })
157
+ heat_data.append([project.get("latitude"), project.get("longitude"), 1])
158
+
159
+ # تعيين نقطة المركز والتكبير
160
+ if filtered_projects:
161
+ center_lat = sum(p.get("latitude", 0) for p in filtered_projects) / len(filtered_projects)
162
+ center_lon = sum(p.get("longitude", 0) for p in filtered_projects) / len(filtered_projects)
163
+ zoom_level = 6 # مستوى التكبير الافتراضي
164
+ else:
165
+ # مركز المملكة العربية السعودية
166
+ center_lat = 24.7136
167
+ center_lon = 46.6753
168
+ zoom_level = 5
169
+
170
+ # تحديد الإسناد (attribution) بناءً على نمط الخريطة
171
+ attribution = None
172
+ if map_style == "OpenStreetMap":
173
+ attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
174
+ elif map_style.startswith("Stamen"):
175
+ attribution = 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
176
+ elif map_style == "CartoDB Positron":
177
+ attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="https://cartodb.com/attributions">CartoDB</a>'
178
+
179
+ # إنشاء الخريطة
180
+ m = folium.Map(
181
+ location=[center_lat, center_lon],
182
+ zoom_start=zoom_level,
183
+ tiles=map_style,
184
+ attr=attribution # إضافة سمة الإسناد
185
+ )
186
+
187
+ # إضافة أدوات القياس إذا تم اختيارها
188
+ if show_measurements:
189
+ MeasureControl(position='topright', primary_length_unit='kilometers').add_to(m)
190
+
191
+ # إضافة النقاط إلى الخريطة
192
+ if cluster_markers:
193
+ # إنشاء مجموعة تجميع
194
+ marker_cluster = MarkerCluster(name="تجميع المشاريع").add_to(m)
195
+
196
+ # إضافة النقاط إلى المجموعة
197
+ for location in locations:
198
+ # إنشاء النافذة المنبثقة
199
+ popup_html = f"""
200
+ <div style='direction: rtl; text-align: right;'>
201
+ <h4>{location['name']}</h4>
202
+ <p><strong>الوصف:</strong> {location['description']}</p>
203
+ <p><strong>المدينة:</strong> {location['city']}</p>
204
+ <p><strong>الحالة:</strong> {location['status']}</p>
205
+ <p><strong>الإحداثيات:</strong> {location['lat']:.6f}, {location['lon']:.6f}</p>
206
+ <button onclick="window.open('/project/{location['project_id']}', '_self')">عرض تفاصيل المشروع</button>
207
+ </div>
208
+ """
209
+
210
+ # تحديد لون العلامة حسب حالة المشروع
211
+ icon_color = 'green'
212
+ if location['status'] == 'قيد التنفيذ':
213
+ icon_color = 'orange'
214
+ elif location['status'] == 'متوقف':
215
+ icon_color = 'red'
216
+ elif location['status'] == 'مكتمل':
217
+ icon_color = 'blue'
218
+
219
+ # إضافة العلامة
220
+ folium.Marker(
221
+ location=[location['lat'], location['lon']],
222
+ popup=folium.Popup(popup_html, max_width=300),
223
+ tooltip=location['name'],
224
+ icon=folium.Icon(color=icon_color, icon='info-sign')
225
+ ).add_to(marker_cluster)
226
+ else:
227
+ # إضافة النقاط مباشرة إلى الخريطة
228
+ for location in locations:
229
+ # إنشاء النافذة المنبثقة
230
+ popup_html = f"""
231
+ <div style='direction: rtl; text-align: right;'>
232
+ <h4>{location['name']}</h4>
233
+ <p><strong>الوصف:</strong> {location['description']}</p>
234
+ <p><strong>المدينة:</strong> {location['city']}</p>
235
+ <p><strong>الحالة:</strong> {location['status']}</p>
236
+ <p><strong>الإحداثيات:</strong> {location['lat']:.6f}, {location['lon']:.6f}</p>
237
+ <button onclick="window.open('/project/{location['project_id']}', '_self')">عرض تفاصيل المشروع</button>
238
+ </div>
239
+ """
240
+
241
+ # تحديد لون العلامة حسب حالة المشروع
242
+ icon_color = 'green'
243
+ if location['status'] == 'قيد التنفيذ':
244
+ icon_color = 'orange'
245
+ elif location['status'] == 'متوقف':
246
+ icon_color = 'red'
247
+ elif location['status'] == 'مكتمل':
248
+ icon_color = 'blue'
249
+
250
+ # إضافة العلامة
251
+ folium.Marker(
252
+ location=[location['lat'], location['lon']],
253
+ popup=folium.Popup(popup_html, max_width=300),
254
+ tooltip=location['name'],
255
+ icon=folium.Icon(color=icon_color, icon='info-sign')
256
+ ).add_to(m)
257
+
258
+ # إضافة خريطة حرارية إذا تم اختيارها
259
+ if show_heatmap and heat_data:
260
+ HeatMap(heat_data, radius=15).add_to(m)
261
+
262
+ # إضافة طبقات متنوعة للخريطة
263
+ folium.TileLayer('OpenStreetMap').add_to(m)
264
+ folium.TileLayer('Stamen Terrain').add_to(m)
265
+ folium.TileLayer('Stamen Toner').add_to(m)
266
+ folium.TileLayer('CartoDB positron').add_to(m)
267
+ folium.TileLayer('CartoDB dark_matter').add_to(m)
268
+
269
+ # إضافة أدوات التحكم بالطبقات
270
+ folium.LayerControl().add_to(m)
271
+
272
+ # عرض الخريطة
273
+ st_map = folium_static(m, width=1000, height=600)
274
+
275
+ # التفاعل مع النقر على الخريطة (سيتم تنفيذه عندما يتم دعم التفاعل)
276
+ # حاليًا لا يوجد دعم مباشر للتفاعل مع الخريطة في Streamlit
277
+
278
+ # عرض بيانات المشاريع في جدول
279
+ st.markdown("### قائمة المشاريع على الخريطة")
280
+
281
+ projects_df = pd.DataFrame(filtered_projects)
282
+
283
+ # إعادة تسمية الأعمدة بالعربية
284
+ renamed_columns = {
285
+ "name": "اسم المشروع",
286
+ "city": "المدينة",
287
+ "status": "الحالة",
288
+ "description": "الوصف",
289
+ "project_id": "معرف المشروع",
290
+ "latitude": "خط العرض",
291
+ "longitude": "خط الطول"
292
+ }
293
+
294
+ # تحديد الأعمدة للعرض
295
+ display_columns = ["name", "city", "status", "project_id"]
296
+
297
+ # إنشاء جدول للعرض
298
+ display_df = projects_df[display_columns].rename(columns=renamed_columns)
299
+
300
+ # عرض الجدول
301
+ st.dataframe(display_df, width=1000, height=400)
302
+
303
+ # زر لاختيار مشروع لعرض التضاريس
304
+ selected_project_id = st.selectbox(
305
+ "اختر مشروعًا لعرض التضاريس ثلاثية الأبعاد",
306
+ options=projects_df["project_id"].tolist(),
307
+ format_func=lambda x: next((p["name"] for p in filtered_projects if p["project_id"] == x), x),
308
+ key="select_project_for_terrain"
309
+ )
310
+
311
+ # زر عرض التضاريس
312
+ if styled_button("عرض التضاريس", key="show_terrain_btn", type="primary", icon="🏔️"):
313
+ # العثور على المشروع المحدد
314
+ selected_project = next((p for p in filtered_projects if p["project_id"] == selected_project_id), None)
315
+
316
+ if selected_project:
317
+ # تخزين الموقع المحدد في حالة الجلسة
318
+ st.session_state.selected_location = {
319
+ "latitude": selected_project["latitude"],
320
+ "longitude": selected_project["longitude"],
321
+ "name": selected_project["name"],
322
+ "project_id": selected_project["project_id"]
323
+ }
324
+
325
+ # جلب بيانات التضاريس
326
+ try:
327
+ terrain_data = self._fetch_terrain_data(
328
+ selected_project["latitude"],
329
+ selected_project["longitude"]
330
+ )
331
+
332
+ # تخزين بيانات التضاريس في حالة الجلسة
333
+ st.session_state.terrain_data = terrain_data
334
+
335
+ # الانتقال إلى تبويب عرض التضاريس
336
+ st.rerun()
337
+ except Exception as e:
338
+ st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
339
+ else:
340
+ st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.")
341
+
342
+ def _render_3d_terrain(self):
343
+ """عرض التضاريس ثلاثي الأبعاد"""
344
+ st.markdown("""
345
+ <div class='custom-box info-box'>
346
+ <h3>🏔️ عرض التضاريس ثلاثي الأبعاد</h3>
347
+ <p>عرض تفاعلي ثلاثي الأبعاد لتضاريس موقع المشروع المحدد.</p>
348
+ <p>يمكنك تدوير وتكبير العرض للحصول على رؤية أفضل للموقع.</p>
349
+ </div>
350
+ """, unsafe_allow_html=True)
351
+
352
+ # التحقق من وجود موقع محدد
353
+ if st.session_state.selected_location is None:
354
+ st.info("يرجى اختيار موقع من تبويب 'الخريطة التفاعلية' أولاً.")
355
+
356
+ # بديل: السماح بإدخال الإحداثيات يدوياً
357
+ st.markdown("### إدخال الإحداثيات يدوياً")
358
+
359
+ col1, col2 = st.columns(2)
360
+
361
+ with col1:
362
+ manual_lat = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="manual_lat")
363
+
364
+ with col2:
365
+ manual_lon = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="manual_lon")
366
+
367
+ if styled_button("عرض التضاريس", key="manual_terrain_btn", type="primary", icon="🏔️"):
368
+ try:
369
+ # جلب بيانات التضاريس
370
+ terrain_data = self._fetch_terrain_data(manual_lat, manual_lon)
371
+
372
+ # تخزين بيانات التضاريس والموقع في حالة الجلسة
373
+ st.session_state.terrain_data = terrain_data
374
+ st.session_state.selected_location = {
375
+ "latitude": manual_lat,
376
+ "longitude": manual_lon,
377
+ "name": f"الموقع المخصص ({manual_lat:.4f}, {manual_lon:.4f})",
378
+ "project_id": "custom"
379
+ }
380
+
381
+ # إعادة تشغيل التطبيق لتحديث العرض
382
+ st.rerun()
383
+ except Exception as e:
384
+ st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
385
+
386
+ # عرض خريطة لتحديد الموقع
387
+ st.markdown("### حدد موقعًا على الخريطة")
388
+ m = folium.Map(
389
+ location=[24.7136, 46.6753],
390
+ zoom_start=6,
391
+ attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
392
+ )
393
+ folium_static(m, width=1000, height=500)
394
+
395
+ st.info("ملاحظة: لا يمكن تحديد موقع على الخريطة مباشرة في هذا الإصدار. يرجى إدخال الإحداثيات يدوياً أو اختيار مشروع من القائمة.")
396
+
397
+ return
398
+
399
+ # عرض معلومات الموقع المحدد
400
+ st.markdown(f"### تضاريس موقع: {st.session_state.selected_location['name']}")
401
+ st.markdown(f"الإحداثيات: {st.session_state.selected_location['latitude']:.6f}, {st.session_state.selected_location['longitude']:.6f}")
402
+
403
+ # التحقق من وجود بيانات التضاريس
404
+ if st.session_state.terrain_data is None:
405
+ st.warning("لا توجد بيانات تضاريس متاحة لهذا الموقع. جاري جلب البيانات...")
406
+
407
+ try:
408
+ # جلب بيانات التضاريس
409
+ terrain_data = self._fetch_terrain_data(
410
+ st.session_state.selected_location["latitude"],
411
+ st.session_state.selected_location["longitude"]
412
+ )
413
+
414
+ # تخزين بيانات التضاريس في حالة الجلسة
415
+ st.session_state.terrain_data = terrain_data
416
+
417
+ # إعادة تشغيل التطبيق لتحديث العرض
418
+ st.rerun()
419
+ except Exception as e:
420
+ st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
421
+ return
422
+
423
+ # عرض الخريطة ثنائية الأبعاد للموقع
424
+ st.markdown("### خريطة الموقع")
425
+
426
+ # إنشاء خريطة صغيرة للموقع
427
+ mini_map = folium.Map(
428
+ location=[st.session_state.selected_location["latitude"], st.session_state.selected_location["longitude"]],
429
+ zoom_start=10,
430
+ attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
431
+ )
432
+ folium.Marker(location=[st.session_state.selected_location["latitude"], st.session_state.selected_location["longitude"]], tooltip="الموقع المحدد").add_to(mini_map)
433
+ folium_static(mini_map, width=700, height=300)
434
+
435
+ # عرض بيانات التضاريس
436
+ st.markdown("### نموذج التضاريس ثلاثي الأبعاد")
437
+
438
+ # تحويل بيانات التضاريس إلى DataFrame
439
+ df = pd.DataFrame(st.session_state.terrain_data)
440
+
441
+ # اختيار نظام ألوان
442
+ color_schemes = {
443
+ "Viridis": "Viridis",
444
+ "أخضر إلى بني": "Greens",
445
+ "أزرق إلى أحمر": "RdBu",
446
+ "أرجواني إلى أخضر": "PuGn",
447
+ "نظام الارتفاعات": "Terrain"
448
+ }
449
+
450
+ color_scheme = st.selectbox(
451
+ "نظام الألوان",
452
+ options=list(color_schemes.keys()),
453
+ index=4,
454
+ key="3d_color_scheme"
455
+ )
456
+
457
+ # خيارات العرض
458
+ col1, col2, col3 = st.columns(3)
459
+
460
+ with col1:
461
+ exaggeration = st.slider("تضخيم الارتفاع", 1, 50, 15, key="terrain_exaggeration")
462
+
463
+ with col2:
464
+ radius = st.slider("نطاق العرض (كم)", 1, 20, 5, key="terrain_radius")
465
+
466
+ with col3:
467
+ resolution = st.slider("دقة العرض", 10, 100, 50, key="terrain_resolution")
468
+
469
+ if not df.empty and len(df) > 1:
470
+ # إعادة جلب البيانات إذا تغير النطاق
471
+ current_lat = st.session_state.selected_location["latitude"]
472
+ current_lon = st.session_state.selected_location["longitude"]
473
+ current_radius = radius
474
+
475
+ # جلب بيانات جديدة إذا تغير النطاق
476
+ if styled_button("تحديث النطاق", key="update_radius_btn"):
477
+ try:
478
+ # جلب بيانات التضاريس
479
+ terrain_data = self._fetch_terrain_data(
480
+ current_lat,
481
+ current_lon,
482
+ radius_km=current_radius
483
+ )
484
+
485
+ # تخزين بيانات التضاريس في حالة الجلسة
486
+ st.session_state.terrain_data = terrain_data
487
+
488
+ # إعادة تشغيل التطبيق لتحديث العرض
489
+ st.rerun()
490
+ except Exception as e:
491
+ st.error(f"حدث خطأ أثناء تحديث بيانات التضاريس: {str(e)}")
492
+
493
+ # تحويل البيانات إلى تنسيق مناسب لـ PyDeck
494
+ x = df["longitude"].values
495
+ y = df["latitude"].values
496
+ z = df["elevation"].values * exaggeration # تضخيم الارتفاع
497
+
498
+ # تطبيع الارتفاعات للحصول على ألوان مناسبة
499
+ normalized_elevation = (z - z.min()) / (z.max() - z.min() if z.max() != z.min() else 1)
500
+
501
+ # الحصول على نظام الألوان
502
+ cmap = self._get_color_map(color_schemes[color_scheme])
503
+
504
+ # إنشاء عمود الألوان
505
+ df["color"] = [
506
+ cmap(ne) if ne <= 1.0 else cmap(1.0)
507
+ for ne in normalized_elevation
508
+ ]
509
+
510
+ # تهيئة عرض PyDeck
511
+ view_state = pdk.ViewState(
512
+ latitude=current_lat,
513
+ longitude=current_lon,
514
+ zoom=10,
515
+ pitch=45,
516
+ bearing=0
517
+ )
518
+
519
+ # إنشاء طبقة التضاريس
520
+ terrain_layer = pdk.Layer(
521
+ "ColumnLayer",
522
+ data=df,
523
+ get_position=["longitude", "latitude"],
524
+ get_elevation="elevation * " + str(exaggeration),
525
+ get_fill_color="color",
526
+ get_radius=resolution,
527
+ pickable=True,
528
+ auto_highlight=True,
529
+ elevation_scale=1,
530
+ elevation_range=[0, 1000],
531
+ coverage=1,
532
+ )
533
+
534
+ # إضافة طبقة لعلامة الموقع المحدد
535
+ marker_df = pd.DataFrame({
536
+ "latitude": [current_lat],
537
+ "longitude": [current_lon],
538
+ "size": [400]
539
+ })
540
+
541
+ marker_layer = pdk.Layer(
542
+ "ScatterplotLayer",
543
+ data=marker_df,
544
+ get_position=["longitude", "latitude"],
545
+ get_radius="size",
546
+ get_fill_color=[255, 0, 0, 200],
547
+ pickable=True,
548
+ )
549
+
550
+ # تهيئة العرض
551
+ r = pdk.Deck(
552
+ layers=[terrain_layer, marker_layer],
553
+ initial_view_state=view_state,
554
+ map_style="mapbox://styles/mapbox/satellite-v9",
555
+ tooltip={
556
+ "html": "<b>ارتفاع:</b> {elevation} متر<br/><b>إحداثيات:</b> {latitude:.6f}, {longitude:.6f}",
557
+ "style": {
558
+ "backgroundColor": "steelblue",
559
+ "color": "white",
560
+ "direction": "rtl",
561
+ "text-align": "right"
562
+ }
563
+ }
564
+ )
565
+
566
+ # عرض نموذج التضاريس
567
+ st.pydeck_chart(r)
568
+
569
+ # إضافة معلومات إضافية
570
+ st.markdown("### معلومات الارتفاع")
571
+
572
+ # حساب الإحصاءات
573
+ min_elevation = df["elevation"].min()
574
+ max_elevation = df["elevation"].max()
575
+ avg_elevation = df["elevation"].mean()
576
+
577
+ # عرض الإحصاءات
578
+ stat_col1, stat_col2, stat_col3 = st.columns(3)
579
+
580
+ with stat_col1:
581
+ st.metric("أدنى ارتفاع", f"{min_elevation:.1f} متر")
582
+
583
+ with stat_col2:
584
+ st.metric("متوسط الارتفاع", f"{avg_elevation:.1f} متر")
585
+
586
+ with stat_col3:
587
+ st.metric("أعلى ارتفاع", f"{max_elevation:.1f} متر")
588
+
589
+ # زر لتصدير البيانات
590
+ if styled_button("تصدير بيانات التضاريس", key="export_terrain_btn", type="secondary", icon="📊"):
591
+ # تحويل البيانات إلى CSV
592
+ csv = df.to_csv(index=False)
593
+
594
+ # إنشاء رابط تنزيل
595
+ b64 = base64.b64encode(csv.encode()).decode()
596
+ href = f'<a href="data:file/csv;base64,{b64}" download="terrain_data_{st.session_state.selected_location["project_id"]}.csv" class="btn">تنزيل البيانات (CSV)</a>'
597
+ st.markdown(href, unsafe_allow_html=True)
598
+ else:
599
+ st.error("لا توجد بيانات كافية لعرض نموذج التضاريس. حاول اختيار موقع آخر أو زيادة النطاق.")
600
+
601
+ def _render_location_analysis(self):
602
+ """عرض تحليل المواقع"""
603
+ st.markdown("""
604
+ <div class='custom-box info-box'>
605
+ <h3>📊 تحليل موقع المشروع</h3>
606
+ <p>تحليل متقدم لموقع المشروع وتضاريسه والظروف المحيطة.</p>
607
+ <p>يمكنك تحليل الارتفاعات والمسافات وقياس التكاليف المرتبطة بالموقع.</p>
608
+ </div>
609
+ """, unsafe_allow_html=True)
610
+
611
+ # التحقق من وجود مواقع
612
+ if len(st.session_state.project_locations) == 0:
613
+ st.warning("لا توجد مواقع مشاريع متاحة للتحليل. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.")
614
+ return
615
+
616
+ # اختيار موقع أو موقعين للتحليل
617
+ analysis_type = st.radio(
618
+ "نوع التحليل",
619
+ options=["تحليل موقع واحد", "مقارنة موقعين"],
620
+ key="location_analysis_type",
621
+ horizontal=True
622
+ )
623
+
624
+ # تحويل المواقع إلى DataFrame
625
+ projects_df = pd.DataFrame(st.session_state.project_locations)
626
+
627
+ if analysis_type == "تحليل موقع واحد":
628
+ # اختيار موقع للتحليل
629
+ selected_project_id = st.selectbox(
630
+ "اختر موقع المشروع للتحليل",
631
+ options=projects_df["project_id"].tolist(),
632
+ format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
633
+ key="analysis_project"
634
+ )
635
+
636
+ # العثور على المشروع المحدد
637
+ selected_project = next((p for p in st.session_state.project_locations if p["project_id"] == selected_project_id), None)
638
+
639
+ if selected_project:
640
+ # عرض معلومات المشروع
641
+ st.markdown(f"### تحليل موقع: {selected_project['name']}")
642
+
643
+ # عرض خريطة الموقع
644
+ st.markdown("#### موقع المشروع")
645
+
646
+ # إنشاء خريطة صغيرة للموقع
647
+ m2 = folium.Map(
648
+ location=[selected_project["latitude"], selected_project["longitude"]],
649
+ zoom_start=10,
650
+ attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
651
+ )
652
+ folium.Marker(location=[selected_project["latitude"], selected_project["longitude"]], tooltip=selected_project["name"]).add_to(m2)
653
+
654
+ # إضافة دائرة بنصف قطر محدد
655
+ analysis_radius = st.slider("نطاق التحليل (كم)", 1, 50, 10, key="analysis_radius")
656
+ folium.Circle(
657
+ location=[selected_project["latitude"], selected_project["longitude"]],
658
+ radius=analysis_radius * 1000, # تحويل إلى أمتار
659
+ color="red",
660
+ fill=True,
661
+ fill_opacity=0.2
662
+ ).add_to(m2)
663
+
664
+ folium_static(m2, width=700, height=400)
665
+
666
+ # تحليل الموقع
667
+ st.markdown("#### عوامل الموقع")
668
+
669
+ # تحليل اعتباري للموقع (يمكن استبداله بتحليل حقيقي من خدمات مثل Google Places API)
670
+
671
+ # عوامل افتراضية - ستتغير هذه باستخدام بيانات حقيقية
672
+ factors = {
673
+ "قرب المدينة": random.uniform(0.4, 1.0),
674
+ "توفر المياه": random.uniform(0.3, 0.9),
675
+ "سهولة الوصول": random.uniform(0.5, 1.0),
676
+ "الظروف الجوية": random.uniform(0.6, 1.0),
677
+ "التضاريس": random.uniform(0.3, 0.8),
678
+ "توفر العمالة": random.uniform(0.5, 0.9),
679
+ "البنية التحتية": random.uniform(0.4, 0.9),
680
+ "المخاطر البيئية": random.uniform(0.3, 0.7)
681
+ }
682
+
683
+ # مخطط شريطي للعوامل
684
+ factors_df = pd.DataFrame({
685
+ "العامل": list(factors.keys()),
686
+ "التقييم": list(factors.values())
687
+ })
688
+
689
+ # الترتيب تنازلياً
690
+ factors_df = factors_df.sort_values(by="التقييم", ascending=False)
691
+
692
+ # عرض الرسم البياني
693
+ st.bar_chart(factors_df.set_index("العامل"))
694
+
695
+ # تقييم إجمالي للموقع
696
+ overall_score = sum(factors.values()) / len(factors)
697
+
698
+ # عرض التقييم الإجمالي
699
+ st.markdown(f"#### التقييم الإجمالي للموقع: {overall_score:.2f}/1.0")
700
+
701
+ # مؤشر تقدم للتقييم
702
+ st.progress(overall_score)
703
+
704
+ # تصنيف التقييم
705
+ if overall_score >= 0.8:
706
+ rating = "ممتاز"
707
+ color = "green"
708
+ elif overall_score >= 0.6:
709
+ rating = "جيد"
710
+ color = "blue"
711
+ elif overall_score >= 0.4:
712
+ rating = "مقبول"
713
+ color = "orange"
714
+ else:
715
+ rating = "ضعيف"
716
+ color = "red"
717
+
718
+ st.markdown(f"<h4 style='color: {color};'>تصنيف الموقع: {rating}</h4>", unsafe_allow_html=True)
719
+
720
+ # توصيات للموقع
721
+ st.markdown("#### توصيات الموقع")
722
+
723
+ recommendations = [
724
+ "تحسين طرق الوصول للموقع لزيادة كفاءة نقل المواد والمعدات.",
725
+ "إجراء دراسة جيوتقنية مفصلة للتضاريس قبل البدء في أعمال الحفر.",
726
+ "التأكد من توفر مصادر المياه الكافية لاحتياجات المشروع.",
727
+ "التنسيق مع السلطات المحلية لتسهيل توصيل الخدمات للموقع.",
728
+ "وضع خطة للتعامل مع الظروف الجوية المتقلبة في المنطقة."
729
+ ]
730
+
731
+ for rec in recommendations:
732
+ st.markdown(f"- {rec}")
733
+
734
+ # المرافق القريبة
735
+ st.markdown("#### المرافق القريبة (تمثيل افتراضي)")
736
+
737
+ # بيانات افتراضية للمرافق القريبة
738
+ nearby_facilities = {
739
+ "مستشفى": random.uniform(5, 30),
740
+ "مدرسة": random.uniform(2, 15),
741
+ "محطة وقود": random.uniform(2, 20),
742
+ "مركز تسوق": random.uniform(3, 25),
743
+ "مكتب حكومي": random.uniform(7, 35),
744
+ "مطار": random.uniform(15, 100),
745
+ "ميناء": random.uniform(20, 150)
746
+ }
747
+
748
+ # عرض المرافق في جدول
749
+ facilities_df = pd.DataFrame({
750
+ "المرفق": list(nearby_facilities.keys()),
751
+ "المسافة (كم)": list(nearby_facilities.values())
752
+ })
753
+
754
+ # ترتيب حسب المسافة
755
+ facilities_df = facilities_df.sort_values(by="المسافة (كم)")
756
+
757
+ # عرض الجدول
758
+ st.dataframe(facilities_df, width=700)
759
+
760
+ # تقرير تكلفة الموقع
761
+ st.markdown("#### تقديرات تكلفة الموقع")
762
+
763
+ # بنود التكلفة الافتراضية
764
+ cost_items = {
765
+ "تكلفة تسوية الأرض": random.uniform(50000, 200000),
766
+ "تكلفة البنية التحتية": random.uniform(100000, 500000),
767
+ "تكلفة النقل الإضافية": random.uniform(30000, 150000),
768
+ "تكلفة الحماية من المخاطر البيئية": random.uniform(20000, 100000),
769
+ "تكلفة توصيل الخدمات": random.uniform(40000, 200000)
770
+ }
771
+
772
+ # عرض بنود التكلفة
773
+ st.markdown("##### بنود التكلفة")
774
+
775
+ for item, cost in cost_items.items():
776
+ st.markdown(f"- {item}: {format_currency(cost)} ريال")
777
+
778
+ # إجمالي التكلفة
779
+ total_cost = sum(cost_items.values())
780
+ st.markdown(f"##### إجمالي تكلفة الموقع: {format_currency(total_cost)} ريال")
781
+
782
+ # خيارات تحسين الموقع
783
+ st.markdown("#### خيارات تحسين الموقع")
784
+
785
+ improvement_options = [
786
+ {"name": "تسوية الأرض وإزالة العوائق", "cost": 75000, "impact": 0.15},
787
+ {"name": "تحسين طرق الوصول", "cost": 120000, "impact": 0.2},
788
+ {"name": "بناء نظام صرف للمياه", "cost": 90000, "impact": 0.18},
789
+ {"name": "تعزيز البنية التحتية", "cost": 180000, "impact": 0.25},
790
+ {"name": "نظام حماية من العوامل الجوية", "cost": 60000, "impact": 0.12}
791
+ ]
792
+
793
+ # عرض خيارات التحسين
794
+ st.markdown("اختر خيارات التحسين لتقييم التأثير والتكلفة:")
795
+
796
+ selected_improvements = []
797
+ for i, option in enumerate(improvement_options):
798
+ if st.checkbox(f"{option['name']} - {format_currency(option['cost'])} ريال", key=f"imp_{i}"):
799
+ selected_improvements.append(option)
800
+
801
+ if selected_improvements:
802
+ # حساب التأثير والتكلفة الإجمالية
803
+ total_impact = sum(imp["impact"] for imp in selected_improvements)
804
+ total_improvement_cost = sum(imp["cost"] for imp in selected_improvements)
805
+
806
+ # عرض النتائج
807
+ st.markdown(f"##### تحسين التقييم المتوقع: +{total_impact:.2f}")
808
+ new_score = min(1.0, overall_score + total_impact)
809
+ st.markdown(f"##### التقييم الجديد المتوقع: {new_score:.2f}/1.0")
810
+ st.progress(new_score)
811
+
812
+ # تصنيف التقييم الجديد
813
+ if new_score >= 0.8:
814
+ new_rating = "ممتاز"
815
+ new_color = "green"
816
+ elif new_score >= 0.6:
817
+ new_rating = "جيد"
818
+ new_color = "blue"
819
+ elif new_score >= 0.4:
820
+ new_rating = "مقبول"
821
+ new_color = "orange"
822
+ else:
823
+ new_rating = "ضعيف"
824
+ new_color = "red"
825
+
826
+ st.markdown(f"<h5 style='color: {new_color};'>التصنيف الجديد المتوقع: {new_rating}</h5>", unsafe_allow_html=True)
827
+
828
+ # عرض التكلفة الإجمالية
829
+ st.markdown(f"##### تكلفة التحسينات: {format_currency(total_improvement_cost)} ريال")
830
+ else:
831
+ st.error("لم يتم العثور على المشروع المحدد.")
832
+ else: # مقارنة موقعين
833
+ # اختيار موقعين للمقارنة
834
+ col1, col2 = st.columns(2)
835
+
836
+ with col1:
837
+ project_id_1 = st.selectbox(
838
+ "الموقع الأول",
839
+ options=projects_df["project_id"].tolist(),
840
+ format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
841
+ key="compare_project_1"
842
+ )
843
+
844
+ with col2:
845
+ # استبعاد الموقع الأول من الخيارات
846
+ remaining_options = [pid for pid in projects_df["project_id"].tolist() if pid != project_id_1]
847
+
848
+ if remaining_options:
849
+ project_id_2 = st.selectbox(
850
+ "الموقع الثاني",
851
+ options=remaining_options,
852
+ format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
853
+ key="compare_project_2"
854
+ )
855
+ else:
856
+ st.warning("يجب أن يكون هناك موقعان على الأقل للمقارنة.")
857
+ return
858
+
859
+ # العثور على المشروعين المحددين
860
+ project_1 = next((p for p in st.session_state.project_locations if p["project_id"] == project_id_1), None)
861
+ project_2 = next((p for p in st.session_state.project_locations if p["project_id"] == project_id_2), None)
862
+
863
+ if project_1 and project_2:
864
+ # عرض عنوان المقارنة
865
+ st.markdown(f"### مقارنة بين موقعي {project_1['name']} و {project_2['name']}")
866
+
867
+ # عرض خريطة توضح الموقعين
868
+ st.markdown("#### الموقعان على الخريطة")
869
+
870
+ # حساب المركز والزوم المناسب
871
+ center_lat = (project_1["latitude"] + project_2["latitude"]) / 2
872
+ center_lon = (project_1["longitude"] + project_2["longitude"]) / 2
873
+
874
+ # حساب المسافة بين الموقعين
875
+ distance = self._calculate_distance(
876
+ project_1["latitude"], project_1["longitude"],
877
+ project_2["latitude"], project_2["longitude"]
878
+ )
879
+
880
+ # تحديد مستوى التكبير حسب المسافة
881
+ zoom_level = 12 if distance < 10 else (10 if distance < 50 else 8)
882
+
883
+ # إنشاء الخريطة
884
+ compare_map = folium.Map(
885
+ location=[center_lat, center_lon],
886
+ zoom_start=zoom_level,
887
+ attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
888
+ )
889
+
890
+ # إضافة العلامات للموقعين
891
+ folium.Marker(
892
+ location=[project_1["latitude"], project_1["longitude"]],
893
+ tooltip=project_1["name"],
894
+ icon=folium.Icon(color="blue", icon="info-sign")
895
+ ).add_to(compare_map)
896
+
897
+ folium.Marker(
898
+ location=[project_2["latitude"], project_2["longitude"]],
899
+ tooltip=project_2["name"],
900
+ icon=folium.Icon(color="red", icon="info-sign")
901
+ ).add_to(compare_map)
902
+
903
+ # إضافة خط يربط بين الموقعين
904
+ folium.PolyLine(
905
+ locations=[
906
+ [project_1["latitude"], project_1["longitude"]],
907
+ [project_2["latitude"], project_2["longitude"]]
908
+ ],
909
+ color="green",
910
+ weight=3,
911
+ opacity=0.7,
912
+ tooltip=f"المسافة: {distance:.2f} كم"
913
+ ).add_to(compare_map)
914
+
915
+ # عرض الخريطة
916
+ folium_static(compare_map, width=800, height=500)
917
+
918
+ # عرض المسافة بين الموقعين
919
+ st.markdown(f"#### المسافة بين الموقعين: {distance:.2f} كيلومتر")
920
+
921
+ # مقارنة معلومات الموقعين
922
+ st.markdown("#### مقارنة المعلومات الأساسية")
923
+
924
+ # إنشاء جدول المقارنة
925
+ comparison_data = {
926
+ "المعلومات": ["المدينة", "الحالة", "خط العرض", "خط الطول", "الوصف"],
927
+ project_1["name"]: [
928
+ project_1.get("city", ""),
929
+ project_1.get("status", ""),
930
+ f"{project_1['latitude']:.6f}",
931
+ f"{project_1['longitude']:.6f}",
932
+ project_1.get("description", "")
933
+ ],
934
+ project_2["name"]: [
935
+ project_2.get("city", ""),
936
+ project_2.get("status", ""),
937
+ f"{project_2['latitude']:.6f}",
938
+ f"{project_2['longitude']:.6f}",
939
+ project_2.get("description", "")
940
+ ]
941
+ }
942
+
943
+ comparison_df = pd.DataFrame(comparison_data)
944
+ st.dataframe(comparison_df, width=800)
945
+
946
+ # مقارنة العوامل البيئية والمكانية
947
+ st.markdown("#### مقارنة العوامل")
948
+
949
+ # بيانات افتراضية للعوامل - ستتغير هذه باستخدام بيانات حقيقية
950
+ factors_comparison = {
951
+ "العامل": ["قرب المدينة", "توفر المياه", "سهولة الوصول", "الظروف الجوية", "التضاريس", "توفر العمالة", "البنية التحتية", "المخاطر البيئية"],
952
+ project_1["name"]: [random.uniform(0.4, 1.0) for _ in range(8)],
953
+ project_2["name"]: [random.uniform(0.4, 1.0) for _ in range(8)]
954
+ }
955
+
956
+ # تحويل إلى DataFrame
957
+ factors_df = pd.DataFrame(factors_comparison)
958
+
959
+ # رسم بياني شريطي للمقارنة
960
+ st.bar_chart(factors_df.set_index("العامل"))
961
+
962
+ # حساب إجمالي التقييم لكل موقع
963
+ project_1_score = sum(factors_comparison[project_1["name"]]) / len(factors_comparison[project_1["name"]])
964
+ project_2_score = sum(factors_comparison[project_2["name"]]) / len(factors_comparison[project_2["name"]])
965
+
966
+ # عرض التقييم الإجمالي
967
+ col1, col2 = st.columns(2)
968
+
969
+ with col1:
970
+ st.markdown(f"##### تقييم {project_1['name']}: {project_1_score:.2f}/1.0")
971
+ st.progress(project_1_score)
972
+
973
+ with col2:
974
+ st.markdown(f"##### تقييم {project_2['name']}: {project_2_score:.2f}/1.0")
975
+ st.progress(project_2_score)
976
+
977
+ # تحديد الموقع المفضل
978
+ preferred_site = project_1["name"] if project_1_score > project_2_score else project_2["name"]
979
+ score_diff = abs(project_1_score - project_2_score)
980
+
981
+ if score_diff < 0.1:
982
+ recommendation = "الموقعان متقاربان في التقييم ويمكن اعتبارهما متكافئين."
983
+ color = "blue"
984
+ else:
985
+ recommendation = f"الموقع الأفضل هو: {preferred_site}"
986
+ color = "green"
987
+
988
+ st.markdown(f"<h4 style='color: {color};'>{recommendation}</h4>", unsafe_allow_html=True)
989
+
990
+ # تحليل التكلفة
991
+ st.markdown("#### مقارنة تقديرات التكلفة")
992
+
993
+ # بنود الت��لفة الافتراضية
994
+ cost_items = ["تسوية الأرض", "البنية التحتية", "النقل", "الحماية من المخاطر", "توصيل الخدمات"]
995
+
996
+ site_1_costs = [random.uniform(50000, 200000) for _ in range(len(cost_items))]
997
+ site_2_costs = [random.uniform(50000, 200000) for _ in range(len(cost_items))]
998
+
999
+ # إنشاء DataFrame للتكاليف
1000
+ cost_df = pd.DataFrame({
1001
+ "بند التكلفة": cost_items,
1002
+ f"{project_1['name']} (ريال)": site_1_costs,
1003
+ f"{project_2['name']} (ريال)": site_2_costs
1004
+ })
1005
+
1006
+ # عرض جدول التكاليف
1007
+ st.dataframe(cost_df, width=800)
1008
+
1009
+ # حساب إجمالي التكلفة لكل موقع
1010
+ total_cost_1 = sum(site_1_costs)
1011
+ total_cost_2 = sum(site_2_costs)
1012
+
1013
+ # عرض إجمالي التكلفة
1014
+ col1, col2 = st.columns(2)
1015
+
1016
+ with col1:
1017
+ st.metric(
1018
+ f"إجمالي تكلفة {project_1['name']}",
1019
+ f"{format_currency(total_cost_1)} ريال"
1020
+ )
1021
+
1022
+ with col2:
1023
+ st.metric(
1024
+ f"إجمالي تكلفة {project_2['name']}",
1025
+ f"{format_currency(total_cost_2)} ريال",
1026
+ f"{format_currency(total_cost_2 - total_cost_1)}"
1027
+ )
1028
+
1029
+ # تحليل إضافي للمقارنة
1030
+ st.markdown("#### ملخص المقارنة")
1031
+
1032
+ comparison_summary = f"""
1033
+ بناءً على التحليل المقدم، يمكن استخلاص الملاحظات التالية:
1034
+
1035
+ 1. **المسافة بين الموقعين:** {distance:.2f} كيلومتر.
1036
+ 2. **التقييم:** {project_1['name']} بتقييم {project_1_score:.2f}/1.0، و{project_2['name']} بتقييم {project_2_score:.2f}/1.0.
1037
+ 3. **التكلفة:** {project_1['name']} بتكلفة {format_currency(total_cost_1)} ريال، و{project_2['name']} بتكلفة {format_currency(total_cost_2)} ريال.
1038
+
1039
+ بالنظر إلى العوامل أعلاه، فإن الموقع **{preferred_site}** هو الخيار الأفضل من حيث التوازن بين التقييم والتكلفة.
1040
+ """
1041
+
1042
+ st.markdown(comparison_summary)
1043
+ else:
1044
+ st.error("لم يتم العثور على أحد المشروعين المحددين.")
1045
+
1046
+ def _render_location_management(self):
1047
+ """عرض إدارة المواقع"""
1048
+ st.markdown("""
1049
+ <div class='custom-box info-box'>
1050
+ <h3>📍 إدارة مواقع المشاريع</h3>
1051
+ <p>إضافة وتعديل مواقع المشاريع وتصدير واستيراد البيانات.</p>
1052
+ <p>يمكنك إدخال مواقع المشاريع الجديدة وتعديل المواقع الموجودة وحذفها.</p>
1053
+ </div>
1054
+ """, unsafe_allow_html=True)
1055
+
1056
+ # تبويبات فرعية للإدارة
1057
+ subtabs = st.tabs([
1058
+ "إضافة موقع جديد",
1059
+ "تحرير المواقع",
1060
+ "استيراد/تصدير المواقع"
1061
+ ])
1062
+
1063
+ # تبويب إضافة موقع جديد
1064
+ with subtabs[0]:
1065
+ self._render_add_location()
1066
+
1067
+ # تبويب تحرير المواقع
1068
+ with subtabs[1]:
1069
+ self._render_edit_locations()
1070
+
1071
+ # تبويب استيراد/تصدير المواقع
1072
+ with subtabs[2]:
1073
+ self._render_import_export_locations()
1074
+
1075
+ def _render_add_location(self):
1076
+ """عرض نموذج إضافة موقع جديد"""
1077
+ st.markdown("### إضافة موقع مشروع جديد")
1078
+
1079
+ # نموذج إضافة موقع جديد
1080
+ with st.form(key="add_location_form"):
1081
+ # معلومات أساسية
1082
+ project_name = st.text_input("اسم المشروع", key="new_project_name")
1083
+ project_description = st.text_area("وصف المشروع", key="new_project_description")
1084
+
1085
+ # معلومات الموقع
1086
+ col1, col2 = st.columns(2)
1087
+
1088
+ with col1:
1089
+ city = st.text_input("المدينة", key="new_city")
1090
+ status = st.selectbox(
1091
+ "حالة المشروع",
1092
+ options=["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"],
1093
+ key="new_status"
1094
+ )
1095
+
1096
+ with col2:
1097
+ latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="new_latitude")
1098
+ longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="new_longitude")
1099
+
1100
+ # عرض الموقع على خريطة صغيرة
1101
+ mini_map = folium.Map(
1102
+ location=[latitude, longitude],
1103
+ zoom_start=10,
1104
+ attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
1105
+ )
1106
+ folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map)
1107
+ folium_static(mini_map, width=700, height=300)
1108
+
1109
+ # زر الإضافة
1110
+ submit_button = st.form_submit_button("إضافة الموقع")
1111
+
1112
+ # معالجة النموذج عند الإرسال
1113
+ if submit_button:
1114
+ if not project_name:
1115
+ st.error("يرجى إدخال اسم المشروع.")
1116
+ else:
1117
+ # إنشاء معرف فريد للمشروع
1118
+ project_id = f"PRJ{len(st.session_state.project_locations) + 1:03d}"
1119
+
1120
+ # إضافة المشروع الجديد
1121
+ new_project = {
1122
+ "project_id": project_id,
1123
+ "name": project_name,
1124
+ "description": project_description,
1125
+ "city": city,
1126
+ "status": status,
1127
+ "latitude": latitude,
1128
+ "longitude": longitude
1129
+ }
1130
+
1131
+ # إضافة المشروع إلى القائمة
1132
+ st.session_state.project_locations.append(new_project)
1133
+
1134
+ # حفظ البيانات
1135
+ self._save_locations_data()
1136
+
1137
+ # عرض رسالة نجاح
1138
+ st.success(f"تمت إضافة موقع المشروع '{project_name}' بنجاح.")
1139
+
1140
+ # إعادة تحميل الصفحة
1141
+ st.rerun()
1142
+
1143
+ def _render_edit_locations(self):
1144
+ """عرض واجهة تحرير المواقع الموجودة"""
1145
+ st.markdown("### تحرير مواقع المشاريع")
1146
+
1147
+ if len(st.session_state.project_locations) == 0:
1148
+ st.warning("لا توجد مواقع مشاريع للتحرير. يرجى إضافة مواقع أولاً.")
1149
+ return
1150
+
1151
+ # عرض قائمة المشاريع
1152
+ projects_df = pd.DataFrame(st.session_state.project_locations)
1153
+
1154
+ # إعادة تسمية الأعمدة بالعربية
1155
+ renamed_columns = {
1156
+ "name": "اسم المشروع",
1157
+ "city": "المدينة",
1158
+ "status": "الحالة",
1159
+ "description": "الوصف",
1160
+ "project_id": "معرف المشروع",
1161
+ "latitude": "خط العرض",
1162
+ "longitude": "خط الطول"
1163
+ }
1164
+
1165
+ # تحديد الأعمدة للعرض
1166
+ display_columns = ["project_id", "name", "city", "status"]
1167
+
1168
+ # إنشاء جدول للعرض
1169
+ display_df = projects_df[display_columns].rename(columns=renamed_columns)
1170
+
1171
+ # عرض الجدول
1172
+ st.dataframe(display_df, width=800, height=300)
1173
+
1174
+ # اختيار مشروع للتحرير
1175
+ selected_project_id = st.selectbox(
1176
+ "اختر مشروعًا للتحرير",
1177
+ options=projects_df["project_id"].tolist(),
1178
+ format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
1179
+ key="edit_project_id"
1180
+ )
1181
+
1182
+ # العثور على المشروع المحدد
1183
+ selected_project_index = next((i for i, p in enumerate(st.session_state.project_locations) if p["project_id"] == selected_project_id), None)
1184
+
1185
+ if selected_project_index is not None:
1186
+ selected_project = st.session_state.project_locations[selected_project_index]
1187
+
1188
+ # نموذج تحرير المشروع
1189
+ with st.form(key="edit_location_form"):
1190
+ st.markdown(f"### تحرير مشروع: {selected_project['name']}")
1191
+
1192
+ # معلومات أساسية
1193
+ project_name = st.text_input("اسم المشروع", value=selected_project["name"], key="edit_project_name")
1194
+ project_description = st.text_area("وصف المشروع", value=selected_project.get("description", ""), key="edit_project_description")
1195
+
1196
+ # معلومات الموقع
1197
+ col1, col2 = st.columns(2)
1198
+
1199
+ with col1:
1200
+ city = st.text_input("المدينة", value=selected_project.get("city", ""), key="edit_city")
1201
+ status = st.selectbox(
1202
+ "حالة المشروع",
1203
+ options=["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"],
1204
+ index=["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"].index(selected_project.get("status", "مخطط")),
1205
+ key="edit_status"
1206
+ )
1207
+
1208
+ with col2:
1209
+ latitude = st.number_input("خط العرض", value=selected_project["latitude"], step=0.0001, format="%.6f", key="edit_latitude")
1210
+ longitude = st.number_input("خط الطول", value=selected_project["longitude"], step=0.0001, format="%.6f", key="edit_longitude")
1211
+
1212
+ # عرض الموقع على خريطة صغيرة
1213
+ mini_map = folium.Map(
1214
+ location=[latitude, longitude],
1215
+ zoom_start=10,
1216
+ attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
1217
+ )
1218
+ folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map)
1219
+ folium_static(mini_map, width=700, height=300)
1220
+
1221
+ # أزرار الإجراءات
1222
+ col1, col2 = st.columns(2)
1223
+
1224
+ with col1:
1225
+ update_button = st.form_submit_button("تحديث المعلومات")
1226
+
1227
+ with col2:
1228
+ delete_button = st.form_submit_button("حذف المشروع", type="secondary")
1229
+
1230
+ # معالجة تحديث المعلومات
1231
+ if update_button:
1232
+ if not project_name:
1233
+ st.error("لا يمكن ترك اسم المشروع فارغًا.")
1234
+ else:
1235
+ # تحديث معلومات المشروع
1236
+ st.session_state.project_locations[selected_project_index] = {
1237
+ "project_id": selected_project["project_id"],
1238
+ "name": project_name,
1239
+ "description": project_description,
1240
+ "city": city,
1241
+ "status": status,
1242
+ "latitude": latitude,
1243
+ "longitude": longitude
1244
+ }
1245
+
1246
+ # حفظ البيانات
1247
+ self._save_locations_data()
1248
+
1249
+ # عرض رسالة نجاح
1250
+ st.success(f"تم تحديث معلومات المشروع '{project_name}' بنجاح.")
1251
+
1252
+ # إعادة تحميل الصفحة
1253
+ st.rerun()
1254
+
1255
+ # معالجة حذف المشروع
1256
+ if delete_button:
1257
+ # نافذة تأكيد الحذف
1258
+ st.warning(f"هل أنت متأكد من رغبتك في حذف المشروع '{selected_project['name']}'؟")
1259
+
1260
+ confirm_col1, confirm_col2 = st.columns(2)
1261
+
1262
+ with confirm_col1:
1263
+ if st.button("نعم، حذف المشروع", key="confirm_delete"):
1264
+ # حذف المشروع
1265
+ st.session_state.project_locations.pop(selected_project_index)
1266
+
1267
+ # حفظ البيانات
1268
+ self._save_locations_data()
1269
+
1270
+ # عرض رسالة نجاح
1271
+ st.success(f"تم حذف المشروع '{selected_project['name']}' بنجاح.")
1272
+
1273
+ # إعادة تحميل الصفحة
1274
+ st.rerun()
1275
+
1276
+ with confirm_col2:
1277
+ if st.button("لا، إلغاء الحذف", key="cancel_delete"):
1278
+ st.rerun()
1279
+ else:
1280
+ st.error("لم يتم العثور على المشروع المحدد.")
1281
+
1282
+ def _render_import_export_locations(self):
1283
+ """عرض واجهة استيراد وتصدير المواقع"""
1284
+ st.markdown("### استيراد وتصدير مواقع المشاريع")
1285
+
1286
+ # تبويبات فرعية للاستيراد والتصدير
1287
+ export_tab, import_tab = st.tabs(["تصدير المواقع", "استيراد المواقع"])
1288
+
1289
+ # تبويب تصدير المواقع
1290
+ with export_tab:
1291
+ st.markdown("#### تصدير مواقع المشاريع")
1292
+
1293
+ if len(st.session_state.project_locations) == 0:
1294
+ st.warning("لا توجد مواقع مشاريع للتصدير.")
1295
+ else:
1296
+ # اختيار تنسيق التصدير
1297
+ export_format = st.radio(
1298
+ "اختر تنسيق التصدير",
1299
+ options=["CSV", "Excel", "JSON"],
1300
+ horizontal=True,
1301
+ key="export_format"
1302
+ )
1303
+
1304
+ # زر التصدير
1305
+ if styled_button("تصدير المواقع", key="export_btn", type="primary", icon="📤"):
1306
+ # تصدير البيانات
1307
+ exported_data = self._export_locations(export_format.lower())
1308
+
1309
+ if exported_data:
1310
+ # تحديد نوع الملف ومعلومات التنزيل
1311
+ if export_format == "CSV":
1312
+ mime_type = "text/csv"
1313
+ file_ext = "csv"
1314
+ elif export_format == "Excel":
1315
+ mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
1316
+ file_ext = "xlsx"
1317
+ else: # JSON
1318
+ mime_type = "application/json"
1319
+ file_ext = "json"
1320
+
1321
+ # إنشاء رابط التنزيل
1322
+ b64 = base64.b64encode(exported_data).decode()
1323
+ href = f'<a href="data:{mime_type};base64,{b64}" download="project_locations.{file_ext}" class="btn">تنزيل ملف {export_format}</a>'
1324
+ st.markdown(href, unsafe_allow_html=True)
1325
+
1326
+ # عرض معاينة البيانات
1327
+ if export_format == "CSV":
1328
+ st.markdown("#### معاينة البيانات المصدرة")
1329
+ st.text(exported_data.decode("utf-8"))
1330
+ elif export_format == "JSON":
1331
+ st.markdown("#### معاينة البيانات المصدرة")
1332
+ st.json(json.loads(exported_data.decode("utf-8")))
1333
+
1334
+ # تبويب استيراد المواقع
1335
+ with import_tab:
1336
+ st.markdown("#### استيراد مواقع المشاريع")
1337
+
1338
+ # اختيار تنسيق الاستيراد
1339
+ import_format = st.radio(
1340
+ "اختر تنسيق الاستيراد",
1341
+ options=["CSV", "Excel", "JSON"],
1342
+ horizontal=True,
1343
+ key="import_format"
1344
+ )
1345
+
1346
+ # تحميل الملف
1347
+ uploaded_file = st.file_uploader(f"تحميل ملف {import_format}", type=[import_format.lower()])
1348
+
1349
+ if uploaded_file:
1350
+ # معاينة الملف
1351
+ st.markdown("#### معاينة الملف المحمل")
1352
+
1353
+ if import_format == "CSV":
1354
+ df = pd.read_csv(uploaded_file)
1355
+ st.dataframe(df)
1356
+ elif import_format == "Excel":
1357
+ df = pd.read_excel(uploaded_file)
1358
+ st.dataframe(df)
1359
+ else: # JSON
1360
+ json_data = json.load(uploaded_file)
1361
+ st.json(json_data)
1362
+
1363
+ # خيارات الاستيراد
1364
+ import_mode = st.radio(
1365
+ "طريقة الاستيراد",
1366
+ options=["إضافة إلى المواقع الحالية", "استبدال جميع المواقع"],
1367
+ key="import_mode"
1368
+ )
1369
+
1370
+ # زر الاستيراد
1371
+ if styled_button("استيراد المواقع", key="import_btn", type="primary", icon="📥"):
1372
+ # إعادة قراءة الملف (قد يكون تم استنفاد التدفق)
1373
+ uploaded_file.seek(0)
1374
+
1375
+ try:
1376
+ # استيراد البيانات
1377
+ imported_count = self._import_locations(uploaded_file, import_format.lower())
1378
+
1379
+ if import_mode == "استبدال جميع المواقع":
1380
+ st.success(f"تم استبدال جميع المواقع بنجاح. عدد المواقع الجديدة: {imported_count}")
1381
+ else:
1382
+ st.success(f"تمت إضافة {imported_count} مواقع جديدة بنجاح.")
1383
+
1384
+ # إعادة تحميل الصفحة
1385
+ st.rerun()
1386
+ except Exception as e:
1387
+ st.error(f"حدث خطأ أثناء استيراد البيانات: {str(e)}")
1388
+
1389
+ def _fetch_terrain_data(self, latitude, longitude, radius_km=5):
1390
+ """جلب بيانات التضاريس من واجهة برمجة التطبيقات"""
1391
+ # حساب نطاق الإحداثيات
1392
+ # 1 درجة تقريبًا = 111 كم
1393
+ delta = radius_km / 111.0
1394
+
1395
+ # إنشاء شبكة نقاط
1396
+ lat_min, lat_max = latitude - delta, latitude + delta
1397
+ lon_min, lon_max = longitude - delta, longitude + delta
1398
+
1399
+ # عدد نقاط الشبكة
1400
+ grid_size = 20
1401
+
1402
+ # إنشاء شبكة إحداثيات
1403
+ lats = np.linspace(lat_min, lat_max, grid_size)
1404
+ lons = np.linspace(lon_min, lon_max, grid_size)
1405
+
1406
+ # تهيئة مصفوفة النتائج
1407
+ results = []
1408
+
1409
+ # بناء سلسلة الإحداثيات للطلب
1410
+ locations = []
1411
+ for lat in lats:
1412
+ for lon in lons:
1413
+ locations.append(f"{lat:.6f},{lon:.6f}")
1414
+
1415
+ # تقسيم الطلبات إلى مجموعات (واجهة البرمجة تقبل 100 نقطة كحد أقصى)
1416
+ batch_size = 100
1417
+ for i in range(0, len(locations), batch_size):
1418
+ batch = locations[i:i+batch_size]
1419
+
1420
+ # محاولة استخدام خدمة OpenTopoData
1421
+ try:
1422
+ url = f"{self.opentopodata_api}?locations={'|'.join(batch)}"
1423
+ response = requests.get(url)
1424
+
1425
+ if response.status_code == 200:
1426
+ data = response.json()
1427
+ if "results" in data:
1428
+ for result in data["results"]:
1429
+ if "elevation" in result:
1430
+ results.append({
1431
+ "latitude": result["location"]["lat"],
1432
+ "longitude": result["location"]["lng"],
1433
+ "elevation": result["elevation"]
1434
+ })
1435
+ else:
1436
+ # استخدام بيانات افتراضية في حالة فشل الطلب
1437
+ st.warning(f"فشل جلب بيانات التضاريس من الخدمة (رمز الحالة: {response.status_code}). استخدام بيانات افتراضية.")
1438
+
1439
+ # إنشاء بيانات افتراضية
1440
+ for j, loc in enumerate(batch):
1441
+ lat, lon = map(float, loc.split(","))
1442
+ # حساب ارتفاع افتراضي بناءً على المسافة من المركز
1443
+ dist = self._calculate_distance(latitude, longitude, lat, lon)
1444
+ # إضافة ضوضاء عشوائية للحصول على تضاريس أكثر واقعية
1445
+ noise = np.sin(lat * 10) * np.cos(lon * 10) * 50
1446
+ elevation = 500 - dist * 100 + noise
1447
+
1448
+ results.append({
1449
+ "latitude": lat,
1450
+ "longitude": lon,
1451
+ "elevation": max(0, elevation)
1452
+ })
1453
+ except Exception as e:
1454
+ st.warning(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}. استخدام بيانات افتراضية.")
1455
+
1456
+ # إنشاء بيانات افتراضية
1457
+ for j, loc in enumerate(batch):
1458
+ lat, lon = map(float, loc.split(","))
1459
+ # حساب ارتفاع افتراضي بناءً على المسافة من المركز
1460
+ dist = self._calculate_distance(latitude, longitude, lat, lon)
1461
+ # إضافة ضوضاء عشوائية للحصول على تضاريس أكثر واقعية
1462
+ noise = np.sin(lat * 10) * np.cos(lon * 10) * 50
1463
+ elevation = 500 - dist * 100 + noise
1464
+
1465
+ results.append({
1466
+ "latitude": lat,
1467
+ "longitude": lon,
1468
+ "elevation": max(0, elevation)
1469
+ })
1470
+
1471
+ return results
1472
+
1473
+ def _calculate_distance(self, lat1, lon1, lat2, lon2):
1474
+ """حساب المسافة بين نقطتين بالكيلومترات باستخدام صيغة هافرساين"""
1475
+ from math import radians, sin, cos, sqrt, atan2
1476
+
1477
+ # تحويل الإحداثيات إلى راديان
1478
+ lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
1479
+
1480
+ # صيغة هافرساين
1481
+ dlon = lon2 - lon1
1482
+ dlat = lat2 - lat1
1483
+ a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
1484
+ c = 2 * atan2(sqrt(a), sqrt(1-a))
1485
+ distance = 6371 * c # نصف قطر الأرض بالكيلومترات
1486
+
1487
+ return distance
1488
+
1489
+ def _get_color_map(self, scheme):
1490
+ """الحصول على خريطة الألوان حسب النظام المختار"""
1491
+ import matplotlib.cm as cm
1492
+ import matplotlib.colors as colors
1493
+
1494
+ # الحصو�� على خريطة الألوان
1495
+ colormap = cm.get_cmap(scheme)
1496
+
1497
+ # إرجاع دالة لتطبيق خريطة الألوان
1498
+ return lambda x: colors.rgb2hex(colormap(x))
1499
+
1500
+ def _export_locations(self, format):
1501
+ """تصدير مواقع المشاريع إلى ملف"""
1502
+ try:
1503
+ # تحويل البيانات إلى DataFrame
1504
+ df = pd.DataFrame(st.session_state.project_locations)
1505
+
1506
+ # تصدير البيانات حسب التنسيق المطلوب
1507
+ if format == "csv":
1508
+ csv_data = df.to_csv(index=False).encode("utf-8")
1509
+ return csv_data
1510
+ elif format == "excel":
1511
+ # إنشاء ملف إكسل مؤقت
1512
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as temp:
1513
+ df.to_excel(temp.name, index=False, engine="xlsxwriter")
1514
+ temp.flush()
1515
+
1516
+ # قراءة الملف كبيانات ثنائية
1517
+ with open(temp.name, "rb") as f:
1518
+ excel_data = f.read()
1519
+
1520
+ # حذف الملف المؤقت
1521
+ os.unlink(temp.name)
1522
+
1523
+ return excel_data
1524
+ elif format == "json":
1525
+ json_data = json.dumps(st.session_state.project_locations, ensure_ascii=False, indent=4).encode("utf-8")
1526
+ return json_data
1527
+ else:
1528
+ st.error(f"تنسيق غير مدعوم: {format}")
1529
+ return None
1530
+ except Exception as e:
1531
+ st.error(f"حدث خطأ أثناء تصدير البيانات: {str(e)}")
1532
+ return None
1533
+
1534
+ def _import_locations(self, uploaded_file, format):
1535
+ """استيراد مواقع المشاريع من ملف"""
1536
+ try:
1537
+ imported_data = []
1538
+
1539
+ # تحميل البيانات حسب التنسيق
1540
+ if format == "csv":
1541
+ df = pd.read_csv(uploaded_file)
1542
+ imported_data = df.to_dict("records")
1543
+ elif format == "excel":
1544
+ df = pd.read_excel(uploaded_file)
1545
+ imported_data = df.to_dict("records")
1546
+ elif format == "json":
1547
+ imported_data = json.load(uploaded_file)
1548
+ else:
1549
+ raise ValueError(f"تنسيق غير مدعوم: {format}")
1550
+
1551
+ # التحقق من صحة البيانات
1552
+ required_fields = ["project_id", "name", "latitude", "longitude"]
1553
+
1554
+ for item in imported_data:
1555
+ missing_fields = [field for field in required_fields if field not in item]
1556
+
1557
+ if missing_fields:
1558
+ raise ValueError(f"الحقول المطلوبة مفقودة: {', '.join(missing_fields)}")
1559
+
1560
+ # تحديث البيانات
1561
+ if "import_mode" in st.session_state and st.session_state.import_mode == "استبدال جميع المواقع":
1562
+ # استبدال جميع البيانات
1563
+ st.session_state.project_locations = imported_data
1564
+ else:
1565
+ # إضافة البيانات الجديدة فقط
1566
+ existing_ids = {p["project_id"] for p in st.session_state.project_locations}
1567
+ new_items = [item for item in imported_data if item["project_id"] not in existing_ids]
1568
+ st.session_state.project_locations.extend(new_items)
1569
+ imported_data = new_items
1570
+
1571
+ # حفظ البيانات
1572
+ self._save_locations_data()
1573
+
1574
+ return len(imported_data)
1575
+ except Exception as e:
1576
+ raise Exception(f"حدث خطأ أثناء استيراد البيانات: {str(e)}")
1577
+
1578
+ def _save_locations_data(self):
1579
+ """حفظ بيانات المواقع"""
1580
+ try:
1581
+ # إنشاء مسار الملف
1582
+ file_path = os.path.join(self.data_dir, "project_locations.json")
1583
+
1584
+ # حفظ البيانات كملف JSON
1585
+ with open(file_path, "w", encoding="utf-8") as f:
1586
+ json.dump(st.session_state.project_locations, f, ensure_ascii=False, indent=4)
1587
+ except Exception as e:
1588
+ st.error(f"حدث خطأ أثناء حفظ بيانات المواقع: {str(e)}")
1589
+
1590
+ def _load_locations_data(self):
1591
+ """تحميل بيانات المواقع"""
1592
+ try:
1593
+ # إنشاء مسار الملف
1594
+ file_path = os.path.join(self.data_dir, "project_locations.json")
1595
+
1596
+ # التحقق من وجود الملف
1597
+ if os.path.exists(file_path):
1598
+ # تحميل البيانات من ملف JSON
1599
+ with open(file_path, "r", encoding="utf-8") as f:
1600
+ st.session_state.project_locations = json.load(f)
1601
+ else:
1602
+ # تهيئة بيانات اختبارية
1603
+ self._initialize_sample_projects()
1604
+ except Exception as e:
1605
+ st.error(f"حدث خطأ أثناء تحميل بيانات المواقع: {str(e)}")
1606
+ # تهيئة بيانات اختبارية
1607
+ self._initialize_sample_projects()
1608
+
1609
+ def _initialize_sample_projects(self):
1610
+ """تهيئة بيانات اختبارية للمشاريع"""
1611
+ # قائمة بأسماء مدن المملكة العربية السعودية
1612
+ saudi_cities = [
1613
+ {"name": "الرياض", "lat": 24.7136, "lon": 46.6753},
1614
+ {"name": "جدة", "lat": 21.4858, "lon": 39.1925},
1615
+ {"name": "مكة المكرمة", "lat": 21.3891, "lon": 39.8579},
1616
+ {"name": "المدينة المنورة", "lat": 24.5247, "lon": 39.5692},
1617
+ {"name": "الدمام", "lat": 26.4207, "lon": 50.0888},
1618
+ {"name": "الطائف", "lat": 21.2704, "lon": 40.4157},
1619
+ {"name": "تبوك", "lat": 28.3835, "lon": 36.5662},
1620
+ {"name": "بريدة", "lat": 26.3267, "lon": 43.9717},
1621
+ {"name": "الخبر", "lat": 26.2172, "lon": 50.1971},
1622
+ {"name": "أبها", "lat": 18.2164, "lon": 42.5053}
1623
+ ]
1624
+
1625
+ # قائمة بأنواع المشاريع
1626
+ project_types = [
1627
+ "إنشاء مبنى سكني",
1628
+ "تطوير طريق سريع",
1629
+ "بناء جسر",
1630
+ "إنشاء مدرسة",
1631
+ "تطوير حديقة عامة",
1632
+ "بناء مستشفى",
1633
+ "إنشاء محطة تحلية مياه",
1634
+ "تطوير مركز تجاري",
1635
+ "بناء مصنع",
1636
+ "توسعة مطار"
1637
+ ]
1638
+
1639
+ # قائمة بحالات المشاريع
1640
+ project_statuses = ["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"]
1641
+
1642
+ # إنشاء مشاريع اختبارية
1643
+ sample_projects = []
1644
+
1645
+ for i in range(10):
1646
+ city = saudi_cities[i]
1647
+
1648
+ # إضافة اختلاف عشوائي صغير للإحداثيات
1649
+ lat_offset = random.uniform(-0.05, 0.05)
1650
+ lon_offset = random.uniform(-0.05, 0.05)
1651
+
1652
+ project = {
1653
+ "project_id": f"PRJ{i+1:03d}",
1654
+ "name": f"{project_types[i]} في {city['name']}",
1655
+ "description": f"مشروع {project_types[i]} بمدينة {city['name']}. هذا وصف اختباري للمشروع يوضح تفاصيله وأهدافه ونطاق العمل.",
1656
+ "city": city["name"],
1657
+ "status": random.choice(project_statuses),
1658
+ "latitude": city["lat"] + lat_offset,
1659
+ "longitude": city["lon"] + lon_offset
1660
+ }
1661
+
1662
+ sample_projects.append(project)
1663
+
1664
+ # حفظ المشاريع الاختبارية في حالة الجلسة
1665
+ st.session_state.project_locations = sample_projects
1666
+
1667
+
1668
+ if __name__ == "__main__":
1669
+ """تشغيل وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد بشكل مستقل"""
1670
+ interactive_map = InteractiveMap()
1671
+ interactive_map.render()
modules/maps/interactive_map.py.bak ADDED
@@ -0,0 +1,1647 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
6
+ تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import streamlit as st
12
+ import pandas as pd
13
+ import numpy as np
14
+ import pydeck as pdk
15
+ import folium
16
+ from folium.plugins import MarkerCluster, HeatMap, MeasureControl
17
+ from streamlit_folium import folium_static
18
+ import requests
19
+ import json
20
+ import random
21
+ from typing import List, Dict, Any, Tuple, Optional
22
+ import tempfile
23
+ import base64
24
+ from PIL import Image
25
+ from io import BytesIO
26
+
27
+ # إضافة مسار النظام للوصول للملفات المشتركة
28
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
29
+
30
+ # استيراد مكونات واجهة المستخدم
31
+ from utils.components.header import render_header
32
+ from utils.components.credits import render_credits
33
+ from utils.helpers import format_number, format_currency, styled_button
34
+
35
+
36
+ class InteractiveMap:
37
+ """فئة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد"""
38
+
39
+ def __init__(self):
40
+ """تهيئة وحدة الخريطة التفاعلية"""
41
+ # تهيئة مجلدات حفظ البيانات
42
+ self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/maps"))
43
+ os.makedirs(self.data_dir, exist_ok=True)
44
+
45
+ # مفاتيح API لخدمات الخرائط
46
+ self.mapbox_token = os.environ.get("MAPBOX_TOKEN", "")
47
+ self.opentopodata_api = "https://api.opentopodata.org/v1/srtm30m"
48
+
49
+ # تهيئة حالة الجلسة
50
+ if 'project_locations' not in st.session_state:
51
+ st.session_state.project_locations = []
52
+
53
+ if 'selected_location' not in st.session_state:
54
+ st.session_state.selected_location = None
55
+
56
+ if 'terrain_data' not in st.session_state:
57
+ st.session_state.terrain_data = None
58
+
59
+ # بيانات اختبارية للمشاريع (سيتم استبدالها بالبيانات الفعلية من قاعدة البيانات)
60
+ self._initialize_sample_projects()
61
+
62
+ def render(self):
63
+ """عرض واجهة وحدة الخريطة التفاعلية"""
64
+ # عرض الشعار والعنوان الرئيسي
65
+ render_header("خريطة مواقع المشاريع التفاعلية")
66
+
67
+ # تبويبات الوحدة
68
+ tabs = st.tabs([
69
+ "الخريطة التفاعلية",
70
+ "عرض التضاريس ثلاثي الأبعاد",
71
+ "تحليل المواقع",
72
+ "إدارة المواقع"
73
+ ])
74
+
75
+ # تبويب الخريطة التفاعلية
76
+ with tabs[0]:
77
+ self._render_interactive_map()
78
+
79
+ # تبويب عرض التضاريس ثلاثي الأبعاد
80
+ with tabs[1]:
81
+ self._render_3d_terrain()
82
+
83
+ # تبويب تحليل المواقع
84
+ with tabs[2]:
85
+ self._render_location_analysis()
86
+
87
+ # تبويب إدارة المواقع
88
+ with tabs[3]:
89
+ self._render_location_management()
90
+
91
+ # عرض حقوق النشر
92
+ render_credits()
93
+
94
+ def _render_interactive_map(self):
95
+ """عرض الخريطة التفاعلية"""
96
+ st.markdown("""
97
+ <div class='custom-box info-box'>
98
+ <h3>🗺️ الخريطة التفاعلية لمواقع المشاريع</h3>
99
+ <p>خريطة تفاعلية تعرض مواقع جميع المشاريع مع إمكانية التكبير والتصغير والتحرك.</p>
100
+ <p>يمكنك النقر على أي موقع للحصول على تفاصيل المشروع.</p>
101
+ </div>
102
+ """, unsafe_allow_html=True)
103
+
104
+ # مربع البحث
105
+ search_query = st.text_input("البحث عن موقع أو مشروع", key="map_search")
106
+
107
+ # أزرار تحكم للخريطة
108
+ col1, col2, col3, col4 = st.columns(4)
109
+
110
+ with col1:
111
+ map_style = st.selectbox(
112
+ "نمط الخريطة",
113
+ options=["OpenStreetMap", "Stamen Terrain", "Stamen Toner", "CartoDB Positron"],
114
+ key="map_style"
115
+ )
116
+
117
+ with col2:
118
+ cluster_markers = st.checkbox("تجميع المواقع القريبة", value=True, key="cluster_markers")
119
+
120
+ with col3:
121
+ show_heatmap = st.checkbox("عرض خريطة حرارية للمواقع", value=False, key="show_heatmap")
122
+
123
+ with col4:
124
+ show_measurements = st.checkbox("أدوات القياس", value=False, key="show_measurements")
125
+
126
+ # إنشاء الخريطة
127
+ if len(st.session_state.project_locations) > 0:
128
+ # بيانات النقاط على الخريطة
129
+ locations = []
130
+
131
+ # تصفية المشاريع حسب البحث
132
+ filtered_projects = st.session_state.project_locations
133
+ if search_query:
134
+ filtered_projects = [
135
+ p for p in filtered_projects
136
+ if search_query.lower() in p.get("name", "").lower() or
137
+ search_query.lower() in p.get("description", "").lower() or
138
+ search_query.lower() in p.get("city", "").lower()
139
+ ]
140
+
141
+ # عرض عدد النتائج
142
+ if search_query:
143
+ st.markdown(f"عدد النتائج: {len(filtered_projects)}")
144
+
145
+ # تحضير البيانات للخريطة
146
+ heat_data = []
147
+ for project in filtered_projects:
148
+ locations.append({
149
+ "lat": project.get("latitude"),
150
+ "lon": project.get("longitude"),
151
+ "name": project.get("name"),
152
+ "description": project.get("description"),
153
+ "city": project.get("city"),
154
+ "status": project.get("status"),
155
+ "project_id": project.get("project_id")
156
+ })
157
+ heat_data.append([project.get("latitude"), project.get("longitude"), 1])
158
+
159
+ # تعيين نقطة المركز والتكبير
160
+ if filtered_projects:
161
+ center_lat = sum(p.get("latitude", 0) for p in filtered_projects) / len(filtered_projects)
162
+ center_lon = sum(p.get("longitude", 0) for p in filtered_projects) / len(filtered_projects)
163
+ zoom_level = 6 # مستوى التكبير الافتراضي
164
+ else:
165
+ # مركز المملكة العربية السعودية
166
+ center_lat = 24.7136
167
+ center_lon = 46.6753
168
+ zoom_level = 5
169
+
170
+ # تحديد الإسناد (attribution) بناءً على نمط الخريطة
171
+ attribution = None
172
+ if map_style == "OpenStreetMap":
173
+ attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
174
+ elif map_style.startswith("Stamen"):
175
+ attribution = 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
176
+ elif map_style == "CartoDB Positron":
177
+ attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="https://cartodb.com/attributions">CartoDB</a>'
178
+
179
+ # إنشاء الخريطة
180
+ m = folium.Map(
181
+ location=[center_lat, center_lon],
182
+ zoom_start=zoom_level,
183
+ tiles=map_style,
184
+ attr=attribution # إضافة سمة الإسناد
185
+ )
186
+
187
+ # إضافة أدوات القياس إذا تم اختيارها
188
+ if show_measurements:
189
+ MeasureControl(position='topright', primary_length_unit='kilometers').add_to(m)
190
+
191
+ # إضافة النقاط إلى الخريطة
192
+ if cluster_markers:
193
+ # إنشاء مجموعة تجميع
194
+ marker_cluster = MarkerCluster(name="تجميع المشاريع").add_to(m)
195
+
196
+ # إضافة النقاط إلى المجموعة
197
+ for location in locations:
198
+ # إنشاء النافذة المنبثقة
199
+ popup_html = f"""
200
+ <div style='direction: rtl; text-align: right;'>
201
+ <h4>{location['name']}</h4>
202
+ <p><strong>الوصف:</strong> {location['description']}</p>
203
+ <p><strong>المدينة:</strong> {location['city']}</p>
204
+ <p><strong>الحالة:</strong> {location['status']}</p>
205
+ <p><strong>الإحداثيات:</strong> {location['lat']:.6f}, {location['lon']:.6f}</p>
206
+ <button onclick="window.open('/project/{location['project_id']}', '_self')">عرض تفاصيل المشروع</button>
207
+ </div>
208
+ """
209
+
210
+ # تحديد لون العلامة حسب حالة المشروع
211
+ icon_color = 'green'
212
+ if location['status'] == 'قيد التنفيذ':
213
+ icon_color = 'orange'
214
+ elif location['status'] == 'متوقف':
215
+ icon_color = 'red'
216
+ elif location['status'] == 'مكتمل':
217
+ icon_color = 'blue'
218
+
219
+ # إضافة العلامة
220
+ folium.Marker(
221
+ location=[location['lat'], location['lon']],
222
+ popup=folium.Popup(popup_html, max_width=300),
223
+ tooltip=location['name'],
224
+ icon=folium.Icon(color=icon_color, icon='info-sign')
225
+ ).add_to(marker_cluster)
226
+ else:
227
+ # إضافة النقاط مباشرة إلى الخريطة
228
+ for location in locations:
229
+ # إنشاء النافذة المنبثقة
230
+ popup_html = f"""
231
+ <div style='direction: rtl; text-align: right;'>
232
+ <h4>{location['name']}</h4>
233
+ <p><strong>الوصف:</strong> {location['description']}</p>
234
+ <p><strong>المدينة:</strong> {location['city']}</p>
235
+ <p><strong>الحالة:</strong> {location['status']}</p>
236
+ <p><strong>الإحداثيات:</strong> {location['lat']:.6f}, {location['lon']:.6f}</p>
237
+ <button onclick="window.open('/project/{location['project_id']}', '_self')">عرض تفاصيل المشروع</button>
238
+ </div>
239
+ """
240
+
241
+ # تحديد لون العلامة حسب حالة المشروع
242
+ icon_color = 'green'
243
+ if location['status'] == 'قيد التنفيذ':
244
+ icon_color = 'orange'
245
+ elif location['status'] == 'متوقف':
246
+ icon_color = 'red'
247
+ elif location['status'] == 'مكتمل':
248
+ icon_color = 'blue'
249
+
250
+ # إضافة العلامة
251
+ folium.Marker(
252
+ location=[location['lat'], location['lon']],
253
+ popup=folium.Popup(popup_html, max_width=300),
254
+ tooltip=location['name'],
255
+ icon=folium.Icon(color=icon_color, icon='info-sign')
256
+ ).add_to(m)
257
+
258
+ # إضافة خريطة حرارية إذا تم اختيارها
259
+ if show_heatmap and heat_data:
260
+ HeatMap(heat_data, radius=15).add_to(m)
261
+
262
+ # إضافة طبقات متنوعة للخريطة
263
+ folium.TileLayer('OpenStreetMap').add_to(m)
264
+ folium.TileLayer('Stamen Terrain').add_to(m)
265
+ folium.TileLayer('Stamen Toner').add_to(m)
266
+ folium.TileLayer('CartoDB positron').add_to(m)
267
+ folium.TileLayer('CartoDB dark_matter').add_to(m)
268
+
269
+ # إضافة أدوات التحكم بالطبقات
270
+ folium.LayerControl().add_to(m)
271
+
272
+ # عرض الخريطة
273
+ st_map = folium_static(m, width=1000, height=600)
274
+
275
+ # التفاعل مع النقر على الخريطة (سيتم تنفيذه عندما يتم دعم التفاعل)
276
+ # حاليًا لا يوجد دعم مباشر للتفاعل مع الخريطة في Streamlit
277
+
278
+ # عرض بيانات المشاريع في جدول
279
+ st.markdown("### قائمة المشاريع على الخريطة")
280
+
281
+ projects_df = pd.DataFrame(filtered_projects)
282
+
283
+ # إعادة تسمية الأعمدة بالعربية
284
+ renamed_columns = {
285
+ "name": "اسم المشروع",
286
+ "city": "المدينة",
287
+ "status": "الحالة",
288
+ "description": "الوصف",
289
+ "project_id": "معرف المشروع",
290
+ "latitude": "خط العرض",
291
+ "longitude": "خط الطول"
292
+ }
293
+
294
+ # تحديد الأعمدة للعرض
295
+ display_columns = ["name", "city", "status", "project_id"]
296
+
297
+ # إنشاء جدول للعرض
298
+ display_df = projects_df[display_columns].rename(columns=renamed_columns)
299
+
300
+ # عرض الجدول
301
+ st.dataframe(display_df, width=1000, height=400)
302
+
303
+ # زر لاختيار مشروع لعرض التضاريس
304
+ selected_project_id = st.selectbox(
305
+ "اختر مشروعًا لعرض التضاريس ثلاثية الأبعاد",
306
+ options=projects_df["project_id"].tolist(),
307
+ format_func=lambda x: next((p["name"] for p in filtered_projects if p["project_id"] == x), x),
308
+ key="select_project_for_terrain"
309
+ )
310
+
311
+ # زر عرض التضاريس
312
+ if styled_button("عرض التضاريس", key="show_terrain_btn", type="primary", icon="🏔️"):
313
+ # العثور على المشروع المحدد
314
+ selected_project = next((p for p in filtered_projects if p["project_id"] == selected_project_id), None)
315
+
316
+ if selected_project:
317
+ # تخزين الموقع المحدد في حالة الجلسة
318
+ st.session_state.selected_location = {
319
+ "latitude": selected_project["latitude"],
320
+ "longitude": selected_project["longitude"],
321
+ "name": selected_project["name"],
322
+ "project_id": selected_project["project_id"]
323
+ }
324
+
325
+ # جلب بيانات التضاريس
326
+ try:
327
+ terrain_data = self._fetch_terrain_data(
328
+ selected_project["latitude"],
329
+ selected_project["longitude"]
330
+ )
331
+
332
+ # تخزين بيانات التضاريس في حالة الجلسة
333
+ st.session_state.terrain_data = terrain_data
334
+
335
+ # الانتقال إلى تبويب عرض التضاريس
336
+ st.experimental_rerun()
337
+ except Exception as e:
338
+ st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
339
+ else:
340
+ st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.")
341
+
342
+ def _render_3d_terrain(self):
343
+ """عرض التضاريس ثلاثي الأبعاد"""
344
+ st.markdown("""
345
+ <div class='custom-box info-box'>
346
+ <h3>🏔️ عرض التضاريس ثلاثي الأبعاد</h3>
347
+ <p>عرض تفاعلي ثلاثي الأبعاد لتضاريس موقع المشروع المحدد.</p>
348
+ <p>يمكنك تدوير وتكبير العرض للحصول على رؤية أفضل للموقع.</p>
349
+ </div>
350
+ """, unsafe_allow_html=True)
351
+
352
+ # التحقق من وجود موقع محدد
353
+ if st.session_state.selected_location is None:
354
+ st.info("يرجى اختيار موقع من تبويب 'الخريطة التفاعلية' أولاً.")
355
+
356
+ # بديل: السماح بإدخال الإحداثيات يدوياً
357
+ st.markdown("### إدخال الإحداثيات يدوياً")
358
+
359
+ col1, col2 = st.columns(2)
360
+
361
+ with col1:
362
+ manual_lat = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="manual_lat")
363
+
364
+ with col2:
365
+ manual_lon = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="manual_lon")
366
+
367
+ if styled_button("عرض التضاريس", key="manual_terrain_btn", type="primary", icon="🏔️"):
368
+ try:
369
+ # جلب بيانات التضاريس
370
+ terrain_data = self._fetch_terrain_data(manual_lat, manual_lon)
371
+
372
+ # تخزين بيانات التضاريس والموقع في حالة الجلسة
373
+ st.session_state.terrain_data = terrain_data
374
+ st.session_state.selected_location = {
375
+ "latitude": manual_lat,
376
+ "longitude": manual_lon,
377
+ "name": "موقع مخصص",
378
+ "project_id": "custom"
379
+ }
380
+
381
+ st.success("تم جلب بيانات التضاريس بنجاح!")
382
+ st.experimental_rerun()
383
+ except Exception as e:
384
+ st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
385
+
386
+ return
387
+
388
+ # عرض معلومات الموقع المحدد
389
+ location = st.session_state.selected_location
390
+ st.markdown(f"### عرض تضاريس موقع: {location['name']}")
391
+ st.markdown(f"**الإحداثيات:** {location['latitude']:.6f}, {location['longitude']:.6f}")
392
+
393
+ # تجهيز بيانات التضاريس
394
+ if st.session_state.terrain_data is None:
395
+ # محاولة جلب بيانات التضاريس
396
+ try:
397
+ terrain_data = self._fetch_terrain_data(
398
+ location["latitude"],
399
+ location["longitude"]
400
+ )
401
+
402
+ # تخزين بيانات التضاريس في حالة الجلسة
403
+ st.session_state.terrain_data = terrain_data
404
+ except Exception as e:
405
+ st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
406
+ return
407
+
408
+ # استخدام بيانات التضاريس المخزنة
409
+ terrain_data = st.session_state.terrain_data
410
+
411
+ # عرض نطاق التضاريس وإعدادات الارتفاع
412
+ col1, col2, col3 = st.columns(3)
413
+
414
+ with col1:
415
+ elevation_scale = st.slider(
416
+ "مقياس الارتفاع",
417
+ min_value=1,
418
+ max_value=50,
419
+ value=15,
420
+ key="elevation_scale"
421
+ )
422
+
423
+ with col2:
424
+ radius = st.slider(
425
+ "نطاق العرض (كم)",
426
+ min_value=1,
427
+ max_value=20,
428
+ value=5,
429
+ key="terrain_radius"
430
+ )
431
+
432
+ with col3:
433
+ color_scheme = st.selectbox(
434
+ "نظام الألوان",
435
+ options=["terrain", "elevation", "custom"],
436
+ key="color_scheme"
437
+ )
438
+
439
+ # إنشاء نموذج PyDeck للعرض ثلاثي الأبعاد
440
+ try:
441
+ # تحويل بيانات التضاريس إلى DataFrame
442
+ terrain_df = pd.DataFrame(terrain_data)
443
+
444
+ # تعيين حجم الخلية بناءً على النطاق
445
+ cell_size = radius * 100 # تحويل الكيلومترات إلى أمتار وتقسيمها
446
+
447
+ # إنشاء طبقة التضاريس
448
+ terrain_layer = pdk.Layer(
449
+ "TerrainLayer",
450
+ data=None,
451
+ elevation_decoder={
452
+ "elevations": "elevation",
453
+ "bounds": terrain_df["bounds"].iloc[0]
454
+ },
455
+ texture=None,
456
+ elevation_data=terrain_df["terrain"].iloc[0],
457
+ elevation_scale=elevation_scale,
458
+ color_map=self._get_color_map(color_scheme),
459
+ wireframe=True,
460
+ pickable=True
461
+ )
462
+
463
+ # إنشاء طبقة النقطة المركزية
464
+ point_layer = pdk.Layer(
465
+ "ScatterplotLayer",
466
+ data=[{
467
+ "position": [location["longitude"], location["latitude"]],
468
+ "name": location["name"]
469
+ }],
470
+ get_position="position",
471
+ get_radius=100,
472
+ get_fill_color=[255, 0, 0, 200],
473
+ pickable=True
474
+ )
475
+
476
+ # إنشاء عرض PyDeck
477
+ INITIAL_VIEW_STATE = pdk.ViewState(
478
+ longitude=location["longitude"],
479
+ latitude=location["latitude"],
480
+ zoom=12,
481
+ max_zoom=20,
482
+ pitch=45,
483
+ bearing=0
484
+ )
485
+
486
+ deck = pdk.Deck(
487
+ map_style="mapbox://styles/mapbox/satellite-v9",
488
+ initial_view_state=INITIAL_VIEW_STATE,
489
+ api_keys={"mapbox": self.mapbox_token} if self.mapbox_token else None,
490
+ layers=[terrain_layer, point_layer],
491
+ tooltip={
492
+ "html": "<b>{name}</b>",
493
+ "style": {
494
+ "backgroundColor": "steelblue",
495
+ "color": "white"
496
+ }
497
+ }
498
+ )
499
+
500
+ # عرض النموذج ثلاثي الأبعاد
501
+ st.pydeck_chart(deck)
502
+
503
+ # عرض تحليل التضاريس
504
+ if "elevation_stats" in terrain_df:
505
+ elevation_stats = terrain_df["elevation_stats"].iloc[0]
506
+
507
+ st.markdown("### تحليل التضاريس")
508
+
509
+ stats_col1, stats_col2, stats_col3, stats_col4 = st.columns(4)
510
+
511
+ with stats_col1:
512
+ st.metric("أدنى ارتفاع", f"{elevation_stats['min']:.1f} م")
513
+
514
+ with stats_col2:
515
+ st.metric("أعلى ارتفاع", f"{elevation_stats['max']:.1f} م")
516
+
517
+ with stats_col3:
518
+ st.metric("متوسط الارتفاع", f"{elevation_stats['mean']:.1f} م")
519
+
520
+ with stats_col4:
521
+ st.metric("فرق الارتفاع", f"{elevation_stats['range']:.1f} م")
522
+
523
+ # عرض رسم بياني للارتفاعات
524
+ if "elevation_profile" in terrain_df:
525
+ elevation_profile = terrain_df["elevation_profile"].iloc[0]
526
+
527
+ # إنشاء DataFrame للرسم البياني
528
+ profile_df = pd.DataFrame(elevation_profile)
529
+
530
+ # عرض الرسم البياني
531
+ st.markdown("### مقطع الارتفاع")
532
+
533
+ # استخدام Plotly Express
534
+ import plotly.express as px
535
+
536
+ fig = px.line(
537
+ profile_df,
538
+ x="distance",
539
+ y="elevation",
540
+ title="مقطع الارتفاع عبر الموقع",
541
+ labels={"distance": "المسافة (كم)", "elevation": "الارتفاع (م)"}
542
+ )
543
+
544
+ fig.update_layout(
545
+ title_font_size=20,
546
+ font_family="Arial",
547
+ font_size=14,
548
+ height=400
549
+ )
550
+
551
+ st.plotly_chart(fig, use_container_width=True)
552
+
553
+ # أزرار التحكم الإضافية
554
+ col1, col2 = st.columns(2)
555
+
556
+ with col1:
557
+ if styled_button("إعادة تحميل بيانات التضاريس", key="reload_terrain", type="primary", icon="🔄"):
558
+ # حذف بيانات التضاريس الحالية
559
+ st.session_state.terrain_data = None
560
+ st.experimental_rerun()
561
+
562
+ with col2:
563
+ if styled_button("العودة للخريطة التفاعلية", key="back_to_map", type="secondary", icon="🗺️"):
564
+ # إعادة تعيين الموقع المحدد
565
+ st.session_state.selected_location = None
566
+ st.session_state.terrain_data = None
567
+ st.experimental_rerun()
568
+
569
+ except Exception as e:
570
+ st.error(f"حدث خطأ أثناء عرض التضاريس ثلاثي الأبعاد: {str(e)}")
571
+
572
+ def _render_location_analysis(self):
573
+ """عرض تحليل المواقع"""
574
+ st.markdown("""
575
+ <div class='custom-box info-box'>
576
+ <h3>📊 تحليل المواقع</h3>
577
+ <p>تحليل لمواقع المشاريع وتوزيعها الجغرافي.</p>
578
+ <p>يمكنك عرض إحصائيات وتقارير متنوعة حول مواقع المشاريع.</p>
579
+ </div>
580
+ """, unsafe_allow_html=True)
581
+
582
+ # التحقق من وجود مواقع
583
+ if not st.session_state.project_locations:
584
+ st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.")
585
+ return
586
+
587
+ # تحويل بيانات المواقع إلى DataFrame
588
+ locations_df = pd.DataFrame(st.session_state.project_locations)
589
+
590
+ # عرض توزيع المشاريع حسب المدينة
591
+ st.markdown("### توزيع المشاريع حسب المدينة")
592
+
593
+ city_counts = locations_df["city"].value_counts().reset_index()
594
+ city_counts.columns = ["المدينة", "عدد المشاريع"]
595
+
596
+ # عرض الرسم البياني
597
+ import plotly.express as px
598
+
599
+ fig = px.bar(
600
+ city_counts,
601
+ x="المدينة",
602
+ y="عدد المشاريع",
603
+ title="توزيع المشاريع حسب المدينة",
604
+ color="عدد المشاريع",
605
+ color_continuous_scale="Viridis"
606
+ )
607
+
608
+ fig.update_layout(
609
+ title_font_size=20,
610
+ font_family="Arial",
611
+ font_size=14,
612
+ height=400
613
+ )
614
+
615
+ st.plotly_chart(fig, use_container_width=True)
616
+
617
+ # عرض توزيع المشاريع حسب الحالة
618
+ st.markdown("### توزيع المشاريع حسب الحالة")
619
+
620
+ status_counts = locations_df["status"].value_counts().reset_index()
621
+ status_counts.columns = ["الحالة", "عدد المشاريع"]
622
+
623
+ # عرض الرسم البياني
624
+ fig2 = px.pie(
625
+ status_counts,
626
+ values="عدد المشاريع",
627
+ names="الحالة",
628
+ title="توزيع المشاريع حسب الحالة",
629
+ color_discrete_sequence=px.colors.qualitative.Set3
630
+ )
631
+
632
+ fig2.update_layout(
633
+ title_font_size=20,
634
+ font_family="Arial",
635
+ font_size=14,
636
+ height=400
637
+ )
638
+
639
+ st.plotly_chart(fig2, use_container_width=True)
640
+
641
+ # عرض تحليل المسافات بين المشاريع
642
+ st.markdown("### تحليل المسافات بين المشاريع")
643
+
644
+ # حساب مصفوفة المسافات
645
+ if len(locations_df) > 1:
646
+ # اختيار مشروع كنقطة مرجعية
647
+ reference_project = st.selectbox(
648
+ "اختر مشروعًا كنقطة مرجعية",
649
+ options=locations_df["project_id"].tolist(),
650
+ format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
651
+ key="reference_project"
652
+ )
653
+
654
+ # العثور على المشروع المرجعي
655
+ ref_project_data = locations_df[locations_df["project_id"] == reference_project].iloc[0]
656
+
657
+ # حساب المسافات
658
+ distances = []
659
+ for _, project in locations_df.iterrows():
660
+ if project["project_id"] != reference_project:
661
+ distance = self._calculate_distance(
662
+ ref_project_data["latitude"], ref_project_data["longitude"],
663
+ project["latitude"], project["longitude"]
664
+ )
665
+
666
+ distances.append({
667
+ "project_id": project["project_id"],
668
+ "name": project["name"],
669
+ "city": project["city"],
670
+ "distance": distance
671
+ })
672
+
673
+ # تحويل البيانات إلى DataFrame
674
+ distances_df = pd.DataFrame(distances)
675
+
676
+ # ترتيب المشاريع حسب المسافة
677
+ distances_df = distances_df.sort_values("distance")
678
+
679
+ # عرض المسافات
680
+ st.markdown(f"المسافات من مشروع: **{ref_project_data['name']}**")
681
+
682
+ # إعادة تسمية الأعمدة
683
+ distances_df = distances_df.rename(columns={
684
+ "name": "اسم المشروع",
685
+ "city": "المدينة",
686
+ "distance": "المسافة (كم)"
687
+ })
688
+
689
+ # تنسيق المسافة
690
+ distances_df["المسافة (كم)"] = distances_df["المسافة (كم)"].round(2)
691
+
692
+ # عرض الجدول
693
+ st.dataframe(distances_df[["اسم المشروع", "المدينة", "المسافة (كم)"]], width=800)
694
+
695
+ # عرض رسم بياني للمسافات
696
+ fig3 = px.bar(
697
+ distances_df,
698
+ x="اسم المشروع",
699
+ y="المسافة (كم)",
700
+ title=f"المسافات من مشروع {ref_project_data['name']}",
701
+ color="المسافة (كم)",
702
+ color_continuous_scale="Viridis"
703
+ )
704
+
705
+ fig3.update_layout(
706
+ title_font_size=20,
707
+ font_family="Arial",
708
+ font_size=14,
709
+ height=400
710
+ )
711
+
712
+ st.plotly_chart(fig3, use_container_width=True)
713
+
714
+ # عرض المشاريع القريبة على خريطة
715
+ st.markdown("### المشاريع القريبة على الخريطة")
716
+
717
+ # إنشاء الخريطة
718
+ m2 = folium.Map(
719
+ location=[ref_project_data["latitude"], ref_project_data["longitude"]],
720
+ zoom_start=8,
721
+ tiles="OpenStreetMap",
722
+ attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
723
+ )
724
+
725
+ # إضافة المشروع المرجعي
726
+ folium.Marker(
727
+ location=[ref_project_data["latitude"], ref_project_data["longitude"]],
728
+ popup=ref_project_data["name"],
729
+ tooltip=ref_project_data["name"],
730
+ icon=folium.Icon(color='red', icon='star')
731
+ ).add_to(m2)
732
+
733
+ # إضافة الدوائر
734
+ folium.Circle(
735
+ location=[ref_project_data["latitude"], ref_project_data["longitude"]],
736
+ radius=50000, # 50 كم
737
+ color='red',
738
+ fill=True,
739
+ fill_opacity=0.1,
740
+ popup="50 كم"
741
+ ).add_to(m2)
742
+
743
+ folium.Circle(
744
+ location=[ref_project_data["latitude"], ref_project_data["longitude"]],
745
+ radius=100000, # 100 كم
746
+ color='orange',
747
+ fill=True,
748
+ fill_opacity=0.1,
749
+ popup="100 كم"
750
+ ).add_to(m2)
751
+
752
+ folium.Circle(
753
+ location=[ref_project_data["latitude"], ref_project_data["longitude"]],
754
+ radius=200000, # 200 كم
755
+ color='blue',
756
+ fill=True,
757
+ fill_opacity=0.1,
758
+ popup="200 كم"
759
+ ).add_to(m2)
760
+
761
+ # إضافة المشاريع الأخرى
762
+ for _, project in distances_df.iterrows():
763
+ project_data = locations_df[locations_df["project_id"] == project["project_id"]].iloc[0]
764
+
765
+ folium.Marker(
766
+ location=[project_data["latitude"], project_data["longitude"]],
767
+ popup=f"{project_data['name']} - {project['المسافة (كم)']} كم",
768
+ tooltip=project_data["name"],
769
+ icon=folium.Icon(color='green', icon='info-sign')
770
+ ).add_to(m2)
771
+
772
+ # إضافة خط للربط
773
+ folium.PolyLine(
774
+ locations=[
775
+ [ref_project_data["latitude"], ref_project_data["longitude"]],
776
+ [project_data["latitude"], project_data["longitude"]]
777
+ ],
778
+ color='gray',
779
+ weight=2,
780
+ opacity=0.5,
781
+ popup=f"{project['المسافة (كم)']} كم"
782
+ ).add_to(m2)
783
+
784
+ # عرض الخريطة
785
+ folium_static(m2, width=800, height=500)
786
+ else:
787
+ st.info("يجب وجود أكثر من مشروع واحد لحساب المسافات.")
788
+
789
+ def _render_location_management(self):
790
+ """عرض إدارة المواقع"""
791
+ st.markdown("""
792
+ <div class='custom-box info-box'>
793
+ <h3>⚙️ إدارة المواقع</h3>
794
+ <p>إضافة وتحرير وحذف مواقع المشاريع.</p>
795
+ <p>يمكنك إضافة مواقع جديدة أو تحديث المواقع الموجودة.</p>
796
+ </div>
797
+ """, unsafe_allow_html=True)
798
+
799
+ # تبويبات إدارة المواقع
800
+ management_tabs = st.tabs(["إضافة موقع جديد", "تحرير المواقع الموجودة", "استيراد وتصدير المواقع"])
801
+
802
+ # تبويب إضافة موقع جديد
803
+ with management_tabs[0]:
804
+ self._render_add_location()
805
+
806
+ # تبويب تحرير المواقع الموجودة
807
+ with management_tabs[1]:
808
+ self._render_edit_locations()
809
+
810
+ # تبويب استيراد وتصدير المواقع
811
+ with management_tabs[2]:
812
+ self._render_import_export_locations()
813
+
814
+ def _render_add_location(self):
815
+ """عرض نموذج إضافة موقع جديد"""
816
+ st.markdown("### إضافة موقع مشروع جديد")
817
+
818
+ # البيانات الأساسية
819
+ project_name = st.text_input("اسم المشروع", key="new_project_name")
820
+ project_desc = st.text_area("وصف المشروع", key="new_project_desc")
821
+
822
+ col1, col2 = st.columns(2)
823
+
824
+ with col1:
825
+ project_city = st.text_input("المدينة", key="new_project_city")
826
+ project_status = st.selectbox(
827
+ "حالة المشروع",
828
+ options=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"],
829
+ key="new_project_status"
830
+ )
831
+
832
+ with col2:
833
+ project_id = st.text_input("معرف المشروع (اختياري)", key="new_project_id", placeholder="سيتم إنشاؤه تلقائيًا إذا تُرك فارغًا")
834
+
835
+ # إدخال إحداثيات الموقع
836
+ st.markdown("#### إحداثيات الموقع")
837
+ location_method = st.radio(
838
+ "طريقة تحديد الموقع",
839
+ options=["إدخال يدوي", "اختيار من الخريطة"],
840
+ key="new_location_method"
841
+ )
842
+
843
+ # تحديد الموقع
844
+ if location_method == "إدخال يدوي":
845
+ loc_col1, loc_col2 = st.columns(2)
846
+
847
+ with loc_col1:
848
+ latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="new_latitude")
849
+
850
+ with loc_col2:
851
+ longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="new_longitude")
852
+
853
+ # عرض الموقع على خريطة صغيرة
854
+ mini_map = folium.Map(location=[latitude, longitude], zoom_start=10)
855
+ folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map)
856
+ folium_static(mini_map, width=700, height=300)
857
+ else:
858
+ st.markdown("#### اختر الموقع من الخريطة")
859
+ st.info("انقر على الخريطة لتحديد الموقع.")
860
+
861
+ # إنشاء خريطة
862
+ m = folium.Map(location=[24.7136, 46.6753], zoom_start=6)
863
+
864
+ # إضافة محدد النقر
865
+ m.add_child(folium.ClickForMarker(popup="الموقع المحدد"))
866
+
867
+ # عرض الخريطة
868
+ map_data = folium_static(m, width=700, height=400)
869
+
870
+ # استخراج الإحداثيات المحددة (ليس مدعومًا حاليًا في Streamlit)
871
+ st.warning("ملاحظة: خاصية النقر على الخريطة غير مدعومة حاليًا في Streamlit. يرجى استخدام الإدخال اليدوي.")
872
+
873
+ latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="map_latitude")
874
+ longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="map_longitude")
875
+
876
+ # زر إضافة الموقع
877
+ if styled_button("إضافة الموقع", key="add_location", type="primary", icon="➕"):
878
+ if not project_name or not project_desc or not project_city:
879
+ st.error("يرجى تعبئة جميع الحقول المطلوبة.")
880
+ else:
881
+ # إنشاء معرف فريد للمشروع إذا لم يتم تحديده
882
+ if not project_id:
883
+ project_id = f"PRJ-{len(st.session_state.project_locations) + 1:04d}"
884
+
885
+ # إنشاء كائن الموقع
886
+ new_location = {
887
+ "name": project_name,
888
+ "description": project_desc,
889
+ "city": project_city,
890
+ "status": project_status,
891
+ "latitude": latitude,
892
+ "longitude": longitude,
893
+ "project_id": project_id,
894
+ "created_at": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S"),
895
+ "updated_at": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
896
+ }
897
+
898
+ # إضافة الموقع للقائمة
899
+ st.session_state.project_locations.append(new_location)
900
+
901
+ # حفظ البيانات
902
+ self._save_locations_data()
903
+
904
+ st.success(f"تمت إضافة موقع المشروع '{project_name}' بنجاح!")
905
+ st.balloons()
906
+
907
+ def _render_edit_locations(self):
908
+ """عرض واجهة تحرير المواقع الموجودة"""
909
+ st.markdown("### تحرير أو حذف مواقع المشاريع")
910
+
911
+ # التحقق من وجود مواقع
912
+ if not st.session_state.project_locations:
913
+ st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع أولاً.")
914
+ return
915
+
916
+ # اختيار المشروع للتحرير
917
+ selected_project_id = st.selectbox(
918
+ "اختر مشروعًا للتحرير",
919
+ options=[p["project_id"] for p in st.session_state.project_locations],
920
+ format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
921
+ key="edit_project_select"
922
+ )
923
+
924
+ # العثور على المشروع المحدد
925
+ selected_project = next((p for p in st.session_state.project_locations if p["project_id"] == selected_project_id), None)
926
+
927
+ if selected_project:
928
+ # عرض نموذج التحرير
929
+ st.markdown(f"### تحرير مشروع: {selected_project['name']}")
930
+
931
+ # البيانات الأساسية
932
+ project_name = st.text_input("اسم المشروع", value=selected_project["name"], key="edit_project_name")
933
+ project_desc = st.text_area("وصف المشروع", value=selected_project["description"], key="edit_project_desc")
934
+
935
+ col1, col2 = st.columns(2)
936
+
937
+ with col1:
938
+ project_city = st.text_input("المدينة", value=selected_project["city"], key="edit_project_city")
939
+ project_status = st.selectbox(
940
+ "حالة المشروع",
941
+ options=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"],
942
+ index=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"].index(selected_project["status"]),
943
+ key="edit_project_status"
944
+ )
945
+
946
+ with col2:
947
+ st.text_input("معرف المشروع", value=selected_project["project_id"], disabled=True, key="edit_project_id")
948
+
949
+ # إدخال إحداثيات الموقع
950
+ st.markdown("#### إحداثيات الموقع")
951
+
952
+ # تحديد الموقع
953
+ loc_col1, loc_col2 = st.columns(2)
954
+
955
+ with loc_col1:
956
+ latitude = st.number_input(
957
+ "خط العرض",
958
+ value=selected_project["latitude"],
959
+ step=0.0001,
960
+ format="%.6f",
961
+ key="edit_latitude"
962
+ )
963
+
964
+ with loc_col2:
965
+ longitude = st.number_input(
966
+ "خط الطول",
967
+ value=selected_project["longitude"],
968
+ step=0.0001,
969
+ format="%.6f",
970
+ key="edit_longitude"
971
+ )
972
+
973
+ # عرض الموقع على خريطة صغيرة
974
+ mini_map = folium.Map(location=[latitude, longitude], zoom_start=10)
975
+ folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map)
976
+ folium_static(mini_map, width=700, height=300)
977
+
978
+ # أزرار الإجراءات
979
+ col1, col2 = st.columns(2)
980
+
981
+ with col1:
982
+ if styled_button("حفظ التغييرات", key="save_location_changes", type="primary", icon="💾"):
983
+ if not project_name or not project_desc or not project_city:
984
+ st.error("يرجى تعبئة جميع الحقول المطلوبة.")
985
+ else:
986
+ # تحديث بيانات المشروع
987
+ selected_project["name"] = project_name
988
+ selected_project["description"] = project_desc
989
+ selected_project["city"] = project_city
990
+ selected_project["status"] = project_status
991
+ selected_project["latitude"] = latitude
992
+ selected_project["longitude"] = longitude
993
+ selected_project["updated_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
994
+
995
+ # حفظ البيانات
996
+ self._save_locations_data()
997
+
998
+ st.success(f"تم تحديث بيانات المشروع '{project_name}' بنجاح!")
999
+ st.experimental_rerun()
1000
+
1001
+ with col2:
1002
+ if styled_button("حذف المشروع", key="delete_location", type="danger", icon="🗑️"):
1003
+ # تأكيد الحذف
1004
+ st.warning(f"هل أنت متأكد من حذف المشروع '{selected_project['name']}'؟")
1005
+
1006
+ confirm_col1, confirm_col2 = st.columns(2)
1007
+
1008
+ with confirm_col1:
1009
+ if styled_button("تأكيد الحذف", key="confirm_delete", type="danger", icon="✓"):
1010
+ # إزالة المشروع من القائمة
1011
+ st.session_state.project_locations.remove(selected_project)
1012
+
1013
+ # حفظ البيانات
1014
+ self._save_locations_data()
1015
+
1016
+ st.success(f"تم حذف المشروع '{selected_project['name']}' بنجاح!")
1017
+ st.experimental_rerun()
1018
+
1019
+ with confirm_col2:
1020
+ if styled_button("إلغاء", key="cancel_delete", type="secondary", icon="❌"):
1021
+ st.experimental_rerun()
1022
+
1023
+ def _render_import_export_locations(self):
1024
+ """عرض واجهة استيراد وتصدير المواقع"""
1025
+ st.markdown("### استيراد وتصدير مواقع المشاريع")
1026
+
1027
+ col1, col2 = st.columns(2)
1028
+
1029
+ with col1:
1030
+ st.markdown("#### تصدير المواقع")
1031
+
1032
+ export_format = st.selectbox(
1033
+ "صيغة التصدير",
1034
+ options=["CSV", "JSON", "GeoJSON"],
1035
+ key="export_format"
1036
+ )
1037
+
1038
+ if styled_button("تصدير المواقع", key="export_locations", type="primary", icon="📤"):
1039
+ self._export_locations(export_format)
1040
+
1041
+ with col2:
1042
+ st.markdown("#### استيراد المواقع")
1043
+
1044
+ import_format = st.selectbox(
1045
+ "صيغة الاستيراد",
1046
+ options=["CSV", "JSON", "GeoJSON"],
1047
+ key="import_format"
1048
+ )
1049
+
1050
+ uploaded_file = st.file_uploader(
1051
+ "اختر ملف للاستيراد",
1052
+ type=["csv", "json", "geojson"],
1053
+ key="import_locations_file"
1054
+ )
1055
+
1056
+ if uploaded_file is not None:
1057
+ if styled_button("استيراد المواقع", key="import_locations", type="success", icon="📥"):
1058
+ self._import_locations(uploaded_file, import_format)
1059
+
1060
+ # عرض إحصائيات البيانات
1061
+ st.markdown("### إحصائيات البيانات")
1062
+
1063
+ stats_col1, stats_col2, stats_col3 = st.columns(3)
1064
+
1065
+ with stats_col1:
1066
+ st.metric("عدد المشاريع", len(st.session_state.project_locations))
1067
+
1068
+ with stats_col2:
1069
+ cities = set(p["city"] for p in st.session_state.project_locations)
1070
+ st.metric("عدد المدن", len(cities))
1071
+
1072
+ with stats_col3:
1073
+ statuses = {}
1074
+ for p in st.session_state.project_locations:
1075
+ statuses[p["status"]] = statuses.get(p["status"], 0) + 1
1076
+
1077
+ status_str = ", ".join([f"{k}: {v}" for k, v in statuses.items()])
1078
+ st.metric("توزيع الحالات", status_str if statuses else "لا توجد بيانات")
1079
+
1080
+ # خيارات متقدمة
1081
+ with st.expander("خيارات متقدمة"):
1082
+ if styled_button("حذف جميع المواقع", key="clear_locations", type="danger", icon="🗑️"):
1083
+ # تأكيد الحذف
1084
+ st.warning("هل أنت متأكد من حذف جميع مواقع المشاريع؟ لا يمكن التراجع عن هذا الإجراء.")
1085
+
1086
+ confirm_col1, confirm_col2 = st.columns(2)
1087
+
1088
+ with confirm_col1:
1089
+ if styled_button("تأكيد الحذف", key="confirm_clear", type="danger", icon="✓"):
1090
+ # مسح القائمة
1091
+ st.session_state.project_locations = []
1092
+
1093
+ # حفظ البيانات
1094
+ self._save_locations_data()
1095
+
1096
+ st.success("تم حذف جميع مواقع المشاريع بنجاح!")
1097
+ st.experimental_rerun()
1098
+
1099
+ with confirm_col2:
1100
+ if styled_button("إلغاء", key="cancel_clear", type="secondary", icon="❌"):
1101
+ st.experimental_rerun()
1102
+
1103
+ def _fetch_terrain_data(self, latitude, longitude, radius_km=5):
1104
+ """جلب بيانات التضاريس من واجهة برمجة التطبيقات"""
1105
+ try:
1106
+ # تحديث حالة الجلسة
1107
+ import plotly.express as px
1108
+
1109
+ # تعيين الإحداثيات وحجم المنطقة
1110
+ center_lat, center_lon = latitude, longitude
1111
+
1112
+ # تحويل نصف القطر من كم إلى درجات (تقريبي)
1113
+ radius_deg = radius_km / 111.0 # تقريب: 1 درجة = 111 كم
1114
+
1115
+ # تحديد حدود المنطقة
1116
+ min_lat = center_lat - radius_deg
1117
+ max_lat = center_lat + radius_deg
1118
+ min_lon = center_lon - radius_deg
1119
+ max_lon = center_lon + radius_deg
1120
+
1121
+ # إنشاء شبكة من النقاط
1122
+ resolution = 50 # عدد النقاط في كل اتجاه
1123
+ lats = np.linspace(min_lat, max_lat, resolution)
1124
+ lons = np.linspace(min_lon, max_lon, resolution)
1125
+
1126
+ # إنشاء مصفوفة للإحداثيات
1127
+ grid_lats, grid_lons = np.meshgrid(lats, lons)
1128
+
1129
+ # تحويل الشبكة إلى قائمة من النقاط
1130
+ points = []
1131
+ for i in range(grid_lats.shape[0]):
1132
+ for j in range(grid_lats.shape[1]):
1133
+ points.append((grid_lats[i, j], grid_lons[i, j]))
1134
+
1135
+ # تقسيم النقاط إلى مجموعات لتقليل عدد الطلبات
1136
+ batch_size = 100
1137
+ batches = [points[i:i + batch_size] for i in range(0, len(points), batch_size)]
1138
+
1139
+ # إنشاء بيانات التضاريس
1140
+ elevation_data = np.zeros((len(lats), len(lons)))
1141
+
1142
+ # محاكاة بيانات التضاريس (يمكن استبدالها بواجهة برمجة تطبيقات حقيقية)
1143
+ for batch_idx, batch in enumerate(batches):
1144
+ # في بيئة الإنتاج، سيتم استبدال هذا بطلب API حقيقي
1145
+ # هنا نقوم بمحاكاة بيانات التضاريس لأغراض العرض
1146
+ for point_idx, (lat, lon) in enumerate(batch):
1147
+ # حساب المؤشر في مصفوفة الارتفاع
1148
+ lat_idx = np.abs(lats - lat).argmin()
1149
+ lon_idx = np.abs(lons - lon).argmin()
1150
+
1151
+ # محاكاة الارتفاع (في بيئة الإنتاج سيكون هذا من واجهة برمجة التطبيقات)
1152
+ # هنا نصنع تضاريس اصطناعية باستخدام دالة جيبية
1153
+ dist_from_center = np.sqrt(
1154
+ (lat - center_lat) ** 2 + (lon - center_lon) ** 2
1155
+ )
1156
+
1157
+ # إنشاء بعض التلال والوديان الاصطناعية
1158
+ elevation = 500 + 200 * np.sin(dist_from_center * 100) + 100 * np.cos(lat * 30) + 150 * np.sin(lon * 40)
1159
+
1160
+ # إضافة بعض الضوضاء العشوائية
1161
+ elevation += np.random.normal(0, 30)
1162
+
1163
+ # تخزين الارتفاع
1164
+ elevation_data[lat_idx, lon_idx] = elevation
1165
+
1166
+ # حساب إحصائيات الارتفاع
1167
+ elevation_stats = {
1168
+ "min": float(np.min(elevation_data)),
1169
+ "max": float(np.max(elevation_data)),
1170
+ "mean": float(np.mean(elevation_data)),
1171
+ "range": float(np.max(elevation_data) - np.min(elevation_data))
1172
+ }
1173
+
1174
+ # إنشاء مقطع ارتفاع من الشمال إلى الجنوب عبر المركز
1175
+ center_lon_idx = np.abs(lons - center_lon).argmin()
1176
+ ns_profile = []
1177
+ for i, lat in enumerate(lats):
1178
+ ns_profile.append({
1179
+ "distance": (lat - min_lat) * 111.0, # تحويل الدرجات إلى كم
1180
+ "elevation": float(elevation_data[i, center_lon_idx])
1181
+ })
1182
+
1183
+ # إنشاء مقطع ارتفاع من الشرق إلى الغرب عبر المركز
1184
+ center_lat_idx = np.abs(lats - center_lat).argmin()
1185
+ ew_profile = []
1186
+ for i, lon in enumerate(lons):
1187
+ ew_profile.append({
1188
+ "distance": (lon - min_lon) * 111.0 * np.cos(np.radians(center_lat)), # تحويل الدرجات إلى كم مع تصحيح خط العرض
1189
+ "elevation": float(elevation_data[center_lat_idx, i])
1190
+ })
1191
+
1192
+ # دمج المقاطع
1193
+ elevation_profile = ns_profile + ew_profile
1194
+
1195
+ # تحضير بيانات التضاريس للعرض ثلاثي الأبعاد
1196
+ bounds = [min_lon, min_lat, max_lon, max_lat]
1197
+
1198
+ # تحويل مصفوفة الارتفاع إلى تنسيق مناسب لـ PyDeck
1199
+ terrain_array = elevation_data.astype(np.float32)
1200
+
1201
+ # إنشاء كائن للتضاريس
1202
+ terrain_data = [{
1203
+ "bounds": bounds,
1204
+ "terrain": terrain_array.tolist(),
1205
+ "elevation_stats": elevation_stats,
1206
+ "elevation_profile": elevation_profile
1207
+ }]
1208
+
1209
+ return terrain_data
1210
+
1211
+ except Exception as e:
1212
+ st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
1213
+ raise e
1214
+
1215
+ def _calculate_distance(self, lat1, lon1, lat2, lon2):
1216
+ """حساب المسافة بين نقطتين بالكيلومترات باستخدام صيغة هافرساين"""
1217
+ import math
1218
+
1219
+ # تحويل الإحداثيات إلى راديان
1220
+ lat1 = math.radians(lat1)
1221
+ lon1 = math.radians(lon1)
1222
+ lat2 = math.radians(lat2)
1223
+ lon2 = math.radians(lon2)
1224
+
1225
+ # صيغة هافرساين
1226
+ dlon = lon2 - lon1
1227
+ dlat = lat2 - lat1
1228
+ a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
1229
+ c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
1230
+ distance = 6371 * c # نصف قطر الأرض بالكيلومترات
1231
+
1232
+ return distance
1233
+
1234
+ def _get_color_map(self, scheme):
1235
+ """الحصول على خريطة الألوان حسب النظام المختار"""
1236
+ if scheme == "terrain":
1237
+ return [
1238
+ [0, (0, 50, 0)],
1239
+ [0.1, (0, 100, 0)],
1240
+ [0.25, (0, 150, 0)],
1241
+ [0.4, (200, 170, 0)],
1242
+ [0.6, (150, 100, 0)],
1243
+ [0.8, (100, 50, 0)],
1244
+ [1, (200, 200, 200)]
1245
+ ]
1246
+ elif scheme == "elevation":
1247
+ return [
1248
+ [0, (0, 0, 100)],
1249
+ [0.2, (0, 100, 150)],
1250
+ [0.4, (0, 150, 50)],
1251
+ [0.6, (150, 150, 0)],
1252
+ [0.8, (150, 50, 0)],
1253
+ [1, (100, 0, 0)]
1254
+ ]
1255
+ else: # custom
1256
+ return [
1257
+ [0, (30, 100, 200)],
1258
+ [0.3, (60, 170, 250)],
1259
+ [0.5, (200, 220, 150)],
1260
+ [0.7, (180, 120, 60)],
1261
+ [0.9, (110, 60, 30)],
1262
+ [1, (80, 30, 10)]
1263
+ ]
1264
+
1265
+ def _export_locations(self, format):
1266
+ """تصدير مواقع المشاريع إلى ملف"""
1267
+ try:
1268
+ if not st.session_state.project_locations:
1269
+ st.error("لا توجد مواقع مشاريع للتصدير.")
1270
+ return
1271
+
1272
+ if format == "CSV":
1273
+ # تصدير إلى CSV
1274
+ df = pd.DataFrame(st.session_state.project_locations)
1275
+
1276
+ csv_data = df.to_csv(index=False)
1277
+
1278
+ st.download_button(
1279
+ label="تنزيل ملف CSV",
1280
+ data=csv_data,
1281
+ file_name="project_locations.csv",
1282
+ mime="text/csv"
1283
+ )
1284
+
1285
+ elif format == "JSON":
1286
+ # تصدير إلى JSON
1287
+ json_data = json.dumps(st.session_state.project_locations, ensure_ascii=False, indent=2)
1288
+
1289
+ st.download_button(
1290
+ label="تنزيل ملف JSON",
1291
+ data=json_data,
1292
+ file_name="project_locations.json",
1293
+ mime="application/json"
1294
+ )
1295
+
1296
+ elif format == "GeoJSON":
1297
+ # تصدير إلى GeoJSON
1298
+ features = []
1299
+
1300
+ for location in st.session_state.project_locations:
1301
+ feature = {
1302
+ "type": "Feature",
1303
+ "geometry": {
1304
+ "type": "Point",
1305
+ "coordinates": [location["longitude"], location["latitude"]]
1306
+ },
1307
+ "properties": {
1308
+ "name": location["name"],
1309
+ "description": location["description"],
1310
+ "city": location["city"],
1311
+ "status": location["status"],
1312
+ "project_id": location["project_id"],
1313
+ "created_at": location.get("created_at", ""),
1314
+ "updated_at": location.get("updated_at", "")
1315
+ }
1316
+ }
1317
+
1318
+ features.append(feature)
1319
+
1320
+ geojson = {
1321
+ "type": "FeatureCollection",
1322
+ "features": features
1323
+ }
1324
+
1325
+ geojson_data = json.dumps(geojson, ensure_ascii=False, indent=2)
1326
+
1327
+ st.download_button(
1328
+ label="تنزيل ملف GeoJSON",
1329
+ data=geojson_data,
1330
+ file_name="project_locations.geojson",
1331
+ mime="application/geo+json"
1332
+ )
1333
+
1334
+ st.success(f"تم تصدير {len(st.session_state.project_locations)} موقع بنجاح!")
1335
+
1336
+ except Exception as e:
1337
+ st.error(f"حدث خطأ أثناء تصدير المواقع: {str(e)}")
1338
+
1339
+ def _import_locations(self, uploaded_file, format):
1340
+ """استيراد مواقع المشاريع من ملف"""
1341
+ try:
1342
+ if format == "CSV":
1343
+ # استيراد من CSV
1344
+ df = pd.read_csv(uploaded_file)
1345
+
1346
+ # التحقق من وجود الأعمدة المطلوبة
1347
+ required_columns = ["name", "latitude", "longitude"]
1348
+ missing_columns = [col for col in required_columns if col not in df.columns]
1349
+
1350
+ if missing_columns:
1351
+ st.error(f"الملف لا يحتوي على الأعمدة التالية: {', '.join(missing_columns)}")
1352
+ return
1353
+
1354
+ # تحويل DataFrame إلى قائمة من القواميس
1355
+ imported_locations = df.to_dict("records")
1356
+
1357
+ elif format == "JSON":
1358
+ # استيراد من JSON
1359
+ imported_locations = json.loads(uploaded_file.read())
1360
+
1361
+ elif format == "GeoJSON":
1362
+ # استيراد من GeoJSON
1363
+ geojson = json.loads(uploaded_file.read())
1364
+
1365
+ # التحقق من صحة التنسيق
1366
+ if "type" not in geojson or geojson["type"] != "FeatureCollection" or "features" not in geojson:
1367
+ st.error("تنسيق GeoJSON غير صحيح.")
1368
+ return
1369
+
1370
+ # تحويل المميزات إلى مواقع
1371
+ imported_locations = []
1372
+
1373
+ for feature in geojson["features"]:
1374
+ if feature["type"] == "Feature" and feature["geometry"]["type"] == "Point":
1375
+ coords = feature["geometry"]["coordinates"]
1376
+ properties = feature["properties"]
1377
+
1378
+ location = {
1379
+ "name": properties.get("name", ""),
1380
+ "description": properties.get("description", ""),
1381
+ "city": properties.get("city", ""),
1382
+ "status": properties.get("status", "جديد"),
1383
+ "longitude": coords[0],
1384
+ "latitude": coords[1],
1385
+ "project_id": properties.get("project_id", f"PRJ-{len(imported_locations)+1:04d}"),
1386
+ "created_at": properties.get("created_at", pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")),
1387
+ "updated_at": properties.get("updated_at", pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S"))
1388
+ }
1389
+
1390
+ imported_locations.append(location)
1391
+
1392
+ # التحقق من وجود البيانات المطلوبة في الملف المستورد
1393
+ valid_locations = []
1394
+ for location in imported_locations:
1395
+ # التحقق من وجود الحقول المطلوبة
1396
+ if "name" not in location or "latitude" not in location or "longitude" not in location:
1397
+ continue
1398
+
1399
+ # إضافة القيم الافتراضية إذا لم تكن موجودة
1400
+ if "description" not in location:
1401
+ location["description"] = ""
1402
+
1403
+ if "city" not in location:
1404
+ location["city"] = ""
1405
+
1406
+ if "status" not in location:
1407
+ location["status"] = "جديد"
1408
+
1409
+ if "project_id" not in location:
1410
+ location["project_id"] = f"PRJ-{len(valid_locations)+1:04d}"
1411
+
1412
+ if "created_at" not in location:
1413
+ location["created_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
1414
+
1415
+ if "updated_at" not in location:
1416
+ location["updated_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
1417
+
1418
+ valid_locations.append(location)
1419
+
1420
+ if not valid_locations:
1421
+ st.error("لم يتم العثور على مواقع صالحة في الملف.")
1422
+ return
1423
+
1424
+ # سؤال المستخدم عن كيفية الاستيراد
1425
+ import_mode = st.radio(
1426
+ "كيفية الاستيراد",
1427
+ options=["إضافة إلى المواقع الموجودة", "استبدال المواقع الموجودة"],
1428
+ key="import_mode"
1429
+ )
1430
+
1431
+ if styled_button("تأكيد الاستيراد", key="confirm_import", type="success", icon="✓"):
1432
+ if import_mode == "إضافة إلى المواقع الموجودة":
1433
+ # إضافة المواقع المستوردة إلى القائمة الحالية
1434
+ st.session_state.project_locations.extend(valid_locations)
1435
+ else:
1436
+ # استبدال المواقع الموجودة بالمواقع المستوردة
1437
+ st.session_state.project_locations = valid_locations
1438
+
1439
+ # حفظ البيانات
1440
+ self._save_locations_data()
1441
+
1442
+ st.success(f"تم استيراد {len(valid_locations)} موقع بنجاح!")
1443
+ st.experimental_rerun()
1444
+
1445
+ except Exception as e:
1446
+ st.error(f"حدث خطأ أثناء استيراد المواقع: {str(e)}")
1447
+
1448
+ def _save_locations_data(self):
1449
+ """حفظ بيانات المواقع"""
1450
+ try:
1451
+ # التأكد من وجود المجلد
1452
+ os.makedirs(self.data_dir, exist_ok=True)
1453
+
1454
+ # حفظ البيانات
1455
+ locations_file = os.path.join(self.data_dir, "project_locations.json")
1456
+
1457
+ with open(locations_file, 'w', encoding='utf-8') as f:
1458
+ json.dump(st.session_state.project_locations, f, ensure_ascii=False, indent=2)
1459
+ except Exception as e:
1460
+ st.error(f"حدث خطأ أثناء حفظ بيانات المواقع: {str(e)}")
1461
+
1462
+ def _load_locations_data(self):
1463
+ """تحميل بيانات المواقع"""
1464
+ try:
1465
+ # التحقق من وجود الملف
1466
+ locations_file = os.path.join(self.data_dir, "project_locations.json")
1467
+
1468
+ if os.path.exists(locations_file):
1469
+ with open(locations_file, 'r', encoding='utf-8') as f:
1470
+ locations = json.load(f)
1471
+
1472
+ # تحديث حالة الجلسة
1473
+ st.session_state.project_locations = locations
1474
+ except Exception as e:
1475
+ st.error(f"حدث خطأ أثناء تحميل بيانات المواقع: {str(e)}")
1476
+
1477
+ def _initialize_sample_projects(self):
1478
+ """تهيئة بيانات اختبارية للمشاريع"""
1479
+ # التحقق من وجود بيانات محفوظة
1480
+ locations_file = os.path.join(self.data_dir, "project_locations.json")
1481
+
1482
+ if os.path.exists(locations_file):
1483
+ # تحميل البيانات المحفوظة
1484
+ self._load_locations_data()
1485
+ return
1486
+
1487
+ # إنشاء بيانات اختبارية إذا لم تكن هناك بيانات محفوظة
1488
+ sample_projects = [
1489
+ {
1490
+ "name": "تطوير شبكة الطرق في منطقة الرياض",
1491
+ "description": "مشروع تطوير وتوسعة شبكة الطرق الرئيسية في منطقة الرياض",
1492
+ "city": "الرياض",
1493
+ "status": "قيد التنفيذ",
1494
+ "latitude": 24.7136,
1495
+ "longitude": 46.6753,
1496
+ "project_id": "PRJ-0001",
1497
+ "created_at": "2025-01-15 10:30:00",
1498
+ "updated_at": "2025-01-15 10:30:00"
1499
+ },
1500
+ {
1501
+ "name": "إنشاء سد وادي حنيفة",
1502
+ "description": "مشروع إنشاء سد لحجز مياه الأمطار في وادي حنيفة",
1503
+ "city": "الرياض",
1504
+ "status": "جديد",
1505
+ "latitude": 24.6748,
1506
+ "longitude": 46.5831,
1507
+ "project_id": "PRJ-0002",
1508
+ "created_at": "2025-02-01 14:45:00",
1509
+ "updated_at": "2025-02-01 14:45:00"
1510
+ },
1511
+ {
1512
+ "name": "تطوير ميناء جدة الإسلامي",
1513
+ "description": "مشروع تطوير وتوسعة ميناء جدة الإسلامي لزيادة الطاقة الاستيعابية",
1514
+ "city": "جدة",
1515
+ "status": "قيد التنفيذ",
1516
+ "latitude": 21.4858,
1517
+ "longitude": 39.1925,
1518
+ "project_id": "PRJ-0003",
1519
+ "created_at": "2024-11-20 09:15:00",
1520
+ "updated_at": "2024-11-20 09:15:00"
1521
+ },
1522
+ {
1523
+ "name": "إنشاء مطار الدمام الجديد",
1524
+ "description": "مشروع إنشاء مطار جديد في مدينة الدمام لتلبية الطلب المتزايد",
1525
+ "city": "الدمام",
1526
+ "status": "متوقف",
1527
+ "latitude": 26.4207,
1528
+ "longitude": 50.0888,
1529
+ "project_id": "PRJ-0004",
1530
+ "created_at": "2024-10-05 11:30:00",
1531
+ "updated_at": "2024-10-05 11:30:00"
1532
+ },
1533
+ {
1534
+ "name": "توسعة جامعة الملك فهد للبترول والمعادن",
1535
+ "description": "مشروع توسعة مباني ومرافق جامعة الملك فهد للبترول والمعادن",
1536
+ "city": "الظهران",
1537
+ "status": "قيد التنفيذ",
1538
+ "latitude": 26.3927,
1539
+ "longitude": 50.1150,
1540
+ "project_id": "PRJ-0005",
1541
+ "created_at": "2025-01-10 08:00:00",
1542
+ "updated_at": "2025-01-10 08:00:00"
1543
+ },
1544
+ {
1545
+ "name": "إنشاء محطة تحلية مياه القنفذة",
1546
+ "description": "مشروع إنشاء محطة تحلية مياه جديدة في محافظة القنفذة",
1547
+ "city": "القنفذة",
1548
+ "status": "جديد",
1549
+ "latitude": 19.1299,
1550
+ "longitude": 41.0825,
1551
+ "project_id": "PRJ-0006",
1552
+ "created_at": "2025-02-20 15:20:00",
1553
+ "updated_at": "2025-02-20 15:20:00"
1554
+ },
1555
+ {
1556
+ "name": "تطوير مجمع حكومي في حائل",
1557
+ "description": "مشروع إنشاء وتطوير مجمع للدوائر الحكومية في مدينة حائل",
1558
+ "city": "حائل",
1559
+ "status": "مكتمل",
1560
+ "latitude": 27.5114,
1561
+ "longitude": 41.7208,
1562
+ "project_id": "PRJ-0007",
1563
+ "created_at": "2024-06-15 10:00:00",
1564
+ "updated_at": "2024-12-10 14:30:00"
1565
+ },
1566
+ {
1567
+ "name": "إنشاء مستشفى الإحساء العام",
1568
+ "description": "مشروع إنشاء مستشفى عام جديد في محافظة الإحساء بسعة 500 سرير",
1569
+ "city": "الإحساء",
1570
+ "status": "قيد التنفيذ",
1571
+ "latitude": 25.3753,
1572
+ "longitude": 49.5873,
1573
+ "project_id": "PRJ-0008",
1574
+ "created_at": "2024-09-01 09:45:00",
1575
+ "updated_at": "2024-09-01 09:45:00"
1576
+ },
1577
+ {
1578
+ "name": "تطوير شبكة الصرف الصحي في أبها",
1579
+ "description": "مشروع تطوير وتوسعة شبكة الصرف الصحي في مدينة أبها",
1580
+ "city": "أبها",
1581
+ "status": "جديد",
1582
+ "latitude": 18.2164,
1583
+ "longitude": 42.5053,
1584
+ "project_id": "PRJ-0009",
1585
+ "created_at": "2025-02-25 11:15:00",
1586
+ "updated_at": "2025-02-25 11:15:00"
1587
+ },
1588
+ {
1589
+ "name": "إنشاء مدينة صناعية في سكاكا",
1590
+ "description": "مشروع إنشاء مدينة صناعية جديدة في منطقة سكاكا",
1591
+ "city": "سكاكا",
1592
+ "status": "متوقف",
1593
+ "latitude": 29.9720,
1594
+ "longitude": 40.2006,
1595
+ "project_id": "PRJ-0010",
1596
+ "created_at": "2024-07-20 13:30:00",
1597
+ "updated_at": "2024-07-20 13:30:00"
1598
+ }
1599
+ ]
1600
+
1601
+ # تحديث حالة الجلسة
1602
+ st.session_state.project_locations = sample_projects
1603
+
1604
+ # حفظ البيانات
1605
+ self._save_locations_data()
1606
+
1607
+
1608
+ # فئة تحويل Folium إلى Streamlit
1609
+ class folium_static:
1610
+ """فئة لعرض خرائط Folium في Streamlit"""
1611
+
1612
+ def __init__(self, fig, width=700, height=500):
1613
+ """عرض خريطة Folium في Streamlit"""
1614
+ import streamlit.components.v1 as components
1615
+
1616
+ # تحويل خريطة Folium إلى HTML
1617
+ fig_html = fig._repr_html_()
1618
+
1619
+ # إنشاء مكون HTML مخصص
1620
+ components.html(fig_html, width=width, height=height)
1621
+
1622
+
1623
+ # تشغيل الوحدة بشكل مستقل
1624
+ def main():
1625
+ """تشغيل وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد بشكل مستقل"""
1626
+ # تهيئة الواجهة
1627
+ st.set_page_config(
1628
+ page_title="الخريطة التفاعلية | WAHBi AI",
1629
+ page_icon="🗺️",
1630
+ layout="wide",
1631
+ initial_sidebar_state="expanded",
1632
+ menu_items={
1633
+ 'Get Help': 'mailto:[email protected]',
1634
+ 'Report a bug': 'mailto:[email protected]',
1635
+ 'About': 'وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد - جزء من نظام WAHBi AI لتحليل المناقصات'
1636
+ }
1637
+ )
1638
+
1639
+ # تهيئة وحدة الخريطة التفاعلية
1640
+ interactive_map = InteractiveMap()
1641
+
1642
+ # عرض واجهة الوحدة
1643
+ interactive_map.render()
1644
+
1645
+ # تشغيل الوحدة عند استدعاء الملف مباشرة
1646
+ if __name__ == "__main__":
1647
+ main()
modules/maps/maps_app.py CHANGED
@@ -1,456 +1,53 @@
 
 
 
1
  """
2
- وحدة الخرائط والمواقع - نظام تحليل المناقصات
3
  """
4
 
 
 
5
  import streamlit as st
6
  import pandas as pd
7
  import numpy as np
8
- import folium
9
- from streamlit_folium import folium_static
10
- import json
11
- import os
12
- import sys
13
- from pathlib import Path
14
 
15
- # إضافة مسار المشروع للنظام
16
- sys.path.append(str(Path(__file__).parent.parent))
 
 
 
17
 
18
- # استيراد محسن واجهة المستخدم
19
- from styling.enhanced_ui import UIEnhancer
20
 
21
  class MapsApp:
22
- """تطبيق الخرائط والمواقع"""
23
 
24
  def __init__(self):
25
- """تهيئة تطبيق الخرائط والمواقع"""
26
- self.ui = UIEnhancer(page_title="الخرائط والمواقع - نظام تحليل المناقصات", page_icon="🗺️")
27
- self.ui.apply_theme_colors()
28
-
29
- # بيانات المشاريع (نموذجية)
30
- self.projects_data = [
31
- {
32
- "id": "P001",
33
- "name": "إنشاء مبنى إداري - الرياض",
34
- "location": "الرياض",
35
- "coordinates": [24.7136, 46.6753],
36
- "status": "جاري التنفيذ",
37
- "budget": 15000000,
38
- "completion": 45,
39
- "client": "وزارة الإسكان",
40
- "start_date": "2024-10-15",
41
- "end_date": "2025-12-30"
42
- },
43
- {
44
- "id": "P002",
45
- "name": "تطوير طريق الملك فهد - جدة",
46
- "location": "جدة",
47
- "coordinates": [21.5433, 39.1728],
48
- "status": "قيد الدراسة",
49
- "budget": 8500000,
50
- "completion": 0,
51
- "client": "أمانة جدة",
52
- "start_date": "2025-05-01",
53
- "end_date": "2026-02-28"
54
- },
55
- {
56
- "id": "P003",
57
- "name": "إنشاء مجمع سكني - الدمام",
58
- "location": "الدمام",
59
- "coordinates": [26.4207, 50.0888],
60
- "status": "مكتمل",
61
- "budget": 22000000,
62
- "completion": 100,
63
- "client": "شركة الإسكان للتطوير",
64
- "start_date": "2023-08-10",
65
- "end_date": "2025-01-15"
66
- },
67
- {
68
- "id": "P004",
69
- "name": "بناء مدرسة - أبها",
70
- "location": "أبها",
71
- "coordinates": [18.2164, 42.5053],
72
- "status": "جاري التنفيذ",
73
- "budget": 5200000,
74
- "completion": 75,
75
- "client": "وزارة التعليم",
76
- "start_date": "2024-06-20",
77
- "end_date": "2025-07-30"
78
- },
79
- {
80
- "id": "P005",
81
- "name": "تطوير شبكة مياه - المدينة المنورة",
82
- "location": "المدينة المنورة",
83
- "coordinates": [24.5247, 39.5692],
84
- "status": "جاري التنفيذ",
85
- "budget": 12800000,
86
- "completion": 30,
87
- "client": "شركة المياه الوطنية",
88
- "start_date": "2024-11-05",
89
- "end_date": "2026-03-15"
90
- }
91
- ]
92
-
93
- def run(self):
94
- """تشغيل تطبيق الخرائط والمواقع"""
95
- # إنشاء قائمة العناصر
96
- menu_items = [
97
- {"name": "لوحة المعلومات", "icon": "house"},
98
- {"name": "المناقصات والعقود", "icon": "file-text"},
99
- {"name": "تحليل المستندات", "icon": "file-earmark-text"},
100
- {"name": "نظام التسعير", "icon": "calculator"},
101
- {"name": "حاسبة تكاليف البناء", "icon": "building"},
102
- {"name": "الموارد والتكاليف", "icon": "people"},
103
- {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
104
- {"name": "إدارة المشاريع", "icon": "kanban"},
105
- {"name": "الخرائط والمواقع", "icon": "geo-alt"},
106
- {"name": "الجدول الزمني", "icon": "calendar3"},
107
- {"name": "الإشعارات", "icon": "bell"},
108
- {"name": "مقارنة المستندات", "icon": "files"},
109
- {"name": "المساعد الذكي", "icon": "robot"},
110
- {"name": "التقارير", "icon": "bar-chart"},
111
- {"name": "الإعدادات", "icon": "gear"}
112
- ]
113
-
114
- # إنشاء الشريط الجانبي
115
- selected = self.ui.create_sidebar(menu_items)
116
-
117
- # إنشاء ترويسة الصفحة
118
- self.ui.create_header("الخرائط والمواقع", "عرض وإدارة مواقع المشاريع")
119
-
120
- # إنشاء علامات تبويب للوظائف المختلفة
121
- tabs = st.tabs(["خريطة المشاريع", "تفاصيل المواقع", "إضافة موقع جديد", "تحليل المناطق"])
122
-
123
- # علامة تبويب خريطة المشاريع
124
- with tabs[0]:
125
- self.show_projects_map()
126
-
127
- # علامة تبويب تفاصيل المواقع
128
- with tabs[1]:
129
- self.show_location_details()
130
-
131
- # علامة تبويب إضافة موقع جديد
132
- with tabs[2]:
133
- self.add_new_location()
134
-
135
- # علامة تبويب تحليل المناطق
136
- with tabs[3]:
137
- self.analyze_regions()
138
 
139
- def show_projects_map(self):
140
- """عرض خريطة المشاريع"""
141
- # إنشاء فلاتر للخريطة
142
- col1, col2, col3 = st.columns(3)
143
-
144
- with col1:
145
- status_filter = st.multiselect(
146
- "حالة المشروع",
147
- options=["الكل", "جاري التنفيذ", "قيد الدراسة", "مكتمل"],
148
- default=["الكل"]
149
- )
150
-
151
- with col2:
152
- location_filter = st.multiselect(
153
- "الموقع",
154
- options=["الكل"] + list(set([p["location"] for p in self.projects_data])),
155
- default=["الكل"]
156
- )
157
-
158
- with col3:
159
- budget_range = st.slider(
160
- "نطاق الميزانية (مليون ريال)",
161
- 0.0, 25.0, (0.0, 25.0),
162
- step=0.5
163
- )
164
-
165
- # تطبيق الفلاتر
166
- filtered_projects = self.projects_data
167
-
168
- if "الكل" not in status_filter and status_filter:
169
- filtered_projects = [p for p in filtered_projects if p["status"] in status_filter]
170
-
171
- if "الكل" not in location_filter and location_filter:
172
- filtered_projects = [p for p in filtered_projects if p["location"] in location_filter]
173
-
174
- filtered_projects = [p for p in filtered_projects if budget_range[0] * 1000000 <= p["budget"] <= budget_range[1] * 1000000]
175
-
176
- # إنشاء الخريطة
177
- st.markdown("### خريطة المشاريع")
178
-
179
- # تحديد مركز الخريطة (وسط المملكة العربية السعودية تقريباً)
180
- center = [24.0, 45.0]
181
-
182
- # إنشاء خريطة folium
183
- m = folium.Map(location=center, zoom_start=5, tiles="OpenStreetMap")
184
-
185
- # إضافة المشاريع إلى الخريطة
186
- for project in filtered_projects:
187
- # تحديد لون العلامة بناءً على حالة المشروع
188
- if project["status"] == "جاري التنفيذ":
189
- color = "blue"
190
- elif project["status"] == "قيد الدراسة":
191
- color = "orange"
192
- elif project["status"] == "مكتمل":
193
- color = "green"
194
- else:
195
- color = "gray"
196
-
197
- # إنشاء نص النافذة المنبثقة
198
- popup_text = f"""
199
- <div dir="rtl" style="text-align: right; width: 200px;">
200
- <h4>{project['name']}</h4>
201
- <p><strong>الحالة:</strong> {project['status']}</p>
202
- <p><strong>الميزانية:</strong> {project['budget']:,} ريال</p>
203
- <p><strong>نسبة الإنجاز:</strong> {project['completion']}%</p>
204
- <p><strong>العميل:</strong> {project['client']}</p>
205
- <p><strong>تاريخ البدء:</strong> {project['start_date']}</p>
206
- <p><strong>تاريخ الانتهاء:</strong> {project['end_date']}</p>
207
- <a href="#" onclick="alert('تم فتح تفاصيل المشروع');">عرض التفاصيل</a>
208
- </div>
209
- """
210
-
211
- # إضافة علامة للمشروع
212
- folium.Marker(
213
- location=project["coordinates"],
214
- popup=folium.Popup(popup_text, max_width=300),
215
- tooltip=project["name"],
216
- icon=folium.Icon(color=color, icon="info-sign")
217
- ).add_to(m)
218
-
219
- # عرض الخريطة
220
- folium_static(m, width=1000, height=500)
221
-
222
- # عرض إحصائيات المشاريع
223
- st.markdown("### إحصائيات المشاريع")
224
-
225
- col1, col2, col3, col4 = st.columns(4)
226
 
227
- with col1:
228
- self.ui.create_metric_card(
229
- "إجمالي المشاريع",
230
- str(len(filtered_projects)),
231
- None,
232
- self.ui.COLORS['primary']
233
- )
234
-
235
- with col2:
236
- projects_in_progress = len([p for p in filtered_projects if p["status"] == "جاري التنفيذ"])
237
- self.ui.create_metric_card(
238
- "مشاريع جارية",
239
- str(projects_in_progress),
240
- None,
241
- self.ui.COLORS['secondary']
242
- )
243
-
244
- with col3:
245
- total_budget = sum([p["budget"] for p in filtered_projects])
246
- self.ui.create_metric_card(
247
- "إجمالي الميزانية",
248
- f"{total_budget/1000000:.1f} مليون ريال",
249
- None,
250
- self.ui.COLORS['accent']
251
- )
252
-
253
- with col4:
254
- avg_completion = np.mean([p["completion"] for p in filtered_projects])
255
- self.ui.create_metric_card(
256
- "متوسط نسبة الإنجاز",
257
- f"{avg_completion:.1f}%",
258
- None,
259
- self.ui.COLORS['success']
260
- )
261
-
262
- def show_location_details(self):
263
- """عرض تفاصيل المواقع"""
264
- st.markdown("### تفاصيل مواقع المشاريع")
265
-
266
- # إنشاء جدول بيانات المشاريع
267
- projects_df = pd.DataFrame(self.projects_data)
268
- projects_df = projects_df.rename(columns={
269
- "id": "رقم المشروع",
270
- "name": "اسم المشروع",
271
- "location": "الموقع",
272
- "status": "الحالة",
273
- "budget": "الميزانية (ريال)",
274
- "completion": "نسبة الإنجاز (%)",
275
- "client": "العميل",
276
- "start_date": "تاريخ البدء",
277
- "end_date": "تاريخ الانتهاء"
278
- })
279
-
280
- # حذف عمود الإحداثيات من العرض
281
- projects_df = projects_df.drop(columns=["coordinates"])
282
-
283
- # عرض الجدول
284
- st.dataframe(
285
- projects_df,
286
- use_container_width=True,
287
- hide_index=True
288
- )
289
-
290
- # إضافة خيار تصدير البيانات
291
- col1, col2 = st.columns([1, 5])
292
- with col1:
293
- self.ui.create_button("تصدير البيانات", "primary")
294
-
295
- # عرض تفاصيل مشروع محدد
296
- st.markdown("### تفاصيل مشروع محدد")
297
-
298
- selected_project = st.selectbox(
299
- "اختر مشروعاً لعرض التفاصيل",
300
- options=[p["name"] for p in self.projects_data]
301
- )
302
-
303
- # العثور على المشروع المحدد
304
- project = next((p for p in self.projects_data if p["name"] == selected_project), None)
305
-
306
- if project:
307
- col1, col2 = st.columns([2, 1])
308
-
309
- with col1:
310
- # عرض تفاصيل المشروع
311
- st.markdown(f"#### {project['name']}")
312
- st.markdown(f"**الموقع:** {project['location']}")
313
- st.markdown(f"**الحالة:** {project['status']}")
314
- st.markdown(f"**الميزانية:** {project['budget']:,} ريال")
315
- st.markdown(f"**نسبة الإنجاز:** {project['completion']}%")
316
- st.markdown(f"**العميل:** {project['client']}")
317
- st.markdown(f"**تاريخ البدء:** {project['start_date']}")
318
- st.markdown(f"**تاريخ الانتهاء:** {project['end_date']}")
319
-
320
- # أزرار الإجراءات
321
- col1, col2, col3 = st.columns(3)
322
- with col1:
323
- self.ui.create_button("تعديل البيانات", "primary")
324
- with col2:
325
- self.ui.create_button("عرض المستندات", "secondary")
326
- with col3:
327
- self.ui.create_button("تقرير الموقع", "accent")
328
-
329
- with col2:
330
- # عرض خريطة مصغرة للمشروع
331
- m = folium.Map(location=project["coordinates"], zoom_start=12)
332
- folium.Marker(
333
- location=project["coordinates"],
334
- tooltip=project["name"],
335
- icon=folium.Icon(color="red", icon="info-sign")
336
- ).add_to(m)
337
- folium_static(m, width=300, height=300)
338
-
339
- def add_new_location(self):
340
- """إضافة موقع جديد"""
341
- st.markdown("### إضافة موقع مشروع جديد")
342
-
343
- # نموذج إضافة موقع جديد
344
- with st.form("new_location_form"):
345
- col1, col2 = st.columns(2)
346
-
347
- with col1:
348
- project_id = st.text_input("رقم المشروع", value="P00" + str(len(self.projects_data) + 1))
349
- project_name = st.text_input("اسم المشروع")
350
- location = st.text_input("الموقع")
351
- status = st.selectbox(
352
- "الحالة",
353
- options=["جاري التنفيذ", "قيد الدراسة", "مكتمل"]
354
- )
355
- budget = st.number_input("الميزانية (ريال)", min_value=0, step=100000)
356
-
357
- with col2:
358
- completion = st.slider("نسبة الإنجاز (%)", 0, 100, 0)
359
- client = st.text_input("العميل")
360
- start_date = st.date_input("تاريخ البدء")
361
- end_date = st.date_input("تاريخ الانتهاء")
362
-
363
- st.markdown("### تحديد الموقع على الخريطة")
364
- st.markdown("انقر على الخريطة لتحديد موقع المشروع أو أدخل الإحداثيات يدوياً")
365
-
366
- col1, col2 = st.columns(2)
367
-
368
- with col1:
369
- latitude = st.number_input("خط العرض", value=24.0, format="%.4f")
370
-
371
- with col2:
372
- longitude = st.number_input("خط الطول", value=45.0, format="%.4f")
373
-
374
- # عرض الخريطة لتحديد الموقع
375
- m = folium.Map(location=[latitude, longitude], zoom_start=5)
376
- folium.Marker(
377
- location=[latitude, longitude],
378
- tooltip="موقع المشروع الجديد",
379
- icon=folium.Icon(color="red", icon="info-sign")
380
- ).add_to(m)
381
- folium_static(m, width=700, height=300)
382
-
383
- # زر الإرسال
384
- submit_button = st.form_submit_button("إضافة المشروع")
385
-
386
- if submit_button:
387
- # إضافة المشروع الجديد (في تطبيق حقيقي، سيتم حفظ البيانات في قاعدة البيانات)
388
- st.success("تم إضافة المشروع بنجاح!")
389
-
390
- # إعادة تعيين النموذج
391
- st.experimental_rerun()
392
-
393
- def analyze_regions(self):
394
- """تحليل المناطق"""
395
- st.markdown("### تحليل المناطق")
396
-
397
- # إنشاء بيانات المناطق (نموذجية)
398
- regions_data = {
399
- "المنطقة": ["الرياض", "مكة المكرمة", "المدينة المنورة", "القصيم", "المنطقة الشرقية", "عسير", "تبوك", "حائل", "الحدود الشمالية", "جازان", "نجران", "الباحة", "الجوف"],
400
- "عدد المشاريع": [15, 12, 8, 5, 18, 7, 4, 3, 2, 6, 3, 2, 3],
401
- "إجمالي الميزانية (مليون ريال)": [120, 95, 45, 30, 150, 40, 25, 18, 12, 35, 20, 15, 22],
402
- "متوسط مدة المشروع (شهر)": [18, 16, 14, 12, 20, 15, 12, 10, 9, 14, 12, 10, 11]
403
- }
404
-
405
- regions_df = pd.DataFrame(regions_data)
406
-
407
- # عرض خريطة حرارية للمناطق
408
- st.markdown("#### توزيع المشاريع حسب المناطق")
409
-
410
- # في تطبيق حقيقي، يمكن استخدام خريطة حرارية حقيقية للمملكة
411
- st.image("https://via.placeholder.com/800x400?text=خريطة+حرارية+للمشاريع+حسب+المناطق", use_column_width=True)
412
-
413
- # عرض إحصائيات المناطق
414
- st.markdown("#### إحصائيات المناطق")
415
-
416
- # عرض الجدول
417
- st.dataframe(
418
- regions_df,
419
- use_container_width=True,
420
- hide_index=True
421
- )
422
-
423
- # عرض رسوم بيانية للمقارنة
424
- st.markdown("#### مقارنة المناطق")
425
-
426
- chart_type = st.radio(
427
- "نوع الرسم البياني",
428
- options=["عدد المشاريع", "إجمالي الميزانية", "متوسط مدة المشروع"],
429
- horizontal=True
430
- )
431
-
432
- if chart_type == "عدد المشاريع":
433
- chart_data = regions_df[["المنطقة", "عدد المشاريع"]].sort_values(by="عدد المشاريع", ascending=False)
434
- st.bar_chart(chart_data.set_index("المنطقة"))
435
- elif chart_type == "إجما��ي الميزانية":
436
- chart_data = regions_df[["المنطقة", "إجمالي الميزانية (مليون ريال)"]].sort_values(by="إجمالي الميزانية (مليون ريال)", ascending=False)
437
- st.bar_chart(chart_data.set_index("المنطقة"))
438
- else:
439
- chart_data = regions_df[["المنطقة", "متوسط مدة المشروع (شهر)"]].sort_values(by="متوسط مدة المشروع (شهر)", ascending=False)
440
- st.bar_chart(chart_data.set_index("المنطقة"))
441
-
442
- # تحليل الكثافة
443
- st.markdown("#### تحليل كثافة المشاريع")
444
  st.markdown("""
445
- يوضح هذا التحليل توزيع المشاريع حسب المناطق الجغرافية، مما يساعد في:
446
- - تحديد المناطق ذات النشاط العالي
447
- - تحديد فرص النمو في المناطق الأقل نشاطاً
448
- - تخطيط الموارد بناءً على التوزيع الجغرافي
449
- """)
450
-
451
- # في تطبيق حقيقي، يمكن إضافة تحليلات أكثر تفصيلاً
 
 
452
 
453
- # تشغيل التطبيق
454
  if __name__ == "__main__":
455
- maps_app = MapsApp()
456
- maps_app.run()
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
  """
5
+ وحدة تطبيق الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
6
  """
7
 
8
+ import os
9
+ import sys
10
  import streamlit as st
11
  import pandas as pd
12
  import numpy as np
 
 
 
 
 
 
13
 
14
+ # إضافة مسار النظام للوصول للملفات المشتركة
15
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
16
+
17
+ # استيراد مكونات الخريطة التفاعلية
18
+ from modules.maps.interactive_map import InteractiveMap
19
 
 
 
20
 
21
  class MapsApp:
22
+ """وحدة تطبيق الخريطة التفاعلية"""
23
 
24
  def __init__(self):
25
+ """تهيئة وحدة تطبيق الخريطة التفاعلية"""
26
+ self.interactive_map = InteractiveMap()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ def render(self):
29
+ """عرض واجهة وحدة تطبيق الخريطة التفاعلية"""
30
+ st.markdown("<h2 class='module-title'>وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد</h2>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  st.markdown("""
33
+ <div class="module-description">
34
+ تمكنك هذه الوحدة من عرض وإدارة مواقع المشاريع على خريطة تفاعلية، مع إمكانية عرض التضاريس بشكل ثلاثي الأبعاد.
35
+ يمكنك إضافة وتحرير مواقع المشاريع، وتحليل توزيعها الجغرافي، وعرض المعلومات الطبوغرافية للمواقع.
36
+ </div>
37
+ """, unsafe_allow_html=True)
38
+
39
+ # عرض وحدة الخريطة التفاعلية
40
+ self.interactive_map.render()
41
+
42
 
43
+ # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
44
  if __name__ == "__main__":
45
+ st.set_page_config(
46
+ page_title="الخريطة التفاعلية | WAHBi AI",
47
+ page_icon="🗺️",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded"
50
+ )
51
+
52
+ app = MapsApp()
53
+ app.render()
modules/notifications/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # ملف تهيئة وحدة الإشعارات الذكية
modules/notifications/notifications_app.py CHANGED
@@ -1,672 +1,53 @@
 
 
 
1
  """
2
- وحدة الإشعارات الذكية - نظام تحليل المناقصات
3
  """
4
 
5
- import streamlit as st
6
- import pandas as pd
7
- import datetime
8
- import json
9
  import os
10
  import sys
11
- from pathlib import Path
 
 
12
 
13
- # إضافة مسار المشروع للنظام
14
- sys.path.append(str(Path(__file__).parent.parent))
 
 
 
15
 
16
- # استيراد محسن واجهة المستخدم
17
- from styling.enhanced_ui import UIEnhancer
18
 
19
  class NotificationsApp:
20
- """تطبيق الإشعارات الذكية"""
21
 
22
  def __init__(self):
23
- """تهيئة تطبيق الإشعارات الذكية"""
24
- self.ui = UIEnhancer(page_title="الإشعارات الذكية - نظام تحليل المناقصات", page_icon="🔔")
25
- self.ui.apply_theme_colors()
26
-
27
- # بيانات الإشعارات (نموذجية)
28
- self.notifications_data = [
29
- {
30
- "id": "N001",
31
- "title": "موعد تسليم مناقصة",
32
- "message": "موعد تسليم مناقصة T-2025-001 (إنشاء مبنى إداري) بعد 5 أيام",
33
- "type": "deadline",
34
- "priority": "high",
35
- "related_entity": "T-2025-001",
36
- "created_at": "2025-03-25T10:30:00",
37
- "is_read": False
38
- },
39
- {
40
- "id": "N002",
41
- "title": "ترسية مناقصة",
42
- "message": "تم ترسية مناقصة T-2025-003 (توريد معدات) بنجاح",
43
- "type": "award",
44
- "priority": "medium",
45
- "related_entity": "T-2025-003",
46
- "created_at": "2025-03-28T14:15:00",
47
- "is_read": True
48
- },
49
- {
50
- "id": "N003",
51
- "title": "تحديث مستندات",
52
- "message": "تم تحديث مستندات مناقصة T-2025-002 (صيانة طرق)",
53
- "type": "document",
54
- "priority": "medium",
55
- "related_entity": "T-2025-002",
56
- "created_at": "2025-03-29T09:45:00",
57
- "is_read": False
58
- },
59
- {
60
- "id": "N004",
61
- "title": "تغيير في المواصفات",
62
- "message": "تم تغيير المواصفات الفنية لمناقصة T-2025-001 (إنشاء مبنى إداري)",
63
- "type": "change",
64
- "priority": "high",
65
- "related_entity": "T-2025-001",
66
- "created_at": "2025-03-27T11:20:00",
67
- "is_read": False
68
- },
69
- {
70
- "id": "N005",
71
- "title": "تأخير في المشروع",
72
- "message": "تأخير في تنفيذ مشروع P002 (تطوير طريق الملك فهد - جدة)",
73
- "type": "delay",
74
- "priority": "high",
75
- "related_entity": "P002",
76
- "created_at": "2025-03-26T16:10:00",
77
- "is_read": True
78
- },
79
- {
80
- "id": "N006",
81
- "title": "اكتمال مرحلة",
82
- "message": "اكتمال مرحلة الأساسات في مشروع P001 (إنشاء مبنى إداري - الرياض)",
83
- "type": "milestone",
84
- "priority": "low",
85
- "related_entity": "P001",
86
- "created_at": "2025-03-24T13:30:00",
87
- "is_read": True
88
- },
89
- {
90
- "id": "N007",
91
- "title": "طلب معلومات إضافية",
92
- "message": "طلب معلومات إضافية لمناقصة T-2025-004 (تجهيز مختبرات)",
93
- "type": "request",
94
- "priority": "medium",
95
- "related_entity": "T-2025-004",
96
- "created_at": "2025-03-30T08:15:00",
97
- "is_read": False
98
- },
99
- {
100
- "id": "N008",
101
- "title": "تحديث أسعار المواد",
102
- "message": "تم تحديث أسعار مواد البناء في قاعدة البيانات",
103
- "type": "update",
104
- "priority": "low",
105
- "related_entity": "DB-MATERIALS",
106
- "created_at": "2025-03-29T15:40:00",
107
- "is_read": False
108
- },
109
- {
110
- "id": "N009",
111
- "title": "اجتماع فريق العمل",
112
- "message": "اجتماع فريق العمل لمناقشة مناقصة T-2025-001 غداً الساعة 10:00 صباحاً",
113
- "type": "meeting",
114
- "priority": "medium",
115
- "related_entity": "T-2025-001",
116
- "created_at": "2025-03-28T16:20:00",
117
- "is_read": True
118
- },
119
- {
120
- "id": "N010",
121
- "title": "تغيير في الميزانية",
122
- "message": "تم تغيير الميزانية المخصصة لمشروع P004 (بناء مدرسة - أبها)",
123
- "type": "budget",
124
- "priority": "high",
125
- "related_entity": "P004",
126
- "created_at": "2025-03-25T14:50:00",
127
- "is_read": False
128
- }
129
- ]
130
-
131
- # إعدادات الإشعارات (نموذجية)
132
- self.notification_settings = {
133
- "deadline": True,
134
- "award": True,
135
- "document": True,
136
- "change": True,
137
- "delay": True,
138
- "milestone": True,
139
- "request": True,
140
- "update": True,
141
- "meeting": True,
142
- "budget": True,
143
- "email_notifications": True,
144
- "sms_notifications": False,
145
- "push_notifications": True,
146
- "notification_frequency": "realtime"
147
- }
148
-
149
- def run(self):
150
- """تشغيل تطبيق الإشعارات الذكية"""
151
- # إنشاء قائمة العناصر
152
- menu_items = [
153
- {"name": "لوحة المعلومات", "icon": "house"},
154
- {"name": "المناقصات والعقود", "icon": "file-text"},
155
- {"name": "تحليل المستندات", "icon": "file-earmark-text"},
156
- {"name": "نظام التسعير", "icon": "calculator"},
157
- {"name": "حاسبة تكاليف البناء", "icon": "building"},
158
- {"name": "الموارد والتكاليف", "icon": "people"},
159
- {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
160
- {"name": "إدارة المشاريع", "icon": "kanban"},
161
- {"name": "الخرائط والمواقع", "icon": "geo-alt"},
162
- {"name": "الجدول الزمني", "icon": "calendar3"},
163
- {"name": "الإشعارات", "icon": "bell"},
164
- {"name": "مقارنة المستندات", "icon": "files"},
165
- {"name": "المساعد الذكي", "icon": "robot"},
166
- {"name": "التقارير", "icon": "bar-chart"},
167
- {"name": "الإعدادات", "icon": "gear"}
168
- ]
169
-
170
- # إنشاء الشريط الجانبي
171
- selected = self.ui.create_sidebar(menu_items)
172
-
173
- # إنشاء ترويسة الصفحة
174
- self.ui.create_header("الإشعارات الذكية", "إدارة ومتابعة الإشعارات والتنبيهات")
175
-
176
- # إنشاء علامات تبويب للوظائف المختلفة
177
- tabs = st.tabs(["الإشعارات الحالية", "إعدادات الإشعارات", "إنشاء إشعار", "سجل الإشعارات"])
178
-
179
- # علامة تبويب الإشعارات الحالية
180
- with tabs[0]:
181
- self.show_current_notifications()
182
-
183
- # علامة تبويب إعدادات الإشعارات
184
- with tabs[1]:
185
- self.show_notification_settings()
186
-
187
- # علامة تبويب إنشاء إشعار
188
- with tabs[2]:
189
- self.create_notification()
190
-
191
- # علامة تبويب سجل الإشعارات
192
- with tabs[3]:
193
- self.show_notification_history()
194
-
195
- def show_current_notifications(self):
196
- """عرض الإشعارات الحالية"""
197
- st.markdown("### الإشعارات الحالية")
198
-
199
- # إنشاء فلاتر للإشعارات
200
- col1, col2, col3 = st.columns(3)
201
-
202
- with col1:
203
- type_filter = st.multiselect(
204
- "نوع الإشعار",
205
- options=["الكل", "موعد نهائي", "ترسية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"],
206
- default=["الكل"]
207
- )
208
-
209
- with col2:
210
- priority_filter = st.multiselect(
211
- "الأولوية",
212
- options=["الكل", "عالية", "متوسطة", "منخفضة"],
213
- default=["الكل"]
214
- )
215
-
216
- with col3:
217
- read_filter = st.radio(
218
- "الحالة",
219
- options=["الكل", "غير مقروءة", "مقروءة"],
220
- horizontal=True
221
- )
222
-
223
- # تطبيق الفلاتر
224
- filtered_notifications = self.notifications_data
225
-
226
- # تحويل أنواع الإشعارات من الإنجليزية إلى العربية للفلترة
227
- type_mapping = {
228
- "موعد نهائي": "deadline",
229
- "ترسية": "award",
230
- "مستند": "document",
231
- "تغيير": "change",
232
- "تأخير": "delay",
233
- "مرحلة": "milestone",
234
- "طلب": "request",
235
- "تحديث": "update",
236
- "اجتماع": "meeting",
237
- "ميزانية": "budget"
238
- }
239
-
240
- # تحويل الأولويات من العربية إلى الإنجليزية للفلترة
241
- priority_mapping = {
242
- "عالية": "high",
243
- "متوسطة": "medium",
244
- "منخفضة": "low"
245
- }
246
-
247
- if "الكل" not in type_filter and type_filter:
248
- filtered_types = [type_mapping[t] for t in type_filter if t in type_mapping]
249
- filtered_notifications = [n for n in filtered_notifications if n["type"] in filtered_types]
250
-
251
- if "الكل" not in priority_filter and priority_filter:
252
- filtered_priorities = [priority_mapping[p] for p in priority_filter if p in priority_mapping]
253
- filtered_notifications = [n for n in filtered_notifications if n["priority"] in filtered_priorities]
254
-
255
- if read_filter == "غير مقروءة":
256
- filtered_notifications = [n for n in filtered_notifications if not n["is_read"]]
257
- elif read_filter == "مقروءة":
258
- filtered_notifications = [n for n in filtered_notifications if n["is_read"]]
259
-
260
- # عرض عدد الإشعارات غير المقروءة
261
- unread_count = len([n for n in filtered_notifications if not n["is_read"]])
262
-
263
- st.markdown(f"**عدد الإشعارات غير المقروءة:** {unread_count}")
264
-
265
- # زر تحديث وتعليم الكل كمقروء
266
- col1, col2 = st.columns([1, 1])
267
- with col1:
268
- if st.button("تحديث الإشعارات", use_container_width=True):
269
- st.success("تم تحديث الإشعارات بنجاح")
270
-
271
- with col2:
272
- if st.button("تعليم الكل كمقروء", use_container_width=True):
273
- st.success("تم تعليم جميع الإشعارات كمقروءة")
274
-
275
- # عرض الإشعارات
276
- if not filtered_notifications:
277
- st.info("لا توجد إشعارات تطابق الفلاتر المحددة")
278
- else:
279
- for notification in filtered_notifications:
280
- self.display_notification(notification)
281
-
282
- def display_notification(self, notification):
283
- """عرض إشعار واحد"""
284
- # تحديد لون الإشعار بناءً على الأولوية
285
- if notification["priority"] == "high":
286
- color = self.ui.COLORS['danger']
287
- priority_text = "عالية"
288
- elif notification["priority"] == "medium":
289
- color = self.ui.COLORS['warning']
290
- priority_text = "متوسطة"
291
- else:
292
- color = self.ui.COLORS['secondary']
293
- priority_text = "منخفضة"
294
-
295
- # تحويل نوع الإشعار إلى العربية
296
- type_mapping = {
297
- "deadline": "موعد نهائي",
298
- "award": "ترسية",
299
- "document": "مستند",
300
- "change": "تغيير",
301
- "delay": "تأخير",
302
- "milestone": "مرحلة",
303
- "request": "طلب",
304
- "update": "تحديث",
305
- "meeting": "اجتماع",
306
- "budget": "ميزانية"
307
- }
308
-
309
- notification_type = type_mapping.get(notification["type"], notification["type"])
310
-
311
- # تحويل التاريخ إلى تنسيق مناسب
312
- created_at = datetime.datetime.fromisoformat(notification["created_at"])
313
- formatted_date = created_at.strftime("%Y-%m-%d %H:%M")
314
-
315
- # تحديد أيقونة الإشعار
316
- icon_mapping = {
317
- "deadline": "⏰",
318
- "award": "🏆",
319
- "document": "📄",
320
- "change": "🔄",
321
- "delay": "⚠️",
322
- "milestone": "🏁",
323
- "request": "❓",
324
- "update": "🔄",
325
- "meeting": "👥",
326
- "budget": "💰"
327
- }
328
-
329
- icon = icon_mapping.get(notification["type"], "📌")
330
-
331
- # إنشاء بطاقة الإشعار
332
- st.markdown(
333
- f"""
334
- <div style="border-left: 5px solid {color}; padding: 10px; margin-bottom: 10px; background-color: {'#f8f9fa' if st.session_state.theme == 'light' else '#2b2b2b'}; border-radius: 5px; {'opacity: 0.7;' if notification['is_read'] else ''}">
335
- <div style="display: flex; justify-content: space-between; align-items: center;">
336
- <div>
337
- <h4 style="margin: 0;">{icon} {notification['title']}</h4>
338
- <p style="margin: 5px 0;">{notification['message']}</p>
339
- <div style="display: flex; gap: 10px; font-size: 0.8em; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">
340
- <span>النوع: {notification_type}</span>
341
- <span>الأولوية: {priority_text}</span>
342
- <span>التاريخ: {formatted_date}</span>
343
- </div>
344
- </div>
345
- <div>
346
- <button style="background: none; border: none; cursor: pointer; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">✓</button>
347
- <button style="background: none; border: none; cursor: pointer; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">🗑️</button>
348
- </div>
349
- </div>
350
- </div>
351
- """,
352
- unsafe_allow_html=True
353
- )
354
-
355
- def show_notification_settings(self):
356
- """عرض إعدادات الإشعارات"""
357
- st.markdown("### إعدادات الإشعارات")
358
-
359
- # إنشاء نموذج الإعدادات
360
- with st.form("notification_settings_form"):
361
- st.markdown("#### أنواع الإشعارات")
362
-
363
- col1, col2 = st.columns(2)
364
-
365
- with col1:
366
- deadline = st.checkbox("المواعيد النهائية", value=self.notification_settings["deadline"])
367
- award = st.checkbox("ترسية المناقصات", value=self.notification_settings["award"])
368
- document = st.checkbox("تحديثات المستندات", value=self.notification_settings["document"])
369
- change = st.checkbox("التغييرات في المواصفات", value=self.notification_settings["change"])
370
- delay = st.checkbox("التأخيرات في المشاريع", value=self.notification_settings["delay"])
371
-
372
- with col2:
373
- milestone = st.checkbox("اكتمال المراحل", value=self.notification_settings["milestone"])
374
- request = st.checkbox("طلبات المعلومات", value=self.notification_settings["request"])
375
- update = st.checkbox("تحديثات النظام", value=self.notification_settings["update"])
376
- meeting = st.checkbox("الاجتماعات", value=self.notification_settings["meeting"])
377
- budget = st.checkbox("تغييرات الميزانية", value=self.notification_settings["budget"])
378
-
379
- st.markdown("#### طرق الإشعار")
380
-
381
- col1, col2, col3 = st.columns(3)
382
-
383
- with col1:
384
- email_notifications = st.checkbox("البريد الإلكتروني", value=self.notification_settings["email_notifications"])
385
-
386
- with col2:
387
- sms_notifications = st.checkbox("الرسائل النصية", value=self.notification_settings["sms_notifications"])
388
-
389
- with col3:
390
- push_notifications = st.checkbox("إشعارات الويب", value=self.notification_settings["push_notifications"])
391
-
392
- st.markdown("#### تكرار الإشعارات")
393
-
394
- notification_frequency = st.radio(
395
- "تكرار الإشعارات",
396
- options=["في الوقت الحقيقي", "مرة واحدة يومياً", "مرة واحدة أسبوعياً"],
397
- index=0 if self.notification_settings["notification_frequency"] == "realtime" else 1 if self.notification_settings["notification_frequency"] == "daily" else 2,
398
- horizontal=True
399
- )
400
-
401
- # زر حفظ الإعدادات
402
- submit_button = st.form_submit_button("حفظ الإعدادات")
403
-
404
- if submit_button:
405
- # تحديث الإعدادات (في تطبيق حقيقي، سيتم حفظ الإعدادات في قاعدة البيانات)
406
- self.notification_settings.update({
407
- "deadline": deadline,
408
- "award": award,
409
- "document": document,
410
- "change": change,
411
- "delay": delay,
412
- "milestone": milestone,
413
- "request": request,
414
- "update": update,
415
- "meeting": meeting,
416
- "budget": budget,
417
- "email_notifications": email_notifications,
418
- "sms_notifications": sms_notifications,
419
- "push_notifications": push_notifications,
420
- "notification_frequency": "realtime" if notification_frequency == "في الوقت الحقيقي" else "daily" if notification_frequency == "مرة واحدة يومياً" else "weekly"
421
- })
422
-
423
- st.success("تم حفظ الإعدادات بنجاح")
424
-
425
- # إعدادات متقدمة
426
- st.markdown("### إعدادات متقدمة")
427
-
428
- with st.expander("إعدادات متقدمة"):
429
- st.markdown("#### جدولة الإشعارات")
430
-
431
- col1, col2 = st.columns(2)
432
-
433
- with col1:
434
- st.time_input("وقت الإشعارات اليومية", datetime.time(9, 0))
435
-
436
- with col2:
437
- st.selectbox(
438
- "يوم الإشعارات الأسبوعية",
439
- options=["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"],
440
- index=0
441
- )
442
-
443
- st.markdown("#### فلترة الإشعارات")
444
-
445
- min_priority = st.select_slider(
446
- "الحد الأدنى للأولوية",
447
- options=["منخفضة", "متوسطة", "عالية"],
448
- value="منخفضة"
449
- )
450
-
451
- st.markdown("#### حفظ الإشعارات")
452
-
453
- retention_period = st.slider(
454
- "فترة الاحتفاظ بالإشعارات (بالأيام)",
455
- min_value=7,
456
- max_value=365,
457
- value=90,
458
- step=1
459
- )
460
-
461
- if st.button("حفظ الإعدادات المتقدمة"):
462
- st.success("تم حفظ الإعدادات المتقدمة بنجاح")
463
 
464
- def create_notification(self):
465
- """إنشاء إشعار جديد"""
466
- st.markdown("### إنشاء إشعار جديد")
467
-
468
- # إنشاء نموذج إشعار جديد
469
- with st.form("new_notification_form"):
470
- title = st.text_input("عنوان الإشعار")
471
- message = st.text_area("نص الإشعار")
472
-
473
- col1, col2 = st.columns(2)
474
-
475
- with col1:
476
- notification_type = st.selectbox(
477
- "نوع الإشعار",
478
- options=["موعد نهائي", "ترسية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"]
479
- )
480
-
481
- # تحويل نوع الإشعار إلى الإنجليزية
482
- type_mapping = {
483
- "موعد نهائي": "deadline",
484
- "ترسية": "award",
485
- "مستند": "document",
486
- "تغيير": "change",
487
- "تأخير": "delay",
488
- "مرحلة": "milestone",
489
- "طلب": "request",
490
- "تحديث": "update",
491
- "اجتماع": "meeting",
492
- "ميزانية": "budget"
493
- }
494
-
495
- notification_type_en = type_mapping.get(notification_type, "update")
496
-
497
- with col2:
498
- priority = st.selectbox(
499
- "الأولوية",
500
- options=["عالية", "متوسطة", "منخفضة"]
501
- )
502
-
503
- # تحويل الأولوية إلى الإنجليزية
504
- priority_mapping = {
505
- "عالية": "high",
506
- "متوسطة": "medium",
507
- "منخفضة": "low"
508
- }
509
-
510
- priority_en = priority_mapping.get(priority, "medium")
511
-
512
- related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة أو المشروع)")
513
-
514
- col1, col2 = st.columns(2)
515
-
516
- with col1:
517
- send_email = st.checkbox("إرسال بريد إلكتروني")
518
-
519
- with col2:
520
- send_push = st.checkbox("إرسال إشعار ويب")
521
-
522
- # زر إنشاء الإشعار
523
- submit_button = st.form_submit_button("إنشاء الإشعار")
524
-
525
- if submit_button:
526
- if not title or not message:
527
- st.error("يرجى ملء جميع الحقول المطلوبة")
528
- else:
529
- # إنشاء الإشعار الجديد (في تطبيق حقيقي، سيتم حفظ الإشعار في قاعدة البيانات)
530
- new_notification = {
531
- "id": f"N{len(self.notifications_data) + 1:03d}",
532
- "title": title,
533
- "message": message,
534
- "type": notification_type_en,
535
- "priority": priority_en,
536
- "related_entity": related_entity,
537
- "created_at": datetime.datetime.now().isoformat(),
538
- "is_read": False
539
- }
540
-
541
- # إضافة الإشعار إلى القائمة (في تطبيق حقيقي، سيتم إضافته إلى قاعدة البيانات)
542
- self.notifications_data.append(new_notification)
543
-
544
- st.success("تم إنشاء الإشعار بنجاح")
545
-
546
- # إظهار تفاصيل الإرسال
547
- if send_email:
548
- st.info("تم إرسال الإشعار عبر البريد الإلكتروني")
549
-
550
- if send_push:
551
- st.info("تم إرسال إشعار الويب")
552
-
553
- def show_notification_history(self):
554
- """عرض سجل الإشعارات"""
555
- st.markdown("### سجل الإشعارات")
556
-
557
- # إنشاء فلاتر للسجل
558
- col1, col2 = st.columns(2)
559
-
560
- with col1:
561
- date_range = st.date_input(
562
- "نطاق التاريخ",
563
- value=(
564
- datetime.datetime.now() - datetime.timedelta(days=30),
565
- datetime.datetime.now()
566
- )
567
- )
568
-
569
- with col2:
570
- entity_filter = st.text_input("البحث حسب الكيان المرتبط")
571
-
572
- # تحويل البيانات إلى DataFrame
573
- notifications_df = pd.DataFrame(self.notifications_data)
574
-
575
- # تحويل حقل created_at إلى datetime
576
- notifications_df["created_at"] = pd.to_datetime(notifications_df["created_at"])
577
-
578
- # تطبيق فلتر التاريخ
579
- if len(date_range) == 2:
580
- start_date, end_date = date_range
581
- start_date = pd.to_datetime(start_date)
582
- end_date = pd.to_datetime(end_date) + datetime.timedelta(days=1) # لتضمين اليوم الأخير
583
- notifications_df = notifications_df[(notifications_df["created_at"] >= start_date) & (notifications_df["created_at"] <= end_date)]
584
-
585
- # تطبيق فلتر الكيان المرتبط
586
- if entity_filter:
587
- notifications_df = notifications_df[notifications_df["related_entity"].str.contains(entity_filter, case=False)]
588
-
589
- # تحويل أنواع الإشعارات من الإنجليزية إلى العربية للعرض
590
- type_mapping = {
591
- "deadline": "موعد نهائي",
592
- "award": "ترسية",
593
- "document": "مستند",
594
- "change": "تغيير",
595
- "delay": "تأخير",
596
- "milestone": "مرحلة",
597
- "request": "طلب",
598
- "update": "تحديث",
599
- "meeting": "اجتماع",
600
- "budget": "ميزانية"
601
- }
602
-
603
- notifications_df["type_ar"] = notifications_df["type"].map(type_mapping)
604
-
605
- # تحويل الأولويات من الإنجليزية إلى العربية للعرض
606
- priority_mapping = {
607
- "high": "عالية",
608
- "medium": "متوسطة",
609
- "low": "منخفضة"
610
- }
611
-
612
- notifications_df["priority_ar"] = notifications_df["priority"].map(priority_mapping)
613
-
614
- # تحويل حالة القراءة إلى نص
615
- notifications_df["is_read_text"] = notifications_df["is_read"].map({True: "مقروءة", False: "غير مقروءة"})
616
-
617
- # تنسيق التاريخ
618
- notifications_df["created_at_formatted"] = notifications_df["created_at"].dt.strftime("%Y-%m-%d %H:%M")
619
-
620
- # إعادة ترتيب الأعمدة وتغيير أسمائها
621
- display_df = notifications_df[[
622
- "id", "title", "type_ar", "priority_ar", "related_entity", "created_at_formatted", "is_read_text"
623
- ]].rename(columns={
624
- "id": "الرقم",
625
- "title": "العنوان",
626
- "type_ar": "النوع",
627
- "priority_ar": "الأولوية",
628
- "related_entity": "الكيان المرتبط",
629
- "created_at_formatted": "تاريخ الإنشاء",
630
- "is_read_text": "الحالة"
631
- })
632
-
633
- # عرض الجدول
634
- st.dataframe(
635
- display_df,
636
- use_container_width=True,
637
- hide_index=True
638
- )
639
-
640
- # إضافة خيارات التصدير
641
- col1, col2 = st.columns([1, 5])
642
- with col1:
643
- if st.button("تصدير البيانات", use_container_width=True):
644
- st.success("تم تصدير البيانات بنجاح")
645
-
646
- # عرض إحصائيات
647
- st.markdown("### إحصائيات الإشعارات")
648
-
649
- col1, col2, col3 = st.columns(3)
650
-
651
- with col1:
652
- # إحصائيات حسب النوع
653
- type_counts = notifications_df["type_ar"].value_counts()
654
- st.markdown("#### الإشعارات حسب النوع")
655
- st.bar_chart(type_counts)
656
-
657
- with col2:
658
- # إحصائيات حسب الأولوية
659
- priority_counts = notifications_df["priority_ar"].value_counts()
660
- st.markdown("#### الإشعارات حسب الأولوية")
661
- st.bar_chart(priority_counts)
662
-
663
- with col3:
664
- # إحصائيات حسب الحالة
665
- read_counts = notifications_df["is_read_text"].value_counts()
666
- st.markdown("#### الإشعارات حسب الحالة")
667
- st.bar_chart(read_counts)
668
 
669
- # تشغيل التطبيق
 
670
  if __name__ == "__main__":
671
- notifications_app = NotificationsApp()
672
- notifications_app.run()
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
  """
5
+ وحدة تطبيق نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات
6
  """
7
 
 
 
 
 
8
  import os
9
  import sys
10
+ import streamlit as st
11
+ import pandas as pd
12
+ import numpy as np
13
 
14
+ # إضافة مسار النظام للوصول للملفات المشتركة
15
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
16
+
17
+ # استيراد مكونات الإشعارات الذكية
18
+ from modules.notifications.smart_notifications import SmartNotificationSystem
19
 
 
 
20
 
21
  class NotificationsApp:
22
+ """وحدة تطبيق نظام الإشعارات الذكي"""
23
 
24
  def __init__(self):
25
+ """تهيئة وحدة تطبيق نظام الإشعارات الذكي"""
26
+ self.smart_notification_system = SmartNotificationSystem()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ def render(self):
29
+ """عرض واجهة وحدة تطبيق نظام الإشعارات الذكي"""
30
+ st.markdown("<h2 class='module-title'>نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات</h2>", unsafe_allow_html=True)
31
+
32
+ st.markdown("""
33
+ <div class="module-description">
34
+ يتيح لك نظام الإشعارات الذكي متابعة تحديثات المشاريع وتلقي تنبيهات مخصصة حسب أدوارك واهتماماتك.
35
+ يمكنك تخصيص إعدادات الإشعارات وجدولة التذكيرات للمواعيد النهائية والمهام.
36
+ </div>
37
+ """, unsafe_allow_html=True)
38
+
39
+ # عرض نظام الإشعارات الذكي
40
+ self.smart_notification_system.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+
43
+ # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
44
  if __name__ == "__main__":
45
+ st.set_page_config(
46
+ page_title="نظام الإشعارات الذكي | WAHBi AI",
47
+ page_icon="🔔",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded"
50
+ )
51
+
52
+ app = NotificationsApp()
53
+ app.render()
modules/notifications/smart_notifications.py ADDED
@@ -0,0 +1,1237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ وحدة نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات
6
+ تتيح هذه الوحدة متابعة تحديثات المشاريع وإرسال تنبيهات ذكية مخصصة للمستخدمين بناءً على أدوارهم واهتماماتهم
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import streamlit as st
12
+ import pandas as pd
13
+ import numpy as np
14
+ import json
15
+ import datetime
16
+ import time
17
+ import threading
18
+ import logging
19
+ from typing import List, Dict, Any, Tuple, Optional, Union
20
+
21
+ # إضافة مسار النظام للوصول للملفات المشتركة
22
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
23
+
24
+ # استيراد مكونات واجهة المستخدم
25
+ from utils.components.header import render_header
26
+ from utils.components.credits import render_credits
27
+ from utils.helpers import format_number, format_currency, styled_button
28
+
29
+
30
+ class SmartNotificationSystem:
31
+ """فئة نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات"""
32
+
33
+ def __init__(self):
34
+ """تهيئة نظام الإشعارات الذكي"""
35
+ # تهيئة مجلدات حفظ البيانات
36
+ self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/notifications"))
37
+ os.makedirs(self.data_dir, exist_ok=True)
38
+
39
+ # تهيئة قائمة الإشعارات
40
+ if 'notifications' not in st.session_state:
41
+ st.session_state.notifications = []
42
+
43
+ if 'unread_count' not in st.session_state:
44
+ st.session_state.unread_count = 0
45
+
46
+ if 'notification_channels' not in st.session_state:
47
+ st.session_state.notification_channels = {
48
+ "browser": True,
49
+ "email": False,
50
+ "sms": False,
51
+ "mobile_app": False
52
+ }
53
+
54
+ if 'notification_preferences' not in st.session_state:
55
+ st.session_state.notification_preferences = {
56
+ "project_updates": True,
57
+ "document_analysis": True,
58
+ "deadline_reminders": True,
59
+ "risk_alerts": True,
60
+ "price_changes": True,
61
+ "team_mentions": True,
62
+ "system_updates": True
63
+ }
64
+
65
+ # تحميل الإشعارات المحفوظة
66
+ self._load_notifications()
67
+
68
+ # تسجيل الأحداث
69
+ logging.basicConfig(
70
+ level=logging.INFO,
71
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
72
+ handlers=[
73
+ logging.FileHandler(os.path.join(self.data_dir, "notifications.log")),
74
+ logging.StreamHandler()
75
+ ]
76
+ )
77
+ self.logger = logging.getLogger("smart_notifications")
78
+
79
+ def render(self):
80
+ """عرض واجهة نظام الإشعارات الذكي"""
81
+ render_header("نظام الإشعارات الذكي")
82
+
83
+ # تبويبات الوحدة
84
+ tabs = st.tabs([
85
+ "جميع الإشعارات",
86
+ "إشعارات غير مقروءة",
87
+ "إعدادات الإشعارات",
88
+ "جدولة الإشعارات",
89
+ "تقارير وإحصائيات"
90
+ ])
91
+
92
+ # تبويب جميع الإشعارات
93
+ with tabs[0]:
94
+ self._render_all_notifications()
95
+
96
+ # تبويب الإشعارات غير المقروءة
97
+ with tabs[1]:
98
+ self._render_unread_notifications()
99
+
100
+ # تبويب إعدادات الإشعارات
101
+ with tabs[2]:
102
+ self._render_notification_settings()
103
+
104
+ # تبويب جدولة الإشعارات
105
+ with tabs[3]:
106
+ self._render_notification_scheduling()
107
+
108
+ # تبويب تقارير وإحصائيات
109
+ with tabs[4]:
110
+ self._render_notification_analytics()
111
+
112
+ # عرض حقوق النشر
113
+ render_credits()
114
+
115
+ def _render_all_notifications(self):
116
+ """عرض جميع الإشعارات"""
117
+ st.markdown("""
118
+ <div class='custom-box info-box'>
119
+ <h3>🔔 جميع الإشعارات</h3>
120
+ <p>عرض كافة الإشعارات والتنبيهات الخاصة بالمشاريع والنظام.</p>
121
+ </div>
122
+ """, unsafe_allow_html=True)
123
+
124
+ # أزرار التحكم
125
+ col1, col2, col3 = st.columns([1, 1, 1])
126
+
127
+ with col1:
128
+ if styled_button("تحديث الإشعارات", key="refresh_notifications", type="primary", icon="🔄"):
129
+ self._load_notifications()
130
+ st.success("تم تحديث الإشعارات بنجاح")
131
+
132
+ with col2:
133
+ if styled_button("تعليم الكل كمقروء", key="mark_all_read", type="secondary", icon="✓"):
134
+ self._mark_all_as_read()
135
+ st.success("تم تعليم جميع الإشعارات كمقروءة")
136
+
137
+ with col3:
138
+ if styled_button("حذف جميع الإشعارات", key="clear_notifications", type="danger", icon="🗑️"):
139
+ confirmed = st.text_input("اكتب 'تأكيد' لحذف جميع الإشعارات", key="confirm_clear")
140
+ if confirmed == "تأكيد":
141
+ self._clear_all_notifications()
142
+ st.success("تم حذف جميع الإشعارات بنجاح")
143
+
144
+ # فلترة الإشعارات
145
+ filter_col1, filter_col2 = st.columns(2)
146
+
147
+ with filter_col1:
148
+ notification_type = st.multiselect(
149
+ "تصفية حسب النوع",
150
+ options=[
151
+ "تحديث مشروع", "وثيقة جديدة", "تذكير موعد نهائي",
152
+ "تنبيه مخاطر", "تغيير سعر", "إشارة فريق العمل", "تحديث النظام"
153
+ ],
154
+ key="filter_notification_type"
155
+ )
156
+
157
+ with filter_col2:
158
+ date_range = st.date_input(
159
+ "نطاق التاريخ",
160
+ value=(
161
+ datetime.datetime.now() - datetime.timedelta(days=30),
162
+ datetime.datetime.now()
163
+ ),
164
+ key="filter_date_range"
165
+ )
166
+
167
+ # تصفية الإشعارات
168
+ filtered_notifications = self._filter_notifications(
169
+ notification_type=notification_type,
170
+ date_range=date_range
171
+ )
172
+
173
+ # عرض الإشعارات المصفاة
174
+ if filtered_notifications:
175
+ for notification in filtered_notifications:
176
+ self._render_notification_card(notification)
177
+ else:
178
+ st.info("لا توجد إشعارات متاحة")
179
+
180
+ def _render_unread_notifications(self):
181
+ """عرض الإشعارات غير المقروءة"""
182
+ st.markdown("""
183
+ <div class='custom-box info-box'>
184
+ <h3>🔔 الإشعارات غير المقروءة</h3>
185
+ <p>عرض الإشعارات والتنبيهات التي لم تتم قراءتها بعد.</p>
186
+ </div>
187
+ """, unsafe_allow_html=True)
188
+
189
+ # أزرار التحكم
190
+ col1, col2 = st.columns(2)
191
+
192
+ with col1:
193
+ if styled_button("تحديث الإشعارات", key="refresh_unread", type="primary", icon="🔄"):
194
+ self._load_notifications()
195
+ st.success("تم تحديث الإشعارات بنجاح")
196
+
197
+ with col2:
198
+ if styled_button("تعليم الكل كمقروء", key="mark_unread_read", type="secondary", icon="✓"):
199
+ self._mark_all_as_read()
200
+ st.success("تم تعليم جميع الإشعارات كمقروءة")
201
+
202
+ # فلترة الإشعارات غير المقروءة
203
+ unread_notifications = [n for n in st.session_state.notifications if not n.get("read", False)]
204
+
205
+ # عرض الإشعارات غير المقروءة
206
+ if unread_notifications:
207
+ for notification in unread_notifications:
208
+ self._render_notification_card(notification, show_mark_button=True)
209
+ else:
210
+ st.success("لا توجد إشعارات غير مقروءة")
211
+
212
+ def _render_notification_settings(self):
213
+ """عرض إعدادات الإشعارات"""
214
+ st.markdown("""
215
+ <div class='custom-box info-box'>
216
+ <h3>⚙️ إعدادات الإشعارات</h3>
217
+ <p>تخصيص إعدادات وتفضيلات الإشعارات الخاصة بك.</p>
218
+ </div>
219
+ """, unsafe_allow_html=True)
220
+
221
+ # قسم قنوات الإشعارات
222
+ st.markdown("### قنوات الإشعارات")
223
+ st.markdown("حدد الطرق التي ترغب في تلقي الإشعارات من خلالها.")
224
+
225
+ channels_col1, channels_col2 = st.columns(2)
226
+
227
+ with channels_col1:
228
+ st.session_state.notification_channels["browser"] = st.checkbox(
229
+ "إشعارات المتصفح",
230
+ value=st.session_state.notification_channels.get("browser", True),
231
+ key="channel_browser"
232
+ )
233
+
234
+ st.session_state.notification_channels["email"] = st.checkbox(
235
+ "البريد الإلكتروني",
236
+ value=st.session_state.notification_channels.get("email", False),
237
+ key="channel_email"
238
+ )
239
+
240
+ if st.session_state.notification_channels["email"]:
241
+ email = st.text_input(
242
+ "البريد الإلكتروني ��لإشعارات",
243
+ value=st.session_state.get("notification_email", ""),
244
+ key="notification_email"
245
+ )
246
+ st.session_state.notification_email = email
247
+
248
+ with channels_col2:
249
+ st.session_state.notification_channels["sms"] = st.checkbox(
250
+ "الرسائل النصية (SMS)",
251
+ value=st.session_state.notification_channels.get("sms", False),
252
+ key="channel_sms"
253
+ )
254
+
255
+ if st.session_state.notification_channels["sms"]:
256
+ phone = st.text_input(
257
+ "رقم الهاتف للإشعارات",
258
+ value=st.session_state.get("notification_phone", ""),
259
+ key="notification_phone"
260
+ )
261
+ st.session_state.notification_phone = phone
262
+
263
+ st.session_state.notification_channels["mobile_app"] = st.checkbox(
264
+ "تطبيق الهاتف المحمول",
265
+ value=st.session_state.notification_channels.get("mobile_app", False),
266
+ key="channel_mobile_app"
267
+ )
268
+
269
+ # قسم تفضيلات الإشعارات
270
+ st.markdown("### أنواع الإشعارات")
271
+ st.markdown("حدد أنواع الإشعارات التي ترغب في تلقيها.")
272
+
273
+ prefs_col1, prefs_col2 = st.columns(2)
274
+
275
+ with prefs_col1:
276
+ st.session_state.notification_preferences["project_updates"] = st.checkbox(
277
+ "تحديثات المشاريع",
278
+ value=st.session_state.notification_preferences.get("project_updates", True),
279
+ key="pref_project_updates"
280
+ )
281
+
282
+ st.session_state.notification_preferences["document_analysis"] = st.checkbox(
283
+ "تحليل المستندات",
284
+ value=st.session_state.notification_preferences.get("document_analysis", True),
285
+ key="pref_document_analysis"
286
+ )
287
+
288
+ st.session_state.notification_preferences["deadline_reminders"] = st.checkbox(
289
+ "تذكيرات المواعيد النهائية",
290
+ value=st.session_state.notification_preferences.get("deadline_reminders", True),
291
+ key="pref_deadline_reminders"
292
+ )
293
+
294
+ st.session_state.notification_preferences["risk_alerts"] = st.checkbox(
295
+ "تنبيهات المخاطر",
296
+ value=st.session_state.notification_preferences.get("risk_alerts", True),
297
+ key="pref_risk_alerts"
298
+ )
299
+
300
+ with prefs_col2:
301
+ st.session_state.notification_preferences["price_changes"] = st.checkbox(
302
+ "تغييرات الأسعار",
303
+ value=st.session_state.notification_preferences.get("price_changes", True),
304
+ key="pref_price_changes"
305
+ )
306
+
307
+ st.session_state.notification_preferences["team_mentions"] = st.checkbox(
308
+ "إشارات فريق العمل",
309
+ value=st.session_state.notification_preferences.get("team_mentions", True),
310
+ key="pref_team_mentions"
311
+ )
312
+
313
+ st.session_state.notification_preferences["system_updates"] = st.checkbox(
314
+ "تحديثات النظام",
315
+ value=st.session_state.notification_preferences.get("system_updates", True),
316
+ key="pref_system_updates"
317
+ )
318
+
319
+ # إعدادات التكرار
320
+ st.markdown("### إعدادات التكرار")
321
+
322
+ frequency = st.radio(
323
+ "تكرار الإشعارات المتشابهة",
324
+ options=["فوري", "تجميع كل ساعة", "تجميع كل يوم", "مخصص"],
325
+ index=0,
326
+ key="notification_frequency"
327
+ )
328
+
329
+ if frequency == "مخصص":
330
+ custom_hours = st.number_input(
331
+ "التجميع كل (ساعات)",
332
+ min_value=1,
333
+ max_value=24,
334
+ value=4,
335
+ key="custom_frequency_hours"
336
+ )
337
+ st.session_state.custom_frequency_hours = custom_hours
338
+
339
+ # إعدادات متقدمة
340
+ with st.expander("إعدادات متقدمة"):
341
+ st.checkbox(
342
+ "عرض الإشعارات عند بدء تشغيل النظام",
343
+ value=True,
344
+ key="show_on_startup"
345
+ )
346
+
347
+ st.checkbox(
348
+ "الإشعارات الصوتية",
349
+ value=False,
350
+ key="audio_notifications"
351
+ )
352
+
353
+ st.checkbox(
354
+ "حفظ سجل الإشعارات",
355
+ value=True,
356
+ key="log_notifications"
357
+ )
358
+
359
+ # ��ستدعاء القيمة من session_state إذا كانت موجودة أو استخدام القيمة الافتراضية
360
+ retention_days = st.slider(
361
+ "الاحتفاظ بالإشعارات (أيام)",
362
+ min_value=7,
363
+ max_value=365,
364
+ value=st.session_state.get("retention_days_value", 90),
365
+ key="retention_days"
366
+ )
367
+ # حفظ القيمة في مفتاح آخر بعد تحديثها عن طريق المستخدم
368
+ if "retention_days_value" not in st.session_state:
369
+ st.session_state.retention_days_value = retention_days
370
+
371
+ # زر حفظ الإعدادات
372
+ if styled_button("حفظ الإعدادات", key="save_notification_settings", type="primary", icon="💾"):
373
+ self._save_notification_settings()
374
+ st.success("تم حفظ إعدادات الإشعارات بنجاح")
375
+
376
+ def _render_notification_scheduling(self):
377
+ """عرض واجهة جدولة الإشعارات"""
378
+ st.markdown("""
379
+ <div class='custom-box info-box'>
380
+ <h3>🕒 جدولة الإشعارات</h3>
381
+ <p>إنشاء وإدارة الإشعارات المجدولة والتذكيرات الدورية.</p>
382
+ </div>
383
+ """, unsafe_allow_html=True)
384
+
385
+ # إنشاء تذكير جديد
386
+ st.markdown("### إنشاء تذكير جديد")
387
+
388
+ col1, col2 = st.columns(2)
389
+
390
+ with col1:
391
+ reminder_name = st.text_input("عنوان التذكير", key="new_reminder_name")
392
+ reminder_desc = st.text_area("وصف التذكير", key="new_reminder_desc")
393
+ reminder_date = st.date_input("تاريخ التذكير", key="new_reminder_date")
394
+ reminder_time = st.time_input("وقت التذكير", key="new_reminder_time")
395
+
396
+ with col2:
397
+ reminder_type = st.selectbox(
398
+ "نوع التذكير",
399
+ options=[
400
+ "موعد نهائي للمناقصة",
401
+ "اجتماع مشروع",
402
+ "زيارة موقع",
403
+ "تسليم مستندات",
404
+ "دفعة مالية",
405
+ "مراجعة أداء",
406
+ "أخرى"
407
+ ],
408
+ key="new_reminder_type"
409
+ )
410
+
411
+ reminder_priority = st.select_slider(
412
+ "الأولوية",
413
+ options=["منخفضة", "متوسطة", "عالية", "حرجة"],
414
+ value="متوسطة",
415
+ key="new_reminder_priority"
416
+ )
417
+
418
+ reminder_repeat = st.selectbox(
419
+ "التكرار",
420
+ options=[
421
+ "مرة واحدة",
422
+ "يومياً",
423
+ "أسبوعياً",
424
+ "شهرياً",
425
+ "سنوياً"
426
+ ],
427
+ key="new_reminder_repeat"
428
+ )
429
+
430
+ if reminder_type == "أخرى":
431
+ custom_type = st.text_input("حدد نوع التذكير", key="custom_reminder_type")
432
+
433
+ # زر إضافة التذكير
434
+ if styled_button("إضافة التذكير", key="add_reminder", type="primary", icon="➕"):
435
+ if not reminder_name or not reminder_desc:
436
+ st.error("يرجى تعبئة حقول العنوان والوصف")
437
+ else:
438
+ self._add_scheduled_notification(
439
+ title=reminder_name,
440
+ message=reminder_desc,
441
+ notification_date=datetime.datetime.combine(reminder_date, reminder_time),
442
+ notification_type=reminder_type if reminder_type != "أخرى" else custom_type,
443
+ priority=reminder_priority,
444
+ repeat=reminder_repeat
445
+ )
446
+ st.success("تم إضافة التذكير بنجاح")
447
+
448
+ # عرض التذكيرات المجدولة
449
+ st.markdown("### التذكيرات المجدولة")
450
+
451
+ # التحقق من وجود تذكيرات مجدولة
452
+ scheduled_notifications = self._get_scheduled_notifications()
453
+
454
+ if scheduled_notifications:
455
+ # عرض التذكيرات في جدول
456
+ scheduled_df = pd.DataFrame(scheduled_notifications)
457
+
458
+ # تنسيق البيانات للعرض
459
+ display_df = scheduled_df.copy()
460
+ display_df["التاريخ والوقت"] = display_df["notification_date"].apply(lambda x: x.strftime("%Y-%m-%d %H:%M"))
461
+ display_df["العنوان"] = display_df["title"]
462
+ display_df["النوع"] = display_df["notification_type"]
463
+ display_df["الأولوية"] = display_df["priority"]
464
+ display_df["التكرار"] = display_df["repeat"]
465
+
466
+ # عرض الجدول
467
+ st.dataframe(
468
+ display_df[["العنوان", "النوع", "التاريخ والوقت", "الأولوية", "التكرار"]],
469
+ use_container_width=True
470
+ )
471
+
472
+ # عرض الإشعارات المجدولة كبطاقات
473
+ for notification in scheduled_notifications:
474
+ with st.expander(f"{notification['title']} - {notification['notification_date'].strftime('%Y-%m-%d %H:%M')}"):
475
+ notification_col1, notification_col2 = st.columns([3, 1])
476
+
477
+ with notification_col1:
478
+ st.markdown(f"**الوصف:** {notification['message']}")
479
+ st.markdown(f"**النوع:** {notification['notification_type']}")
480
+ st.markdown(f"**الأولوية:** {notification['priority']}")
481
+ st.markdown(f"**التكرار:** {notification['repeat']}")
482
+
483
+ with notification_col2:
484
+ if styled_button("تعديل", key=f"edit_{notification['id']}", type="secondary", icon="✏️"):
485
+ # تنفيذ في المرحلة القادمة
486
+ st.info("ميزة التعديل قيد التطوير")
487
+
488
+ if styled_button("حذف", key=f"delete_{notification['id']}", type="danger", icon="🗑️"):
489
+ self._delete_scheduled_notification(notification['id'])
490
+ st.rerun()
491
+ else:
492
+ st.info("لا توجد تذكيرات مجدولة")
493
+
494
+ def _render_notification_analytics(self):
495
+ """عرض تقارير وإحصائيات الإشعارات"""
496
+ st.markdown("""
497
+ <div class='custom-box info-box'>
498
+ <h3>📊 تقارير وإحصائيات الإشعارات</h3>
499
+ <p>تحليل وعرض إحصائيات الإشعارات والتنبيهات.</p>
500
+ </div>
501
+ """, unsafe_allow_html=True)
502
+
503
+ # إحصائيات عامة
504
+ st.markdown("### إحصائيات عامة")
505
+
506
+ # التحقق من وجود إشعارات
507
+ if st.session_state.notifications:
508
+ # إعداد البيانات
509
+ total_count = len(st.session_state.notifications)
510
+ read_count = len([n for n in st.session_state.notifications if n.get("read", False)])
511
+ unread_count = total_count - read_count
512
+
513
+ # تصنيف الإشعارات حسب النوع
514
+ notification_types = {}
515
+ for notification in st.session_state.notifications:
516
+ notification_type = notification.get("notification_type", "أخرى")
517
+ notification_types[notification_type] = notification_types.get(notification_type, 0) + 1
518
+
519
+ # عرض الإحصائيات
520
+ metric_col1, metric_col2, metric_col3 = st.columns(3)
521
+
522
+ with metric_col1:
523
+ st.metric("إجمالي الإشعارات", total_count)
524
+
525
+ with metric_col2:
526
+ st.metric("الإشعارات المقروءة", read_count, delta=f"{read_count/total_count*100:.1f}%" if total_count > 0 else "0%")
527
+
528
+ with metric_col3:
529
+ st.metric("الإشعارات غير المقروءة", unread_count, delta=f"{unread_count/total_count*100:.1f}%" if total_count > 0 else "0%")
530
+
531
+ # رسم بياني لتوزيع الإشعارات حسب النوع
532
+ st.markdown("### توزيع الإشعارات حسب النوع")
533
+
534
+ # إنشاء DataFrame للرسم البياني
535
+ types_df = pd.DataFrame({
536
+ "النوع": list(notification_types.keys()),
537
+ "العدد": list(notification_types.values())
538
+ })
539
+
540
+ # رسم بياني دائري
541
+ import plotly.express as px
542
+
543
+ fig = px.pie(
544
+ types_df,
545
+ values="العدد",
546
+ names="النوع",
547
+ title="توزيع الإشعارات حسب النوع",
548
+ color_discrete_sequence=px.colors.sequential.RdBu
549
+ )
550
+
551
+ fig.update_layout(
552
+ title_font_size=20,
553
+ font_family="Arial",
554
+ font_size=14,
555
+ height=400
556
+ )
557
+
558
+ st.plotly_chart(fig, use_container_width=True)
559
+
560
+ # رسم بياني لتوزيع الإشعارات حسب الوقت
561
+ st.markdown("### توزيع الإشعارات حسب الوقت")
562
+
563
+ # تحويل التواريخ إلى DataFrame
564
+ dates = [
565
+ n.get("timestamp", datetime.datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0)
566
+ for n in st.session_state.notifications
567
+ if "timestamp" in n
568
+ ]
569
+
570
+ if dates:
571
+ date_counts = pd.Series(dates).value_counts().sort_index()
572
+
573
+ # إنشاء DataFrame للرسم البياني
574
+ date_df = pd.DataFrame({
575
+ "التاريخ": date_counts.index,
576
+ "العدد": date_counts.values
577
+ })
578
+
579
+ # رسم بياني خطي
580
+ fig2 = px.line(
581
+ date_df,
582
+ x="التاريخ",
583
+ y="العدد",
584
+ title="توزيع الإشعارات حسب التاريخ",
585
+ markers=True
586
+ )
587
+
588
+ fig2.update_layout(
589
+ title_font_size=20,
590
+ font_family="Arial",
591
+ font_size=14,
592
+ height=400
593
+ )
594
+
595
+ st.plotly_chart(fig2, use_container_width=True)
596
+
597
+ # تصدير البيانات
598
+ st.markdown("### تصدير بيانات الإشعارات")
599
+
600
+ export_col1, export_col2 = st.columns(2)
601
+
602
+ with export_col1:
603
+ if styled_button("تصدير CSV", key="export_csv", type="primary", icon="📄"):
604
+ # تحويل الإشعارات إلى DataFrame
605
+ export_df = pd.DataFrame(st.session_state.notifications)
606
+
607
+ # تنسيق البيانات
608
+ if "timestamp" in export_df.columns:
609
+ export_df["timestamp"] = export_df["timestamp"].apply(
610
+ lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if isinstance(x, datetime.datetime) else str(x)
611
+ )
612
+
613
+ # تصدير إلى CSV
614
+ csv_data = export_df.to_csv(index=False)
615
+
616
+ # تنزيل الملف
617
+ st.download_button(
618
+ label="تنزيل ملف CSV",
619
+ data=csv_data,
620
+ file_name=f"notifications_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
621
+ mime="text/csv"
622
+ )
623
+
624
+ with export_col2:
625
+ if styled_button("تصدير JSON", key="export_json", type="primary", icon="📄"):
626
+ # تنسيق البيانات
627
+ export_data = []
628
+ for notification in st.session_state.notifications:
629
+ export_item = notification.copy()
630
+ if "timestamp" in export_item and isinstance(export_item["timestamp"], datetime.datetime):
631
+ export_item["timestamp"] = export_item["timestamp"].strftime("%Y-%m-%d %H:%M:%S")
632
+ export_data.append(export_item)
633
+
634
+ # تحويل إلى JSON
635
+ json_data = json.dumps(export_data, ensure_ascii=False, indent=2)
636
+
637
+ # تنزيل الملف
638
+ st.download_button(
639
+ label="تنزيل ملف JSON",
640
+ data=json_data,
641
+ file_name=f"notifications_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
642
+ mime="application/json"
643
+ )
644
+ else:
645
+ st.info("لا توجد بيانات كافية لعرض الرسم البياني")
646
+ else:
647
+ st.info("لا توجد إشعارات لعرض الإحصائيات")
648
+
649
+ def _render_notification_card(self, notification, show_mark_button=False):
650
+ """عرض بطاقة إشعار"""
651
+ # تعيين نمط البطاقة حسب الأولوية والحالة
652
+ card_style = "notification-card"
653
+ if not notification.get("read", False):
654
+ card_style += " unread-notification"
655
+
656
+ priority = notification.get("priority", "متوسطة")
657
+ if priority == "عالية" or priority == "حرجة":
658
+ card_style += " high-priority-notification"
659
+
660
+ # تعيين الأيقونة حسب نوع الإشعار
661
+ icon_map = {
662
+ "تحديث مشروع": "🔄",
663
+ "وثيقة جديدة": "📄",
664
+ "تذكير موعد نهائي": "⏰",
665
+ "تنبيه مخاطر": "⚠️",
666
+ "تغيير سعر": "💰",
667
+ "إشارة فريق العمل": "👥",
668
+ "تحديث الن��ام": "🖥️"
669
+ }
670
+
671
+ notification_type = notification.get("notification_type", "تحديث مشروع")
672
+ icon = icon_map.get(notification_type, "🔔")
673
+
674
+ # تنسيق التاريخ
675
+ timestamp = notification.get("timestamp", datetime.datetime.now())
676
+ if isinstance(timestamp, datetime.datetime):
677
+ time_str = timestamp.strftime("%Y-%m-%d %H:%M")
678
+ else:
679
+ time_str = str(timestamp)
680
+
681
+ # إنشاء HTML للبطاقة
682
+ card_html = f"""
683
+ <div class="{card_style}">
684
+ <div class="notification-header">
685
+ <span class="notification-icon">{icon}</span>
686
+ <span class="notification-title">{notification.get('title', 'إشعار جديد')}</span>
687
+ <span class="notification-time">{time_str}</span>
688
+ </div>
689
+ <div class="notification-body">
690
+ <p>{notification.get('message', '')}</p>
691
+ </div>
692
+ <div class="notification-footer">
693
+ <span class="notification-type">{notification_type}</span>
694
+ <span class="notification-priority">{priority}</span>
695
+ </div>
696
+ </div>
697
+ """
698
+
699
+ # عرض البطاقة
700
+ st.markdown(card_html, unsafe_allow_html=True)
701
+
702
+ # إضافة أزرار التحكم
703
+ if show_mark_button:
704
+ col1, col2 = st.columns([1, 4])
705
+
706
+ with col1:
707
+ if styled_button("تعليم كمقروء", key=f"mark_read_{notification.get('id', '')}", type="secondary", icon="✓"):
708
+ self._mark_notification_as_read(notification.get('id', ''))
709
+ st.rerun()
710
+
711
+ with col2:
712
+ if notification.get("link"):
713
+ if styled_button("عرض التفاصيل", key=f"view_details_{notification.get('id', '')}", type="primary", icon="🔍"):
714
+ # افتح الرابط المرتبط بالإشعار
715
+ # ملاحظة: هذا سيعمل بشكل مختلف حسب بيئة التشغيل
716
+ st.markdown(f"[عرض التفاصيل]({notification.get('link')})")
717
+
718
+ def add_notification(self, title, message, notification_type="تحديث مشروع", priority="متوسطة", link=None):
719
+ """
720
+ إضافة إشعار جديد
721
+
722
+ المعلمات:
723
+ title: عنوان الإشعار
724
+ message: نص الإشعار
725
+ notification_type: نوع الإشعار
726
+ priority: أولوية الإشعار
727
+ link: رابط مرتبط بالإشعار (اختياري)
728
+
729
+ الإرجاع:
730
+ معرف الإشعار الجديد
731
+ """
732
+ # إنشاء معرف فريد للإشعار
733
+ notification_id = f"notif_{int(time.time())}_{len(st.session_state.notifications)}"
734
+
735
+ # إنشاء كائن الإشعار
736
+ notification = {
737
+ "id": notification_id,
738
+ "title": title,
739
+ "message": message,
740
+ "notification_type": notification_type,
741
+ "priority": priority,
742
+ "read": False,
743
+ "timestamp": datetime.datetime.now(),
744
+ "link": link
745
+ }
746
+
747
+ # إضافة الإشعار لقائمة الإشعارات
748
+ st.session_state.notifications.append(notification)
749
+
750
+ # زيادة عداد الإشعارات غير المقروءة
751
+ st.session_state.unread_count += 1
752
+
753
+ # حفظ الإشعارات
754
+ self._save_notifications()
755
+
756
+ # تسجيل الإشعار
757
+ self.logger.info(
758
+ f"تمت إضافة إشعار جديد: {title} ({notification_type})"
759
+ )
760
+
761
+ return notification_id
762
+
763
+ def _mark_notification_as_read(self, notification_id):
764
+ """
765
+ تعليم إشعار كمقروء
766
+
767
+ المعلمات:
768
+ notification_id: معرف الإشعار
769
+
770
+ الإرجاع:
771
+ قيمة بوليانية تشير إلى نجاح العملية
772
+ """
773
+ # البحث عن الإشعار
774
+ for i, notification in enumerate(st.session_state.notifications):
775
+ if notification.get("id") == notification_id and not notification.get("read", False):
776
+ # تعليم الإشعار كمقروء
777
+ st.session_state.notifications[i]["read"] = True
778
+
779
+ # تحديث عداد الإشعارات غير المقروءة
780
+ st.session_state.unread_count = max(0, st.session_state.unread_count - 1)
781
+
782
+ # حفظ الإشعارات
783
+ self._save_notifications()
784
+
785
+ return True
786
+
787
+ return False
788
+
789
+ def _mark_all_as_read(self):
790
+ """
791
+ تعليم جميع الإشعارات كمقروءة
792
+
793
+ الإرجاع:
794
+ عدد الإشعارات التي تم تعليمها
795
+ """
796
+ count = 0
797
+
798
+ # تعليم جميع الإشعارات كمقروءة
799
+ for i, notification in enumerate(st.session_state.notifications):
800
+ if not notification.get("read", False):
801
+ st.session_state.notifications[i]["read"] = True
802
+ count += 1
803
+
804
+ # إعادة تعيين عداد الإشعارات غير المقروءة
805
+ st.session_state.unread_count = 0
806
+
807
+ # حفظ الإشعارات
808
+ self._save_notifications()
809
+
810
+ return count
811
+
812
+ def _clear_all_notifications(self):
813
+ """
814
+ حذف جميع الإشعارات
815
+
816
+ الإرجاع:
817
+ عدد الإشعارات التي تم حذفها
818
+ """
819
+ count = len(st.session_state.notifications)
820
+
821
+ # مسح قائمة الإشعارات
822
+ st.session_state.notifications = []
823
+
824
+ # إعادة تعيين عداد الإشعارات غير المقروءة
825
+ st.session_state.unread_count = 0
826
+
827
+ # حفظ الإشعارات
828
+ self._save_notifications()
829
+
830
+ return count
831
+
832
+ def _filter_notifications(self, notification_type=None, date_range=None):
833
+ """
834
+ تصفية الإشعارات حسب النوع والتاريخ
835
+
836
+ المعلمات:
837
+ notification_type: قائمة أنواع الإشعارات
838
+ date_range: نطاق تاريخ الإشعارات
839
+
840
+ الإرجاع:
841
+ قائمة الإشعارات المصفاة
842
+ """
843
+ filtered_notifications = st.session_state.notifications.copy()
844
+
845
+ # تصفية حسب النوع
846
+ if notification_type and len(notification_type) > 0:
847
+ filtered_notifications = [
848
+ n for n in filtered_notifications
849
+ if n.get("notification_type") in notification_type
850
+ ]
851
+
852
+ # تصفية حسب نطاق التاريخ
853
+ if date_range and len(date_range) == 2:
854
+ start_date, end_date = date_range
855
+
856
+ # تحويل التواريخ إلى datetime
857
+ start_date = datetime.datetime.combine(start_date, datetime.time.min)
858
+ end_date = datetime.datetime.combine(end_date, datetime.time.max)
859
+
860
+ filtered_notifications = [
861
+ n for n in filtered_notifications
862
+ if isinstance(n.get("timestamp"), datetime.datetime) and
863
+ start_date <= n.get("timestamp") <= end_date
864
+ ]
865
+
866
+ return filtered_notifications
867
+
868
+ def _add_scheduled_notification(self, title, message, notification_date, notification_type="تذكير", priority="متوسطة", repeat="مرة واحدة"):
869
+ """
870
+ إضافة إشعار مجدول
871
+
872
+ المعلمات:
873
+ title: عنوان الإشعار
874
+ message: نص الإشعار
875
+ notification_date: تاريخ ووقت الإشعار
876
+ notification_type: نوع الإشعار
877
+ priority: أولوية الإشعار
878
+ repeat: نمط تكرار الإشعار
879
+
880
+ الإرجاع:
881
+ معرف الإشعار المجدول
882
+ """
883
+ # إنشاء معرف فريد للإشعار المجدول
884
+ scheduled_id = f"sched_{int(time.time())}_{len(self._get_scheduled_notifications())}"
885
+
886
+ # إنشاء كائن الإشعار المجدول
887
+ scheduled_notification = {
888
+ "id": scheduled_id,
889
+ "title": title,
890
+ "message": message,
891
+ "notification_date": notification_date,
892
+ "notification_type": notification_type,
893
+ "priority": priority,
894
+ "repeat": repeat,
895
+ "created_at": datetime.datetime.now(),
896
+ "last_triggered": None
897
+ }
898
+
899
+ # إضافة الإشعار المجدول للقائمة
900
+ scheduled_notifications = self._get_scheduled_notifications()
901
+ scheduled_notifications.append(scheduled_notification)
902
+
903
+ # حفظ الإشعارات المجدولة
904
+ self._save_scheduled_notifications(scheduled_notifications)
905
+
906
+ # تسجيل الإشعار المجدول
907
+ self.logger.info(
908
+ f"تمت إضافة إشعار مجدول: {title} ({notification_date.strftime('%Y-%m-%d %H:%M')})"
909
+ )
910
+
911
+ return scheduled_id
912
+
913
+ def _delete_scheduled_notification(self, notification_id):
914
+ """
915
+ حذف إشعار مجدول
916
+
917
+ المعلمات:
918
+ notification_id: معرف الإشعار المجدول
919
+
920
+ الإرجاع:
921
+ قيمة بوليانية تشير إلى نج��ح العملية
922
+ """
923
+ scheduled_notifications = self._get_scheduled_notifications()
924
+
925
+ # البحث عن الإشعار المجدول
926
+ for i, notification in enumerate(scheduled_notifications):
927
+ if notification.get("id") == notification_id:
928
+ # حذف الإشعار المجدول
929
+ del scheduled_notifications[i]
930
+
931
+ # حفظ الإشعارات المجدولة
932
+ self._save_scheduled_notifications(scheduled_notifications)
933
+
934
+ # تسجيل الحذف
935
+ self.logger.info(
936
+ f"تم حذف الإشعار المجدول: {notification_id}"
937
+ )
938
+
939
+ return True
940
+
941
+ return False
942
+
943
+ def _get_scheduled_notifications(self):
944
+ """
945
+ الحصول على قائمة الإشعارات المجدولة
946
+
947
+ الإرجاع:
948
+ قائمة الإشعارات المجدولة
949
+ """
950
+ try:
951
+ # التحقق من وجود ملف الإشعارات المجدولة
952
+ scheduled_file = os.path.join(self.data_dir, "scheduled_notifications.json")
953
+
954
+ if os.path.exists(scheduled_file):
955
+ with open(scheduled_file, 'r', encoding='utf-8') as f:
956
+ scheduled_data = json.load(f)
957
+
958
+ # تحويل التواريخ من نصوص إلى كائنات datetime
959
+ for notification in scheduled_data:
960
+ if "notification_date" in notification:
961
+ notification["notification_date"] = datetime.datetime.fromisoformat(notification["notification_date"])
962
+
963
+ if "created_at" in notification:
964
+ notification["created_at"] = datetime.datetime.fromisoformat(notification["created_at"])
965
+
966
+ if "last_triggered" in notification and notification["last_triggered"]:
967
+ notification["last_triggered"] = datetime.datetime.fromisoformat(notification["last_triggered"])
968
+
969
+ return scheduled_data
970
+
971
+ return []
972
+
973
+ except Exception as e:
974
+ self.logger.error(f"حدث خطأ أثناء قراءة الإشعارات المجدولة: {str(e)}")
975
+ return []
976
+
977
+ def _save_scheduled_notifications(self, scheduled_notifications):
978
+ """
979
+ حفظ قائمة الإشعارات المجدولة
980
+
981
+ المعلمات:
982
+ scheduled_notifications: قائمة الإشعارات المجدولة
983
+ """
984
+ try:
985
+ # التأكد من وجود المجلد
986
+ os.makedirs(self.data_dir, exist_ok=True)
987
+
988
+ # تحويل كائنات datetime إلى نصوص
989
+ scheduled_data = []
990
+
991
+ for notification in scheduled_notifications:
992
+ notification_copy = notification.copy()
993
+
994
+ if "notification_date" in notification_copy and isinstance(notification_copy["notification_date"], datetime.datetime):
995
+ notification_copy["notification_date"] = notification_copy["notification_date"].isoformat()
996
+
997
+ if "created_at" in notification_copy and isinstance(notification_copy["created_at"], datetime.datetime):
998
+ notification_copy["created_at"] = notification_copy["created_at"].isoformat()
999
+
1000
+ if "last_triggered" in notification_copy and isinstance(notification_copy["last_triggered"], datetime.datetime):
1001
+ notification_copy["last_triggered"] = notification_copy["last_triggered"].isoformat()
1002
+
1003
+ scheduled_data.append(notification_copy)
1004
+
1005
+ # حفظ البيانات
1006
+ scheduled_file = os.path.join(self.data_dir, "scheduled_notifications.json")
1007
+
1008
+ with open(scheduled_file, 'w', encoding='utf-8') as f:
1009
+ json.dump(scheduled_data, f, ensure_ascii=False, indent=2)
1010
+
1011
+ except Exception as e:
1012
+ self.logger.error(f"حدث خطأ أثناء حفظ الإشعارات المجدولة: {str(e)}")
1013
+
1014
+ def _save_notification_settings(self):
1015
+ """حفظ إعدادات الإشعارات"""
1016
+ try:
1017
+ # التأكد من وجود المجلد
1018
+ os.makedirs(self.data_dir, exist_ok=True)
1019
+
1020
+ # إعداد البيانات
1021
+ settings_data = {
1022
+ "notification_channels": st.session_state.notification_channels,
1023
+ "notification_preferences": st.session_state.notification_preferences,
1024
+ "notification_email": st.session_state.get("notification_email", ""),
1025
+ "notification_phone": st.session_state.get("notification_phone", ""),
1026
+ "notification_frequency": st.session_state.get("notification_frequency", "فوري"),
1027
+ "custom_frequency_hours": st.session_state.get("custom_frequency_hours", 4),
1028
+ "show_on_startup": st.session_state.get("show_on_startup", True),
1029
+ "audio_notifications": st.session_state.get("audio_notifications", False),
1030
+ "log_notifications": st.session_state.get("log_notifications", True),
1031
+ "retention_days": st.session_state.get("retention_days", 90)
1032
+ }
1033
+
1034
+ # حفظ البيانات
1035
+ settings_file = os.path.join(self.data_dir, "notification_settings.json")
1036
+
1037
+ with open(settings_file, 'w', encoding='utf-8') as f:
1038
+ json.dump(settings_data, f, ensure_ascii=False, indent=2)
1039
+
1040
+ # تسجيل الحفظ
1041
+ self.logger.info("تم حفظ إعدادات الإشعارات بنجاح")
1042
+
1043
+ except Exception as e:
1044
+ self.logger.error(f"حدث خطأ أثناء حفظ إعدادات الإشعارات: {str(e)}")
1045
+
1046
+ def _load_notification_settings(self):
1047
+ """تحميل إعدادات الإشعارات"""
1048
+ try:
1049
+ # التحقق من وجود ملف الإعدادات
1050
+ settings_file = os.path.join(self.data_dir, "notification_settings.json")
1051
+
1052
+ if os.path.exists(settings_file):
1053
+ with open(settings_file, 'r', encoding='utf-8') as f:
1054
+ settings_data = json.load(f)
1055
+
1056
+ # تحديث حالة الجلسة
1057
+ st.session_state.notification_channels = settings_data.get("notification_channels", {})
1058
+ st.session_state.notification_preferences = settings_data.get("notification_preferences", {})
1059
+ st.session_state.notification_email = settings_data.get("notification_email", "")
1060
+ st.session_state.notification_phone = settings_data.get("notification_phone", "")
1061
+ st.session_state.notification_frequency = settings_data.get("notification_frequency", "فوري")
1062
+ st.session_state.custom_frequency_hours = settings_data.get("custom_frequency_hours", 4)
1063
+ st.session_state.show_on_startup = settings_data.get("show_on_startup", True)
1064
+ st.session_state.audio_notifications = settings_data.get("audio_notifications", False)
1065
+ st.session_state.log_notifications = settings_data.get("log_notifications", True)
1066
+ st.session_state.retention_days = settings_data.get("retention_days", 90)
1067
+
1068
+ # تسجيل التحميل
1069
+ self.logger.info("تم تحميل إعدادات الإشعارات بنجاح")
1070
+
1071
+ except Exception as e:
1072
+ self.logger.error(f"حدث خطأ أثناء تحميل إعدادات الإشعارات: {str(e)}")
1073
+
1074
+ def _save_notifications(self):
1075
+ """حفظ الإشعارات"""
1076
+ try:
1077
+ # التأكد من وجود المجلد
1078
+ os.makedirs(self.data_dir, exist_ok=True)
1079
+
1080
+ # تحويل كائنات datetime إلى نصوص
1081
+ notifications_data = []
1082
+
1083
+ for notification in st.session_state.notifications:
1084
+ notification_copy = notification.copy()
1085
+
1086
+ if "timestamp" in notification_copy and isinstance(notification_copy["timestamp"], datetime.datetime):
1087
+ notification_copy["timestamp"] = notification_copy["timestamp"].isoformat()
1088
+
1089
+ notifications_data.append(notification_copy)
1090
+
1091
+ # حفظ البيانات
1092
+ notifications_file = os.path.join(self.data_dir, "notifications.json")
1093
+
1094
+ with open(notifications_file, 'w', encoding='utf-8') as f:
1095
+ json.dump(notifications_data, f, ensure_ascii=False, indent=2)
1096
+
1097
+ except Exception as e:
1098
+ self.logger.error(f"حدث خطأ أثناء حفظ الإشعارات: {str(e)}")
1099
+
1100
+ def _load_notifications(self):
1101
+ """تحميل الإشعارات"""
1102
+ try:
1103
+ # التحقق من وجود ملف الإشعارات
1104
+ notifications_file = os.path.join(self.data_dir, "notifications.json")
1105
+
1106
+ if os.path.exists(notifications_file):
1107
+ with open(notifications_file, 'r', encoding='utf-8') as f:
1108
+ notifications_data = json.load(f)
1109
+
1110
+ # تحويل النصوص إلى كائنات datetime
1111
+ for notification in notifications_data:
1112
+ if "timestamp" in notification:
1113
+ notification["timestamp"] = datetime.datetime.fromisoformat(notification["timestamp"])
1114
+
1115
+ # تحديث حالة الجلسة
1116
+ st.session_state.notifications = notifications_data
1117
+
1118
+ # حساب عدد الإشعارات غير المقروءة
1119
+ st.session_state.unread_count = len([
1120
+ n for n in st.session_state.notifications
1121
+ if not n.get("read", False)
1122
+ ])
1123
+
1124
+ # تحميل إعدادات الإشعارات
1125
+ self._load_notification_settings()
1126
+
1127
+ # تسجيل التحميل
1128
+ self.logger.info(f"تم تحميل {len(notifications_data)} إشعار بنجاح")
1129
+
1130
+ except Exception as e:
1131
+ self.logger.error(f"حدث خطأ أثناء تحميل الإشعارات: {str(e)}")
1132
+
1133
+ def check_scheduled_notifications(self):
1134
+ """
1135
+ التحقق من الإشعارات المجدولة وإطلاقها إذا حان وقتها
1136
+
1137
+ الإرجاع:
1138
+ عدد الإشعارات التي تم إطلاقها
1139
+ """
1140
+ count = 0
1141
+
1142
+ # الحصول على الإشعارات المجدولة
1143
+ scheduled_notifications = self._get_scheduled_notifications()
1144
+
1145
+ # الوقت الحالي
1146
+ now = datetime.datetime.now()
1147
+
1148
+ # التحقق من كل إشعار مجدول
1149
+ for notification in scheduled_notifications:
1150
+ notification_date = notification.get("notification_date")
1151
+
1152
+ if notification_date and notification_date <= now:
1153
+ # إنشاء إشعار جديد
1154
+ self.add_notification(
1155
+ title=notification.get("title"),
1156
+ message=notification.get("message"),
1157
+ notification_type=notification.get("notification_type"),
1158
+ priority=notification.get("priority")
1159
+ )
1160
+
1161
+ # تحديث آخر مرة تم فيها إطلاق الإشعار
1162
+ notification["last_triggered"] = now
1163
+
1164
+ # التعامل مع التكرار
1165
+ repeat = notification.get("repeat", "مرة واحدة")
1166
+
1167
+ if repeat == "مرة واحدة":
1168
+ # حذف الإشعار المجدول
1169
+ self._delete_scheduled_notification(notification.get("id"))
1170
+ else:
1171
+ # حساب التاريخ التالي
1172
+ if repeat == "يومياً":
1173
+ new_date = notification_date + datetime.timedelta(days=1)
1174
+ elif repeat == "أسبوعياً":
1175
+ new_date = notification_date + datetime.timedelta(weeks=1)
1176
+ elif repeat == "شهرياً":
1177
+ # إضافة شهر (تقريبي)
1178
+ new_month = notification_date.month + 1
1179
+ new_year = notification_date.year
1180
+
1181
+ if new_month > 12:
1182
+ new_month = 1
1183
+ new_year += 1
1184
+
1185
+ new_date = notification_date.replace(year=new_year, month=new_month)
1186
+ elif repeat == "سنوياً":
1187
+ new_date = notification_date.replace(year=notification_date.year + 1)
1188
+ else:
1189
+ # افتراضي: يومياً
1190
+ new_date = notification_date + datetime.timedelta(days=1)
1191
+
1192
+ # تحديث تاريخ الإشعار المجدول
1193
+ notification["notification_date"] = new_date
1194
+
1195
+ count += 1
1196
+
1197
+ # حفظ الإشعارات المجدولة إذا تم تغييرها
1198
+ if count > 0:
1199
+ self._save_scheduled_notifications(scheduled_notifications)
1200
+
1201
+ return count
1202
+
1203
+
1204
+ # تطبيق وحدة نظام الإشعارات الذكي
1205
+ class NotificationsApp:
1206
+ """وحدة تطبيق نظام الإشعارات الذكي"""
1207
+
1208
+ def __init__(self):
1209
+ """تهيئة وحدة تطبيق نظام الإشعارات الذكي"""
1210
+ self.smart_notification_system = SmartNotificationSystem()
1211
+
1212
+ def render(self):
1213
+ """عرض واجهة وحدة تطبيق نظام الإشعارات الذكي"""
1214
+ st.markdown("<h2 class='module-title'>نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات</h2>", unsafe_allow_html=True)
1215
+
1216
+ st.markdown("""
1217
+ <div class="module-description">
1218
+ يتيح لك نظام الإشعارات الذكي متابعة تحديثات المشاريع وتلقي تنبيهات مخصصة حسب أدوارك واهتماماتك.
1219
+ يمكنك تخصيص إعدادات الإشعارات وجدولة التذكيرات للمواعيد النهائية والمهام.
1220
+ </div>
1221
+ """, unsafe_allow_html=True)
1222
+
1223
+ # عرض نظام الإشعارات الذكي
1224
+ self.smart_notification_system.render()
1225
+
1226
+
1227
+ # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
1228
+ if __name__ == "__main__":
1229
+ st.set_page_config(
1230
+ page_title="نظام الإشعارات الذكي | WAHBi AI",
1231
+ page_icon="🔔",
1232
+ layout="wide",
1233
+ initial_sidebar_state="expanded"
1234
+ )
1235
+
1236
+ app = NotificationsApp()
1237
+ app.render()
modules/pricing/constants.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ثوابت وحدة التسعير
3
+ """
4
+
5
+ # أوزان المحتوى المحلي
6
+ LOCAL_CONTENT_WEIGHTS = {
7
+ 'منتجات_البناء': 1.5, # المنتجات الأساسية في البناء لها وزن أكبر
8
+ 'المنتجات_الإنشائية': 1.5, # المنتجات الإنشائية لها وزن أكبر
9
+ 'منتجات_التشطيب': 1.0, # منتجات التشطيب لها وزن عادي
10
+ 'الخدمات_الهندسية': 1.3, # الخدمات الهندسية لها وزن أكبر
11
+ 'الخدمات_الإدارية': 1.0, # الخدمات الإدارية لها وزن عادي
12
+ 'القوى_العاملة_الفنية': 1.2, # القوى العاملة الفنية لها وزن أكبر
13
+ 'القوى_العاملة_العادية': 1.0, # القوى العاملة العادية لها وزن عادي
14
+ 'القوى_العاملة_الإدارية': 0.8 # القوى العاملة الإدارية لها وزن أقل
15
+ }
16
+
17
+ # فئات التكاليف
18
+ COST_CATEGORIES = {
19
+ 'مباشرة': [
20
+ 'مواد',
21
+ 'عمالة',
22
+ 'معدات',
23
+ 'مقاولين من الباطن'
24
+ ],
25
+ 'غير_مباشرة': [
26
+ 'إدارة المشروع',
27
+ 'ضمانات بنكية',
28
+ 'تأمينات',
29
+ 'مكاتب الموقع',
30
+ 'نقل وسكن',
31
+ 'مرافق',
32
+ 'أمن وسلامة'
33
+ ],
34
+ 'مصاريف_عامة': [
35
+ 'مصاريف إدارية',
36
+ 'رواتب إدارية',
37
+ 'إيجارات',
38
+ 'اتصالات',
39
+ 'قرطاسية',
40
+ 'تسويق وعلاقات عامة'
41
+ ],
42
+ 'احتياطيات': [
43
+ 'احتياطي مخاطر',
44
+ 'احتياطي تضخم',
45
+ 'احتياطي تغييرات'
46
+ ]
47
+ }
48
+
49
+ # أنواع التسعير
50
+ PRICING_TYPES = {
51
+ 'قياسي': 'التسعير المتوازن لجميع البنود',
52
+ 'غير_متزن': 'تحميل بعض البنود بسعر أعلى وتخفيض بنود أخرى مع الحفاظ على نفس الإجمالي',
53
+ 'تنافسي': 'التسعير بناءً على أسعار المنافسين',
54
+ 'ربحية': 'التسعير بناءً على هامش الربح المستهدف'
55
+ }
56
+
57
+ # أنواع استراتيجيات التسعير غير المتزن
58
+ UNBALANCED_PRICING_STRATEGIES = {
59
+ 'تحميل_أمامي': 'زيادة أسعار البنود المبكرة في المشروع',
60
+ 'تحميل_خلفي': 'زيادة أسعار البنود المتأخرة في المشروع',
61
+ 'تحميل_مؤكد': 'زيادة أسعار البنود المؤكدة التنفيذ',
62
+ 'تخفيض_متغير': 'تخفيض أسعار البنود المحتمل تغير كمياتها'
63
+ }
64
+
65
+ # معلمات افتراضية للمشروع
66
+ DEFAULT_PROJECT_PARAMS = {
67
+ 'نسبة_المصاريف_العامة': 8.0, # 8% من التكاليف المباشرة
68
+ 'نسبة_الأرباح': 10.0, # 10% من التكاليف الكلية
69
+ 'نسبة_احتياطي_المخاطر': 5.0, # 5% من التكاليف المباشرة
70
+ 'نسبة_ضمان_ابتدائي': 2.0, # 2% من قيمة العطاء
71
+ 'نسبة_ضمان_نهائي': 5.0, # 5% من قيمة العطاء
72
+ 'نسبة_محتجزات': 10.0, # 10% من قيمة المستخلصات
73
+ 'نسبة_دفعة_مقدمة': 10.0 # 10% من قيمة العطاء
74
+ }
75
+
76
+ # وحدات القياس
77
+ UNITS_OF_MEASURE = {
78
+ 'طولية': ['م.ط', 'متر طولي', 'م'],
79
+ 'مسطحة': ['م2', 'متر مربع'],
80
+ 'حجمية': ['م3', 'متر مكعب'],
81
+ 'وزن': ['كجم', 'طن', 'جم'],
82
+ 'عدد': ['عدد', 'وحدة', 'قطعة'],
83
+ 'زمن': ['يوم', 'ساعة', 'شهر'],
84
+ 'نقطة': ['نقطة', 'مخرج']
85
+ }
86
+
87
+ # نسب الزيادة في التكاليف
88
+ COST_INCREASE_FACTORS = {
89
+ 'تعقيد_مرتفع': 1.25, # زيادة 25% للأعمال المعقدة
90
+ 'تعقيد_متوسط': 1.15, # زيادة 15% للأعمال متوسطة التعقيد
91
+ 'منطقة_نائية': 1.2, # زيادة 20% للمناطق النائية
92
+ 'ظروف_جوية_قاسية': 1.15, # زيادة 15% للظروف الجوية القاسية
93
+ 'ظروف_الموقع_صعبة': 1.2, # زيادة 20% لظروف الموقع الصعبة
94
+ 'عاجل': 1.3 # زيادة 30% للأعمال العاجلة
95
+ }
96
+
97
+ # أنواع المشاريع
98
+ PROJECT_TYPES = [
99
+ 'سكني',
100
+ 'تجاري',
101
+ 'صناعي',
102
+ 'تعليمي',
103
+ 'صحي',
104
+ 'بنية تحتية',
105
+ 'طرق',
106
+ 'نقل',
107
+ 'طاقة',
108
+ 'مياه وصرف صحي',
109
+ 'اتصالات',
110
+ 'عسكري',
111
+ 'ترفيهي',
112
+ 'متعدد الاستخدام'
113
+ ]
modules/pricing/construction_calculator.py ADDED
@@ -0,0 +1,787 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ حاسبة تكاليف البناء المتكاملة
3
+ تتضمن العناصر التالية:
4
+ - المواد الخام
5
+ - المعدات
6
+ - العمالة
7
+ - المصاريف الإدارية
8
+ - هامش الربح
9
+ """
10
+
11
+ import streamlit as st
12
+ import pandas as pd
13
+ import numpy as np
14
+ import plotly.express as px
15
+ import plotly.graph_objects as go
16
+
17
+
18
+ def render_construction_calculator():
19
+ """
20
+ عرض حاسبة تكاليف البناء المتكاملة
21
+ """
22
+ # التأكد من وجود المتغيرات في حالة الجلسة
23
+ if 'materials_cost' not in st.session_state:
24
+ st.session_state.materials_cost = 0.0
25
+ if 'equipment_cost' not in st.session_state:
26
+ st.session_state.equipment_cost = 0.0
27
+ if 'labor_cost' not in st.session_state:
28
+ st.session_state.labor_cost = 0.0
29
+ if 'admin_cost' not in st.session_state:
30
+ st.session_state.admin_cost = 0.0
31
+ if 'profit_margin' not in st.session_state:
32
+ st.session_state.profit_margin = 15.0
33
+
34
+ st.markdown("<h2 class='module-title'>حاسبة تكاليف البناء المتكاملة</h2>", unsafe_allow_html=True)
35
+
36
+ # معلومات المشروع
37
+ st.markdown("<h3>معلومات المشروع</h3>", unsafe_allow_html=True)
38
+
39
+ col1, col2 = st.columns(2)
40
+
41
+ with col1:
42
+ project_name = st.text_input("اسم المشروع", "مشروع سكني")
43
+ project_location = st.text_input("موقع المشروع", "الرياض - حي النرجس")
44
+
45
+ with col2:
46
+ project_area = st.number_input("المساحة الإجمالية (م²)", min_value=1, value=500)
47
+ project_type = st.selectbox(
48
+ "نوع المشروع",
49
+ options=[
50
+ "سكني", "تجاري", "صناعي", "إداري", "صحي", "تعليمي",
51
+ "بنية تحتية", "طرق", "جسور", "أخرى"
52
+ ]
53
+ )
54
+
55
+ # التبويبات الرئيسية للحاسبة
56
+ tabs = st.tabs([
57
+ "المواد الخام", "المعدات", "العمالة", "المصاريف الإدارية", "هامش الربح", "التقرير النهائي"
58
+ ])
59
+
60
+ # تعريف المتغيرات العامة
61
+ if "materials_cost" not in st.session_state:
62
+ st.session_state.materials_cost = 0.0
63
+ if "equipment_cost" not in st.session_state:
64
+ st.session_state.equipment_cost = 0.0
65
+ if "labor_cost" not in st.session_state:
66
+ st.session_state.labor_cost = 0.0
67
+ if "admin_cost" not in st.session_state:
68
+ st.session_state.admin_cost = 0.0
69
+ if "profit_margin" not in st.session_state:
70
+ st.session_state.profit_margin = 10.0
71
+ if "materials" not in st.session_state:
72
+ st.session_state.materials = []
73
+ if "equipment" not in st.session_state:
74
+ st.session_state.equipment = []
75
+ if "labor" not in st.session_state:
76
+ st.session_state.labor = []
77
+ if "admin_expenses" not in st.session_state:
78
+ st.session_state.admin_expenses = []
79
+
80
+ # تبويب المواد الخام
81
+ with tabs[0]:
82
+ render_materials_tab()
83
+
84
+ # تبويب المعدات
85
+ with tabs[1]:
86
+ render_equipment_tab()
87
+
88
+ # تبويب العمالة
89
+ with tabs[2]:
90
+ render_labor_tab()
91
+
92
+ # تبويب المصاريف الإدارية
93
+ with tabs[3]:
94
+ render_admin_tab()
95
+
96
+ # تبويب هامش الربح
97
+ with tabs[4]:
98
+ render_profit_tab()
99
+
100
+ # تبويب التقرير النهائي
101
+ with tabs[5]:
102
+ render_final_report(project_name, project_location, project_area, project_type)
103
+
104
+
105
+ def render_materials_tab():
106
+ """
107
+ عرض تبويب المواد الخام
108
+ """
109
+ st.markdown("<h3>تكاليف المواد الخام</h3>", unsafe_allow_html=True)
110
+
111
+ # إضافة مادة جديدة
112
+ st.markdown("<h4>إضافة مادة جديدة</h4>", unsafe_allow_html=True)
113
+
114
+ col1, col2, col3, col4 = st.columns(4)
115
+
116
+ with col1:
117
+ material_name = st.text_input("اسم المادة", key="new_material_name")
118
+ with col2:
119
+ material_quantity = st.number_input("الكمية", min_value=0.0, step=0.1, key="new_material_quantity")
120
+ with col3:
121
+ material_unit = st.selectbox(
122
+ "الوحدة",
123
+ options=["م²", "م³", "طن", "كجم", "لتر", "قطعة", "لفة", "كيس", "أخرى"],
124
+ key="new_material_unit"
125
+ )
126
+ with col4:
127
+ material_price = st.number_input("السعر للوحدة (ريال)", min_value=0.0, step=0.01, key="new_material_price")
128
+
129
+ if st.button("إضافة مادة", key="add_material_btn"):
130
+ total_price = material_quantity * material_price
131
+ new_material = {
132
+ "name": material_name,
133
+ "quantity": material_quantity,
134
+ "unit": material_unit,
135
+ "price": material_price,
136
+ "total": total_price
137
+ }
138
+ st.session_state.materials.append(new_material)
139
+ st.success(f"تمت إضافة {material_name} بنجاح!")
140
+
141
+ # عرض قائمة المواد المضافة
142
+ if st.session_state.materials:
143
+ st.markdown("<h4>قائمة المواد المضافة</h4>", unsafe_allow_html=True)
144
+
145
+ materials_df = pd.DataFrame(st.session_state.materials)
146
+ materials_df.columns = ["اسم المادة", "الكمية", "الوحدة", "السعر للوحدة", "التكلفة الإجمالية"]
147
+ st.dataframe(materials_df)
148
+
149
+ total_materials_cost = sum(item["total"] for item in st.session_state.materials)
150
+ st.session_state.materials_cost = total_materials_cost
151
+
152
+ st.markdown(f"<h4>إجمالي تكلفة المواد: <span style='color:var(--primary-color)'>{total_materials_cost:,.2f} ريال</span></h4>", unsafe_allow_html=True)
153
+
154
+ # رسم بياني للمواد حسب التكلفة
155
+ if len(st.session_state.materials) > 1:
156
+ st.markdown("<h4>توزيع تكاليف المواد</h4>", unsafe_allow_html=True)
157
+
158
+ fig = px.pie(
159
+ materials_df,
160
+ values="التكلفة الإجمالية",
161
+ names="اسم المادة",
162
+ title="توزيع تكاليف المواد",
163
+ color_discrete_sequence=px.colors.sequential.Teal,
164
+ hole=0.4
165
+ )
166
+ fig.update_layout(
167
+ font=dict(family="Almarai, Arial", size=14),
168
+ margin=dict(t=50, b=50, l=20, r=20)
169
+ )
170
+ st.plotly_chart(fig, use_container_width=True)
171
+
172
+ st.markdown("---")
173
+
174
+ # استيراد بيانات المواد من ملف
175
+ st.markdown("<h4>استيراد بيانات المواد من ملف</h4>", unsafe_allow_html=True)
176
+ uploaded_file = st.file_uploader("اختر ملف Excel أو CSV", type=["xlsx", "csv"], key="materials_upload")
177
+
178
+ if uploaded_file is not None:
179
+ if uploaded_file.name.endswith('.csv'):
180
+ df = pd.read_csv(uploaded_file)
181
+ else:
182
+ df = pd.read_excel(uploaded_file)
183
+
184
+ st.success("تم استيراد البيانات بنجاح!")
185
+ st.dataframe(df)
186
+
187
+ if st.button("إضافة المواد من الملف"):
188
+ try:
189
+ # تحويل أسماء الأعمدة للمطابقة مع النظام
190
+ column_mapping = {
191
+ "المادة": "name",
192
+ "اسم المادة": "name",
193
+ "الكمية": "quantity",
194
+ "الوحدة": "unit",
195
+ "السعر": "price",
196
+ "سعر الوحدة": "price"
197
+ }
198
+
199
+ mapped_df = df.rename(columns=column_mapping)
200
+
201
+ # حساب التكلفة الإجمالية لكل مادة
202
+ for _, row in mapped_df.iterrows():
203
+ total_price = row["quantity"] * row["price"]
204
+ new_material = {
205
+ "name": row["name"],
206
+ "quantity": row["quantity"],
207
+ "unit": row["unit"],
208
+ "price": row["price"],
209
+ "total": total_price
210
+ }
211
+ st.session_state.materials.append(new_material)
212
+
213
+ st.success("تمت إضافة جميع المواد من الملف بنجاح!")
214
+
215
+ except Exception as e:
216
+ st.error(f"حدث خطأ: {str(e)}")
217
+ st.error("تأكد من أن الملف يحتوي على الأعمدة المطلوبة: اسم المادة، الكمية، الوحدة، السعر للوحدة")
218
+
219
+
220
+ def render_equipment_tab():
221
+ """
222
+ عرض تبويب المعدات
223
+ """
224
+ st.markdown("<h3>تكاليف المعدات</h3>", unsafe_allow_html=True)
225
+
226
+ # إضافة معدة جديدة
227
+ st.markdown("<h4>إضافة معدة جديدة</h4>", unsafe_allow_html=True)
228
+
229
+ col1, col2, col3 = st.columns(3)
230
+
231
+ with col1:
232
+ equipment_name = st.text_input("اسم المعدة", key="new_equipment_name")
233
+ with col2:
234
+ rental_type = st.selectbox(
235
+ "نوع الإيجار",
236
+ options=["يومي", "أسبوعي", "شهري", "سنوي", "مملوكة (استهلاك)"],
237
+ key="rental_type"
238
+ )
239
+ with col3:
240
+ usage_period = st.number_input(f"مدة الاستخدام ({rental_type})", min_value=1, value=1, key="usage_period")
241
+
242
+ col4, col5, col6 = st.columns(3)
243
+
244
+ with col4:
245
+ equipment_rate = st.number_input(f"سعر الإيجار لكل ({rental_type}) (ريال)", min_value=0.0, step=0.01, key="equipment_rate")
246
+ with col5:
247
+ fuel_cost = st.number_input("تكلفة الوقود اليومية (ريال)", min_value=0.0, step=0.01, key="fuel_cost")
248
+ with col6:
249
+ operator_cost = st.number_input("تكلفة المشغل اليومية (ريال)", min_value=0.0, step=0.01, key="operator_cost")
250
+
251
+ # حساب إجمالي التكلفة
252
+ rental_days = {
253
+ "يومي": 1,
254
+ "أسبوعي": 7,
255
+ "شهري": 30,
256
+ "سنوي": 365,
257
+ "مملوكة (استهلاك)": 1
258
+ }
259
+
260
+ total_days = usage_period * rental_days[rental_type]
261
+ total_equipment_cost = equipment_rate * usage_period
262
+ total_fuel_cost = fuel_cost * total_days
263
+ total_operator_cost = operator_cost * total_days
264
+ total_cost = total_equipment_cost + total_fuel_cost + total_operator_cost
265
+
266
+ if st.button("إضافة معدة", key="add_equipment_btn"):
267
+ new_equipment = {
268
+ "name": equipment_name,
269
+ "rental_type": rental_type,
270
+ "usage_period": usage_period,
271
+ "equipment_rate": equipment_rate,
272
+ "fuel_cost": fuel_cost,
273
+ "operator_cost": operator_cost,
274
+ "total": total_cost
275
+ }
276
+ st.session_state.equipment.append(new_equipment)
277
+ st.success(f"تمت إضافة {equipment_name} بنجاح!")
278
+
279
+ # عرض تفاصيل الحساب
280
+ st.markdown("<div class='card' style='margin-top: 10px;'>", unsafe_allow_html=True)
281
+ st.markdown(f"<p>عدد أيام الاستخدام الإجمالية: {total_days} يوم</p>", unsafe_allow_html=True)
282
+ st.markdown(f"<p>تكلفة إيجار المعدة: {total_equipment_cost:,.2f} ريال</p>", unsafe_allow_html=True)
283
+ st.markdown(f"<p>تكلفة الوقود: {total_fuel_cost:,.2f} ريال</p>", unsafe_allow_html=True)
284
+ st.markdown(f"<p>تكلفة المشغل: {total_operator_cost:,.2f} ريال</p>", unsafe_allow_html=True)
285
+ st.markdown(f"<h4>التكلفة الإجمالية للمعدة: {total_cost:,.2f} ريال</h4>", unsafe_allow_html=True)
286
+ st.markdown("</div>", unsafe_allow_html=True)
287
+
288
+ # عرض قائمة المعدات المضافة
289
+ if st.session_state.equipment:
290
+ st.markdown("<h4>قائمة المعدات المضافة</h4>", unsafe_allow_html=True)
291
+
292
+ equipment_data = []
293
+ for item in st.session_state.equipment:
294
+ equipment_data.append({
295
+ "اسم المعدة": item["name"],
296
+ "نوع الإيجار": item["rental_type"],
297
+ "مدة الاستخدام": item["usage_period"],
298
+ "إيجار الوحدة": item["equipment_rate"],
299
+ "تكلفة الوقود": item["fuel_cost"],
300
+ "تكلفة المشغل": item["operator_cost"],
301
+ "التكلفة الإجمالية": item["total"]
302
+ })
303
+
304
+ equipment_df = pd.DataFrame(equipment_data)
305
+ st.dataframe(equipment_df)
306
+
307
+ total_equipment_cost = sum(item["total"] for item in st.session_state.equipment)
308
+ st.session_state.equipment_cost = total_equipment_cost
309
+
310
+ st.markdown(f"<h4>إجمالي تكلفة المعدات: <span style='color:var(--primary-color)'>{total_equipment_cost:,.2f} ريال</span></h4>", unsafe_allow_html=True)
311
+
312
+ # رسم بياني للمعدات حسب التكلفة
313
+ if len(st.session_state.equipment) > 1:
314
+ st.markdown("<h4>توزيع تكاليف المعدات</h4>", unsafe_allow_html=True)
315
+
316
+ fig = go.Figure()
317
+
318
+ fig.add_trace(go.Bar(
319
+ x=[item["اسم المعدة"] for item in equipment_data],
320
+ y=[item["التكلفة الإجمالية"] for item in equipment_data],
321
+ name="التكلفة الإجمالية",
322
+ marker_color="teal"
323
+ ))
324
+
325
+ fig.update_layout(
326
+ title="تكاليف المعدات",
327
+ xaxis_title="المعدة",
328
+ yaxis_title="التكلفة (ريال)",
329
+ font=dict(family="Almarai, Arial", size=14),
330
+ margin=dict(t=50, b=50, l=20, r=20)
331
+ )
332
+
333
+ st.plotly_chart(fig, use_container_width=True)
334
+
335
+
336
+ def render_labor_tab():
337
+ """
338
+ عرض تبويب العمالة
339
+ """
340
+ st.markdown("<h3>تكاليف العمالة</h3>", unsafe_allow_html=True)
341
+
342
+ # إضافة عامل أو مجموعة عمال
343
+ st.markdown("<h4>إضافة عمالة جديدة</h4>", unsafe_allow_html=True)
344
+
345
+ col1, col2, col3 = st.columns(3)
346
+
347
+ with col1:
348
+ labor_type = st.text_input("نوع العمالة", key="new_labor_type")
349
+ with col2:
350
+ labor_count = st.number_input("العدد", min_value=1, value=1, key="new_labor_count")
351
+ with col3:
352
+ payment_type = st.selectbox(
353
+ "نوع الدفع",
354
+ options=["يومي", "أسبوعي", "شهري", "بالقطعة"],
355
+ key="new_payment_type"
356
+ )
357
+
358
+ col4, col5, col6 = st.columns(3)
359
+
360
+ with col4:
361
+ wage_rate = st.number_input(f"الأجرة ({payment_type}) (ريال)", min_value=0.0, step=0.01, key="new_wage_rate")
362
+ with col5:
363
+ work_period = st.number_input(f"مدة العمل ({payment_type})", min_value=1, value=30, key="new_work_period")
364
+ with col6:
365
+ benefits_percent = st.slider("نسبة البدلات والتأمين (%)", min_value=0, max_value=50, value=15, key="new_benefits_percent")
366
+
367
+ # حساب إجمالي التكلفة
368
+ days_factor = {
369
+ "يومي": 1,
370
+ "أسبوعي": 7,
371
+ "شهري": 30,
372
+ "بالقطعة": 1
373
+ }
374
+
375
+ monthly_days = work_period * days_factor[payment_type] / 30 # تحويل الأيام إلى شهور
376
+
377
+ if payment_type == "بالقطعة":
378
+ total_labor_cost = labor_count * wage_rate * work_period
379
+ else:
380
+ # حساب الراتب الشهري
381
+ monthly_wage = wage_rate * 30 / days_factor[payment_type]
382
+ # حساب تكلفة البدلات والتأمين
383
+ benefits_cost = monthly_wage * (benefits_percent / 100)
384
+ # إجمالي التكلفة الشهرية
385
+ monthly_total_cost = monthly_wage + benefits_cost
386
+ # إجمالي التكلفة
387
+ total_labor_cost = labor_count * monthly_total_cost * monthly_days
388
+
389
+ if st.button("إضافة عمالة"):
390
+ new_labor = {
391
+ "type": labor_type,
392
+ "count": labor_count,
393
+ "payment_type": payment_type,
394
+ "wage_rate": wage_rate,
395
+ "work_period": work_period,
396
+ "benefits_percent": benefits_percent,
397
+ "total": total_labor_cost
398
+ }
399
+ st.session_state.labor.append(new_labor)
400
+ st.success(f"تمت إضافة {labor_type} بنجاح!")
401
+
402
+ # عرض تفاصيل الحساب
403
+ st.markdown("<div class='card' style='margin-top: 10px;'>", unsafe_allow_html=True)
404
+ if payment_type != "بالقطعة":
405
+ monthly_wage = wage_rate * 30 / days_factor[payment_type]
406
+ benefits_cost = monthly_wage * (benefits_percent / 100)
407
+ monthly_total_cost = monthly_wage + benefits_cost
408
+
409
+ st.markdown(f"<p>الراتب الشهري للعامل: {monthly_wage:,.2f} ريال</p>", unsafe_allow_html=True)
410
+ st.markdown(f"<p>تكلفة البدلات والتأمين الشهرية: {benefits_cost:,.2f} ريال</p>", unsafe_allow_html=True)
411
+ st.markdown(f"<p>إجمالي التكلفة الشهرية للعامل: {monthly_total_cost:,.2f} ريال</p>", unsafe_allow_html=True)
412
+ st.markdown(f"<p>مدة العمل بالشهور: {monthly_days:.2f} شهر</p>", unsafe_allow_html=True)
413
+ else:
414
+ st.markdown(f"<p>سعر القطعة: {wage_rate:,.2f} ريال</p>", unsafe_allow_html=True)
415
+ st.markdown(f"<p>عدد القطع: {work_period}</p>", unsafe_allow_html=True)
416
+
417
+ st.markdown(f"<p>عدد العمال: {labor_count}</p>", unsafe_allow_html=True)
418
+ st.markdown(f"<h4>التكلفة الإجمالية للعمالة: {total_labor_cost:,.2f} ريال</h4>", unsafe_allow_html=True)
419
+ st.markdown("</div>", unsafe_allow_html=True)
420
+
421
+ # عرض قائمة العمالة المضافة
422
+ if st.session_state.labor:
423
+ st.markdown("<h4>قائمة العمالة المضافة</h4>", unsafe_allow_html=True)
424
+
425
+ labor_data = []
426
+ for item in st.session_state.labor:
427
+ labor_data.append({
428
+ "نوع العمالة": item["type"],
429
+ "العدد": item["count"],
430
+ "نوع الدفع": item["payment_type"],
431
+ "معدل الأجرة": item["wage_rate"],
432
+ "مدة العمل": item["work_period"],
433
+ "نسبة البدلات": f"{item['benefits_percent']}%",
434
+ "التكلفة الإجمالية": item["total"]
435
+ })
436
+
437
+ labor_df = pd.DataFrame(labor_data)
438
+ st.dataframe(labor_df)
439
+
440
+ total_labor_cost = sum(item["total"] for item in st.session_state.labor)
441
+ st.session_state.labor_cost = total_labor_cost
442
+
443
+ st.markdown(f"<h4>إجمالي تكلفة العمالة: <span style='color:var(--primary-color)'>{total_labor_cost:,.2f} ريال</span></h4>", unsafe_allow_html=True)
444
+
445
+ # رسم بياني للعمالة حسب التكلفة
446
+ if len(st.session_state.labor) > 1:
447
+ st.markdown("<h4>توزيع تكاليف العمالة</h4>", unsafe_allow_html=True)
448
+
449
+ fig = px.bar(
450
+ labor_df,
451
+ x="نوع العمالة",
452
+ y="التكلفة الإجمالية",
453
+ color="العدد",
454
+ title="توزيع تكاليف العمالة",
455
+ color_continuous_scale=px.colors.sequential.Teal
456
+ )
457
+ fig.update_layout(
458
+ font=dict(family="Almarai, Arial", size=14),
459
+ margin=dict(t=50, b=50, l=20, r=20)
460
+ )
461
+ st.plotly_chart(fig, use_container_width=True)
462
+
463
+
464
+ def render_admin_tab():
465
+ """
466
+ عرض تبويب المصاريف الإدارية
467
+ """
468
+ st.markdown("<h3>المصاريف الإدارية والعمومية</h3>", unsafe_allow_html=True)
469
+
470
+ # إضافة مصروف جديد
471
+ st.markdown("<h4>إضافة مصروف جديد</h4>", unsafe_allow_html=True)
472
+
473
+ col1, col2, col3 = st.columns(3)
474
+
475
+ with col1:
476
+ expense_name = st.text_input("اسم المصروف", key="new_expense_name")
477
+ with col2:
478
+ expense_type = st.selectbox(
479
+ "نوع المصروف",
480
+ options=[
481
+ "رواتب إدارية", "إيجارات", "مكتبية", "سفر", "تأمين",
482
+ "استشارات", "رسوم حكومية", "منافع", "أخرى"
483
+ ],
484
+ key="new_expense_type"
485
+ )
486
+ with col3:
487
+ expense_amount = st.number_input("المبلغ (ريال)", min_value=0.0, step=100.0, key="new_expense_amount")
488
+
489
+ if st.button("إضافة مصروف"):
490
+ new_expense = {
491
+ "name": expense_name,
492
+ "type": expense_type,
493
+ "amount": expense_amount
494
+ }
495
+ st.session_state.admin_expenses.append(new_expense)
496
+ st.success(f"تمت إضافة {expense_name} بنجاح!")
497
+
498
+ # عرض قائمة المصاريف المضافة
499
+ if st.session_state.admin_expenses:
500
+ st.markdown("<h4>قائمة المصاريف الإدارية</h4>", unsafe_allow_html=True)
501
+
502
+ admin_data = []
503
+ for item in st.session_state.admin_expenses:
504
+ admin_data.append({
505
+ "اسم المصروف": item["name"],
506
+ "نوع المصروف": item["type"],
507
+ "المبلغ": item["amount"]
508
+ })
509
+
510
+ admin_df = pd.DataFrame(admin_data)
511
+ st.dataframe(admin_df)
512
+
513
+ total_admin_cost = sum(item["amount"] for item in st.session_state.admin_expenses)
514
+ st.session_state.admin_cost = total_admin_cost
515
+
516
+ st.markdown(f"<h4>إجمالي المصاريف الإدارية: <span style='color:var(--primary-color)'>{total_admin_cost:,.2f} ريال</span></h4>", unsafe_allow_html=True)
517
+
518
+ # رسم بياني للمصاريف حسب النوع
519
+ if len(st.session_state.admin_expenses) > 1:
520
+ st.markdown("<h4>توزيع المصاريف الإدارية حسب النوع</h4>", unsafe_allow_html=True)
521
+
522
+ # تجميع المصاريف حسب النوع
523
+ expense_by_type = admin_df.groupby("نوع المصروف")["المبلغ"].sum().reset_index()
524
+
525
+ fig = px.pie(
526
+ expense_by_type,
527
+ values="المبلغ",
528
+ names="نوع المصروف",
529
+ title="توزيع المصاريف الإدارية",
530
+ color_discrete_sequence=px.colors.sequential.Teal,
531
+ hole=0.4
532
+ )
533
+ fig.update_layout(
534
+ font=dict(family="Almarai, Arial", size=14),
535
+ margin=dict(t=50, b=50, l=20, r=20)
536
+ )
537
+ st.plotly_chart(fig, use_container_width=True)
538
+
539
+ # نسبة المصاريف الإدارية
540
+ st.markdown("<h4>احتساب المصاريف الإدارية بالنسبة المئوية</h4>", unsafe_allow_html=True)
541
+
542
+ # حساب التكاليف المباشرة
543
+ direct_costs = st.session_state.materials_cost + st.session_state.equipment_cost + st.session_state.labor_cost
544
+
545
+ col1, col2 = st.columns(2)
546
+
547
+ with col1:
548
+ admin_percent = st.slider("نسبة المصاريف الإدارية من التكاليف المباشرة (%)", min_value=0, max_value=30, value=10, key="admin_percent")
549
+
550
+ with col2:
551
+ calculated_admin_cost = direct_costs * (admin_percent / 100)
552
+ st.markdown(f"<div class='card'><h4>المصاريف الإدارية بالنسبة: <span style='color:var(--primary-color)'>{calculated_admin_cost:,.2f} ريال</span></h4></div>", unsafe_allow_html=True)
553
+
554
+ if st.button("استخدام النسبة المئوية للمصاريف الإدارية"):
555
+ st.session_state.admin_cost = calculated_admin_cost
556
+ st.success("تم تحديث إجمالي المصاريف الإدارية بناء على النسبة المئوية!")
557
+
558
+
559
+ def render_profit_tab():
560
+ """
561
+ عرض تبويب هامش الربح
562
+ """
563
+ st.markdown("<h3>هامش الربح</h3>", unsafe_allow_html=True)
564
+
565
+ # حساب التكاليف المباشرة والإجمالية
566
+ direct_costs = st.session_state.materials_cost + st.session_state.equipment_cost + st.session_state.labor_cost
567
+ total_costs = direct_costs + st.session_state.admin_cost
568
+
569
+ # عرض ملخص التكاليف
570
+ st.markdown("<div class='card'>", unsafe_allow_html=True)
571
+ st.markdown("<h4>ملخص التكاليف</h4>", unsafe_allow_html=True)
572
+ st.markdown(f"<p>إجمالي تكلفة المواد: <span style='color:var(--text-medium)'>{st.session_state.materials_cost:,.2f} ريال</span></p>", unsafe_allow_html=True)
573
+ st.markdown(f"<p>إجمالي تكلفة المعدات: <span style='color:var(--text-medium)'>{st.session_state.equipment_cost:,.2f} ريال</span></p>", unsafe_allow_html=True)
574
+ st.markdown(f"<p>إجمالي تكلفة العمالة: <span style='color:var(--text-medium)'>{st.session_state.labor_cost:,.2f} ريال</span></p>", unsafe_allow_html=True)
575
+ st.markdown(f"<p>إجمالي التكاليف المباشرة: <span style='color:var(--primary-color)'>{direct_costs:,.2f} ريال</span></p>", unsafe_allow_html=True)
576
+ st.markdown(f"<p>إجمالي المصاريف الإدارية: <span style='color:var(--text-medium)'>{st.session_state.admin_cost:,.2f} ريال</span></p>", unsafe_allow_html=True)
577
+ st.markdown(f"<h4>إجمالي التكاليف: <span style='color:var(--primary-color)'>{total_costs:,.2f} ريال</span></h4>", unsafe_allow_html=True)
578
+ st.markdown("</div>", unsafe_allow_html=True)
579
+
580
+ # تحديد هامش الربح
581
+ st.markdown("<h4>تحديد هامش الربح</h4>", unsafe_allow_html=True)
582
+
583
+ col1, col2 = st.columns(2)
584
+
585
+ with col1:
586
+ profit_margin = st.slider("نسبة هامش الربح (%)", min_value=0, max_value=30, value=int(st.session_state.profit_margin), key="profit_margin_slider")
587
+ st.session_state.profit_margin = profit_margin
588
+
589
+ with col2:
590
+ profit_amount = total_costs * (profit_margin / 100)
591
+ st.markdown(f"<div class='card'><h4>قيمة هامش الربح: <span style='color:var(--primary-color)'>{profit_amount:,.2f} ريال</span></h4></div>", unsafe_allow_html=True)
592
+
593
+ # إجمالي قيمة العرض
594
+ total_price = total_costs + profit_amount
595
+ st.markdown("<div class='card' style='background: var(--primary-light);'>", unsafe_allow_html=True)
596
+ st.markdown(f"<h3>إجمالي قيمة العرض: <span style='color:var(--primary-color)'>{total_price:,.2f} ريال</span></h3>", unsafe_allow_html=True)
597
+ st.markdown("</div>", unsafe_allow_html=True)
598
+
599
+ # تحليل الحساسية لهامش الربح
600
+ st.markdown("<h4>تحليل حساسية هامش الربح</h4>", unsafe_allow_html=True)
601
+
602
+ sensitivity_data = []
603
+ for margin in range(5, 31, 5):
604
+ profit = total_costs * (margin / 100)
605
+ total = total_costs + profit
606
+ sensitivity_data.append({
607
+ "نسبة الربح": f"{margin}%",
608
+ "قيمة الربح": profit,
609
+ "إجمالي العرض": total
610
+ })
611
+
612
+ sensitivity_df = pd.DataFrame(sensitivity_data)
613
+
614
+ # رسم بياني لتحليل الحساسية
615
+ fig = go.Figure()
616
+
617
+ fig.add_trace(go.Bar(
618
+ x=[item["نسبة الربح"] for item in sensitivity_data],
619
+ y=[item["قيمة الربح"] for item in sensitivity_data],
620
+ name="قيمة الربح",
621
+ marker_color="rgba(14, 165, 165, 0.7)"
622
+ ))
623
+
624
+ fig.add_trace(go.Scatter(
625
+ x=[item["نسبة الربح"] for item in sensitivity_data],
626
+ y=[item["إجمالي العرض"] for item in sensitivity_data],
627
+ name="إجمالي العرض",
628
+ mode="lines+markers",
629
+ marker=dict(size=8, color="rgba(255, 154, 60, 1.0)"),
630
+ line=dict(width=3, color="rgba(255, 154, 60, 0.7)")
631
+ ))
632
+
633
+ fig.update_layout(
634
+ title="تحليل حساسية هامش الربح",
635
+ xaxis_title="نسبة الربح",
636
+ yaxis_title="القيمة (ريال)",
637
+ font=dict(family="Almarai, Arial", size=14),
638
+ margin=dict(t=50, b=50, l=20, r=20),
639
+ hovermode="x unified"
640
+ )
641
+
642
+ st.plotly_chart(fig, use_container_width=True)
643
+
644
+ # جدول تحليل الحساسية
645
+ st.dataframe(sensitivity_df)
646
+
647
+
648
+ def render_final_report(project_name, project_location, project_area, project_type):
649
+ """
650
+ عرض التقرير النهائي للتكاليف
651
+ """
652
+ st.markdown("<h3>التقرير النهائي لتكاليف المشروع</h3>", unsafe_allow_html=True)
653
+
654
+ # التأكد من وجود المتغيرات المطلوبة في حالة الجلسة وضمان أن لديهم قيم صحيحة
655
+ required_fields = {
656
+ 'materials_cost': 0.0,
657
+ 'equipment_cost': 0.0,
658
+ 'labor_cost': 0.0,
659
+ 'admin_cost': 0.0,
660
+ 'profit_margin': 15.0,
661
+ 'materials': [],
662
+ 'equipment': [],
663
+ 'labor': [],
664
+ 'admin_expenses': []
665
+ }
666
+
667
+ # مرور على كافة الحقول المطلوبة للتأكد من وجودها
668
+ for field, default_value in required_fields.items():
669
+ if field not in st.session_state:
670
+ st.session_state[field] = default_value
671
+
672
+ # التحقق من أن القيم العددية صالحة (غير None وليست NaN)
673
+ if field in ['materials_cost', 'equipment_cost', 'labor_cost', 'admin_cost', 'profit_margin']:
674
+ # إذا كانت القيمة None أو NaN، استخدم القيمة الافتراضية
675
+ if st.session_state[field] is None or pd.isna(st.session_state[field]):
676
+ st.session_state[field] = default_value
677
+
678
+ # حساب التكاليف المباشرة والإجمالية
679
+ direct_costs = st.session_state.materials_cost + st.session_state.equipment_cost + st.session_state.labor_cost
680
+ total_costs = direct_costs + st.session_state.admin_cost
681
+ profit_amount = total_costs * (st.session_state.profit_margin / 100)
682
+ total_price = total_costs + profit_amount
683
+
684
+ # معلومات المشروع
685
+ st.markdown("<div class='card'>", unsafe_allow_html=True)
686
+ st.markdown("<h4>معلومات المشروع</h4>", unsafe_allow_html=True)
687
+ col1, col2 = st.columns(2)
688
+
689
+ with col1:
690
+ st.markdown(f"<p><strong>اسم المشروع:</strong> {project_name}</p>", unsafe_allow_html=True)
691
+ st.markdown(f"<p><strong>نوع المشروع:</strong> {project_type}</p>", unsafe_allow_html=True)
692
+
693
+ with col2:
694
+ st.markdown(f"<p><strong>موقع المشروع:</strong> {project_location}</p>", unsafe_allow_html=True)
695
+ st.markdown(f"<p><strong>المساحة الإجمالية:</strong> {project_area} م²</p>", unsafe_allow_html=True)
696
+
697
+ st.markdown("</div>", unsafe_allow_html=True)
698
+
699
+ # ملخص التكاليف
700
+ st.markdown("<div class='card'>", unsafe_allow_html=True)
701
+ st.markdown("<h4>ملخص التكاليف</h4>", unsafe_allow_html=True)
702
+
703
+ col1, col2 = st.columns(2)
704
+
705
+ with col1:
706
+ st.markdown(f"<p><strong>تكلفة المواد:</strong> {st.session_state.materials_cost:,.2f} ريال</p>", unsafe_allow_html=True)
707
+ st.markdown(f"<p><strong>تكلفة المعدات:</strong> {st.session_state.equipment_cost:,.2f} ريال</p>", unsafe_allow_html=True)
708
+ st.markdown(f"<p><strong>تكلفة العمالة:</strong> {st.session_state.labor_cost:,.2f} ريال</p>", unsafe_allow_html=True)
709
+ st.markdown(f"<p><strong>إجمالي التكاليف المباشرة:</strong> {direct_costs:,.2f} ريال</p>", unsafe_allow_html=True)
710
+
711
+ with col2:
712
+ st.markdown(f"<p><strong>المصاريف الإدارية:</strong> {st.session_state.admin_cost:,.2f} ريال</p>", unsafe_allow_html=True)
713
+ st.markdown(f"<p><strong>إجمالي التكاليف:</strong> {total_costs:,.2f} ريال</p>", unsafe_allow_html=True)
714
+ st.markdown(f"<p><strong>هامش الربح ({st.session_state.profit_margin}%):</strong> {profit_amount:,.2f} ريال</p>", unsafe_allow_html=True)
715
+ st.markdown(f"<h4>إجمالي قيمة العرض: {total_price:,.2f} ريال</h4>", unsafe_allow_html=True)
716
+
717
+ st.markdown("</div>", unsafe_allow_html=True)
718
+
719
+ # عرض التفاصيل بالمتر المربع
720
+ if project_area > 0:
721
+ per_sqm_cost = total_price / project_area
722
+ st.markdown("<div class='card'>", unsafe_allow_html=True)
723
+ st.markdown("<h4>تكلفة المتر المربع</h4>", unsafe_allow_html=True)
724
+ st.markdown(f"<p>تكلفة المتر المربع الإجمالية: <strong>{per_sqm_cost:,.2f} ريال/م²</strong></p>", unsafe_allow_html=True)
725
+ st.markdown("</div>", unsafe_allow_html=True)
726
+ else:
727
+ st.markdown("<div class='card'>", unsafe_allow_html=True)
728
+ st.markdown("<h4>تكلفة المتر المربع</h4>", unsafe_allow_html=True)
729
+ st.markdown("<p>يرجى إدخال مساحة صحيحة للمشروع لحساب تكلفة المتر المربع</p>", unsafe_allow_html=True)
730
+ st.markdown("</div>", unsafe_allow_html=True)
731
+
732
+ # رسم بياني لتوزيع التكاليف
733
+ st.markdown("<h4>توزيع التكاليف</h4>", unsafe_allow_html=True)
734
+
735
+ # تجنب القسمة على صفر
736
+ if total_price > 0:
737
+ cost_distribution = [
738
+ {"النوع": "المواد", "القيمة": st.session_state.materials_cost, "النسبة": st.session_state.materials_cost / total_price * 100},
739
+ {"النوع": "المعدات", "القيمة": st.session_state.equipment_cost, "النسبة": st.session_state.equipment_cost / total_price * 100},
740
+ {"النوع": "العمالة", "القيمة": st.session_state.labor_cost, "النسبة": st.session_state.labor_cost / total_price * 100},
741
+ {"النوع": "المصاريف الإدارية", "القيمة": st.session_state.admin_cost, "النسبة": st.session_state.admin_cost / total_price * 100},
742
+ {"النوع": "هامش الربح", "القيمة": profit_amount, "النسبة": profit_amount / total_price * 100}
743
+ ]
744
+ else:
745
+ # إذا كان المجموع صفر، اجعل جميع النسب المئوية صفر
746
+ cost_distribution = [
747
+ {"النوع": "المواد", "القيمة": st.session_state.materials_cost, "النسبة": 0},
748
+ {"النوع": "المعدات", "القيمة": st.session_state.equipment_cost, "النسبة": 0},
749
+ {"النوع": "العمالة", "القيمة": st.session_state.labor_cost, "النسبة": 0},
750
+ {"النوع": "المصاريف الإدارية", "القيمة": st.session_state.admin_cost, "النسبة": 0},
751
+ {"النوع": "هامش الربح", "القيمة": profit_amount, "النسبة": 0}
752
+ ]
753
+
754
+ cost_df = pd.DataFrame(cost_distribution)
755
+
756
+ fig = px.pie(
757
+ cost_df,
758
+ values="القيمة",
759
+ names="النوع",
760
+ title="توزيع التكاليف والأرباح",
761
+ color_discrete_sequence=px.colors.sequential.Teal,
762
+ hole=0.4
763
+ )
764
+
765
+ fig.update_traces(textposition='inside', textinfo='percent+label')
766
+
767
+ fig.update_layout(
768
+ annotations=[dict(text=f"{total_price:,.0f} ريال", x=0.5, y=0.5, font_size=14, showarrow=False)],
769
+ font=dict(family="Almarai, Arial", size=14),
770
+ margin=dict(t=50, b=50, l=20, r=20)
771
+ )
772
+
773
+ st.plotly_chart(fig, use_container_width=True)
774
+
775
+ # جدول توزيع التكاليف
776
+ st.dataframe(cost_df)
777
+
778
+ # زر لإنشاء تقرير PDF
779
+ col1, col2 = st.columns(2)
780
+
781
+ with col1:
782
+ if st.button("تصدير التقرير إلى PDF"):
783
+ st.success("تم تصدير التقرير بنجاح!")
784
+
785
+ with col2:
786
+ if st.button("حفظ التقرير في قاعدة البيانات"):
787
+ st.success("تم حفظ التقرير في قاعدة البيانات بنجاح!")
modules/pricing/exceptions.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ استثناءات وحدة التسعير
3
+ """
4
+
5
+ class PricingError(Exception):
6
+ """استثناء أساسي لأخطاء التسعير"""
7
+ pass
8
+
9
+
10
+ class LocalContentCalculationError(PricingError):
11
+ """استثناء لأخطاء حساب المحتوى المحلي"""
12
+ pass
13
+
14
+
15
+ class PriceEstimationError(PricingError):
16
+ """استثناء لأخطاء تقدير الأسعار"""
17
+ pass
18
+
19
+
20
+ class ResourceNotFoundError(PricingError):
21
+ """استثناء لعدم وجود المورد المطلوب"""
22
+ pass
23
+
24
+
25
+ class InvalidInputError(PricingError):
26
+ """استثناء للمدخلات غير الصالحة"""
27
+ pass
28
+
29
+
30
+ class ModelLoadingError(PricingError):
31
+ """استثناء لأخطاء تحميل النموذج"""
32
+ pass
33
+
34
+
35
+ class DataProcessingError(PricingError):
36
+ """استثناء لأخطاء معالجة البيانات"""
37
+ pass
38
+
39
+
40
+ class UnbalancedPricingError(PricingError):
41
+ """استثناء لأخطاء التسعير غير المتزن"""
42
+ pass
modules/pricing/price_analysis_component.py ADDED
@@ -0,0 +1,932 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ from datetime import datetime
5
+ import time
6
+
7
+ class PriceAnalysisComponent:
8
+ """مكون تحليل الأسعار للبنود"""
9
+
10
+ def __init__(self):
11
+ """تهيئة مكون تحليل الأسعار"""
12
+ # تهيئة قائمة الوحدات المتاحة
13
+ self.unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
14
+
15
+ # تهيئة فئات التكاليف
16
+ self.cost_categories = [
17
+ "مواد",
18
+ "عمالة",
19
+ "معدات",
20
+ "مقاولي الباطن",
21
+ "مصاريف عامة",
22
+ "أرباح"
23
+ ]
24
+
25
+ # تهيئة قائمة البنود وتحليل أسعارها
26
+ if 'items_price_analysis' not in st.session_state:
27
+ st.session_state.items_price_analysis = {}
28
+
29
+ def render(self):
30
+ """عرض واجهة تحليل الأسعار"""
31
+ st.markdown("<h2 class='module-title'>تحليل أسعار البنود</h2>", unsafe_allow_html=True)
32
+
33
+ # التحقق من وجود بنود في التسعير الحالي
34
+ if 'current_pricing' not in st.session_state or 'items' not in st.session_state.current_pricing:
35
+ st.warning("ليس هناك بنود للتحليل. يرجى إنشاء تسعير أولاً.")
36
+ return
37
+
38
+ # الحصول على البنود من التسعير الحالي
39
+ items = st.session_state.current_pricing['items'].copy()
40
+
41
+ # عرض قائمة البنود
42
+ st.markdown("### قائمة البنود")
43
+ st.dataframe(items[['رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي']],
44
+ use_container_width=True, hide_index=True)
45
+
46
+ # اختيار البند لتحليل السعر
47
+ selected_item_id = st.selectbox(
48
+ "اختر البند لتحليل السعر",
49
+ options=items['رقم البند'].tolist(),
50
+ format_func=lambda x: f"{x}: {items[items['رقم البند'] == x]['وصف البند'].values[0][:50]}..."
51
+ )
52
+
53
+ if selected_item_id:
54
+ # الحصول على البند المحدد
55
+ selected_item = items[items['رقم البند'] == selected_item_id].iloc[0]
56
+
57
+ # عرض تفاصيل البند المختار
58
+ col1, col2, col3 = st.columns(3)
59
+
60
+ with col1:
61
+ st.metric("رقم البند", selected_item['رقم البند'])
62
+
63
+ with col2:
64
+ st.metric("الكمية", f"{selected_item['الكمية']} {selected_item['الوحدة']}")
65
+
66
+ with col3:
67
+ st.metric("سعر الوحدة", f"{selected_item['سعر الوحدة']:,.2f} ريال")
68
+
69
+ st.markdown(f"**وصف البند**: {selected_item['وصف البند']}")
70
+
71
+ # إنشاء أو تحديث تحليل السعر للبند المحدد
72
+ if selected_item_id not in st.session_state.items_price_analysis:
73
+ # إنشاء تحليل سعر افتراضي
74
+ self._create_default_price_analysis(selected_item_id, selected_item)
75
+
76
+ # عرض وتحرير تحليل السعر
77
+ self._render_price_analysis_editor(selected_item_id, selected_item)
78
+
79
+ def _create_default_price_analysis(self, item_id, item):
80
+ """إنشاء تحليل سعر افتراضي للبند"""
81
+ # إنشاء قائمة مكونات تحليل السعر
82
+ components = pd.DataFrame(columns=[
83
+ 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي'
84
+ ])
85
+
86
+ # إضافة مكونات افتراضية بناءً على نوع البند
87
+ is_concrete = 'خرسان' in item['وصف البند']
88
+ is_steel = 'حديد' in item['وصف البند'] or 'تسليح' in item['وصف البند']
89
+ is_bricks = 'بلوك' in item['وصف البند'] or 'طوب' in item['وصف البند']
90
+ is_paint = 'دهان' in item['وصف البند'] or 'طلاء' in item['وصف البند']
91
+ is_insulation = 'عزل' in item['وصف البند']
92
+
93
+ # إضافة المكونات بناءً على نوع البند
94
+ if is_concrete:
95
+ # مكونات الخرسانة
96
+ default_components = pd.DataFrame({
97
+ 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
98
+ 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'],
99
+ 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1],
100
+ 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
101
+ 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150],
102
+ 'الإجمالي': [175, 40, 96, 400, 500, 100, 150]
103
+ })
104
+ components = pd.concat([components, default_components], ignore_index=True)
105
+
106
+ elif is_steel:
107
+ # مكونات الحديد
108
+ default_components = pd.DataFrame({
109
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
110
+ 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'],
111
+ 'الكمية': [1000, 10, 1, 1, 1],
112
+ 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
113
+ 'سعر الوحدة': [4.5, 50, 300, 200, 300],
114
+ 'الإجمالي': [4500, 500, 300, 200, 300]
115
+ })
116
+ components = pd.concat([components, default_components], ignore_index=True)
117
+
118
+ elif is_bricks:
119
+ # مكونات البلوك
120
+ default_components = pd.DataFrame({
121
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
122
+ 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'],
123
+ 'الكمية': [12.5, 0.02, 1, 1, 1],
124
+ 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'],
125
+ 'سعر الوحدة': [8, 500, 80, 15, 20],
126
+ 'الإجمالي': [100, 10, 80, 15, 20]
127
+ })
128
+ components = pd.concat([components, default_components], ignore_index=True)
129
+
130
+ elif is_paint:
131
+ # مكونات الدهانات
132
+ default_components = pd.DataFrame({
133
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
134
+ 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'],
135
+ 'الكمية': [0.4, 0.1, 1, 1, 1],
136
+ 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'],
137
+ 'سعر الوحدة': [80, 20, 35, 5, 10],
138
+ 'الإجمالي': [32, 2, 35, 5, 10]
139
+ })
140
+ components = pd.concat([components, default_components], ignore_index=True)
141
+
142
+ elif is_insulation:
143
+ # مكونات العزل
144
+ default_components = pd.DataFrame({
145
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
146
+ 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'],
147
+ 'الكمية': [1.1, 0.2, 1, 1, 1],
148
+ 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'],
149
+ 'سعر الوحدة': [60, 30, 25, 10, 15],
150
+ 'الإجمالي': [66, 6, 25, 10, 15]
151
+ })
152
+ components = pd.concat([components, default_components], ignore_index=True)
153
+
154
+ else:
155
+ # مكونات عامة افتراضية
156
+ default_components = pd.DataFrame({
157
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
158
+ 'الوصف': ['مواد أساسية', 'عمالة', 'معدات ومعد مساعدة', 'مصاريف عامة', 'أرباح'],
159
+ 'الكمية': [1, 1, 1, 1, 1],
160
+ 'الوحدة': [item['الوحدة'], 'وحدة', 'وحدة', 'وحدة', 'وحدة'],
161
+ 'سعر الوحدة': [
162
+ item['سعر الوحدة'] * 0.6,
163
+ item['سعر الوحدة'] * 0.2,
164
+ item['سعر الوحدة'] * 0.1,
165
+ item['سعر الوحدة'] * 0.05,
166
+ item['سعر الوحدة'] * 0.05
167
+ ],
168
+ 'الإجمالي': [
169
+ item['سعر الوحدة'] * 0.6,
170
+ item['سعر الوحدة'] * 0.2,
171
+ item['سعر الوحدة'] * 0.1,
172
+ item['سعر الوحدة'] * 0.05,
173
+ item['سعر الوحدة'] * 0.05
174
+ ]
175
+ })
176
+ components = pd.concat([components, default_components], ignore_index=True)
177
+
178
+ # حفظ تحليل السعر للبند
179
+ st.session_state.items_price_analysis[item_id] = components
180
+
181
+ def _render_price_analysis_editor(self, item_id, item):
182
+ """عرض محرر تحليل السعر للبند"""
183
+ st.markdown("### تحليل السعر")
184
+
185
+ # الحصول على مكونات تحليل السعر
186
+ components = st.session_state.items_price_analysis[item_id]
187
+
188
+ # عرض تحليل السعر في محرر بيانات
189
+ st.markdown("#### مكونات السعر")
190
+
191
+ edited_components = st.data_editor(
192
+ components,
193
+ use_container_width=True,
194
+ hide_index=True,
195
+ num_rows="dynamic",
196
+ column_config={
197
+ 'نوع التكلفة': st.column_config.SelectboxColumn(
198
+ 'نوع التكلفة',
199
+ help='فئة التكلفة',
200
+ options=self.cost_categories
201
+ ),
202
+ 'الوحدة': st.column_config.SelectboxColumn(
203
+ 'الوحدة',
204
+ help='وحدة القياس',
205
+ options=self.unit_options + ["وحدة", "ساعة", "يوم"]
206
+ ),
207
+ 'الكمية': st.column_config.NumberColumn(
208
+ 'الكمية',
209
+ help='الكمية',
210
+ min_value=0.0,
211
+ format="%.2f"
212
+ ),
213
+ 'سعر الوحدة': st.column_config.NumberColumn(
214
+ 'سعر الوحدة',
215
+ help='سعر الوحدة',
216
+ min_value=0.0,
217
+ format="%.2f"
218
+ ),
219
+ 'الإجمالي': st.column_config.NumberColumn(
220
+ 'الإجمالي',
221
+ help='الإجمالي',
222
+ min_value=0.0,
223
+ format="%.2f"
224
+ )
225
+ }
226
+ )
227
+
228
+ # إعادة حساب الإجمالي لكل مكون
229
+ edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة']
230
+
231
+ # حفظ التعديلات
232
+ st.session_state.items_price_analysis[item_id] = edited_components
233
+
234
+ # حساب إجمالي تحليل السعر
235
+ total_analysis_price = edited_components['الإجمالي'].sum()
236
+ unit_price_from_analysis = total_analysis_price / item['الكمية'] if item['الكمية'] > 0 else 0
237
+
238
+ # عرض ملخص تحليل السعر
239
+ st.markdown("#### ملخص تحليل السعر")
240
+
241
+ col1, col2, col3 = st.columns(3)
242
+
243
+ with col1:
244
+ st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال")
245
+
246
+ with col2:
247
+ st.metric("سعر الوحدة من التحليل", f"{unit_price_from_analysis:,.2f} ريال")
248
+
249
+ with col3:
250
+ # المقارنة مع السعر الأصلي
251
+ diff = unit_price_from_analysis - item['سعر الوحدة']
252
+ st.metric(
253
+ "الفرق عن السعر الأصلي",
254
+ f"{diff:,.2f} ريال",
255
+ delta=f"{(diff/item['سعر الوحدة']*100) if item['سعر الوحدة'] > 0 else 0:.1f}%"
256
+ )
257
+
258
+ # تحليل توزيع التكاليف حسب الفئة
259
+ cost_by_category = edited_components.groupby('نوع التكلفة')['الإجمالي'].sum().reset_index()
260
+
261
+ # عرض مخطط توزيع التكاليف
262
+ st.markdown("#### توزيع التكاليف حسب الفئة")
263
+
264
+ # عرض توزيع التكاليف في جدول
265
+ distribution_df = pd.DataFrame({
266
+ 'نوع التكلفة': cost_by_category['نوع التكلفة'],
267
+ 'القيمة': cost_by_category['الإجمالي'],
268
+ 'النسبة المئوية': (cost_by_category['الإجمالي'] / total_analysis_price * 100).round(2)
269
+ })
270
+
271
+ st.dataframe(
272
+ distribution_df,
273
+ use_container_width=True,
274
+ hide_index=True,
275
+ column_config={
276
+ 'القيمة': st.column_config.NumberColumn(
277
+ 'القيمة',
278
+ help='القيمة',
279
+ format="%.2f"
280
+ ),
281
+ 'النسبة المئوية': st.column_config.ProgressColumn(
282
+ 'النسبة المئوية',
283
+ help='النسبة المئوية',
284
+ format="%.2f%%",
285
+ min_value=0,
286
+ max_value=100
287
+ )
288
+ }
289
+ )
290
+
291
+ # أزرار الإجراءات
292
+ col1, col2, col3 = st.columns(3)
293
+
294
+ with col1:
295
+ if st.button("تحديث سعر البند", use_container_width=True):
296
+ # تحديث سعر البند بناءً على تحليل السعر
297
+ items = st.session_state.current_pricing['items'].copy()
298
+ item_index = items[items['رقم البند'] == item_id].index[0]
299
+
300
+ # تحديث ��عر الوحدة والإجمالي
301
+ items.at[item_index, 'سعر الوحدة'] = unit_price_from_analysis
302
+ items.at[item_index, 'الإجمالي'] = unit_price_from_analysis * items.at[item_index, 'الكمية']
303
+
304
+ # حفظ التعديلات في التسعير الحالي
305
+ st.session_state.current_pricing['items'] = items
306
+
307
+ st.success(f"تم تحديث سعر البند بناءً على تحليل السعر: {unit_price_from_analysis:,.2f} ريال")
308
+ time.sleep(0.5)
309
+ st.rerun()
310
+
311
+ with col2:
312
+ if st.button("تصدير تحليل السعر", use_container_width=True):
313
+ st.success("تم إرسال تحليل السعر للتصدير بنجاح!")
314
+
315
+ with col3:
316
+ if st.button("مسح تحليل السعر", use_container_width=True):
317
+ # حذف تحليل السعر للبند
318
+ if item_id in st.session_state.items_price_analysis:
319
+ del st.session_state.items_price_analysis[item_id]
320
+
321
+ st.warning("تم مسح تحليل السعر للبند")
322
+ time.sleep(0.5)
323
+ st.rerun()
324
+
325
+ def add_to_pricing_app(self, pricing_app):
326
+ """إضافة مكون تحليل الأسعار إلى تطبيق التسعير"""
327
+ # إضافة تبويب جديد
328
+ if not hasattr(pricing_app, 'tabs'):
329
+ pricing_app.tabs = []
330
+
331
+ if len(pricing_app.tabs) == 4: # إذا كان هناك 4 تبويبات فقط
332
+ pricing_app.tabs.append("تحليل أسعار البنود")
333
+
334
+ # إضافة دالة العرض
335
+ pricing_app._render_price_analysis_tab = self.render
336
+
337
+
338
+ def render_integrated_item_input():
339
+ """عرض واجهة إدخال البنود مع تحليل السعر المتكامل"""
340
+
341
+ # ضبط CSS لتحسين ظهور الواجهة العربية
342
+ st.markdown("""
343
+ <style>
344
+ input, .stTextArea textarea {
345
+ direction: rtl;
346
+ text-align: right;
347
+ font-family: 'Arial', 'Tahoma', sans-serif !important;
348
+ }
349
+ .stTextInput > div > div > input {
350
+ text-align: right;
351
+ direction: rtl;
352
+ }
353
+ .pricing-analysis-container {
354
+ border: 1px solid #e0e0e0;
355
+ border-radius: 10px;
356
+ padding: 10px;
357
+ margin-top: 10px;
358
+ background-color: #f9f9f9;
359
+ }
360
+ </style>
361
+ """, unsafe_allow_html=True)
362
+
363
+ # تهيئة قائمة الوحدات المتاحة
364
+ unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
365
+
366
+ # تهيئة فئات التكاليف
367
+ cost_categories = [
368
+ "مواد",
369
+ "عمالة",
370
+ "معدات",
371
+ "مقاولي الباطن",
372
+ "مصاريف عامة",
373
+ "أرباح"
374
+ ]
375
+
376
+ # إنشاء جدول البنود اذا لم يكن موجوداً
377
+ if 'manual_items' not in st.session_state:
378
+ manual_items = pd.DataFrame(columns=[
379
+ 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي'
380
+ ])
381
+
382
+ # إضافة بضعة صفوف افتراضية
383
+ default_items = pd.DataFrame({
384
+ 'رقم البند': ["A1", "A2", "A3", "A4", "A5"],
385
+ 'وصف البند': [
386
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
387
+ "توريد وتركيب حديد التسليح للأساسات",
388
+ "أعمال العزل المائي للأساسات",
389
+ "أعمال الردم والدك للأساسات",
390
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة"
391
+ ],
392
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3"],
393
+ 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0],
394
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0],
395
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0]
396
+ })
397
+
398
+ manual_items = pd.concat([manual_items, default_items])
399
+ st.session_state.manual_items = manual_items
400
+
401
+ # إنشاء جدول تحليل الأسعار اذا لم يكن موجوداً
402
+ if 'items_price_analysis' not in st.session_state:
403
+ st.session_state.items_price_analysis = {}
404
+
405
+ # عرض واجهة إدخال البنود
406
+ st.markdown("### إدخال تفاصيل البنود مع تحليل الأسعار")
407
+
408
+ # عرض البنود الحالية كجدول للعرض
409
+ st.markdown("### جدول البنود الحالية")
410
+ st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
411
+
412
+ # التبويبات لإضافة بند جديد أو تعديل بند
413
+ tabs = st.tabs(["إضافة بند جديد", "تعديل بند حالي"])
414
+
415
+ with tabs[0]: # إضافة بند جديد
416
+ st.markdown("### إضافة بند جديد مع تحليل السعر")
417
+
418
+ col1, col2 = st.columns(2)
419
+
420
+ with col1:
421
+ new_id = st.text_input("رقم البند", value=f"A{len(st.session_state.manual_items)+1}", key="new_id")
422
+ new_desc = st.text_area("وصف البند", value="", key="new_desc")
423
+
424
+ with col2:
425
+ new_unit = st.selectbox("الوحدة", options=unit_options, key="new_unit")
426
+ new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f", key="new_qty")
427
+
428
+ # إنشاء تحليل السعر للبند الجديد
429
+ st.markdown('<div class="pricing-analysis-container">', unsafe_allow_html=True)
430
+ st.markdown("#### تحليل سعر البند")
431
+
432
+ # التعرف التلقائي على نوع البند من الوصف
433
+ is_concrete = False
434
+ is_steel = False
435
+ is_bricks = False
436
+ is_paint = False
437
+ is_insulation = False
438
+
439
+ if new_desc:
440
+ is_concrete = 'خرسان' in new_desc
441
+ is_steel = 'حديد' in new_desc or 'تسليح' in new_desc
442
+ is_bricks = 'بلوك' in new_desc or 'طوب' in new_desc
443
+ is_paint = 'دهان' in new_desc or 'طلاء' in new_desc
444
+ is_insulation = 'عزل' in new_desc
445
+
446
+ # تلميح للمستخدم عن التعرف التلقائي
447
+ if any([is_concrete, is_steel, is_bricks, is_paint, is_insulation]):
448
+ detected_type = ""
449
+ if is_concrete:
450
+ detected_type = "أعمال خرسانة"
451
+ elif is_steel:
452
+ detected_type = "أعمال حديد"
453
+ elif is_bricks:
454
+ detected_type = "أعمال بلوك"
455
+ elif is_paint:
456
+ detected_type = "أعمال دهانات"
457
+ elif is_insulation:
458
+ detected_type = "أعمال عزل"
459
+
460
+ st.info(f"تم التعرف تلقائياً على نوع البند: {detected_type}")
461
+
462
+ # إنشاء مصفوفة فارغة لمكونات البند
463
+ if 'new_components' not in st.session_state:
464
+ # إنشاء DataFrame فارغ
465
+ new_components = pd.DataFrame(columns=[
466
+ 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي'
467
+ ])
468
+
469
+ # إضافة مكونات افتراضية بناءً على نوع البند
470
+ if is_concrete:
471
+ # مكونات الخرسانة
472
+ default_components = pd.DataFrame({
473
+ 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
474
+ 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'],
475
+ 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1],
476
+ 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
477
+ 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150],
478
+ 'الإجمالي': [175, 40, 96, 400, 500, 100, 150]
479
+ })
480
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
481
+
482
+ elif is_steel:
483
+ # مكونات الحديد
484
+ default_components = pd.DataFrame({
485
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
486
+ 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'],
487
+ 'الكمية': [1000, 10, 1, 1, 1],
488
+ 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
489
+ 'سعر الوحدة': [4.5, 50, 300, 200, 300],
490
+ 'الإجمالي': [4500, 500, 300, 200, 300]
491
+ })
492
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
493
+
494
+ elif is_bricks:
495
+ # مكونات البلوك
496
+ default_components = pd.DataFrame({
497
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
498
+ 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'],
499
+ 'الكمية': [12.5, 0.02, 1, 1, 1],
500
+ 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'],
501
+ 'سعر الوحدة': [8, 500, 80, 15, 20],
502
+ 'الإجمالي': [100, 10, 80, 15, 20]
503
+ })
504
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
505
+
506
+ elif is_paint:
507
+ # مكونات الدهانات
508
+ default_components = pd.DataFrame({
509
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
510
+ 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'],
511
+ 'الكمية': [0.4, 0.1, 1, 1, 1],
512
+ 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'],
513
+ 'سعر الوحدة': [80, 20, 35, 5, 10],
514
+ 'الإجمالي': [32, 2, 35, 5, 10]
515
+ })
516
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
517
+
518
+ elif is_insulation:
519
+ # مكونات العزل
520
+ default_components = pd.DataFrame({
521
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
522
+ 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'],
523
+ 'الكمية': [1.1, 0.2, 1, 1, 1],
524
+ 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'],
525
+ 'سعر الوحدة': [60, 30, 25, 10, 15],
526
+ 'الإجمالي': [66, 6, 25, 10, 15]
527
+ })
528
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
529
+
530
+ else:
531
+ # مكونات عامة افتراضية
532
+ default_components = pd.DataFrame({
533
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
534
+ 'الوصف': ['مواد أساسية', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
535
+ 'الكمية': [1, 1, 1, 1, 1],
536
+ 'الوحدة': [new_unit if new_unit else 'وحدة', 'وحدة', 'وحدة', 'وحدة', 'وحدة'],
537
+ 'سعر الوحدة': [100, 50, 30, 20, 20],
538
+ 'الإجمالي': [100, 50, 30, 20, 20]
539
+ })
540
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
541
+
542
+ st.session_state.new_components = new_components
543
+
544
+ # عرض وتحرير مكونات تحليل السعر
545
+ edited_components = st.data_editor(
546
+ st.session_state.new_components,
547
+ use_container_width=True,
548
+ hide_index=True,
549
+ num_rows="dynamic",
550
+ column_config={
551
+ 'نوع التكلفة': st.column_config.SelectboxColumn(
552
+ 'نوع التكلفة',
553
+ help='فئة التكلفة',
554
+ options=cost_categories
555
+ ),
556
+ 'الوحدة': st.column_config.SelectboxColumn(
557
+ 'الوحدة',
558
+ help='وحدة القياس',
559
+ options=unit_options + ["وحدة", "ساعة", "يوم"]
560
+ ),
561
+ 'الكمية': st.column_config.NumberColumn(
562
+ 'الكمية',
563
+ help='الكمية',
564
+ min_value=0.0,
565
+ format="%.2f"
566
+ ),
567
+ 'سعر الوحدة': st.column_config.NumberColumn(
568
+ 'سعر الوحدة',
569
+ help='سعر الوحدة',
570
+ min_value=0.0,
571
+ format="%.2f"
572
+ ),
573
+ 'الإجمالي': st.column_config.NumberColumn(
574
+ 'الإجمالي',
575
+ help='الإجمالي',
576
+ min_value=0.0,
577
+ format="%.2f"
578
+ )
579
+ }
580
+ )
581
+
582
+ # إعادة حساب الإجمالي لكل مكون
583
+ edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة']
584
+
585
+ # حفظ التعديلات
586
+ st.session_state.new_components = edited_components
587
+
588
+ # حساب إجمالي تحليل السعر
589
+ total_analysis_price = edited_components['الإجمالي'].sum()
590
+ unit_price_from_analysis = total_analysis_price / new_qty if new_qty > 0 else 0
591
+
592
+ # عرض ملخص تحليل السعر
593
+ st.markdown("#### ملخص تحليل السعر")
594
+
595
+ col1, col2 = st.columns(2)
596
+
597
+ with col1:
598
+ st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال")
599
+
600
+ with col2:
601
+ st.metric("سعر الوحدة المحسوب", f"{unit_price_from_analysis:,.2f} ريال")
602
+
603
+ st.markdown('</div>', unsafe_allow_html=True)
604
+
605
+ # استخدام السعر المحسوب
606
+ use_calculated_price = st.checkbox("استخدام السعر المحسوب من التحليل", value=True)
607
+
608
+ # تحديد سعر الوحدة النهائي
609
+ if use_calculated_price and new_qty > 0:
610
+ new_price = unit_price_from_analysis
611
+ else:
612
+ new_price = st.number_input("سعر الوحدة", value=unit_price_from_analysis if new_qty > 0 else 0.0, min_value=0.0, format="%.2f", key="new_price")
613
+
614
+ # حساب الإجمالي
615
+ new_total = new_qty * new_price
616
+ st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال")
617
+
618
+ # مقارنة السعر المدخل مع السعر المحسوب
619
+ if not use_calculated_price and new_qty > 0 and unit_price_from_analysis > 0:
620
+ price_diff = new_price - unit_price_from_analysis
621
+ diff_percentage = (price_diff / unit_price_from_analysis) * 100
622
+
623
+ if abs(diff_percentage) > 5: # إذا كان الفرق أكبر من 5%
624
+ if diff_percentage > 0:
625
+ st.warning(f"السعر المدخل أعلى من السعر المحسوب بنسبة {diff_percentage:.2f}%")
626
+ else:
627
+ st.warning(f"السعر المدخل أقل من السعر المحسوب بنسبة {abs(diff_percentage):.2f}%")
628
+
629
+ # زر إضافة البند
630
+ if st.button("إضافة البند"):
631
+ # التحقق من صحة البيانات
632
+ if new_id and new_desc and new_qty > 0:
633
+ # إنشاء صف جديد
634
+ new_row = pd.DataFrame({
635
+ 'رقم البند': [new_id],
636
+ 'وصف البند': [new_desc],
637
+ 'الوحدة': [new_unit],
638
+ 'الكمية': [float(new_qty)],
639
+ 'سعر الوحدة': [float(new_price)],
640
+ 'الإجمالي': [float(new_total)]
641
+ })
642
+
643
+ # إضافة الصف إلى DataFrame
644
+ st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True)
645
+
646
+ # حفظ تحليل سعر البند
647
+ st.session_state.items_price_analysis[new_id] = st.session_state.new_components.copy()
648
+
649
+ # إعادة تهيئة مكونات البند الجديد
650
+ if 'new_components' in st.session_state:
651
+ del st.session_state.new_components
652
+
653
+ st.success("تم إضافة البند وتحليل السعر بنجاح!")
654
+ time.sleep(0.5)
655
+ st.rerun()
656
+ else:
657
+ st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.")
658
+
659
+ with tabs[1]: # تعديل بند حالي
660
+ st.markdown("### تعديل بند حالي مع تحليل السعر")
661
+
662
+ # اختيار البند للتعديل
663
+ edit_item_id = st.selectbox(
664
+ "اختر البند للتعديل",
665
+ options=st.session_state.manual_items['رقم البند'].tolist(),
666
+ format_func=lambda x: f"{x}: {st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == x]['وصف البند'].values[0][:30]}..."
667
+ )
668
+
669
+ if edit_item_id:
670
+ # الحصول على مؤشر الصف للبند المحدد
671
+ idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == edit_item_id].index[0]
672
+ row = st.session_state.manual_items.loc[idx]
673
+
674
+ # إنشاء نموذج تعديل البند
675
+ col1, col2 = st.columns(2)
676
+
677
+ with col1:
678
+ edited_id = st.text_input("رقم البند (تعديل)", value=row['رقم البند'], key="edit_id")
679
+ edited_desc = st.text_area("وصف البند (تعديل)", value=row['وصف البند'], key="edit_desc")
680
+
681
+ with col2:
682
+ edited_unit = st.selectbox(
683
+ "الوحدة (تعديل)",
684
+ options=unit_options,
685
+ index=unit_options.index(row['الوحدة']) if row['الوحدة'] in unit_options else 0,
686
+ key="edit_unit"
687
+ )
688
+ edited_qty = st.number_input("الكمية (تعديل)", value=float(row['الكمية']), min_value=0.0, format="%.2f", key="edit_qty")
689
+
690
+ # إنشاء أو تحرير تحليل السعر ل��بند
691
+ st.markdown('<div class="pricing-analysis-container">', unsafe_allow_html=True)
692
+ st.markdown("#### تحليل سعر البند")
693
+
694
+ # التحقق مما إذا كان البند له تحليل سعر محفوظ
695
+ if edit_item_id in st.session_state.items_price_analysis:
696
+ # استخدام تحليل السعر المحفوظ
697
+ components = st.session_state.items_price_analysis[edit_item_id]
698
+ else:
699
+ # إنشاء تحليل سعر افتراضي
700
+ components = pd.DataFrame(columns=[
701
+ 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي'
702
+ ])
703
+
704
+ # فحص نوع البند من الوصف
705
+ is_concrete = 'خرسان' in row['وصف البند']
706
+ is_steel = 'حديد' in row['وصف البند'] or 'تسليح' in row['وصف البند']
707
+ is_bricks = 'بلوك' in row['وصف البند'] or 'طوب' in row['وصف البند']
708
+ is_paint = 'دهان' in row['وصف البند'] or 'طلاء' in row['وصف البند']
709
+ is_insulation = 'عزل' in row['وصف البند']
710
+
711
+ # إضافة مكونات افتراضية بناءً على نوع البند
712
+ if is_concrete:
713
+ # مكونات الخرسانة
714
+ default_components = pd.DataFrame({
715
+ 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
716
+ 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'],
717
+ 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1],
718
+ 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
719
+ 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150],
720
+ 'الإجمالي': [175, 40, 96, 400, 500, 100, 150]
721
+ })
722
+ components = pd.concat([components, default_components], ignore_index=True)
723
+
724
+ elif is_steel:
725
+ # مكونات الحديد
726
+ default_components = pd.DataFrame({
727
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
728
+ 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'],
729
+ 'الكمية': [1000, 10, 1, 1, 1],
730
+ 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
731
+ 'سعر الوحدة': [4.5, 50, 300, 200, 300],
732
+ 'الإجمالي': [4500, 500, 300, 200, 300]
733
+ })
734
+ components = pd.concat([components, default_components], ignore_index=True)
735
+
736
+ elif is_bricks:
737
+ # مكونات البلوك
738
+ default_components = pd.DataFrame({
739
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
740
+ 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'],
741
+ 'الكمية': [12.5, 0.02, 1, 1, 1],
742
+ 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'],
743
+ 'سعر الوحدة': [8, 500, 80, 15, 20],
744
+ 'الإجمالي': [100, 10, 80, 15, 20]
745
+ })
746
+ components = pd.concat([components, default_components], ignore_index=True)
747
+
748
+ elif is_paint:
749
+ # مكونات الدهانات
750
+ default_components = pd.DataFrame({
751
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
752
+ 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'],
753
+ 'الكمية': [0.4, 0.1, 1, 1, 1],
754
+ 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'],
755
+ 'سعر الوحدة': [80, 20, 35, 5, 10],
756
+ 'الإجمالي': [32, 2, 35, 5, 10]
757
+ })
758
+ components = pd.concat([components, default_components], ignore_index=True)
759
+
760
+ elif is_insulation:
761
+ # مكونات العزل
762
+ default_components = pd.DataFrame({
763
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
764
+ 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'],
765
+ 'الكمية': [1.1, 0.2, 1, 1, 1],
766
+ 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'],
767
+ 'سعر الوحدة': [60, 30, 25, 10, 15],
768
+ 'الإجمالي': [66, 6, 25, 10, 15]
769
+ })
770
+ components = pd.concat([components, default_components], ignore_index=True)
771
+
772
+ else:
773
+ # مكونات عامة افتراضية
774
+ default_components = pd.DataFrame({
775
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
776
+ 'الوصف': ['مواد أساسية', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
777
+ 'الكمية': [1, 1, 1, 1, 1],
778
+ 'الوحدة': [row['الوحدة'], 'وحدة', 'وحدة', 'وحدة', 'وحدة'],
779
+ 'سعر الوحدة': [
780
+ row['سعر الوحدة'] * 0.6,
781
+ row['سعر الوحدة'] * 0.2,
782
+ row['سعر الوحدة'] * 0.1,
783
+ row['سعر الوحدة'] * 0.05,
784
+ row['سعر الوحدة'] * 0.05
785
+ ],
786
+ 'الإجمالي': [
787
+ row['سعر الوحدة'] * 0.6,
788
+ row['سعر الوحدة'] * 0.2,
789
+ row['سعر الوحدة'] * 0.1,
790
+ row['سعر الوحدة'] * 0.05,
791
+ row['سعر الوحدة'] * 0.05
792
+ ]
793
+ })
794
+ components = pd.concat([components, default_components], ignore_index=True)
795
+
796
+ # حفظ تحليل السعر
797
+ st.session_state.items_price_analysis[edit_item_id] = components
798
+
799
+ # عرض وتحرير مكونات تحليل السعر
800
+ edited_components = st.data_editor(
801
+ components,
802
+ use_container_width=True,
803
+ hide_index=True,
804
+ num_rows="dynamic",
805
+ column_config={
806
+ 'نوع التكلفة': st.column_config.SelectboxColumn(
807
+ 'نوع التكلفة',
808
+ help='فئة التكلفة',
809
+ options=cost_categories
810
+ ),
811
+ 'الوحدة': st.column_config.SelectboxColumn(
812
+ 'الوحدة',
813
+ help='وحدة القياس',
814
+ options=unit_options + ["وحدة", "ساعة", "يوم"]
815
+ ),
816
+ 'الكمية': st.column_config.NumberColumn(
817
+ 'الكمية',
818
+ help='الكمية',
819
+ min_value=0.0,
820
+ format="%.2f"
821
+ ),
822
+ 'سعر الوحدة': st.column_config.NumberColumn(
823
+ 'سعر الوحدة',
824
+ help='سعر الوحدة',
825
+ min_value=0.0,
826
+ format="%.2f"
827
+ ),
828
+ 'الإجمالي': st.column_config.NumberColumn(
829
+ 'الإجمالي',
830
+ help='الإجمالي',
831
+ min_value=0.0,
832
+ format="%.2f"
833
+ )
834
+ }
835
+ )
836
+
837
+ # إعادة حساب الإجمالي لكل مكون
838
+ edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة']
839
+
840
+ # حفظ التعديلات
841
+ st.session_state.items_price_analysis[edit_item_id] = edited_components
842
+
843
+ # حساب إجمالي تحليل السعر
844
+ total_analysis_price = edited_components['الإجمالي'].sum()
845
+ unit_price_from_analysis = total_analysis_price / edited_qty if edited_qty > 0 else 0
846
+
847
+ # عرض ملخص تحليل السعر
848
+ st.markdown("#### ملخص تحليل السعر")
849
+
850
+ col1, col2 = st.columns(2)
851
+
852
+ with col1:
853
+ st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال")
854
+
855
+ with col2:
856
+ st.metric("سعر الوحدة المحسوب", f"{unit_price_from_analysis:,.2f} ريال")
857
+
858
+ st.markdown('</div>', unsafe_allow_html=True)
859
+
860
+ # استخدام السعر المحسوب
861
+ use_calculated_price = st.checkbox("استخدام السعر المحسوب من التحليل", value=True, key="use_calc_edit")
862
+
863
+ # تحديد سعر الوحدة النهائي
864
+ if use_calculated_price and edited_qty > 0:
865
+ edited_price = unit_price_from_analysis
866
+ else:
867
+ edited_price = st.number_input(
868
+ "سعر الوحدة (تعديل)",
869
+ value=unit_price_from_analysis if edited_qty > 0 and unit_price_from_analysis > 0 else float(row['سعر الوحدة']),
870
+ min_value=0.0,
871
+ format="%.2f",
872
+ key="edit_price"
873
+ )
874
+
875
+ # حساب الإجمالي
876
+ edited_total = edited_qty * edited_price
877
+ st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال")
878
+
879
+ # مقارنة السعر المدخل مع السعر المحسوب
880
+ if not use_calculated_price and edited_qty > 0 and unit_price_from_analysis > 0:
881
+ price_diff = edited_price - unit_price_from_analysis
882
+ diff_percentage = (price_diff / unit_price_from_analysis) * 100
883
+
884
+ if abs(diff_percentage) > 5: # إذا كان الفرق أكبر من 5%
885
+ if diff_percentage > 0:
886
+ st.warning(f"السعر المدخل أعلى من السعر المحسوب بنسبة {diff_percentage:.2f}%")
887
+ else:
888
+ st.warning(f"السعر المدخل أقل من السعر المحسوب بنسبة {abs(diff_percentage):.2f}%")
889
+
890
+ # أزرار الإجراءات
891
+ col1, col2, col3 = st.columns(3)
892
+
893
+ with col1:
894
+ if st.button("حفظ التعديلات", use_container_width=True):
895
+ # التحقق من صحة البيانات
896
+ if edited_id and edited_desc and edited_qty > 0:
897
+ # التحقق من تغيير رقم البند
898
+ if edited_id != edit_item_id:
899
+ # نقل تحليل السعر إلى الرقم الجديد
900
+ st.session_state.items_price_analysis[edited_id] = st.session_state.items_price_analysis.pop(edit_item_id)
901
+
902
+ # تحديث البند
903
+ st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id
904
+ st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc
905
+ st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit
906
+ st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty
907
+ st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price
908
+ st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total
909
+
910
+ st.success("تم تحديث البند وتحليل السعر بنجاح!")
911
+ time.sleep(0.5)
912
+ st.rerun()
913
+ else:
914
+ st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.")
915
+
916
+ with col2:
917
+ if st.button("استعادة القيم الأصلية", use_container_width=True):
918
+ # إعادة تحميل الصفحة لاستعادة القيم الأصلية
919
+ st.rerun()
920
+
921
+ with col3:
922
+ if st.button("حذف هذا البند", use_container_width=True):
923
+ # حذف تحليل السعر للبند
924
+ if edit_item_id in st.session_state.items_price_analysis:
925
+ del st.session_state.items_price_analysis[edit_item_id]
926
+
927
+ # حذف البند
928
+ st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True)
929
+
930
+ st.warning("تم حذف البند وتحليل السعر!")
931
+ time.sleep(0.5)
932
+ st.rerun()
modules/pricing/pricing_app.py CHANGED
The diff for this file is too large to render. See raw diff