Upload 114 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- app.py +975 -151
- config.py +62 -229
- data/achievements/user_1_achievements.json +82 -0
- data/achievements/user_1_languages.json +3 -0
- data/achievements/user_1_risks.json +3 -0
- data/project_tracker/project_1_kpis.json +63 -0
- data/project_tracker/project_1_status.json +169 -0
- data/projects/.gitkeep +1 -0
- database/db_connector.py +55 -317
- database/models.py +279 -626
- demo_pricing.py +449 -0
- docs/technical_docs.md +165 -0
- docs/user_manual.md +594 -0
- fonts/Amiri-Bold.ttf +0 -0
- fonts/Amiri-Regular.ttf +0 -0
- huggingface_app.py +83 -0
- models/README.md +52 -0
- models/datasets/README.md +61 -0
- models/trained/README.md +27 -0
- modules/achievements/__init__.py +4 -0
- modules/achievements/achievement_system.py +1033 -0
- modules/achievements/achievements_app.py +49 -0
- modules/ai_assistant/__init__.py +5 -0
- modules/ai_assistant/ai_assistant.py +773 -0
- modules/ai_assistant/ai_assistant_app.py +0 -0
- modules/ai_assistant/assistant_app.py +44 -0
- modules/ai_finetuning/__init__.py +1 -0
- modules/ai_finetuning/finetuning_app.py +53 -0
- modules/ai_finetuning/model_finetuning.py +0 -0
- modules/document_analysis/document_analysis_app.py +1114 -0
- modules/document_analysis/services/__init__.py +22 -0
- modules/document_analysis/services/document_parser.py +219 -0
- modules/document_analysis/services/item_extractor.py +131 -0
- modules/document_analysis/services/text_extractor.py +105 -0
- modules/document_comparison/__init__.py +4 -0
- modules/document_comparison/comparison_app.py +43 -0
- modules/document_comparison/document_comparator.py +1503 -0
- modules/maps/README.md +45 -0
- modules/maps/__init__.py +1 -0
- modules/maps/interactive_map.py +1671 -0
- modules/maps/interactive_map.py.bak +1647 -0
- modules/maps/maps_app.py +36 -439
- modules/notifications/__init__.py +1 -0
- modules/notifications/notifications_app.py +39 -658
- modules/notifications/smart_notifications.py +1237 -0
- modules/pricing/constants.py +113 -0
- modules/pricing/construction_calculator.py +787 -0
- modules/pricing/exceptions.py +42 -0
- modules/pricing/price_analysis_component.py +932 -0
- modules/pricing/pricing_app.py +0 -0
app.py
CHANGED
@@ -1,168 +1,992 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import sys
|
3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
-
#
|
6 |
-
|
|
|
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.
|
13 |
-
from modules.
|
14 |
from modules.maps.maps_app import MapsApp
|
15 |
from modules.notifications.notifications_app import NotificationsApp
|
16 |
-
from modules.
|
17 |
-
from modules.
|
18 |
-
from modules.
|
19 |
-
from modules.
|
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 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
#
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
col1, col2, col3 = st.columns(3)
|
68 |
|
69 |
with col1:
|
70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
|
72 |
with col2:
|
73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
|
75 |
with col3:
|
76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
|
78 |
-
#
|
79 |
-
st.markdown("###
|
80 |
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
|
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 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
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 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
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 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
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
|
7 |
-
import
|
|
|
8 |
|
9 |
-
|
|
|
10 |
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
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 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
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
|
6 |
-
import
|
7 |
-
from datetime import datetime
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
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 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
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 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
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 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
def
|
170 |
-
"
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
self.
|
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 |
+

|
62 |
+
|
63 |
+
### الصلاحيات ومستويات الوصول
|
64 |
+
|
65 |
+
النظام يدعم عدة مستويات من الصلاحيات:
|
66 |
+
|
67 |
+
| المستوى | الوصف | الصلاحيات |
|
68 |
+
|---------|-------|-----------|
|
69 |
+
| مدير النظام | المسؤول الرئيسي عن النظام | كامل الصلاحيات |
|
70 |
+
| مدير المناقصات | مسؤول عن إدارة المناقصات | إضافة وتعديل وحذف المناقصات، التسعير |
|
71 |
+
| محلل عقود | مختص بتحليل العقود والمستندات | قراءة وتحليل المستندات |
|
72 |
+
| محاسب | مسؤول عن الجوانب المالية | الوصول للتكاليف والتسعير |
|
73 |
+
| مستخدم عادي | مستخدم بصلاحيات محدودة | عرض المناقصات والتقارير فقط |
|
74 |
+
|
75 |
+
## الواجهة الرئيسية
|
76 |
+
|
77 |
+
### مكونات الواجهة
|
78 |
+
|
79 |
+

|
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 |
+

|
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 |
+

|
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 |
+

|
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 |
+

|
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 |
+

|
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 |
+

|
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 |
+

|
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 |
+

|
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 = '© <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 = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <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='© <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='© <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='© <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='© <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='© <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='© <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 = '© <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 = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <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='© <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(
|
|
|
|
|
|
|
17 |
|
18 |
-
# استيراد محسن واجهة المستخدم
|
19 |
-
from styling.enhanced_ui import UIEnhancer
|
20 |
|
21 |
class MapsApp:
|
22 |
-
"""تطبيق
|
23 |
|
24 |
def __init__(self):
|
25 |
-
"""تهيئة تطبيق
|
26 |
-
self.
|
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
|
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 |
-
|
456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
12 |
|
13 |
-
# إضافة مسار
|
14 |
-
sys.path.append(
|
|
|
|
|
|
|
15 |
|
16 |
-
# استيراد محسن واجهة المستخدم
|
17 |
-
from styling.enhanced_ui import UIEnhancer
|
18 |
|
19 |
class NotificationsApp:
|
20 |
-
"""تطبيق الإشعارات
|
21 |
|
22 |
def __init__(self):
|
23 |
-
"""تهيئة تطبيق الإشعارات
|
24 |
-
self.
|
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
|
465 |
-
"""
|
466 |
-
st.markdown("
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
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 |
-
|
672 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|