Upload 74 files
Browse files- README.md +87 -183
- app.py +15 -8
- data/exports/boq_20250404_004644.xlsx +0 -0
- modules/ai_assistant/ai_app.py +0 -0
- modules/ai_assistant/bim_analyzer.py +113 -0
- modules/ai_assistant/cad_bim_analyzer.py +251 -0
- modules/ai_assistant/contract_analyzer.py +0 -0
- modules/ai_assistant/document_analyzer.py +464 -464
- modules/ai_assistant/engineering_drawing_analyzer.py +142 -0
- modules/data_analysis/data_analysis_app.py +325 -502
- modules/document_analysis/analyzer.py +434 -281
- modules/document_comparison/document_comparison_app.py +0 -0
- modules/project_management/project_management_app.py +666 -666
- modules/resources/resources_app.py +300 -13
- modules/risk_analysis/risk_analyzer.py +81 -79
- modules/scheduling/schedule_app.py +197 -28
- requirements.txt +21 -1
- styling/enhanced_ui.py +133 -135
README.md
CHANGED
@@ -1,206 +1,110 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
emoji: 🚀
|
6 |
-
colorFrom: green
|
7 |
-
colorTo: green
|
8 |
-
short_description: نظام إدارة المناقصات ودراسة المشاريع v2
|
9 |
-
sdk_version: 1.44.0
|
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 |
-
- Python 3.8
|
46 |
-
- Streamlit 1.32.0
|
47 |
-
- المكتبات المذكورة في
|
48 |
|
49 |
-
###
|
50 |
- معالج: Intel Core i5 أو ما يعادله
|
51 |
-
- ذاكرة:
|
52 |
-
- مساحة تخزين:
|
53 |
-
- اتصال بالإنترنت (لبعض الوظائف)
|
54 |
-
|
55 |
-
## التثبيت والإعداد
|
56 |
-
|
57 |
-
### 1. تثبيت Python
|
58 |
-
تأكد من تثبيت Python 3.8 أو أحدث على جهازك. يمكنك تنزيله من [الموقع الرسمي](https://www.python.org/downloads/).
|
59 |
-
|
60 |
-
### 2. تنزيل النظام
|
61 |
-
قم بتنزيل أو استنساخ مستودع النظام:
|
62 |
-
```
|
63 |
-
git clone https://github.com/your-username/tender-management-system.git
|
64 |
-
cd tender-management-system
|
65 |
-
```
|
66 |
|
67 |
-
|
68 |
-
```
|
69 |
-
python -m venv venv
|
70 |
-
```
|
71 |
|
72 |
-
|
73 |
-
|
74 |
-
```
|
75 |
-
venv\Scripts\activate
|
76 |
-
```
|
77 |
-
- في macOS/Linux:
|
78 |
-
```
|
79 |
-
source venv/bin/activate
|
80 |
-
```
|
81 |
-
|
82 |
-
### 5. تثبيت المتطلبات
|
83 |
-
```
|
84 |
pip install -r requirements.txt
|
85 |
```
|
86 |
|
87 |
-
|
88 |
-
```
|
89 |
streamlit run app.py
|
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 |
-
1. انتقل إلى وحدة التسعير من القائمة الجانبية
|
115 |
-
2. اختر المناقصة التي تريد العمل عليها أو أنشئ مناقصة جديدة
|
116 |
-
3. قم بإضافة بنود جدول الكميات أو استيرادها من ملف خارجي
|
117 |
-
4. قم بتسعير البنود يدوياً أو باستخدام الأسعار المرجعية
|
118 |
-
5. قم بتحليل التكاليف وحساب الهوامش
|
119 |
-
6. قم بتصدير جدول الكميات النهائي
|
120 |
-
|
121 |
-
## وحدة الذكاء الاصطناعي
|
122 |
-
|
123 |
-
### الميزات الرئيسية
|
124 |
-
- تحليل مستندات المناقصات باستخدام معالجة اللغة الطبيعية
|
125 |
-
- اقتراح الأسعار المناسبة بناءً على البيانات السابقة
|
126 |
-
- تقييم فرص الفوز بالمناقصات
|
127 |
-
- تحليل المنافسين واستراتيجياتهم
|
128 |
-
- توليد تقارير وملخصات ذكية
|
129 |
-
|
130 |
-
### كيفية الاستخدام
|
131 |
-
1. انتقل إلى وحدة الذكاء الاصطناعي من القائمة الجانبية
|
132 |
-
2. قم بتحميل مستندات المناقصة للتحليل
|
133 |
-
3. اختر نوع التحليل الذي تريد إجراءه
|
134 |
-
4. راجع النتائج والتوصيات
|
135 |
-
5. قم بتصدير التقارير والملخصات
|
136 |
-
|
137 |
-
## وحدة تحليل البيانات
|
138 |
-
|
139 |
-
### الميزات الرئيسية
|
140 |
-
- عرض لوحات معلومات تفاعلية للمناقصات
|
141 |
-
- تحليل اتجاهات الأسعار والتكاليف
|
142 |
-
- تحليل أداء المنافسين
|
143 |
-
- تحليل نسب النجاح والفشل
|
144 |
-
- تصدير التقارير والرسوم البيانية
|
145 |
-
|
146 |
-
### كيفية الاستخدام
|
147 |
-
1. انتقل إلى وحدة تحليل البيانات من القائمة الجانبية
|
148 |
-
2. اختر نوع التحليل الذي تريد إجراءه
|
149 |
-
3. حدد نطاق البيانات والفترة الزمنية
|
150 |
-
4. قم بتخصيص الرسوم البيانية والتقارير
|
151 |
-
5. قم بتصدير النتائج بالتنسيق المطلوب
|
152 |
-
|
153 |
-
## وحدة الموارد
|
154 |
-
|
155 |
-
### الميزات الرئيسية
|
156 |
-
- إدارة الموارد البشرية والمعدات والمواد
|
157 |
-
- تخصيص الموارد للمشاريع المختلفة
|
158 |
-
- تتبع توافر الموارد واستخدامها
|
159 |
-
- تخطيط الاحتياجات المستقبلية من الموارد
|
160 |
-
- تحليل تكاليف الموارد وكفاءتها
|
161 |
-
|
162 |
-
### كيفية الاستخدام
|
163 |
-
1. انتقل إلى وحدة الموارد من القائمة الجانبية
|
164 |
-
2. استعرض الموارد المتاحة (الموظفين، المعدات، المواد)
|
165 |
-
3. قم بتخصيص الموارد للمشاريع
|
166 |
-
4. قم بتحليل استخدام الموارد وتكاليفها
|
167 |
-
5. قم بتخطيط الاحتياجات المستقبلية من الموارد
|
168 |
-
|
169 |
-
## استكشاف الأخطاء وإصلاحها
|
170 |
-
|
171 |
-
### مشاكل تسجيل الدخول
|
172 |
-
- تأكد من إدخال اسم المستخدم وكلمة المرور بشكل صحيح
|
173 |
-
- تأكد من تفعيل لوحة المفاتيح باللغة الصحيحة
|
174 |
-
- إذا نسيت كلمة المرور، اتصل بمسؤول النظام
|
175 |
-
|
176 |
-
### مشاكل في عرض البيانات
|
177 |
-
- تأكد من اتصالك بالإنترنت إذا كانت البيانات تُجلب من مصدر خارجي
|
178 |
-
- قم بتحديث الصفحة أو إعادة تشغيل التطبيق
|
179 |
-
- تأكد من وجود البيانات المطلوبة في قاعدة البيانات
|
180 |
-
|
181 |
-
### مشاكل في تشغيل التطبيق
|
182 |
-
- تأكد من تثبيت جميع المتطلبات بشكل صحيح
|
183 |
-
- تأكد من استخدام إصدار Python المناسب
|
184 |
-
- تحقق من وجود جميع الملفات والمجلدات المطلوبة
|
185 |
-
|
186 |
-
## الدعم والتواصل
|
187 |
-
|
188 |
-
للحصول على المساعدة أو الإبلاغ عن مشكلة، يرجى التواصل معنا عبر:
|
189 |
-
- البريد الإلكتروني: [email protected]
|
190 |
-
- الهاتف: +966 12 345 6789
|
191 |
-
- الموقع الإلكتروني: www.tender-system.com/support
|
192 |
-
|
193 |
-
## الترخيص
|
194 |
-
|
195 |
-
هذا النظام مرخص بموجب رخصة MIT. راجع ملف LICENSE للحصول على مزيد من المعلومات.
|
196 |
-
|
197 |
-
## شكر وتقدير
|
198 |
-
|
199 |
-
نشكر جميع المساهمين في تطوير هذا النظام، ونخص بالذكر:
|
200 |
-
- فريق تطوير البرمجيات
|
201 |
-
- فريق اختبار الجودة
|
202 |
-
- المستخدمين الذين قدموا ملاحظات قيمة
|
203 |
|
204 |
-
|
|
|
|
|
|
|
|
|
205 |
|
206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<div align="center">
|
2 |
+
<img src="https://www2.0zz0.com/2024/11/20/07/988856043.png" alt="شعار الشركة" width="200"/>
|
3 |
+
<h1>نظام دراسة المناقصات و التسعير و تحليل البيانات</h1>
|
4 |
+
<p>نظام متكامل لإدارة وتحليل المناقصات والمشاريع باستخدام الذكاء الاصطناعي</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
+

|
7 |
+

|
8 |
+

|
9 |
+
</div>
|
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 |
+
tender_system/
|
40 |
+
├── app.py # نقطة الدخول الرئيسية للتطبيق
|
41 |
+
├── config.py # إعدادات التطبيق
|
42 |
+
├── requirements.txt # متطلبات المكتبات
|
43 |
+
├── README.md # توثيق النظام
|
44 |
+
├── assets/ # الأصول الثابتة
|
45 |
+
│ ├── images/ # الصور والشعارات
|
46 |
+
│ └── css/ # ملفات التنسيق
|
47 |
+
├── modules/ # وحدات النظام
|
48 |
+
│ ├── ai_assistant/ # المساعد الذكي
|
49 |
+
│ ├── pricing/ # وحدة التسعير
|
50 |
+
│ ├── project_management/ # إدارة المشاريع
|
51 |
+
│ ├── risk_analysis/ # تحليل المخاطر
|
52 |
+
│ ├── document_analysis/ # تحليل المستندات
|
53 |
+
│ ├── scheduling/ # الجدول الزمني
|
54 |
+
│ ├── reports/ # التقارير
|
55 |
+
│ ├── maps/ # الخرائط والمواقع
|
56 |
+
│ ├── resources/ # إدارة الموارد
|
57 |
+
│ └── translation/ # الترجمة
|
58 |
+
├── database/ # قاعدة البيانات
|
59 |
+
│ ├── models.py # نماذج البيانات
|
60 |
+
│ └── db_connector.py # موصل قاعدة البيانات
|
61 |
+
├── styling/ # تنسيق الواجهة
|
62 |
+
│ ├── enhanced_ui.py # تحسينات الواجهة
|
63 |
+
│ └── theme.py # السمات والألوان
|
64 |
+
└── tests/ # اختبارات النظام
|
65 |
+
├── test_app.py # اختبارات التطبيق
|
66 |
+
└── test_modules.py # اختبارات الوحدات
|
67 |
+
```
|
68 |
|
69 |
+
## 🛠️ المتطلبات التقنية
|
70 |
|
71 |
+
### البرمجيات
|
72 |
+
- Python 3.8+
|
73 |
+
- Streamlit 1.32.0+
|
74 |
+
- المكتبات المذكورة في `requirements.txt`
|
75 |
|
76 |
+
### الأجهزة
|
77 |
- معالج: Intel Core i5 أو ما يعادله
|
78 |
+
- ذاكرة: 8GB RAM أو أكثر
|
79 |
+
- مساحة تخزين: 1GB متاحة
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
|
81 |
+
## ⚡ البدء السريع
|
|
|
|
|
|
|
82 |
|
83 |
+
1. تثبيت المتطلبات:
|
84 |
+
```bash
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
pip install -r requirements.txt
|
86 |
```
|
87 |
|
88 |
+
2. تشغيل التطبيق:
|
89 |
+
```bash
|
90 |
streamlit run app.py
|
91 |
```
|
92 |
|
93 |
+
## 📖 الوثائق
|
94 |
+
للمزيد من المعلومات والتفاصيل، يرجى الرجوع إلى:
|
95 |
+
- [دليل المستخدم](./docs/user_guide.md)
|
96 |
+
- [الوثائق التقنية](./docs/technical_docs.md)
|
97 |
+
- [دليل المطور](./docs/developer_guide.md)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
|
99 |
+
## 🤝 المساهمة
|
100 |
+
نرحب بمساهماتكم! يرجى الاطلاع على [دليل المساهمة](CONTRIBUTING.md) للمزيد من المعلومات.
|
101 |
+
|
102 |
+
## 📄 الترخيص
|
103 |
+
هذا المشروع مرخص تحت [رخصة MIT](LICENSE).
|
104 |
|
105 |
+
## 📞 الدعم والتواصل
|
106 |
+
- البريد الإلكتروني: [email protected]
|
107 |
+
- الموقع: www.wahbi-ai.com
|
108 |
+
|
109 |
+
---
|
110 |
+
© 2024 نظام دراسة المناقصات. جميع الحقوق محفوظة.
|
app.py
CHANGED
@@ -20,6 +20,7 @@ from config_manager import ConfigManager
|
|
20 |
|
21 |
# استيراد الوحدات
|
22 |
from modules.document_analysis.document_app import DocumentAnalysisApp
|
|
|
23 |
from modules.pricing.pricing_app import PricingApp
|
24 |
from modules.resources.resources_app import ResourcesApp
|
25 |
from modules.risk_analysis.risk_analyzer import RiskAnalysisApp
|
@@ -51,7 +52,7 @@ config_manager.set_page_config_if_needed(
|
|
51 |
)
|
52 |
|
53 |
# تطبيق التنسيق العام
|
54 |
-
ui_enhancer = UIEnhancer(page_title="نظام تحليل
|
55 |
ui_enhancer.apply_global_styles()
|
56 |
|
57 |
# تطبيق الأنماط الموحدة المخصصة للنظام
|
@@ -61,19 +62,18 @@ with open("pricing_system/static/css/unified_style.css") as f:
|
|
61 |
# إنشاء قائمة العناصر
|
62 |
menu_items = [
|
63 |
{"name": "لوحة المعلومات", "icon": "house"},
|
|
|
|
|
|
|
|
|
64 |
{"name": "المناقصات والعقود", "icon": "file-text"},
|
65 |
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
66 |
-
{"name": "نظام التسعير", "icon": "calculator"},
|
67 |
-
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
68 |
{"name": "الموارد والتكاليف", "icon": "people"},
|
69 |
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
70 |
-
{"name": "إدارة المشاريع", "icon": "kanban"},
|
71 |
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
72 |
-
{"name": "الجدول الزمني", "icon": "calendar3"},
|
73 |
{"name": "الإشعارات", "icon": "bell"},
|
74 |
{"name": "مقارنة المستندات", "icon": "files"},
|
75 |
{"name": "الترجمة", "icon": "translate"},
|
76 |
-
{"name": "المساعد الذكي", "icon": "robot"},
|
77 |
{"name": "تحليل البيانات", "icon": "bar-chart"},
|
78 |
{"name": "الإعدادات", "icon": "gear"}
|
79 |
]
|
@@ -83,7 +83,10 @@ selected = ui_enhancer.create_sidebar(menu_items)
|
|
83 |
|
84 |
# تحديد الوحدة المطلوبة بناءً على اختيار المستخدم
|
85 |
if selected == "لوحة المعلومات":
|
86 |
-
|
|
|
|
|
|
|
87 |
|
88 |
col1, col2, col3 = st.columns(3)
|
89 |
|
@@ -113,6 +116,10 @@ if selected == "لوحة المعلومات":
|
|
113 |
st.button("عرض", key=f"view_{notification['title']}")
|
114 |
st.divider()
|
115 |
|
|
|
|
|
|
|
|
|
116 |
elif selected == "تحليل المستندات":
|
117 |
document_app = DocumentAnalysisApp()
|
118 |
document_app.run()
|
@@ -189,7 +196,7 @@ elif selected == "الترجمة":
|
|
189 |
|
190 |
elif selected == "المساعد الذكي":
|
191 |
ai_app = AIAssistantApp()
|
192 |
-
ai_app.
|
193 |
|
194 |
elif selected == "تحليل البيانات":
|
195 |
data_analysis_app = DataAnalysisApp()
|
|
|
20 |
|
21 |
# استيراد الوحدات
|
22 |
from modules.document_analysis.document_app import DocumentAnalysisApp
|
23 |
+
from modules.data_analysis.data_analysis_app import DataAnalysisApp
|
24 |
from modules.pricing.pricing_app import PricingApp
|
25 |
from modules.resources.resources_app import ResourcesApp
|
26 |
from modules.risk_analysis.risk_analyzer import RiskAnalysisApp
|
|
|
52 |
)
|
53 |
|
54 |
# تطبيق التنسيق العام
|
55 |
+
ui_enhancer = UIEnhancer(page_title="نظام دراسة المناقصات و التسعير و تحليل البيانات", page_icon="📊") #Modified
|
56 |
ui_enhancer.apply_global_styles()
|
57 |
|
58 |
# تطبيق الأنماط الموحدة المخصصة للنظام
|
|
|
62 |
# إنشاء قائمة العناصر
|
63 |
menu_items = [
|
64 |
{"name": "لوحة المعلومات", "icon": "house"},
|
65 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
66 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
67 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
68 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
69 |
{"name": "المناقصات والعقود", "icon": "file-text"},
|
70 |
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
|
|
|
|
71 |
{"name": "الموارد والتكاليف", "icon": "people"},
|
72 |
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
|
|
73 |
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
|
|
74 |
{"name": "الإشعارات", "icon": "bell"},
|
75 |
{"name": "مقارنة المستندات", "icon": "files"},
|
76 |
{"name": "الترجمة", "icon": "translate"},
|
|
|
77 |
{"name": "تحليل البيانات", "icon": "bar-chart"},
|
78 |
{"name": "الإعدادات", "icon": "gear"}
|
79 |
]
|
|
|
83 |
|
84 |
# تحديد الوحدة المطلوبة بناءً على اختيار المستخدم
|
85 |
if selected == "لوحة المعلومات":
|
86 |
+
st.markdown('<div class="header-container">', unsafe_allow_html=True)
|
87 |
+
st.markdown('<h1 class="header-title">لوحة المعلومات</h1>', unsafe_allow_html=True)
|
88 |
+
st.markdown('<p class="header-subtitle">نظرة عامة على المناقصات والمشاريع</p>', unsafe_allow_html=True)
|
89 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
90 |
|
91 |
col1, col2, col3 = st.columns(3)
|
92 |
|
|
|
116 |
st.button("عرض", key=f"view_{notification['title']}")
|
117 |
st.divider()
|
118 |
|
119 |
+
elif selected == "المناقصات والعقود":
|
120 |
+
st.markdown("### منصة اعتماد - المناقصات المتاحة")
|
121 |
+
st.markdown(f'<iframe src="https://tenders.etimad.sa/Tender/AllTendersForVisitor?PageNumber=1" width="100%" height="800" frameborder="0"></iframe>', unsafe_allow_html=True)
|
122 |
+
|
123 |
elif selected == "تحليل المستندات":
|
124 |
document_app = DocumentAnalysisApp()
|
125 |
document_app.run()
|
|
|
196 |
|
197 |
elif selected == "المساعد الذكي":
|
198 |
ai_app = AIAssistantApp()
|
199 |
+
ai_app.render()
|
200 |
|
201 |
elif selected == "تحليل البيانات":
|
202 |
data_analysis_app = DataAnalysisApp()
|
data/exports/boq_20250404_004644.xlsx
ADDED
Binary file (5.26 kB). View file
|
|
modules/ai_assistant/ai_app.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
modules/ai_assistant/bim_analyzer.py
ADDED
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import streamlit as st
|
3 |
+
import numpy as np
|
4 |
+
import pandas as pd
|
5 |
+
from datetime import datetime
|
6 |
+
|
7 |
+
class BIMAnalyzer:
|
8 |
+
"""محلل نماذج BIM المتقدم"""
|
9 |
+
|
10 |
+
def __init__(self):
|
11 |
+
self.supported_formats = ['.ifc', '.rvt', '.nwd', '.dwg']
|
12 |
+
|
13 |
+
def analyze_bim_model(self, file):
|
14 |
+
"""تحليل نموذج BIM"""
|
15 |
+
try:
|
16 |
+
# تحليل المكونات
|
17 |
+
components = self._analyze_components(file)
|
18 |
+
# تحليل التكاليف
|
19 |
+
costs = self._analyze_costs(components)
|
20 |
+
# تحليل التعارضات
|
21 |
+
clashes = self._analyze_clashes(file)
|
22 |
+
|
23 |
+
return {
|
24 |
+
'components': components,
|
25 |
+
'costs': costs,
|
26 |
+
'clashes': clashes,
|
27 |
+
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
28 |
+
}
|
29 |
+
except Exception as e:
|
30 |
+
return {'error': str(e)}
|
31 |
+
|
32 |
+
def _analyze_components(self, file):
|
33 |
+
"""تحليل مكونات النموذج"""
|
34 |
+
return {
|
35 |
+
'structural': {
|
36 |
+
'columns': {'count': 120, 'volume': 450.5},
|
37 |
+
'beams': {'count': 350, 'volume': 680.2},
|
38 |
+
'slabs': {'count': 45, 'volume': 1200.8}
|
39 |
+
},
|
40 |
+
'architectural': {
|
41 |
+
'walls': {'count': 280, 'area': 3500.6},
|
42 |
+
'windows': {'count': 150, 'area': 450.3},
|
43 |
+
'doors': {'count': 95, 'area': 220.5}
|
44 |
+
},
|
45 |
+
'mep': {
|
46 |
+
'hvac': {'length': 2800.5, 'units': 35},
|
47 |
+
'plumbing': {'length': 1500.2, 'fixtures': 180},
|
48 |
+
'electrical': {'length': 3500.8, 'fixtures': 420}
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
def _analyze_costs(self, components):
|
53 |
+
"""تحليل التكاليف بناءً على المكونات"""
|
54 |
+
return {
|
55 |
+
'structural': 2500000,
|
56 |
+
'architectural': 1800000,
|
57 |
+
'mep': 1200000,
|
58 |
+
'total': 5500000
|
59 |
+
}
|
60 |
+
|
61 |
+
def _analyze_clashes(self, file):
|
62 |
+
"""تحليل التعارضات في النموذج"""
|
63 |
+
return {
|
64 |
+
'critical': 5,
|
65 |
+
'major': 12,
|
66 |
+
'minor': 28,
|
67 |
+
'locations': [
|
68 |
+
{'level': 'Ground Floor', 'count': 15},
|
69 |
+
{'level': 'First Floor', 'count': 18},
|
70 |
+
{'level': 'Second Floor', 'count': 12}
|
71 |
+
]
|
72 |
+
}
|
73 |
+
|
74 |
+
def render_analysis(self, analysis_results):
|
75 |
+
"""عرض نتائج التحليل في واجهة المستخدم"""
|
76 |
+
st.header("تحليل نموذج BIM")
|
77 |
+
|
78 |
+
# عرض ملخص المكونات
|
79 |
+
st.subheader("ملخص المكونات")
|
80 |
+
components = analysis_results['components']
|
81 |
+
|
82 |
+
col1, col2, col3 = st.columns(3)
|
83 |
+
|
84 |
+
with col1:
|
85 |
+
st.metric("عدد الأعمدة", components['structural']['columns']['count'])
|
86 |
+
with col2:
|
87 |
+
st.metric("عدد الجسور", components['structural']['beams']['count'])
|
88 |
+
with col3:
|
89 |
+
st.metric("عدد البلاطات", components['structural']['slabs']['count'])
|
90 |
+
|
91 |
+
# عرض التكاليف
|
92 |
+
st.subheader("تحليل التكاليف")
|
93 |
+
costs = analysis_results['costs']
|
94 |
+
|
95 |
+
cost_data = pd.DataFrame({
|
96 |
+
'القسم': ['الهيكل الإنشائي', 'المعماري', 'الكهروميكانيك'],
|
97 |
+
'التكلفة': [costs['structural'], costs['architectural'], costs['mep']]
|
98 |
+
})
|
99 |
+
|
100 |
+
st.bar_chart(cost_data.set_index('القسم'))
|
101 |
+
|
102 |
+
# عرض التعارضات
|
103 |
+
st.subheader("تحليل التعارضات")
|
104 |
+
clashes = analysis_results['clashes']
|
105 |
+
|
106 |
+
col1, col2, col3 = st.columns(3)
|
107 |
+
|
108 |
+
with col1:
|
109 |
+
st.metric("تعارضات حرجة", clashes['critical'], delta="يجب المعالجة فوراً")
|
110 |
+
with col2:
|
111 |
+
st.metric("تعارضات رئيسية", clashes['major'])
|
112 |
+
with col3:
|
113 |
+
st.metric("تعارضات ثانوية", clashes['minor'])
|
modules/ai_assistant/cad_bim_analyzer.py
ADDED
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
"""
|
3 |
+
محلل الرسومات الهندسية وملفات BIM المتقدم
|
4 |
+
"""
|
5 |
+
|
6 |
+
import ezdxf
|
7 |
+
import cv2
|
8 |
+
import numpy as np
|
9 |
+
from PIL import Image
|
10 |
+
import pytesseract
|
11 |
+
from shapely.geometry import Point, Polygon
|
12 |
+
import json
|
13 |
+
import os
|
14 |
+
import logging
|
15 |
+
from datetime import datetime
|
16 |
+
|
17 |
+
class CADBIMAnalyzer:
|
18 |
+
"""محلل الرسومات الهندسية وملفات BIM"""
|
19 |
+
|
20 |
+
def __init__(self, claude_client=None):
|
21 |
+
"""تهيئة المحلل"""
|
22 |
+
self.claude_client = claude_client
|
23 |
+
self.analysis_results = {}
|
24 |
+
|
25 |
+
def analyze_drawing(self, file_path):
|
26 |
+
"""تحليل الرسم الهندسي"""
|
27 |
+
try:
|
28 |
+
file_extension = os.path.splitext(file_path)[1].lower()
|
29 |
+
|
30 |
+
if file_extension in ['.dwg', '.dxf']:
|
31 |
+
return self._analyze_cad_file(file_path)
|
32 |
+
elif file_extension == '.ifc':
|
33 |
+
return self._analyze_bim_file(file_path)
|
34 |
+
elif file_extension in ['.pdf', '.jpg', '.jpeg', '.png']:
|
35 |
+
return self._analyze_image_file(file_path)
|
36 |
+
else:
|
37 |
+
raise ValueError(f"نوع الملف غير مدعوم: {file_extension}")
|
38 |
+
|
39 |
+
except Exception as e:
|
40 |
+
logging.error(f"خطأ في تحليل الرسم: {str(e)}")
|
41 |
+
raise
|
42 |
+
|
43 |
+
def _analyze_cad_file(self, file_path):
|
44 |
+
"""تحليل ملف CAD"""
|
45 |
+
doc = ezdxf.readfile(file_path)
|
46 |
+
msp = doc.modelspace()
|
47 |
+
|
48 |
+
# تحليل العناصر
|
49 |
+
elements = {
|
50 |
+
'lines': len(msp.query('LINE')),
|
51 |
+
'circles': len(msp.query('CIRCLE')),
|
52 |
+
'arcs': len(msp.query('ARC')),
|
53 |
+
'polylines': len(msp.query('LWPOLYLINE')),
|
54 |
+
'dimensions': len(msp.query('DIMENSION')),
|
55 |
+
'text': len(msp.query('TEXT')),
|
56 |
+
'blocks': len(doc.blocks)
|
57 |
+
}
|
58 |
+
|
59 |
+
# تحليل الطبقات
|
60 |
+
layers = {layer.dxf.name: {
|
61 |
+
'color': layer.dxf.color,
|
62 |
+
'linetype': layer.dxf.linetype,
|
63 |
+
'is_on': layer.dxf.on,
|
64 |
+
'is_frozen': layer.dxf.frozen
|
65 |
+
} for layer in doc.layers}
|
66 |
+
|
67 |
+
# استخراج الأبعاد
|
68 |
+
extents = msp.get_extents()
|
69 |
+
if extents:
|
70 |
+
dimensions = {
|
71 |
+
'width': abs(extents.size.x),
|
72 |
+
'height': abs(extents.size.y),
|
73 |
+
'area': abs(extents.size.x * extents.size.y)
|
74 |
+
}
|
75 |
+
else:
|
76 |
+
dimensions = {'width': 0, 'height': 0, 'area': 0}
|
77 |
+
|
78 |
+
# تحليل متقدم باستخدام Claude
|
79 |
+
if self.claude_client:
|
80 |
+
drawing_info = {
|
81 |
+
'elements': elements,
|
82 |
+
'layers': layers,
|
83 |
+
'dimensions': dimensions
|
84 |
+
}
|
85 |
+
|
86 |
+
analysis_prompt = f"""
|
87 |
+
قم بتحليل معلومات الرسم الهندسي التالية وتقديم:
|
88 |
+
1. وصف عام للرسم
|
89 |
+
2. تحليل العناصر والطبقات
|
90 |
+
3. تحديد نوع المشروع والتخصص
|
91 |
+
4. توصيات فنية
|
92 |
+
5. ملاحظات هامة
|
93 |
+
|
94 |
+
معلومات الرسم:
|
95 |
+
{json.dumps(drawing_info, indent=2, ensure_ascii=False)}
|
96 |
+
"""
|
97 |
+
|
98 |
+
response = self.claude_client.messages.create(
|
99 |
+
model="claude-3-sonnet-20240229",
|
100 |
+
max_tokens=4000,
|
101 |
+
temperature=0.7,
|
102 |
+
messages=[{"role": "user", "content": analysis_prompt}]
|
103 |
+
)
|
104 |
+
|
105 |
+
ai_analysis = response.content[0].text
|
106 |
+
else:
|
107 |
+
ai_analysis = "تحليل الذكاء الاصطناعي غير متاح"
|
108 |
+
|
109 |
+
return {
|
110 |
+
'file_info': {
|
111 |
+
'path': file_path,
|
112 |
+
'type': os.path.splitext(file_path)[1],
|
113 |
+
'size': os.path.getsize(file_path),
|
114 |
+
'modified': datetime.fromtimestamp(os.path.getmtime(file_path)).strftime('%Y-%m-%d %H:%M:%S')
|
115 |
+
},
|
116 |
+
'elements': elements,
|
117 |
+
'layers': layers,
|
118 |
+
'dimensions': dimensions,
|
119 |
+
'ai_analysis': ai_analysis
|
120 |
+
}
|
121 |
+
|
122 |
+
def _analyze_bim_file(self, file_path):
|
123 |
+
"""تحليل ملف BIM"""
|
124 |
+
try:
|
125 |
+
# تحليل ملف IFC
|
126 |
+
import ifcopenshell
|
127 |
+
ifc_file = ifcopenshell.open(file_path)
|
128 |
+
|
129 |
+
# تحليل العناصر
|
130 |
+
elements = {
|
131 |
+
'structural': self._analyze_structural_elements(ifc_file),
|
132 |
+
'architectural': self._analyze_architectural_elements(ifc_file),
|
133 |
+
'mep': self._analyze_mep_elements(ifc_file)
|
134 |
+
}
|
135 |
+
|
136 |
+
# تحليل الأبعاد
|
137 |
+
dimensions = self._analyze_dimensions(ifc_file)
|
138 |
+
|
139 |
+
# تحليل متقدم باستخدام Claude
|
140 |
+
if self.claude_client:
|
141 |
+
building_info = {
|
142 |
+
'elements': elements,
|
143 |
+
'dimensions': dimensions,
|
144 |
+
'properties': self._extract_properties(ifc_file)
|
145 |
+
}
|
146 |
+
|
147 |
+
analysis_prompt = f"""
|
148 |
+
قم بتحليل معلومات نموذج BIM التالية وتقديم:
|
149 |
+
1. تحليل شامل للنموذج
|
150 |
+
2. تقييم جودة النمذجة
|
151 |
+
3. توصيات للتحسين
|
152 |
+
4. تقدير التكاليف
|
153 |
+
5. تحليل الاستدامة
|
154 |
+
|
155 |
+
معلومات النموذج:
|
156 |
+
{json.dumps(building_info, indent=2, ensure_ascii=False)}
|
157 |
+
"""
|
158 |
+
|
159 |
+
response = self.claude_client.messages.create(
|
160 |
+
model="claude-3-sonnet-20240229",
|
161 |
+
max_tokens=4000,
|
162 |
+
temperature=0.7,
|
163 |
+
messages=[{"role": "user", "content": analysis_prompt}]
|
164 |
+
)
|
165 |
+
|
166 |
+
ai_analysis = response.content[0].text
|
167 |
+
else:
|
168 |
+
ai_analysis = "تحليل الذكاء الاصطناعي غير متاح"
|
169 |
+
|
170 |
+
return {
|
171 |
+
'file_info': {
|
172 |
+
'path': file_path,
|
173 |
+
'type': 'IFC',
|
174 |
+
'schema': ifc_file.schema,
|
175 |
+
'size': os.path.getsize(file_path)
|
176 |
+
},
|
177 |
+
'elements': elements,
|
178 |
+
'dimensions': dimensions,
|
179 |
+
'analysis': ai_analysis
|
180 |
+
}
|
181 |
+
|
182 |
+
except Exception as e:
|
183 |
+
logging.error(f"خطأ في تحليل ملف BIM: {str(e)}")
|
184 |
+
raise
|
185 |
+
|
186 |
+
def _analyze_image_file(self, file_path):
|
187 |
+
"""تحليل ملف صورة"""
|
188 |
+
# قراءة الصورة
|
189 |
+
if file_path.lower().endswith('.pdf'):
|
190 |
+
# TODO: Add PDF to image conversion
|
191 |
+
pass
|
192 |
+
else:
|
193 |
+
image = cv2.imread(file_path)
|
194 |
+
|
195 |
+
# استخراج النص
|
196 |
+
text = pytesseract.image_to_string(image, lang='ara+eng')
|
197 |
+
|
198 |
+
# تحليل الخطوط والأشكال
|
199 |
+
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
200 |
+
edges = cv2.Canny(gray, 50, 150)
|
201 |
+
lines = cv2.HoughLinesP(edges, 1, np.pi/180, 100)
|
202 |
+
|
203 |
+
# تحليل متقدم باستخدام Claude
|
204 |
+
if self.claude_client:
|
205 |
+
image_info = {
|
206 |
+
'dimensions': image.shape,
|
207 |
+
'extracted_text': text,
|
208 |
+
'detected_lines': len(lines) if lines is not None else 0
|
209 |
+
}
|
210 |
+
|
211 |
+
analysis_prompt = f"""
|
212 |
+
قم بتحليل معلومات الصورة الهندسية التالية وتقديم:
|
213 |
+
1. وصف عام للصورة
|
214 |
+
2. تحليل النص المستخرج
|
215 |
+
3. تحليل الخطوط والأشكال
|
216 |
+
4. تحديد نوع المشروع والتخصص
|
217 |
+
5. توصيات وملاحظات
|
218 |
+
|
219 |
+
معلومات الصورة:
|
220 |
+
{json.dumps(image_info, indent=2, ensure_ascii=False)}
|
221 |
+
"""
|
222 |
+
|
223 |
+
response = self.claude_client.messages.create(
|
224 |
+
model="claude-3-sonnet-20240229",
|
225 |
+
max_tokens=4000,
|
226 |
+
temperature=0.7,
|
227 |
+
messages=[{"role": "user", "content": analysis_prompt}]
|
228 |
+
)
|
229 |
+
|
230 |
+
ai_analysis = response.content[0].text
|
231 |
+
else:
|
232 |
+
ai_analysis = "تحليل الذكاء الاصطناعي غير متاح"
|
233 |
+
|
234 |
+
return {
|
235 |
+
'file_info': {
|
236 |
+
'path': file_path,
|
237 |
+
'type': os.path.splitext(file_path)[1],
|
238 |
+
'size': os.path.getsize(file_path),
|
239 |
+
'modified': datetime.fromtimestamp(os.path.getmtime(file_path)).strftime('%Y-%m-%d %H:%M:%S')
|
240 |
+
},
|
241 |
+
'dimensions': {
|
242 |
+
'width': image.shape[1],
|
243 |
+
'height': image.shape[0],
|
244 |
+
'channels': image.shape[2]
|
245 |
+
},
|
246 |
+
'extracted_text': text,
|
247 |
+
'analysis': {
|
248 |
+
'detected_lines': len(lines) if lines is not None else 0
|
249 |
+
},
|
250 |
+
'ai_analysis': ai_analysis
|
251 |
+
}
|
modules/ai_assistant/contract_analyzer.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
modules/ai_assistant/document_analyzer.py
CHANGED
@@ -40,468 +40,468 @@ except ImportError:
|
|
40 |
logger.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.")
|
41 |
|
42 |
|
43 |
-
class TextExtractor:
|
44 |
-
"""فئة استخراج النصوص من المستندات"""
|
45 |
-
|
46 |
-
def __init__(self, config=None):
|
47 |
-
"""تهيئة مستخرج النصوص"""
|
48 |
-
self.config = config or {}
|
49 |
-
|
50 |
-
def extract_from_pdf(self, file_path):
|
51 |
-
"""استخراج النص من ملف PDF"""
|
52 |
-
try:
|
53 |
-
# محاكاة استخراج النص من PDF
|
54 |
-
# في التطبيق الحقيقي، يمكن استخدام مكتبات مثل PyPDF2 أو pdfplumber
|
55 |
-
return f"تم استخراج النص من ملف PDF: {file_path}"
|
56 |
-
except Exception as e:
|
57 |
-
logger.error(f"خطأ في استخراج النص من PDF: {str(e)}")
|
58 |
-
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
59 |
-
|
60 |
-
def extract_from_docx(self, file_path):
|
61 |
-
"""استخراج النص من ملف DOCX"""
|
62 |
-
try:
|
63 |
-
# محاكاة استخراج النص من DOCX
|
64 |
-
# في التطبيق الحقيقي، يمكن استخدام مكتبة python-docx
|
65 |
-
return f"تم استخراج النص من ملف DOCX: {file_path}"
|
66 |
-
except Exception as e:
|
67 |
-
logger.error(f"خطأ في استخراج النص من DOCX: {str(e)}")
|
68 |
-
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
69 |
-
|
70 |
-
def extract_from_image(self, file_path):
|
71 |
-
"""استخراج النص من صورة باستخدام OCR"""
|
72 |
-
try:
|
73 |
-
# محاكاة استخراج النص من صورة
|
74 |
-
# في التطبيق الحقيقي، يمكن استخدام مكتبة pytesseract
|
75 |
-
return f"تم استخراج النص من صورة: {file_path}"
|
76 |
-
except Exception as e:
|
77 |
-
logger.error(f"خطأ في استخراج النص من صورة: {str(e)}")
|
78 |
-
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
79 |
-
|
80 |
-
def extract(self, file_path):
|
81 |
-
"""استخراج النص من ملف بناءً على نوعه"""
|
82 |
-
_, ext = os.path.splitext(file_path)
|
83 |
-
ext = ext.lower()
|
84 |
-
|
85 |
-
if ext == '.pdf':
|
86 |
-
return self.extract_from_pdf(file_path)
|
87 |
-
elif ext in ('.doc', '.docx'):
|
88 |
-
return self.extract_from_docx(file_path)
|
89 |
-
elif ext in ('.jpg', '.jpeg', '.png'):
|
90 |
-
return self.extract_from_image(file_path)
|
91 |
-
else:
|
92 |
-
return "نوع ملف غير مدعوم"
|
93 |
-
|
94 |
-
|
95 |
-
class ItemExtractor:
|
96 |
-
"""فئة استخراج العناصر من المستندات"""
|
97 |
-
|
98 |
-
def __init__(self, config=None):
|
99 |
-
"""تهيئة مستخرج العناصر"""
|
100 |
-
self.config = config or {}
|
101 |
-
|
102 |
-
def extract_tables(self, document):
|
103 |
-
"""استخراج الجداول من المستند"""
|
104 |
-
try:
|
105 |
-
# محاكاة استخراج الجداول
|
106 |
-
# في التطبيق الحقيقي، يمكن استخدام مكتبات مثل camelot-py أو tabula-py
|
107 |
-
return [
|
108 |
-
{
|
109 |
-
"عنوان": "جدول البنود والكميات",
|
110 |
-
"بيانات": [
|
111 |
-
{"البند": "أعمال الحفر", "الكمية": 1000, "الوحدة": "م³", "السعر": 50, "الإجمالي": 50000},
|
112 |
-
{"البند": "أعمال الخرسانة", "الكمية": 500, "الوحدة": "م³", "السعر": 300, "الإجمالي": 150000},
|
113 |
-
{"البند": "أعمال التشطيبات", "الكمية": 2000, "الوحدة": "م²", "السعر": 100, "الإجمالي": 200000}
|
114 |
-
]
|
115 |
-
},
|
116 |
-
{
|
117 |
-
"عنوان": "جدول الجدول الزمني",
|
118 |
-
"بيانات": [
|
119 |
-
{"المرحلة": "التصميم", "المدة": "30 يوم", "تاريخ البدء": "2025-04-01", "تاريخ الانتهاء": "2025-04-30"},
|
120 |
-
{"المرحلة": "الإنشاء", "المدة": "180 يوم", "تاريخ البدء": "2025-05-01", "تاريخ الانتهاء": "2025-10-31"},
|
121 |
-
{"المرحلة": "التسليم", "المدة": "30 يوم", "تاريخ البدء": "2025-11-01", "تاريخ الانتهاء": "2025-11-30"}
|
122 |
-
]
|
123 |
-
}
|
124 |
-
]
|
125 |
-
except Exception as e:
|
126 |
-
logger.error(f"خطأ في استخراج الجداول: {str(e)}")
|
127 |
-
return []
|
128 |
-
|
129 |
-
def extract_items(self, file_path):
|
130 |
-
"""استخراج البنود من المستند"""
|
131 |
-
try:
|
132 |
-
# محاكاة استخراج البنود
|
133 |
-
return [
|
134 |
-
{"بند": "أعمال الحفر والردم", "قيمة": 250000, "نسبة": "10%"},
|
135 |
-
{"بند": "أعمال الخرسانة المسلحة", "قيمة": 750000, "نسبة": "30%"},
|
136 |
-
{"بند": "أعمال التشطيبات", "قيمة": 500000, "نسبة": "20%"},
|
137 |
-
{"بند": "أعمال الكهرباء", "قيمة": 350000, "نسبة": "14%"},
|
138 |
-
{"بند": "أعمال السباكة", "قيمة": 300000, "نسبة": "12%"},
|
139 |
-
{"بند": "أعمال التكييف", "قيمة": 350000, "نسبة": "14%"}
|
140 |
-
]
|
141 |
-
except Exception as e:
|
142 |
-
logger.error(f"خطأ في استخراج البنود: {str(e)}")
|
143 |
-
return []
|
144 |
-
|
145 |
-
def extract(self, file_path):
|
146 |
-
"""استخراج جميع العناصر من المستند"""
|
147 |
-
return {
|
148 |
-
"بنود": self.extract_items(file_path),
|
149 |
-
"جداول": self.extract_tables(file_path)
|
150 |
-
}
|
151 |
-
|
152 |
-
|
153 |
-
class DocumentParser:
|
154 |
-
"""فئة تحليل المستندات"""
|
155 |
-
|
156 |
-
def __init__(self, config=None):
|
157 |
-
"""تهيئة محلل المستندات"""
|
158 |
-
self.config = config or {}
|
159 |
-
self.text_extractor = TextExtractor(config)
|
160 |
-
self.item_extractor = ItemExtractor(config)
|
161 |
-
|
162 |
-
def parse_contract(self, file_path):
|
163 |
-
"""تحليل مستند عقد"""
|
164 |
-
try:
|
165 |
-
# محاكاة تحليل عقد
|
166 |
-
return {
|
167 |
-
"نوع المستند": "عقد",
|
168 |
-
"معلومات العقد": {
|
169 |
-
"رقم العقد": "CT-2025-001",
|
170 |
-
"تاريخ العقد": "2025-03-15",
|
171 |
-
"قيمة العقد": "2,500,000 ريال",
|
172 |
-
"مدة العقد": "12 شهر",
|
173 |
-
"تاريخ البدء": "2025-04-01",
|
174 |
-
"تاريخ الانتهاء": "2026-03-31"
|
175 |
-
},
|
176 |
-
"أطراف العقد": {
|
177 |
-
"الطرف الأول": "وزارة الإسكان",
|
178 |
-
"الطرف الثاني": "شركة الإنشاءات المتطورة"
|
179 |
-
},
|
180 |
-
"بنود العقد": [
|
181 |
-
"يلتزم الطرف الثاني بتنفيذ المشروع وفقاً للمواصفات والشروط المرفقة",
|
182 |
-
"مدة تنفيذ المشروع 12 شهراً من تاريخ استلام الموقع",
|
183 |
-
"قيمة العقد 2,500,000 ريال شاملة جميع الضرائب والرسوم",
|
184 |
-
"يتم الدفع على دفعات شهرية حسب نسبة الإنجاز",
|
185 |
-
"غرامة التأخير 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%"
|
186 |
-
],
|
187 |
-
"المرفقات": [
|
188 |
-
"جدول الكميات",
|
189 |
-
"المواصفات الفنية",
|
190 |
-
"الجدول الزمني",
|
191 |
-
"الضمانات والتأمينات"
|
192 |
-
],
|
193 |
-
"درجة الثقة": "95%"
|
194 |
-
}
|
195 |
-
except Exception as e:
|
196 |
-
logger.error(f"خطأ في تحليل العقد: {str(e)}")
|
197 |
-
return {"error": f"حدث خطأ أثناء تحليل العقد: {str(e)}"}
|
198 |
-
|
199 |
-
def parse_tender(self, file_path):
|
200 |
-
"""تحليل مستند مناقصة"""
|
201 |
-
try:
|
202 |
-
# محاكاة تحليل مناقصة
|
203 |
-
return {
|
204 |
-
"نوع المستند": "مناقصة",
|
205 |
-
"معلومات المناقصة": {
|
206 |
-
"رقم المناقصة": "T-2025-002",
|
207 |
-
"اسم المشروع": "إنشاء مبنى إداري",
|
208 |
-
"الجهة المالكة": "وزارة المالية",
|
209 |
-
"تاريخ الطرح": "2025-03-01",
|
210 |
-
"تاريخ الإقفال": "2025-04-15",
|
211 |
-
"القيمة التقديرية": "3,000,000 ريال"
|
212 |
-
},
|
213 |
-
"شروط المناقصة": [
|
214 |
-
"تصنيف المقاول: الدرجة الأولى في مجال المباني",
|
215 |
-
"خبرة سابقة: 5 مشاريع مماثلة خلال الـ 10 سنوات الماضية",
|
216 |
-
"الضمان الابتدائي: 2% من قيمة العطاء",
|
217 |
-
"الضمان النهائي: 5% من قيمة العقد",
|
218 |
-
"مدة تنفيذ المشروع: 18 شهراً"
|
219 |
-
],
|
220 |
-
"المستندات المطلوبة": [
|
221 |
-
"شهادة التصنيف",
|
222 |
-
"السجل التجاري",
|
223 |
-
"شهادة الزكاة والدخل",
|
224 |
-
"شهادة التأمينات الاجتماعية",
|
225 |
-
"قائمة المشاريع المماثلة"
|
226 |
-
],
|
227 |
-
"معايير التقييم": [
|
228 |
-
{"المعيار": "السعر", "الوزن": "50%"},
|
229 |
-
{"المعيار": "الخبرة الفنية", "الوزن": "25%"},
|
230 |
-
{"المعيار": "الجدول الزمني", "الوزن": "15%"},
|
231 |
-
{"المعيار": "فريق العمل", "الوزن": "10%"}
|
232 |
-
],
|
233 |
-
"درجة الثقة": "92%"
|
234 |
-
}
|
235 |
-
except Exception as e:
|
236 |
-
logger.error(f"خطأ في تحليل المناقصة: {str(e)}")
|
237 |
-
return {"error": f"حدث خطأ أثناء تحليل المناقصة: {str(e)}"}
|
238 |
-
|
239 |
-
def parse_specifications(self, file_path):
|
240 |
-
"""تحليل كراسة الشروط والمواصفات"""
|
241 |
-
try:
|
242 |
-
# محاكاة تحليل كراسة الشروط والمواصفات
|
243 |
-
return {
|
244 |
-
"نوع المستند": "كراسة شروط ومواصفات",
|
245 |
-
"معلومات المشروع": {
|
246 |
-
"اسم المشروع": "إنشاء مبنى إداري",
|
247 |
-
"الموقع": "الرياض - حي العليا",
|
248 |
-
"المساحة": "5000 متر مربع",
|
249 |
-
"عدد الطوابق": "5 طوابق"
|
250 |
-
},
|
251 |
-
"المواصفات الفنية": {
|
252 |
-
"الهيكل الإنشائي": "خرسانة مسلحة",
|
253 |
-
"الواجهات": "زجاج عاكس وحجر طبيعي",
|
254 |
-
"التشطيبات الداخلية": "رخام للأرضيات، جبس للأسقف، دهانات عالية الجودة للجدران",
|
255 |
-
"أنظمة الكهرباء": "نظام إنارة LED موفر للطاقة، نظام تحكم ذكي",
|
256 |
-
"أنظمة التكييف": "نظام تكييف مركزي مع تحكم منفصل لكل منطقة",
|
257 |
-
"أنظمة السلامة": "نظام إنذار وإطفاء حريق آلي، كاميرات مراقبة، نظام تحكم في الدخول"
|
258 |
-
},
|
259 |
-
"الشروط العامة": [
|
260 |
-
"الالتزام بكود البناء السعودي",
|
261 |
-
"الالتزام بمتطلبات الدفاع المدني",
|
262 |
-
"الالتزام بمتطلبات الاستدامة وكفاءة الطاقة",
|
263 |
-
"تقديم مخططات تنفيذية معتمدة قبل البدء في التنفيذ",
|
264 |
-
"تقديم عينات للمواد للاعتماد قبل التوريد"
|
265 |
-
],
|
266 |
-
"المرفقات": [
|
267 |
-
"المخططات المعمارية",
|
268 |
-
"المخططات الإنشائية",
|
269 |
-
"مخططات الكهرباء",
|
270 |
-
"مخططات التكييف",
|
271 |
-
"مخططات السباكة",
|
272 |
-
"جدول الكميات"
|
273 |
-
],
|
274 |
-
"درجة الثقة": "90%"
|
275 |
-
}
|
276 |
-
except Exception as e:
|
277 |
-
logger.error(f"خطأ في تحليل كراسة الشروط والمواصفات: {str(e)}")
|
278 |
-
return {"error": f"حدث خطأ أثناء تحليل كراسة الشروط والمواصفات: {str(e)}"}
|
279 |
-
|
280 |
-
def parse_dwg(self, file_path):
|
281 |
-
"""تحليل ملف DWG"""
|
282 |
-
try:
|
283 |
-
# محاكاة تحليل ملف DWG
|
284 |
-
return {
|
285 |
-
"نوع المستند": "ملف DWG",
|
286 |
-
"معلومات الملف": {
|
287 |
-
"اسم الملف": os.path.basename(file_path),
|
288 |
-
"حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
|
289 |
-
"تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
|
290 |
-
},
|
291 |
-
"محتويات الملف": {
|
292 |
-
"عدد الطبقات": 15,
|
293 |
-
"عدد الكائنات": 1250,
|
294 |
-
"أبعاد الرسم": "50م × 30م"
|
295 |
-
},
|
296 |
-
"تحليل المساحات": {
|
297 |
-
"المساحة الإجمالية": "4,500 م²",
|
298 |
-
"مساحة البناء": "3,200 م²",
|
299 |
-
"مساحة الخدمات": "800 م²",
|
300 |
-
"مساحة الممرات": "500 م²"
|
301 |
-
},
|
302 |
-
"تحليل العناصر": {
|
303 |
-
"عدد الغرف": 25,
|
304 |
-
"عدد الأبواب": 40,
|
305 |
-
"عدد النوافذ": 30,
|
306 |
-
"عدد الأعمدة": 20
|
307 |
-
},
|
308 |
-
"ملاحظات": [
|
309 |
-
"تصميم يتوافق مع متطلبات كود البناء السعودي",
|
310 |
-
"توزيع جيد
|
311 |
-
"تصميم يراعي متطلبات ذوي الاحتياجات الخاصة",
|
312 |
-
"تصميم يراعي متطلبات السلامة والإخلاء"
|
313 |
-
],
|
314 |
-
"درجة الثقة": "85%"
|
315 |
-
}
|
316 |
-
except Exception as e:
|
317 |
-
logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
|
318 |
-
return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
|
319 |
-
|
320 |
-
def parse(self, file_path):
|
321 |
-
"""تحليل المستند بناءً على نوعه"""
|
322 |
-
try:
|
323 |
-
_, ext = os.path.splitext(file_path)
|
324 |
-
ext = ext.lower()
|
325 |
-
|
326 |
-
# تحديد نوع المستند بناءً على محتواه (محاكاة)
|
327 |
-
file_name = os.path.basename(file_path).lower()
|
328 |
-
|
329 |
-
if ext == '.dwg':
|
330 |
-
return self.parse_dwg(file_path)
|
331 |
-
elif 'contract' in file_name or 'عقد' in file_name:
|
332 |
-
return self.parse_contract(file_path)
|
333 |
-
elif 'tender' in file_name or 'مناقصة' in file_name:
|
334 |
-
return self.parse_tender(file_path)
|
335 |
-
elif 'spec' in file_name or 'شروط' in file_name or 'مواصفات' in file_name:
|
336 |
-
return self.parse_specifications(file_path)
|
337 |
-
else:
|
338 |
-
# تحليل عام للمستند
|
339 |
-
return {
|
340 |
-
"نوع المستند": "مستند عام",
|
341 |
-
"معلومات الملف": {
|
342 |
-
"اسم الملف": os.path.basename(file_path),
|
343 |
-
"حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
|
344 |
-
"تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
|
345 |
-
},
|
346 |
-
"محتوى المستند": {
|
347 |
-
"نص": self.text_extractor.extract(file_path),
|
348 |
-
"عناصر": self.item_extractor.extract(file_path)
|
349 |
-
},
|
350 |
-
"درجة الثقة": "75%"
|
351 |
-
}
|
352 |
-
except Exception as e:
|
353 |
-
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
354 |
-
return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
|
355 |
-
|
356 |
-
|
357 |
-
class AIDocumentAnalyzer:
|
358 |
-
"""فئة تحليل المستندات باستخدام الذكاء الاصطناعي"""
|
359 |
-
|
360 |
-
def __init__(self):
|
361 |
-
"""تهيئة محلل المستندات الذكي"""
|
362 |
-
self.document_parser = DocumentParser()
|
363 |
-
self.api_keys = {}
|
364 |
-
|
365 |
-
def set_api_key(self, provider, key):
|
366 |
-
"""تعيين مفتاح API لمزود خدمة الذكاء الاصطناعي"""
|
367 |
-
self.api_keys[provider] = key
|
368 |
-
|
369 |
-
def get_api_key(self, provider):
|
370 |
-
"""الحصول على مفتاح API لمزود خدمة الذكاء الاصطناعي"""
|
371 |
-
return self.api_keys.get(provider)
|
372 |
-
|
373 |
-
def analyze_document(self, file_path, provider="local"):
|
374 |
-
"""تحليل المستند باستخدام الذكاء الاصطناعي"""
|
375 |
-
try:
|
376 |
-
# تحليل محلي للمستند
|
377 |
-
local_analysis = self.document_parser.parse(file_path)
|
378 |
-
|
379 |
-
if provider == "local":
|
380 |
-
return local_analysis
|
381 |
-
|
382 |
-
# تحليل باستخدام خدمات الذكاء الاصطناعي السحابية
|
383 |
-
if provider == "openai":
|
384 |
-
# محاكاة تحليل باستخدام OpenAI
|
385 |
-
enhanced_analysis = self._enhance_with_openai(local_analysis)
|
386 |
-
return enhanced_analysis
|
387 |
-
elif provider == "claude":
|
388 |
-
# محاكاة تحليل باستخدام Claude
|
389 |
-
enhanced_analysis = self._enhance_with_claude(local_analysis)
|
390 |
-
return enhanced_analysis
|
391 |
-
else:
|
392 |
-
return local_analysis
|
393 |
-
except Exception as e:
|
394 |
-
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
395 |
-
return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
|
396 |
-
|
397 |
-
def _enhance_with_openai(self, analysis):
|
398 |
-
"""تحسين التحليل باستخدام OpenAI"""
|
399 |
-
# محاكاة تحسين التحليل باستخدام OpenAI
|
400 |
-
analysis["مصدر التحليل"] = "OpenAI"
|
401 |
-
analysis["درجة الثقة"] = "98%"
|
402 |
-
|
403 |
-
# إضافة تحليل المخاطر
|
404 |
-
if "تحليل المخاطر" not in analysis:
|
405 |
-
analysis["تحليل المخاطر"] = [
|
406 |
-
{"المخاطرة": "تأخر التوريدات", "الاحتمالية": "متوسطة", "التأثير": "عالي", "استراتيجية التخفيف": "وضع خطة توريدات بديلة"},
|
407 |
-
{"المخاطرة": "زيادة أسعار المواد", "الاحتمالية": "عالية", "التأثير": "عالي", "استراتيجية التخفيف": "تثبيت أسعار المواد الرئيسية مع الموردين"},
|
408 |
-
{"المخاطرة": "نقص العمالة الماهرة", "الاحتمالية": "متوسطة", "التأثير": "متوسط", "استراتيجية التخفيف": "التعاقد المسبق مع مقاولي الباطن"},
|
409 |
-
{"المخاطرة": "تغيير نطاق العمل", "الاحتمالية": "منخفضة", "التأثير": "عالي", "استراتيجية التخفيف": "توثيق نطاق العمل بدقة وإدارة التغيير"}
|
410 |
-
]
|
411 |
-
|
412 |
-
# إضافة توصيات
|
413 |
-
if "التوصيات" not in analysis:
|
414 |
-
analysis["التوصيات"] = [
|
415 |
-
"مراجعة بنود العقد بدقة قبل التوقيع",
|
416 |
-
"التأكد من وضوح نطاق العمل وعدم وجود غموض",
|
417 |
-
"التحقق من توافق المواصفات الفنية مع المعايير المحلية",
|
418 |
-
"وضع خطة إدارة مخاطر شاملة للمشروع",
|
419 |
-
"تخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع"
|
420 |
-
]
|
421 |
-
|
422 |
-
return analysis
|
423 |
-
|
424 |
-
def _enhance_with_claude(self, analysis):
|
425 |
-
"""تحسين التحليل باستخدام Claude"""
|
426 |
-
# محاكاة تحسين التحليل باستخدام Claude
|
427 |
-
analysis["مصدر التحليل"] = "Claude"
|
428 |
-
analysis["درجة الثقة"] = "97%"
|
429 |
-
|
430 |
-
# إضافة تحليل الفرص
|
431 |
-
if "تحليل الفرص" not in analysis:
|
432 |
-
analysis["تحليل الفرص"] = [
|
433 |
-
{"الفرصة": "تحسين التصميم", "الفائدة": "تقليل التكلفة بنسبة 5-10%", "المتطلبات": "مراجعة هندسية شاملة"},
|
434 |
-
{"الفرصة": "استخدام مواد بديلة", "الفائدة": "تقليل وقت التنفيذ", "المتطلبات": "اعتماد المواصفات الجديدة"},
|
435 |
-
{"الفرصة": "زيادة المحتوى المحلي", "الفائدة": "تحسين التصنيف في برنامج القيمة المضافة", "المتطلبات": "تحديد الموردين المحليين"},
|
436 |
-
{"الفرصة": "تطبيق تقنيات البناء الحديثة", "الفائدة": "تحسين الجودة وتقليل الهدر", "المتطلبات": "تدريب فريق العمل"}
|
437 |
-
]
|
438 |
-
|
439 |
-
# إضافة ملخص تنفيذي
|
440 |
-
if "الملخص التنفيذي" not in analysis:
|
441 |
-
analysis["الملخص التنفيذي"] = """
|
442 |
-
يتضمن هذا المستند تفاصيل مشروع إنشاء مبنى إداري بمساحة إجمالية 5000 متر مربع.
|
443 |
-
المشروع يتكون من 5
|
444 |
-
تقدر تكلفة المشروع بحوالي 3 مليون ريال ومدة التنفيذ 18 شهراً.
|
445 |
-
يتميز المشروع بتصميم يراعي متطلبات الاستدامة وكفاءة الطاقة.
|
446 |
-
تم تحديد عدة مخاطر محتملة للمشروع مع استراتيجيات التخفيف المناسبة.
|
447 |
-
كما تم تحديد عدة فرص لتحسين المشروع من حيث التكلفة والجودة ووقت التنفيذ.
|
448 |
-
"""
|
449 |
-
|
450 |
-
return analysis
|
451 |
-
|
452 |
-
def analyze_dwg(self, file_path, provider="local"):
|
453 |
-
"""تحليل ملف DWG باستخدام الذكاء الاصطناعي"""
|
454 |
-
try:
|
455 |
-
# تحليل محلي لملف DWG
|
456 |
-
local_analysis = self.document_parser.parse_dwg(file_path)
|
457 |
-
|
458 |
-
if provider == "local":
|
459 |
-
return local_analysis
|
460 |
-
|
461 |
-
# تحسين التحليل باستخدام خدمات الذكاء الاصطناعي
|
462 |
-
if provider == "openai" or provider == "claude":
|
463 |
-
# إضافة تحليل متقدم
|
464 |
-
local_analysis["تحليل متقدم"] = {
|
465 |
-
"تقييم التصميم": "جيد جداً",
|
466 |
-
"كفاءة استخدام المساحة": "90%",
|
467 |
-
"توافق مع المعايير": "متوافق مع كود البناء السعودي",
|
468 |
-
"اقتراحات التحسين": [
|
469 |
-
"تحسين توزيع المساحات لزيادة كفاءة استخدام المساحة",
|
470 |
-
"تحسين تصميم الممرات لتسهيل الحركة",
|
471 |
-
"إضافة عناصر تصميم مستدامة لتقليل استهلاك الطاقة",
|
472 |
-
"تحسين تصميم الواجهات لزيادة الإضاءة الطبيعية"
|
473 |
-
]
|
474 |
-
}
|
475 |
-
|
476 |
-
# إضافة تقدير التكلفة
|
477 |
-
local_analysis["تقدير التكلفة"] = {
|
478 |
-
"التكلفة الإجمالية التقديرية": "3,200,000 ريال",
|
479 |
-
"تكلفة المتر المربع": "800 ريال",
|
480 |
-
"توزيع التكلفة": [
|
481 |
-
{"البند": "الهيكل الإنشائي", "النسبة": "35%", "القيمة": "1,120,000 ريال"},
|
482 |
-
{"البند": "التشطيبات", "النسبة": "25%", "القيمة": "800,000 ريال"},
|
483 |
-
{"البند": "الأنظمة الكهربائية", "النسبة": "15%", "القيمة": "480,000 ريال"},
|
484 |
-
{"البند": "الأنظمة الميكانيكية", "النسبة": "15%", "القيمة": "480,000 ريال"},
|
485 |
-
{"البند": "الأعمال الخارجية", "النسبة": "10%", "القيمة": "320,000 ريال"}
|
486 |
-
]
|
487 |
-
}
|
488 |
-
|
489 |
-
# إضافة الجدول الزمني
|
490 |
-
local_analysis["الجدول الزمني التقديري"] = {
|
491 |
-
"المدة الإجمالية": "18 شهر",
|
492 |
-
"المراحل": [
|
493 |
-
{"المرحلة": "أعمال الحفر والأساسات", "المدة": "3 أشهر", "النسبة": "15%"},
|
494 |
-
{"المرحلة": "الهيكل الإنشائي", "المدة": "6 أشهر", "النسبة": "35%"},
|
495 |
-
{"المرحلة": "التشطيبات الداخلية", "المدة": "5 أشهر", "النسبة": "25%"},
|
496 |
-
{"المرحلة": "الأنظمة الكهربائية والميكانيكية", "المدة": "3 أشهر", "النسبة": "15%"},
|
497 |
-
{"المرحلة": "الأعمال الخارجية والتسليم", "المدة": "1 شهر", "النسبة": "10%"}
|
498 |
-
]
|
499 |
-
}
|
500 |
-
|
501 |
-
local_analysis["مصدر التحليل"] = provider.capitalize()
|
502 |
-
local_analysis["درجة الثقة"] = "95%"
|
503 |
-
|
504 |
-
return local_analysis
|
505 |
-
except Exception as e:
|
506 |
-
logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
|
507 |
return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
|
|
|
40 |
logger.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.")
|
41 |
|
42 |
|
43 |
+
class TextExtractor:
|
44 |
+
"""فئة استخراج النصوص من المستندات"""
|
45 |
+
|
46 |
+
def __init__(self, config=None):
|
47 |
+
"""تهيئة مستخرج النصوص"""
|
48 |
+
self.config = config or {}
|
49 |
+
|
50 |
+
def extract_from_pdf(self, file_path):
|
51 |
+
"""استخراج النص من ملف PDF"""
|
52 |
+
try:
|
53 |
+
# محاكاة استخراج النص من PDF
|
54 |
+
# في التطبيق الحقيقي، يمكن استخدام مكتبات مثل PyPDF2 أو pdfplumber
|
55 |
+
return f"تم استخراج النص من ملف PDF: {file_path}"
|
56 |
+
except Exception as e:
|
57 |
+
logger.error(f"خطأ في استخراج النص من PDF: {str(e)}")
|
58 |
+
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
59 |
+
|
60 |
+
def extract_from_docx(self, file_path):
|
61 |
+
"""استخراج النص من ملف DOCX"""
|
62 |
+
try:
|
63 |
+
# محاكاة استخراج النص من DOCX
|
64 |
+
# في التطبيق الحقيقي، يمكن استخدام مكتبة python-docx
|
65 |
+
return f"تم استخراج النص من ملف DOCX: {file_path}"
|
66 |
+
except Exception as e:
|
67 |
+
logger.error(f"خطأ في استخراج النص من DOCX: {str(e)}")
|
68 |
+
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
69 |
+
|
70 |
+
def extract_from_image(self, file_path):
|
71 |
+
"""استخراج النص من صورة باستخدام OCR"""
|
72 |
+
try:
|
73 |
+
# محاكاة استخراج النص من صورة
|
74 |
+
# في التطبيق الحقيقي، يمكن استخدام مكتبة pytesseract
|
75 |
+
return f"تم استخراج النص من صورة: {file_path}"
|
76 |
+
except Exception as e:
|
77 |
+
logger.error(f"خطأ في استخراج النص من صورة: {str(e)}")
|
78 |
+
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
79 |
+
|
80 |
+
def extract(self, file_path):
|
81 |
+
"""استخراج النص من ملف بناءً على نوعه"""
|
82 |
+
_, ext = os.path.splitext(file_path)
|
83 |
+
ext = ext.lower()
|
84 |
+
|
85 |
+
if ext == '.pdf':
|
86 |
+
return self.extract_from_pdf(file_path)
|
87 |
+
elif ext in ('.doc', '.docx'):
|
88 |
+
return self.extract_from_docx(file_path)
|
89 |
+
elif ext in ('.jpg', '.jpeg', '.png'):
|
90 |
+
return self.extract_from_image(file_path)
|
91 |
+
else:
|
92 |
+
return "نوع ملف غير مدعوم"
|
93 |
+
|
94 |
+
|
95 |
+
class ItemExtractor:
|
96 |
+
"""فئة استخراج العناصر من المستندات"""
|
97 |
+
|
98 |
+
def __init__(self, config=None):
|
99 |
+
"""تهيئة مستخرج العناصر"""
|
100 |
+
self.config = config or {}
|
101 |
+
|
102 |
+
def extract_tables(self, document):
|
103 |
+
"""استخراج الجداول من المستند"""
|
104 |
+
try:
|
105 |
+
# محاكاة استخراج الجداول
|
106 |
+
# في التطبيق الحقيقي، يمكن استخدام مكتبات مثل camelot-py أو tabula-py
|
107 |
+
return [
|
108 |
+
{
|
109 |
+
"عنوان": "جدول البنود والكميات",
|
110 |
+
"بيانات": [
|
111 |
+
{"البند": "أعمال الحفر", "الكمية": 1000, "الوحدة": "م³", "السعر": 50, "الإجمالي": 50000},
|
112 |
+
{"البند": "أعمال الخرسانة", "الكمية": 500, "الوحدة": "م³", "السعر": 300, "الإجمالي": 150000},
|
113 |
+
{"البند": "أعمال التشطيبات", "الكمية": 2000, "الوحدة": "م²", "السعر": 100, "الإجمالي": 200000}
|
114 |
+
]
|
115 |
+
},
|
116 |
+
{
|
117 |
+
"عنوان": "جدول الجدول الزمني",
|
118 |
+
"بيانات": [
|
119 |
+
{"المرحلة": "التصميم", "المدة": "30 يوم", "تاريخ البدء": "2025-04-01", "تاريخ الانتهاء": "2025-04-30"},
|
120 |
+
{"المرحلة": "الإنشاء", "المدة": "180 يوم", "تاريخ البدء": "2025-05-01", "تاريخ الانتهاء": "2025-10-31"},
|
121 |
+
{"المرحلة": "التسليم", "المدة": "30 يوم", "تاريخ البدء": "2025-11-01", "تاريخ الانتهاء": "2025-11-30"}
|
122 |
+
]
|
123 |
+
}
|
124 |
+
]
|
125 |
+
except Exception as e:
|
126 |
+
logger.error(f"خطأ في استخراج الجداول: {str(e)}")
|
127 |
+
return []
|
128 |
+
|
129 |
+
def extract_items(self, file_path):
|
130 |
+
"""استخراج البنود من المستند"""
|
131 |
+
try:
|
132 |
+
# محاكاة استخراج البنود
|
133 |
+
return [
|
134 |
+
{"بند": "أعمال الحفر والردم", "قيمة": 250000, "نسبة": "10%"},
|
135 |
+
{"بند": "أعمال الخرسانة المسلحة", "قيمة": 750000, "نسبة": "30%"},
|
136 |
+
{"بند": "أعمال التشطيبات", "قيمة": 500000, "نسبة": "20%"},
|
137 |
+
{"بند": "أعمال الكهرباء", "قيمة": 350000, "نسبة": "14%"},
|
138 |
+
{"بند": "أعمال السباكة", "قيمة": 300000, "نسبة": "12%"},
|
139 |
+
{"بند": "أعمال التكييف", "قيمة": 350000, "نسبة": "14%"}
|
140 |
+
]
|
141 |
+
except Exception as e:
|
142 |
+
logger.error(f"خطأ في استخراج البنود: {str(e)}")
|
143 |
+
return []
|
144 |
+
|
145 |
+
def extract(self, file_path):
|
146 |
+
"""استخراج جميع العناصر من المستند"""
|
147 |
+
return {
|
148 |
+
"بنود": self.extract_items(file_path),
|
149 |
+
"جداول": self.extract_tables(file_path)
|
150 |
+
}
|
151 |
+
|
152 |
+
|
153 |
+
class DocumentParser:
|
154 |
+
"""فئة تحليل المستندات"""
|
155 |
+
|
156 |
+
def __init__(self, config=None):
|
157 |
+
"""تهيئة محلل المستندات"""
|
158 |
+
self.config = config or {}
|
159 |
+
self.text_extractor = TextExtractor(config)
|
160 |
+
self.item_extractor = ItemExtractor(config)
|
161 |
+
|
162 |
+
def parse_contract(self, file_path):
|
163 |
+
"""تحليل مستند عقد"""
|
164 |
+
try:
|
165 |
+
# محاكاة تحليل عقد
|
166 |
+
return {
|
167 |
+
"نوع المستند": "عقد",
|
168 |
+
"معلومات العقد": {
|
169 |
+
"رقم العقد": "CT-2025-001",
|
170 |
+
"تاريخ العقد": "2025-03-15",
|
171 |
+
"قيمة العقد": "2,500,000 ريال",
|
172 |
+
"مدة العقد": "12 شهر",
|
173 |
+
"تاريخ البدء": "2025-04-01",
|
174 |
+
"تاريخ الانتهاء": "2026-03-31"
|
175 |
+
},
|
176 |
+
"أطراف العقد": {
|
177 |
+
"الطرف الأول": "وزارة الإسكان",
|
178 |
+
"الطرف الثاني": "شركة الإنشاءات المتطورة"
|
179 |
+
},
|
180 |
+
"بنود العقد": [
|
181 |
+
"يلتزم الطرف الثاني بتنفيذ المشروع وفقاً للمواصفات والشروط المرفقة",
|
182 |
+
"مدة تنفيذ المشروع 12 شهراً من تاريخ استلام الموقع",
|
183 |
+
"قيمة العقد 2,500,000 ريال شاملة جميع الضرائب والرسوم",
|
184 |
+
"يتم الدفع على دفعات شهرية حسب نسبة الإنجاز",
|
185 |
+
"غرامة التأخير 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%"
|
186 |
+
],
|
187 |
+
"المرفقات": [
|
188 |
+
"جدول الكميات",
|
189 |
+
"المواصفات الفنية",
|
190 |
+
"الجدول الزمني",
|
191 |
+
"الضمانات والتأمينات"
|
192 |
+
],
|
193 |
+
"درجة الثقة": "95%"
|
194 |
+
}
|
195 |
+
except Exception as e:
|
196 |
+
logger.error(f"خطأ في تحليل العقد: {str(e)}")
|
197 |
+
return {"error": f"حدث خطأ أثناء تحليل العقد: {str(e)}"}
|
198 |
+
|
199 |
+
def parse_tender(self, file_path):
|
200 |
+
"""تحليل مستند مناقصة"""
|
201 |
+
try:
|
202 |
+
# محاكاة تحليل مناقصة
|
203 |
+
return {
|
204 |
+
"نوع المستند": "مناقصة",
|
205 |
+
"معلومات المناقصة": {
|
206 |
+
"رقم المناقصة": "T-2025-002",
|
207 |
+
"اسم المشروع": "إنشاء مبنى إداري",
|
208 |
+
"الجهة المالكة": "وزارة المالية",
|
209 |
+
"تاريخ الطرح": "2025-03-01",
|
210 |
+
"تاريخ الإقفال": "2025-04-15",
|
211 |
+
"القيمة التقديرية": "3,000,000 ريال"
|
212 |
+
},
|
213 |
+
"شروط المناقصة": [
|
214 |
+
"تصنيف المقاول: الدرجة الأولى في مجال المباني",
|
215 |
+
"خبرة سابقة: 5 مشاريع مماثلة خلال الـ 10 سنوات الماضية",
|
216 |
+
"الضمان الابتدائي: 2% من قيمة العطاء",
|
217 |
+
"الضمان النهائي: 5% من قيمة العقد",
|
218 |
+
"مدة تنفيذ المشروع: 18 شهراً"
|
219 |
+
],
|
220 |
+
"المستندات المطلوبة": [
|
221 |
+
"شهادة التصنيف",
|
222 |
+
"السجل التجاري",
|
223 |
+
"شهادة الزكاة والدخل",
|
224 |
+
"شهادة التأمينات الاجتماعية",
|
225 |
+
"قائمة المشاريع المماثلة"
|
226 |
+
],
|
227 |
+
"معايير التقييم": [
|
228 |
+
{"المعيار": "السعر", "الوزن": "50%"},
|
229 |
+
{"المعيار": "الخبرة الفنية", "الوزن": "25%"},
|
230 |
+
{"المعيار": "الجدول الزمني", "الوزن": "15%"},
|
231 |
+
{"المعيار": "فريق العمل", "الوزن": "10%"}
|
232 |
+
],
|
233 |
+
"درجة الثقة": "92%"
|
234 |
+
}
|
235 |
+
except Exception as e:
|
236 |
+
logger.error(f"خطأ في تحليل المناقصة: {str(e)}")
|
237 |
+
return {"error": f"حدث خطأ أثناء تحليل المناقصة: {str(e)}"}
|
238 |
+
|
239 |
+
def parse_specifications(self, file_path):
|
240 |
+
"""تحليل كراسة الشروط والمواصفات"""
|
241 |
+
try:
|
242 |
+
# محاكاة تحليل كراسة الشروط والمواصفات
|
243 |
+
return {
|
244 |
+
"نوع المستند": "كراسة شروط ومواصفات",
|
245 |
+
"معلومات المشروع": {
|
246 |
+
"اسم المشروع": "إنشاء مبنى إداري",
|
247 |
+
"الموقع": "الرياض - حي العليا",
|
248 |
+
"المساحة": "5000 متر مربع",
|
249 |
+
"عدد الطوابق": "5 طوابق"
|
250 |
+
},
|
251 |
+
"المواصفات الفنية": {
|
252 |
+
"الهيكل الإنشائي": "خرسانة مسلحة",
|
253 |
+
"الواجهات": "زجاج عاكس وحجر طبيعي",
|
254 |
+
"التشطيبات الداخلية": "رخام للأرضيات، جبس للأسقف، دهانات عالية الجودة للجدران",
|
255 |
+
"أنظمة الكهرباء": "نظام إنارة LED موفر للطاقة، نظام تحكم ذكي",
|
256 |
+
"أنظمة التكييف": "نظام تكييف مركزي مع تحكم منفصل لكل منطقة",
|
257 |
+
"أنظمة السلامة": "نظام إنذار وإطفاء حريق آلي، كاميرات مراقبة، نظام تحكم في الدخول"
|
258 |
+
},
|
259 |
+
"الشروط العامة": [
|
260 |
+
"الالتزام بكود البناء السعودي",
|
261 |
+
"الالتزام بمتطلبات الدفاع المدني",
|
262 |
+
"الالتزام بمتطلبات الاستدامة وكفاءة الطاقة",
|
263 |
+
"تقديم مخططات تنفيذية معتمدة قبل البدء في التنفيذ",
|
264 |
+
"تقديم عينات للمواد للاعتماد قبل التوريد"
|
265 |
+
],
|
266 |
+
"المرفقات": [
|
267 |
+
"المخططات المعمارية",
|
268 |
+
"المخططات الإنشائية",
|
269 |
+
"مخططات الكهرباء",
|
270 |
+
"مخططات التكييف",
|
271 |
+
"مخططات السباكة",
|
272 |
+
"جدول الكميات"
|
273 |
+
],
|
274 |
+
"درجة الثقة": "90%"
|
275 |
+
}
|
276 |
+
except Exception as e:
|
277 |
+
logger.error(f"خطأ في تحليل كراسة الشروط والمواصفات: {str(e)}")
|
278 |
+
return {"error": f"حدث خطأ أثناء تحليل كراسة الشروط والمواصفات: {str(e)}"}
|
279 |
+
|
280 |
+
def parse_dwg(self, file_path):
|
281 |
+
"""تحليل ملف DWG"""
|
282 |
+
try:
|
283 |
+
# محاكاة تحليل ملف DWG
|
284 |
+
return {
|
285 |
+
"نوع المستند": "ملف DWG",
|
286 |
+
"معلومات الملف": {
|
287 |
+
"اسم الملف": os.path.basename(file_path),
|
288 |
+
"حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
|
289 |
+
"تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
|
290 |
+
},
|
291 |
+
"محتويات الملف": {
|
292 |
+
"عدد الطبقات": 15,
|
293 |
+
"عدد الكائنات": 1250,
|
294 |
+
"أبعاد الرسم": "50م × 30م"
|
295 |
+
},
|
296 |
+
"تحليل المساحات": {
|
297 |
+
"المساحة الإجمالية": "4,500 م²",
|
298 |
+
"مساحة البناء": "3,200 م²",
|
299 |
+
"مساحة الخدمات": "800 م²",
|
300 |
+
"مساحة الممرات": "500 م²"
|
301 |
+
},
|
302 |
+
"تحليل العناصر": {
|
303 |
+
"عدد الغرف": 25,
|
304 |
+
"عدد الأبواب": 40,
|
305 |
+
"عدد النوافذ": 30,
|
306 |
+
"عدد الأعمدة": 20
|
307 |
+
},
|
308 |
+
"ملاحظات": [
|
309 |
+
"تصميم يتوافق مع متطلبات كود البناء السعودي",
|
310 |
+
"توزيع جيد للمساحات",
|
311 |
+
"تصميم يراعي متطلبات ذوي الاحتياجات الخاصة",
|
312 |
+
"تصميم يراعي متطلبات السلامة والإخلاء"
|
313 |
+
],
|
314 |
+
"درجة الثقة": "85%"
|
315 |
+
}
|
316 |
+
except Exception as e:
|
317 |
+
logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
|
318 |
+
return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
|
319 |
+
|
320 |
+
def parse(self, file_path):
|
321 |
+
"""تحليل المستند بناءً على نوعه"""
|
322 |
+
try:
|
323 |
+
_, ext = os.path.splitext(file_path)
|
324 |
+
ext = ext.lower()
|
325 |
+
|
326 |
+
# تحديد نوع المستند بناءً على محتواه (محاكاة)
|
327 |
+
file_name = os.path.basename(file_path).lower()
|
328 |
+
|
329 |
+
if ext == '.dwg':
|
330 |
+
return self.parse_dwg(file_path)
|
331 |
+
elif 'contract' in file_name or 'عقد' in file_name:
|
332 |
+
return self.parse_contract(file_path)
|
333 |
+
elif 'tender' in file_name or 'مناقصة' in file_name:
|
334 |
+
return self.parse_tender(file_path)
|
335 |
+
elif 'spec' in file_name or 'شروط' in file_name or 'مواصفات' in file_name:
|
336 |
+
return self.parse_specifications(file_path)
|
337 |
+
else:
|
338 |
+
# تحليل عام للمستند
|
339 |
+
return {
|
340 |
+
"نوع المستند": "مستند عام",
|
341 |
+
"معلومات الملف": {
|
342 |
+
"اسم الملف": os.path.basename(file_path),
|
343 |
+
"حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
|
344 |
+
"تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
|
345 |
+
},
|
346 |
+
"محتوى المستند": {
|
347 |
+
"نص": self.text_extractor.extract(file_path),
|
348 |
+
"عناصر": self.item_extractor.extract(file_path)
|
349 |
+
},
|
350 |
+
"درجة الثقة": "75%"
|
351 |
+
}
|
352 |
+
except Exception as e:
|
353 |
+
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
354 |
+
return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
|
355 |
+
|
356 |
+
|
357 |
+
class AIDocumentAnalyzer:
|
358 |
+
"""فئة تحليل المستندات باستخدام الذكاء الاصطناعي"""
|
359 |
+
|
360 |
+
def __init__(self):
|
361 |
+
"""تهيئة محلل المستندات الذكي"""
|
362 |
+
self.document_parser = DocumentParser()
|
363 |
+
self.api_keys = {}
|
364 |
+
|
365 |
+
def set_api_key(self, provider, key):
|
366 |
+
"""تعيين مفتاح API لمزود خدمة الذكاء الاصطناعي"""
|
367 |
+
self.api_keys[provider] = key
|
368 |
+
|
369 |
+
def get_api_key(self, provider):
|
370 |
+
"""الحصول على مفتاح API لمزود خدمة الذكاء الاصطناعي"""
|
371 |
+
return self.api_keys.get(provider)
|
372 |
+
|
373 |
+
def analyze_document(self, file_path, provider="local"):
|
374 |
+
"""تحليل المستند باستخدام الذكاء الاصطناعي"""
|
375 |
+
try:
|
376 |
+
# تحليل محلي للمستند
|
377 |
+
local_analysis = self.document_parser.parse(file_path)
|
378 |
+
|
379 |
+
if provider == "local":
|
380 |
+
return local_analysis
|
381 |
+
|
382 |
+
# تحليل باستخدام خدمات الذكاء الاصطناعي السحابية
|
383 |
+
if provider == "openai":
|
384 |
+
# محاكاة تحليل باستخدام OpenAI
|
385 |
+
enhanced_analysis = self._enhance_with_openai(local_analysis)
|
386 |
+
return enhanced_analysis
|
387 |
+
elif provider == "claude":
|
388 |
+
# محاكاة تحليل باستخدام Claude
|
389 |
+
enhanced_analysis = self._enhance_with_claude(local_analysis)
|
390 |
+
return enhanced_analysis
|
391 |
+
else:
|
392 |
+
return local_analysis
|
393 |
+
except Exception as e:
|
394 |
+
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
395 |
+
return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
|
396 |
+
|
397 |
+
def _enhance_with_openai(self, analysis):
|
398 |
+
"""تحسين التحليل باستخدام OpenAI"""
|
399 |
+
# محاكاة تحسين التحليل باستخدام OpenAI
|
400 |
+
analysis["مصدر التحليل"] = "OpenAI"
|
401 |
+
analysis["درجة الثقة"] = "98%"
|
402 |
+
|
403 |
+
# إضافة تحليل المخاطر
|
404 |
+
if "تحليل المخاطر" not in analysis:
|
405 |
+
analysis["تحليل المخاطر"] = [
|
406 |
+
{"المخاطرة": "تأخر التوريدات", "الاحتمالية": "متوسطة", "التأثير": "عالي", "استراتيجية التخفيف": "وضع خطة توريدات بديلة"},
|
407 |
+
{"المخاطرة": "زيادة أسعار المواد", "الاحتمالية": "عالية", "التأثير": "عالي", "استراتيجية التخفيف": "تثبيت أسعار المواد الرئيسية مع الموردين"},
|
408 |
+
{"المخاطرة": "نقص العمالة الماهرة", "الاحتمالية": "متوسطة", "التأثير": "متوسط", "استراتيجية التخفيف": "التعاقد المسبق مع مقاولي الباطن"},
|
409 |
+
{"المخاطرة": "تغيير نطاق العمل", "الاحتمالية": "منخفضة", "التأثير": "عالي", "استراتيجية التخفيف": "توثيق نطاق العمل بدقة وإدارة التغيير"}
|
410 |
+
]
|
411 |
+
|
412 |
+
# إضافة توصيات
|
413 |
+
if "التوصيات" not in analysis:
|
414 |
+
analysis["التوصيات"] = [
|
415 |
+
"مراجعة بنود العقد بدقة قبل التوقيع",
|
416 |
+
"التأكد من وضوح نطاق العمل وعدم وجود غموض",
|
417 |
+
"التحقق من توافق المواصفات الفنية مع المعايير المحلية",
|
418 |
+
"وضع خطة إدارة مخاطر شاملة للمشروع",
|
419 |
+
"تخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع"
|
420 |
+
]
|
421 |
+
|
422 |
+
return analysis
|
423 |
+
|
424 |
+
def _enhance_with_claude(self, analysis):
|
425 |
+
"""تحسين التحليل باستخدام Claude"""
|
426 |
+
# محاكاة تحسين التحليل باستخدام Claude
|
427 |
+
analysis["مصدر التحليل"] = "Claude"
|
428 |
+
analysis["درجة الثقة"] = "97%"
|
429 |
+
|
430 |
+
# إضافة تحليل الفرص
|
431 |
+
if "تحليل الفرص" not in analysis:
|
432 |
+
analysis["تحليل الفرص"] = [
|
433 |
+
{"الفرصة": "تحسين التصميم", "الفائدة": "تقليل التكلفة بنسبة 5-10%", "المتطلبات": "مراجعة هندسية شاملة"},
|
434 |
+
{"الفرصة": "استخدام مواد بديلة", "الفائدة": "تقليل وقت التنفيذ", "المتطلبات": "اعتماد المواصفات الجديدة"},
|
435 |
+
{"الفرصة": "زيادة المحتوى المحلي", "الفائدة": "تحسين التصنيف في برنامج القيمة المضافة", "المتطلبات": "تحديد الموردين المحليين"},
|
436 |
+
{"الفرصة": "تطبيق تقنيات البناء الحديثة", "الفائدة": "تحسين الجودة وتقليل الهدر", "المتطلبات": "تدريب فريق العمل"}
|
437 |
+
]
|
438 |
+
|
439 |
+
# إضافة ملخص تنفيذي
|
440 |
+
if "الملخص التنفيذي" not in analysis:
|
441 |
+
analysis["الملخص التنفيذي"] = """
|
442 |
+
يتضمن هذا المستند تفاصيل مشروع إنشاء مبنى إداري بمساحة إجمالية 5000 متر مربع.
|
443 |
+
المشروع يتكون من 5 ��وابق ويتضمن مواصفات فنية عالية الجودة.
|
444 |
+
تقدر تكلفة المشروع بحوالي 3 مليون ريال ومدة التنفيذ 18 شهراً.
|
445 |
+
يتميز المشروع بتصميم يراعي متطلبات الاستدامة وكفاءة الطاقة.
|
446 |
+
تم تحديد عدة مخاطر محتملة للمشروع مع استراتيجيات التخفيف المناسبة.
|
447 |
+
كما تم تحديد عدة فرص لتحسين المشروع من حيث التكلفة والجودة ووقت التنفيذ.
|
448 |
+
"""
|
449 |
+
|
450 |
+
return analysis
|
451 |
+
|
452 |
+
def analyze_dwg(self, file_path, provider="local"):
|
453 |
+
"""تحليل ملف DWG باستخدام الذكاء الاصطناعي"""
|
454 |
+
try:
|
455 |
+
# تحليل محلي لملف DWG
|
456 |
+
local_analysis = self.document_parser.parse_dwg(file_path)
|
457 |
+
|
458 |
+
if provider == "local":
|
459 |
+
return local_analysis
|
460 |
+
|
461 |
+
# تحسين التحليل باستخدام خدمات الذكاء الاصطناعي
|
462 |
+
if provider == "openai" or provider == "claude":
|
463 |
+
# إضافة تحليل متقدم
|
464 |
+
local_analysis["تحليل متقدم"] = {
|
465 |
+
"تقييم التصميم": "جيد جداً",
|
466 |
+
"كفاءة استخدام المساحة": "90%",
|
467 |
+
"توافق مع المعايير": "متوافق مع كود البناء السعودي",
|
468 |
+
"اقتراحات التحسين": [
|
469 |
+
"تحسين توزيع المساحات لزيادة كفاءة استخدام المساحة",
|
470 |
+
"تحسين تصميم الممرات لتسهيل الحركة",
|
471 |
+
"إضافة عناصر تصميم مستدامة لتقليل استهلاك الطاقة",
|
472 |
+
"تحسين تصميم الواجهات لزيادة الإضاءة الطبيعية"
|
473 |
+
]
|
474 |
+
}
|
475 |
+
|
476 |
+
# إضافة تقدير التكلفة
|
477 |
+
local_analysis["تقدير التكلفة"] = {
|
478 |
+
"التكلفة الإجمالية التقديرية": "3,200,000 ريال",
|
479 |
+
"تكلفة المتر المربع": "800 ريال",
|
480 |
+
"توزيع التكلفة": [
|
481 |
+
{"البند": "الهيكل الإنشائي", "النسبة": "35%", "القيمة": "1,120,000 ريال"},
|
482 |
+
{"البند": "التشطيبات", "النسبة": "25%", "القيمة": "800,000 ريال"},
|
483 |
+
{"البند": "الأنظمة الكهربائية", "النسبة": "15%", "القيمة": "480,000 ريال"},
|
484 |
+
{"البند": "الأنظمة الميكانيكية", "النسبة": "15%", "القيمة": "480,000 ريال"},
|
485 |
+
{"البند": "الأعمال الخارجية", "النسبة": "10%", "القيمة": "320,000 ريال"}
|
486 |
+
]
|
487 |
+
}
|
488 |
+
|
489 |
+
# إضافة الجدول الزمني
|
490 |
+
local_analysis["الجدول الزمني التقديري"] = {
|
491 |
+
"المدة الإجمالية": "18 شهر",
|
492 |
+
"المراحل": [
|
493 |
+
{"المرحلة": "أعمال الحفر والأساسات", "المدة": "3 أشهر", "النسبة": "15%"},
|
494 |
+
{"المرحلة": "الهيكل الإنشائي", "المدة": "6 أشهر", "النسبة": "35%"},
|
495 |
+
{"المرحلة": "التشطيبات الداخلية", "المدة": "5 أشهر", "النسبة": "25%"},
|
496 |
+
{"المرحلة": "الأنظمة الكهربائية والميكانيكية", "المدة": "3 أشهر", "النسبة": "15%"},
|
497 |
+
{"المرحلة": "الأعمال الخارجية والتسليم", "المدة": "1 شهر", "النسبة": "10%"}
|
498 |
+
]
|
499 |
+
}
|
500 |
+
|
501 |
+
local_analysis["مصدر التحليل"] = provider.capitalize()
|
502 |
+
local_analysis["درجة الثقة"] = "95%"
|
503 |
+
|
504 |
+
return local_analysis
|
505 |
+
except Exception as e:
|
506 |
+
logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
|
507 |
return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
|
modules/ai_assistant/engineering_drawing_analyzer.py
ADDED
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import cv2
|
3 |
+
import numpy as np
|
4 |
+
from PIL import Image
|
5 |
+
import pytesseract
|
6 |
+
import logging
|
7 |
+
from datetime import datetime
|
8 |
+
|
9 |
+
class EngineeringDrawingAnalyzer:
|
10 |
+
"""محلل الرسومات الهندسية المتقدم"""
|
11 |
+
|
12 |
+
def __init__(self, claude_client=None):
|
13 |
+
self.claude_client = claude_client
|
14 |
+
self.supported_formats = ['.dwg', '.dxf', '.pdf', '.jpg', '.jpeg', '.png']
|
15 |
+
|
16 |
+
def analyze_drawing(self, file_path):
|
17 |
+
"""تحليل الرسم الهندسي"""
|
18 |
+
try:
|
19 |
+
# قراءة الصورة
|
20 |
+
image = cv2.imread(file_path)
|
21 |
+
|
22 |
+
# تحليل الخطوط والأشكال
|
23 |
+
lines, shapes = self._analyze_shapes(image)
|
24 |
+
|
25 |
+
# استخراج النص والأبعاد
|
26 |
+
text, dimensions = self._extract_text_and_dimensions(image)
|
27 |
+
|
28 |
+
# تحليل متقدم باستخدام Claude
|
29 |
+
if self.claude_client:
|
30 |
+
drawing_info = {
|
31 |
+
'lines': lines,
|
32 |
+
'shapes': shapes,
|
33 |
+
'text': text,
|
34 |
+
'dimensions': dimensions
|
35 |
+
}
|
36 |
+
|
37 |
+
analysis_prompt = f"""
|
38 |
+
قم بتحليل معلومات الرسم الهندسي التالية وتقديم:
|
39 |
+
1. تحليل شامل للرسم
|
40 |
+
2. تحديد التخصص (إنشائي، معماري، ميكانيكي، كهربائي)
|
41 |
+
3. تحليل التفاصيل الفنية
|
42 |
+
4. توصيات للتحسين
|
43 |
+
5. تقدير التكاليف
|
44 |
+
|
45 |
+
معلومات الرسم:
|
46 |
+
{json.dumps(drawing_info, indent=2, ensure_ascii=False)}
|
47 |
+
"""
|
48 |
+
|
49 |
+
response = self.claude_client.messages.create(
|
50 |
+
model="claude-3-sonnet-20240229",
|
51 |
+
max_tokens=4000,
|
52 |
+
temperature=0.7,
|
53 |
+
messages=[{"role": "user", "content": analysis_prompt}]
|
54 |
+
)
|
55 |
+
|
56 |
+
ai_analysis = response.content[0].text
|
57 |
+
else:
|
58 |
+
ai_analysis = "تحليل الذكاء الاصطناعي غير متاح"
|
59 |
+
|
60 |
+
return {
|
61 |
+
'file_info': {
|
62 |
+
'path': file_path,
|
63 |
+
'type': os.path.splitext(file_path)[1],
|
64 |
+
'size': os.path.getsize(file_path)
|
65 |
+
},
|
66 |
+
'analysis': {
|
67 |
+
'lines': lines,
|
68 |
+
'shapes': shapes,
|
69 |
+
'text': text,
|
70 |
+
'dimensions': dimensions,
|
71 |
+
'ai_analysis': ai_analysis
|
72 |
+
}
|
73 |
+
}
|
74 |
+
|
75 |
+
except Exception as e:
|
76 |
+
logging.error(f"خطأ في تحليل الرسم: {str(e)}")
|
77 |
+
raise
|
78 |
+
|
79 |
+
def _analyze_shapes(self, image):
|
80 |
+
"""تحليل الخطوط والأشكال"""
|
81 |
+
# تحويل الصورة إلى تدرج رمادي
|
82 |
+
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
83 |
+
|
84 |
+
# كشف الحواف
|
85 |
+
edges = cv2.Canny(gray, 50, 150)
|
86 |
+
|
87 |
+
# كشف الخطوط
|
88 |
+
lines = cv2.HoughLinesP(edges, 1, np.pi/180, 100)
|
89 |
+
|
90 |
+
# كشف الأشكال
|
91 |
+
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
92 |
+
|
93 |
+
shapes = []
|
94 |
+
for contour in contours:
|
95 |
+
approx = cv2.approxPolyDP(contour, 0.04 * cv2.arcLength(contour, True), True)
|
96 |
+
if len(approx) == 3:
|
97 |
+
shapes.append('triangle')
|
98 |
+
elif len(approx) == 4:
|
99 |
+
shapes.append('rectangle')
|
100 |
+
elif len(approx) == 5:
|
101 |
+
shapes.append('pentagon')
|
102 |
+
elif len(approx) > 5:
|
103 |
+
shapes.append('circle')
|
104 |
+
|
105 |
+
return {
|
106 |
+
'line_count': len(lines) if lines is not None else 0,
|
107 |
+
'line_angles': self._analyze_line_angles(lines) if lines is not None else [],
|
108 |
+
'line_lengths': self._analyze_line_lengths(lines) if lines is not None else []
|
109 |
+
}, {
|
110 |
+
'shape_count': len(shapes),
|
111 |
+
'shape_types': dict(Counter(shapes))
|
112 |
+
}
|
113 |
+
|
114 |
+
def _extract_text_and_dimensions(self, image):
|
115 |
+
"""استخراج النص والأبعاد"""
|
116 |
+
# استخراج النص
|
117 |
+
text = pytesseract.image_to_string(image, lang='ara+eng')
|
118 |
+
|
119 |
+
# تحليل الأبعاد
|
120 |
+
dimensions = self._analyze_dimensions_from_text(text)
|
121 |
+
|
122 |
+
return text, dimensions
|
123 |
+
|
124 |
+
def _analyze_dimensions_from_text(self, text):
|
125 |
+
"""تحليل الأبعاد من النص"""
|
126 |
+
import re
|
127 |
+
|
128 |
+
# نمط للبحث عن الأبعاد
|
129 |
+
dimension_pattern = r'(\d+(?:\.\d+)?)\s*(م|متر|سم|مم|m|cm|mm)'
|
130 |
+
|
131 |
+
# البحث عن جميع ��لأبعاد
|
132 |
+
dimensions = re.findall(dimension_pattern, text)
|
133 |
+
|
134 |
+
# تحويل الأبعاد إلى قائمة منظمة
|
135 |
+
processed_dimensions = []
|
136 |
+
for value, unit in dimensions:
|
137 |
+
processed_dimensions.append({
|
138 |
+
'value': float(value),
|
139 |
+
'unit': unit
|
140 |
+
})
|
141 |
+
|
142 |
+
return processed_dimensions
|
modules/data_analysis/data_analysis_app.py
CHANGED
@@ -1,502 +1,325 @@
|
|
1 |
-
import streamlit as st
|
2 |
-
import pandas as pd
|
3 |
-
import numpy as np
|
4 |
-
import
|
5 |
-
import plotly.
|
6 |
-
|
7 |
-
import
|
8 |
-
|
9 |
-
import
|
10 |
-
import
|
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 |
-
st.
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
st.
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
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 |
-
st.
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
color_column = st.selectbox("اختر عمود اللون (اختياري):",
|
327 |
-
["لا يوجد"] + self.data.columns.tolist(),
|
328 |
-
key="scatter_color")
|
329 |
-
|
330 |
-
size_column = st.selectbox("اختر عمود الحجم (اختياري):",
|
331 |
-
["لا يوجد"] + numeric_columns,
|
332 |
-
key="scatter_size")
|
333 |
-
|
334 |
-
# إنشاء الرسم البياني
|
335 |
-
if color_column == "لا يوجد" and size_column == "لا يوجد":
|
336 |
-
fig = px.scatter(self.data, x=x_column, y=y_column,
|
337 |
-
title=f"{y_column} مقابل {x_column}")
|
338 |
-
elif color_column != "لا يوجد" and size_column == "لا يوجد":
|
339 |
-
fig = px.scatter(self.data, x=x_column, y=y_column, color=color_column,
|
340 |
-
title=f"{y_column} مقابل {x_column} (مصنف حسب {color_column})")
|
341 |
-
elif color_column == "لا يوجد" and size_column != "لا يوجد":
|
342 |
-
fig = px.scatter(self.data, x=x_column, y=y_column, size=size_column,
|
343 |
-
title=f"{y_column} مقابل {x_column} (الحجم حسب {size_column})")
|
344 |
-
else:
|
345 |
-
fig = px.scatter(self.data, x=x_column, y=y_column, color=color_column, size=size_column,
|
346 |
-
title=f"{y_column} مقابل {x_column} (مصنف حسب {color_column}, الحجم حسب {size_column})")
|
347 |
-
|
348 |
-
fig.update_layout(xaxis_title=x_column, yaxis_title=y_column)
|
349 |
-
st.plotly_chart(fig, use_container_width=True)
|
350 |
-
|
351 |
-
elif chart_type == "مخطط صندوقي":
|
352 |
-
st.subheader("مخطط صندوقي")
|
353 |
-
|
354 |
-
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
355 |
-
|
356 |
-
if not numeric_columns:
|
357 |
-
st.warning("يجب أن يكون هناك عمود رقمي واحد على الأقل لإنشاء مخطط صندوقي")
|
358 |
-
return
|
359 |
-
|
360 |
-
y_column = st.selectbox("اختر عمود القيمة:", numeric_columns, key="box_y")
|
361 |
-
|
362 |
-
# خيارات إضافية
|
363 |
-
x_column = st.selectbox("اختر عمود التصنيف (اختياري):",
|
364 |
-
["لا يوجد"] + self.data.columns.tolist(),
|
365 |
-
key="box_x")
|
366 |
-
|
367 |
-
# إنشاء الرسم البياني
|
368 |
-
if x_column == "لا يوجد":
|
369 |
-
fig = px.box(self.data, y=y_column, title=f"مخطط صندوقي لـ {y_column}")
|
370 |
-
else:
|
371 |
-
fig = px.box(self.data, x=x_column, y=y_column,
|
372 |
-
title=f"مخطط صندوقي لـ {y_column} حسب {x_column}")
|
373 |
-
|
374 |
-
st.plotly_chart(fig, use_container_width=True)
|
375 |
-
|
376 |
-
elif chart_type == "مخطط حراري":
|
377 |
-
st.subheader("مخطط حراري")
|
378 |
-
|
379 |
-
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
380 |
-
|
381 |
-
if len(numeric_columns) < 2:
|
382 |
-
st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإنشاء مخطط حراري")
|
383 |
-
return
|
384 |
-
|
385 |
-
# اختيار الأعمدة للمخطط الحراري
|
386 |
-
selected_columns = st.multiselect("اختر الأعمدة للمخطط الحرا��ي:",
|
387 |
-
numeric_columns,
|
388 |
-
default=numeric_columns[:5] if len(numeric_columns) > 5 else numeric_columns)
|
389 |
-
|
390 |
-
if selected_columns:
|
391 |
-
# حساب مصفوفة الارتباط
|
392 |
-
correlation_matrix = self.data[selected_columns].corr()
|
393 |
-
|
394 |
-
# إنشاء الرسم البياني
|
395 |
-
fig = px.imshow(correlation_matrix, text_auto=True, aspect="auto",
|
396 |
-
title="مخطط حراري لمصفوفة الارتباط")
|
397 |
-
st.plotly_chart(fig, use_container_width=True)
|
398 |
-
else:
|
399 |
-
st.warning("الرجاء اختيار عمود واحد على الأقل")
|
400 |
-
|
401 |
-
def _reports_tab(self):
|
402 |
-
"""علامة تبويب التقارير"""
|
403 |
-
st.header("التقارير")
|
404 |
-
|
405 |
-
if self.data is None:
|
406 |
-
st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
|
407 |
-
return
|
408 |
-
|
409 |
-
st.subheader("إنشاء تقرير")
|
410 |
-
|
411 |
-
# خيارات التقرير
|
412 |
-
report_type = st.selectbox(
|
413 |
-
"اختر نوع التقرير:",
|
414 |
-
["تقرير ملخص", "تقرير تحليلي", "تقرير مقارنة"]
|
415 |
-
)
|
416 |
-
|
417 |
-
if report_type == "تقرير ملخص":
|
418 |
-
st.write("محتوى التقرير:")
|
419 |
-
|
420 |
-
# إنشاء ملخص للبيانات
|
421 |
-
st.write("### ملخص البيانات")
|
422 |
-
st.write(f"عدد الصفوف: {self.data.shape[0]}")
|
423 |
-
st.write(f"عدد الأعمدة: {self.data.shape[1]}")
|
424 |
-
|
425 |
-
# إحصاءات وصفية
|
426 |
-
st.write("### إحصاءات وصفية")
|
427 |
-
st.dataframe(self.data.describe())
|
428 |
-
|
429 |
-
# معلومات عن القيم المفقودة
|
430 |
-
st.write("### القيم المفقودة")
|
431 |
-
missing_data = pd.DataFrame({
|
432 |
-
'العمود': self.data.columns,
|
433 |
-
'عدد القيم المفقودة': self.data.isna().sum().values,
|
434 |
-
'نسبة القيم المفقودة (%)': (self.data.isna().sum().values / len(self.data) * 100).round(2)
|
435 |
-
})
|
436 |
-
st.dataframe(missing_data)
|
437 |
-
|
438 |
-
# توزيع البيانات الرقمية
|
439 |
-
st.write("### توزيع البيانات الرقمية")
|
440 |
-
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
441 |
-
|
442 |
-
if numeric_columns:
|
443 |
-
for i in range(0, len(numeric_columns), 2):
|
444 |
-
cols = st.columns(2)
|
445 |
-
for j in range(2):
|
446 |
-
if i + j < len(numeric_columns):
|
447 |
-
col = numeric_columns[i + j]
|
448 |
-
with cols[j]:
|
449 |
-
fig = px.histogram(self.data, x=col, title=f"توزيع {col}")
|
450 |
-
st.plotly_chart(fig, use_container_width=True)
|
451 |
-
|
452 |
-
# خيارات تصدير التقرير
|
453 |
-
st.subheader("تصدير التقرير")
|
454 |
-
export_format = st.radio("اختر صيغة التصدير:", ["PDF", "Excel", "HTML"])
|
455 |
-
|
456 |
-
if st.button("تصدير التقرير"):
|
457 |
-
st.success(f"تم تصدير التقرير بصيغة {export_format} بنجاح!")
|
458 |
-
|
459 |
-
elif report_type == "تقرير تحليلي":
|
460 |
-
st.info("هذه الميزة قيد التطوير")
|
461 |
-
|
462 |
-
elif report_type == "تقرير مقارنة":
|
463 |
-
st.info("هذه الميزة قيد التطوير")
|
464 |
-
|
465 |
-
def _create_sample_data(self):
|
466 |
-
"""إنشاء بيانات نموذجية للمناقصات"""
|
467 |
-
# إنشاء تواريخ عشوائية
|
468 |
-
start_date = datetime(2023, 1, 1)
|
469 |
-
end_date = datetime(2025, 3, 31)
|
470 |
-
days = (end_date - start_date).days
|
471 |
-
|
472 |
-
# إنشاء بيانات نموذجية
|
473 |
-
data = {
|
474 |
-
'رقم المناقصة': [f'T-{i:04d}' for i in range(1, 101)],
|
475 |
-
'اسم المشروع': [f'مشروع {i}' for i in range(1, 101)],
|
476 |
-
'نوع المشروع': np.random.choice(['بناء', 'صيانة', 'تطوير', 'توريد', 'خدمات'], 100),
|
477 |
-
'الموقع': np.random.choice(['الرياض', 'جدة', 'الدمام', 'مكة', 'المدينة', 'تبوك', 'أبها'], 100),
|
478 |
-
'تاريخ الإعلان': [start_date + pd.Timedelta(days=np.random.randint(0, days)) for _ in range(100)],
|
479 |
-
'تاريخ الإغلاق': [start_date + pd.Timedelta(days=np.random.randint(30, days)) for _ in range(100)],
|
480 |
-
'الميزانية التقديرية': np.random.uniform(1000000, 50000000, 100),
|
481 |
-
'عدد المتقدمين': np.random.randint(1, 20, 100),
|
482 |
-
'سعر العرض': np.random.uniform(900000, 55000000, 100),
|
483 |
-
'نسبة الفوز (%)': np.random.uniform(0, 100, 100),
|
484 |
-
'مدة التنفيذ (أشهر)': np.random.randint(3, 36, 100),
|
485 |
-
'عدد العمال': np.random.randint(10, 500, 100),
|
486 |
-
'تكلفة المواد': np.random.uniform(500000, 30000000, 100),
|
487 |
-
'تكلفة العمالة': np.random.uniform(200000, 15000000, 100),
|
488 |
-
'تكلفة المعدات': np.random.uniform(100000, 10000000, 100),
|
489 |
-
'هامش الربح (%)': np.random.uniform(5, 25, 100),
|
490 |
-
'درجة المخاطرة': np.random.choice(['منخفضة', 'متوسطة', 'عالية'], 100),
|
491 |
-
'حالة المناقصة': np.random.choice(['جارية', 'مغلقة', 'ملغاة', 'فائزة', 'خاسرة'], 100)
|
492 |
-
}
|
493 |
-
|
494 |
-
# إنشاء DataFrame
|
495 |
-
df = pd.DataFrame(data)
|
496 |
-
|
497 |
-
# إضافة بعض العلاقات المنطقية
|
498 |
-
df['إجمالي التكلفة'] = df['تكلفة المواد'] + df['تكلفة العمالة'] + df['تكلفة المعدات']
|
499 |
-
df['الربح المتوقع'] = df['سعر العرض'] - df['إجمالي التكلفة']
|
500 |
-
df['نسبة التكلفة من العرض (%)'] = (df['إجمالي التكلفة'] / df['سعر العرض'] * 100).round(2)
|
501 |
-
|
502 |
-
return df
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import numpy as np
|
4 |
+
import plotly.express as px
|
5 |
+
import plotly.graph_objects as go
|
6 |
+
from datetime import datetime
|
7 |
+
import io
|
8 |
+
import os
|
9 |
+
import json
|
10 |
+
from pathlib import Path
|
11 |
+
|
12 |
+
class DataAnalysisApp:
|
13 |
+
def __init__(self):
|
14 |
+
self.data = None
|
15 |
+
self.file_path = None
|
16 |
+
|
17 |
+
# تهيئة حالة الجلسة
|
18 |
+
if 'analysis_data' not in st.session_state:
|
19 |
+
st.session_state.analysis_data = {
|
20 |
+
'uploaded_files': {},
|
21 |
+
'analysis_results': {},
|
22 |
+
'ai_insights': {}
|
23 |
+
}
|
24 |
+
|
25 |
+
def run(self):
|
26 |
+
st.title("تحليل البيانات المتقدم")
|
27 |
+
|
28 |
+
# إنشاء التبويبات
|
29 |
+
tabs = st.tabs([
|
30 |
+
"تحميل وإدارة البيانات",
|
31 |
+
"التحليل الإحصائي",
|
32 |
+
"التحليل المرئي",
|
33 |
+
"تحليل الذكاء الاصطناعي",
|
34 |
+
"التقارير"
|
35 |
+
])
|
36 |
+
|
37 |
+
with tabs[0]:
|
38 |
+
self._render_data_management()
|
39 |
+
|
40 |
+
with tabs[1]:
|
41 |
+
self._render_statistical_analysis()
|
42 |
+
|
43 |
+
with tabs[2]:
|
44 |
+
self._render_visualization()
|
45 |
+
|
46 |
+
with tabs[3]:
|
47 |
+
self._render_ai_analysis()
|
48 |
+
|
49 |
+
with tabs[4]:
|
50 |
+
self._render_reports()
|
51 |
+
|
52 |
+
def _render_data_management(self):
|
53 |
+
st.header("تحميل وإدارة البيانات")
|
54 |
+
|
55 |
+
# منطقة تحميل الملفات
|
56 |
+
uploaded_files = st.file_uploader(
|
57 |
+
"قم بتحميل ملفات البيانات",
|
58 |
+
type=["csv", "xlsx", "xls", "pdf"],
|
59 |
+
accept_multiple_files=True,
|
60 |
+
key="data_files"
|
61 |
+
)
|
62 |
+
|
63 |
+
if uploaded_files:
|
64 |
+
for file in uploaded_files:
|
65 |
+
try:
|
66 |
+
if file.name.endswith('.pdf'):
|
67 |
+
import PyPDF2
|
68 |
+
pdf_reader = PyPDF2.PdfReader(file)
|
69 |
+
text_content = ""
|
70 |
+
for page in pdf_reader.pages:
|
71 |
+
text_content += page.extract_text()
|
72 |
+
|
73 |
+
st.session_state.analysis_data['uploaded_files'][file.name] = {
|
74 |
+
'data': text_content,
|
75 |
+
'metadata': {
|
76 |
+
'pages': len(pdf_reader.pages),
|
77 |
+
'upload_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
78 |
+
}
|
79 |
+
}
|
80 |
+
st.success(f"تم تحميل الملف PDF {file.name} بنجاح!")
|
81 |
+
|
82 |
+
elif file.name.endswith('.csv'):
|
83 |
+
df = pd.read_csv(file)
|
84 |
+
st.session_state.analysis_data['uploaded_files'][file.name] = {
|
85 |
+
'data': df,
|
86 |
+
'metadata': {
|
87 |
+
'rows': len(df),
|
88 |
+
'columns': len(df.columns),
|
89 |
+
'upload_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
90 |
+
}
|
91 |
+
}
|
92 |
+
st.success(f"تم تحميل الملف {file.name} بنجاح!")
|
93 |
+
else:
|
94 |
+
df = pd.read_excel(file)
|
95 |
+
st.session_state.analysis_data['uploaded_files'][file.name] = {
|
96 |
+
'data': df,
|
97 |
+
'metadata': {
|
98 |
+
'rows': len(df),
|
99 |
+
'columns': len(df.columns),
|
100 |
+
'upload_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
101 |
+
}
|
102 |
+
}
|
103 |
+
st.success(f"تم تحميل الملف {file.name} بنجاح!")
|
104 |
+
|
105 |
+
except Exception as e:
|
106 |
+
st.error(f"خطأ في تحميل الملف {file.name}: {str(e)}")
|
107 |
+
|
108 |
+
# عرض الملفات المحملة
|
109 |
+
if st.session_state.analysis_data['uploaded_files']:
|
110 |
+
st.subheader("الملفات المحملة")
|
111 |
+
for filename, file_info in st.session_state.analysis_data['uploaded_files'].items():
|
112 |
+
with st.expander(f"📄 {filename}"):
|
113 |
+
st.write("معلومات الملف:")
|
114 |
+
if 'rows' in file_info['metadata']:
|
115 |
+
st.write(f"- عدد الصفوف: {file_info['metadata']['rows']}")
|
116 |
+
if 'columns' in file_info['metadata']:
|
117 |
+
st.write(f"- عدد الأعمدة: {file_info['metadata']['columns']}")
|
118 |
+
if 'pages' in file_info['metadata']:
|
119 |
+
st.write(f"- عدد الصفحات: {file_info['metadata']['pages']}")
|
120 |
+
st.write(f"- وقت التحميل: {file_info['metadata']['upload_time']}")
|
121 |
+
|
122 |
+
if st.button(f"عرض البيانات", key=f"show_{filename}"):
|
123 |
+
st.write(file_info['data'].head() if isinstance(file_info['data'], pd.DataFrame) else file_info['data'])
|
124 |
+
|
125 |
+
def _render_statistical_analysis(self):
|
126 |
+
st.header("التحليل الإحصائي")
|
127 |
+
|
128 |
+
if not st.session_state.analysis_data['uploaded_files']:
|
129 |
+
st.info("الرجاء تحميل البيانات أولاً")
|
130 |
+
return
|
131 |
+
|
132 |
+
# اختيار الملف للتحليل
|
133 |
+
selected_file = st.selectbox(
|
134 |
+
"اختر الملف للتحليل",
|
135 |
+
list(st.session_state.analysis_data['uploaded_files'].keys())
|
136 |
+
)
|
137 |
+
|
138 |
+
if selected_file:
|
139 |
+
data = st.session_state.analysis_data['uploaded_files'][selected_file]['data']
|
140 |
+
if isinstance(data, pd.DataFrame):
|
141 |
+
# الإحصاءات الوصفية
|
142 |
+
st.subheader("الإحصاءات الوصفية")
|
143 |
+
st.dataframe(data.describe())
|
144 |
+
|
145 |
+
# تحليل القيم المفقودة
|
146 |
+
st.subheader("تحليل القيم المفقودة")
|
147 |
+
missing_data = pd.DataFrame({
|
148 |
+
'العمود': data.columns,
|
149 |
+
'عدد القيم المفقودة': data.isnull().sum(),
|
150 |
+
'نسبة القيم المفقودة (%)': (data.isnull().sum() / len(data) * 100).round(2)
|
151 |
+
})
|
152 |
+
st.dataframe(missing_data)
|
153 |
+
else:
|
154 |
+
st.info("لا يمكن إجراء تحليل إحصائي على ملفات PDF.")
|
155 |
+
|
156 |
+
|
157 |
+
def _render_visualization(self):
|
158 |
+
st.header("التحليل المرئي")
|
159 |
+
|
160 |
+
if not st.session_state.analysis_data['uploaded_files']:
|
161 |
+
st.info("الرجاء تحميل البيانات أولاً")
|
162 |
+
return
|
163 |
+
|
164 |
+
selected_file = st.selectbox(
|
165 |
+
"اختر الملف للتحليل المرئي",
|
166 |
+
list(st.session_state.analysis_data['uploaded_files'].keys()),
|
167 |
+
key="viz_file_select"
|
168 |
+
)
|
169 |
+
|
170 |
+
if selected_file:
|
171 |
+
data = st.session_state.analysis_data['uploaded_files'][selected_file]['data']
|
172 |
+
if isinstance(data, pd.DataFrame):
|
173 |
+
# اختيار نوع المخطط
|
174 |
+
chart_type = st.selectbox(
|
175 |
+
"اختر نوع المخطط",
|
176 |
+
["رسم بياني شريطي", "رسم بياني خطي", "رسم بياني دائري", "مخطط التشتت", "مخطط الصندوق"],
|
177 |
+
key="chart_type"
|
178 |
+
)
|
179 |
+
|
180 |
+
# تخصيص المخطط
|
181 |
+
if chart_type == "رسم بياني شريطي":
|
182 |
+
x_col = st.selectbox("اختر محور X", data.columns, key="bar_x")
|
183 |
+
y_col = st.selectbox("اختر محور Y", data.select_dtypes(include=['number']).columns, key="bar_y")
|
184 |
+
fig = px.bar(data, x=x_col, y=y_col)
|
185 |
+
st.plotly_chart(fig, use_container_width=True)
|
186 |
+
elif chart_type == "رسم بياني خطي":
|
187 |
+
x_col = st.selectbox("اختر محور X", data.columns, key="line_x")
|
188 |
+
y_cols = st.multiselect("اختر محاور Y", data.select_dtypes(include=['number']).columns, key="line_y")
|
189 |
+
if y_cols:
|
190 |
+
fig = go.Figure()
|
191 |
+
for y_col in y_cols:
|
192 |
+
fig.add_trace(go.Scatter(x=data[x_col], y=data[y_col], mode='lines+markers', name=y_col))
|
193 |
+
fig.update_layout(title=f"مخطط خطي", xaxis_title=x_col, yaxis_title="القيمة")
|
194 |
+
st.plotly_chart(fig, use_container_width=True)
|
195 |
+
else:
|
196 |
+
st.warning("الرجاء اختيار عمود واحد على الأقل للمحور الرأسي")
|
197 |
+
|
198 |
+
elif chart_type == "رسم بياني دائري":
|
199 |
+
col = st.selectbox("اختر العمود", data.columns, key="pie_column")
|
200 |
+
value_counts_df = data[col].value_counts().reset_index()
|
201 |
+
value_counts_df.columns = ['القيمة', 'العدد']
|
202 |
+
fig = px.pie(value_counts_df, names='القيمة', values='العدد', title=f"توزيع {col}")
|
203 |
+
st.plotly_chart(fig, use_container_width=True)
|
204 |
+
elif chart_type == "مخطط التشتت":
|
205 |
+
numeric_columns = data.select_dtypes(include=['number']).columns.tolist()
|
206 |
+
if len(numeric_columns) < 2:
|
207 |
+
st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإنشاء مخطط تشتت")
|
208 |
+
return
|
209 |
+
x_column = st.selectbox("اختر عمود المحور الأفقي (x):", numeric_columns, key="scatter_x")
|
210 |
+
y_column = st.selectbox("اختر عمود المحور الرأسي (y):", numeric_columns, key="scatter_y")
|
211 |
+
fig = px.scatter(data, x=x_column, y=y_column)
|
212 |
+
st.plotly_chart(fig, use_container_width=True)
|
213 |
+
elif chart_type == "مخطط الصندوق":
|
214 |
+
numeric_columns = data.select_dtypes(include=['number']).columns.tolist()
|
215 |
+
if not numeric_columns:
|
216 |
+
st.warning("يجب أن يكون هناك عمود رقمي واحد على الأقل لإنشاء مخطط صندوقي")
|
217 |
+
return
|
218 |
+
y_column = st.selectbox("اختر عمود القيمة:", numeric_columns, key="box_y")
|
219 |
+
fig = px.box(data, y=y_column, title=f"مخطط صندوقي لـ {y_column}")
|
220 |
+
st.plotly_chart(fig, use_container_width=True)
|
221 |
+
else:
|
222 |
+
st.info("لا يمكن إنشاء مخططات مرئية من ملفات PDF.")
|
223 |
+
|
224 |
+
def _render_ai_analysis(self):
|
225 |
+
st.header("تحليل الذكاء الاصطناعي")
|
226 |
+
|
227 |
+
if not st.session_state.analysis_data['uploaded_files']:
|
228 |
+
st.info("الرجاء تحميل البيانات أولاً")
|
229 |
+
return
|
230 |
+
|
231 |
+
selected_file = st.selectbox(
|
232 |
+
"اختر الملف للتحليل",
|
233 |
+
list(st.session_state.analysis_data['uploaded_files'].keys()),
|
234 |
+
key="ai_file_select"
|
235 |
+
)
|
236 |
+
|
237 |
+
if selected_file:
|
238 |
+
data = st.session_state.analysis_data['uploaded_files'][selected_file]['data']
|
239 |
+
if isinstance(data, pd.DataFrame):
|
240 |
+
analysis_type = st.selectbox(
|
241 |
+
"اختر نوع التحليل",
|
242 |
+
["تحليل الاتجاهات", "التنبؤ", "اكتشاف الأنماط", "تحليل العلاقات"]
|
243 |
+
)
|
244 |
+
|
245 |
+
if st.button("تحليل البيانات"):
|
246 |
+
with st.spinner("جاري تحليل البيانات..."):
|
247 |
+
# محاكاة تحليل الذكاء الاصطناعي
|
248 |
+
st.session_state.analysis_data['ai_insights'][selected_file] = {
|
249 |
+
'trends': self._analyze_trends(data),
|
250 |
+
'patterns': self._analyze_patterns(data),
|
251 |
+
'correlations': self._analyze_correlations(data)
|
252 |
+
}
|
253 |
+
|
254 |
+
st.success("تم اكتمال التحليل!")
|
255 |
+
|
256 |
+
# عرض النتائج
|
257 |
+
st.json(st.session_state.analysis_data['ai_insights'][selected_file])
|
258 |
+
else:
|
259 |
+
st.info("لا يمكن إجراء تحليل ذكاء اصطناعي على ملفات PDF.")
|
260 |
+
|
261 |
+
def _render_reports(self):
|
262 |
+
st.header("التقارير")
|
263 |
+
|
264 |
+
if not st.session_state.analysis_data['uploaded_files']:
|
265 |
+
st.info("الرجاء تحميل البيانات أولاً")
|
266 |
+
return
|
267 |
+
|
268 |
+
# إنشاء تقرير
|
269 |
+
if st.button("إنشاء تقرير تحليلي شامل"):
|
270 |
+
report_data = self._generate_comprehensive_report()
|
271 |
+
|
272 |
+
# تصدير التقرير
|
273 |
+
output = io.BytesIO()
|
274 |
+
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
|
275 |
+
for sheet_name, data in report_data.items():
|
276 |
+
pd.DataFrame(data).to_excel(writer, sheet_name=sheet_name)
|
277 |
+
|
278 |
+
st.download_button(
|
279 |
+
label="تحميل التقرير",
|
280 |
+
data=output.getvalue(),
|
281 |
+
file_name=f"analytical_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
|
282 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
283 |
+
)
|
284 |
+
|
285 |
+
def _analyze_trends(self, df):
|
286 |
+
"""تحليل الاتجاهات في البيانات"""
|
287 |
+
# محاكاة تحليل الاتجاهات
|
288 |
+
return {
|
289 |
+
"trend_1": "اتجاه تصاعدي في المبيعات",
|
290 |
+
"trend_2": "انخفاض في التكاليف التشغيلية",
|
291 |
+
"trend_3": "زيادة في رضا العملاء"
|
292 |
+
}
|
293 |
+
|
294 |
+
def _analyze_patterns(self, df):
|
295 |
+
"""اكتشاف الأنماط في البيانات"""
|
296 |
+
# محاكاة اكتشاف الأنماط
|
297 |
+
return {
|
298 |
+
"pattern_1": "نمط موسمي في الطلب",
|
299 |
+
"pattern_2": "نمط دوري في الإنتاج",
|
300 |
+
"pattern_3": "نمط جغرافي في التوزيع"
|
301 |
+
}
|
302 |
+
|
303 |
+
def _analyze_correlations(self, df):
|
304 |
+
"""تحليل العلاقات بين المتغيرات"""
|
305 |
+
# محاكاة تحليل العلاقات
|
306 |
+
numeric_cols = df.select_dtypes(include=['number']).columns
|
307 |
+
correlations = df[numeric_cols].corr().round(2).to_dict()
|
308 |
+
return correlations
|
309 |
+
|
310 |
+
def _generate_comprehensive_report(self):
|
311 |
+
"""إنشاء تقرير شامل"""
|
312 |
+
report = {
|
313 |
+
'ملخص_البيانات': {},
|
314 |
+
'التحليل_الإحصائي': {},
|
315 |
+
'تحليل_الذكاء_الاصطناعي': {},
|
316 |
+
'التوصيات': {}
|
317 |
+
}
|
318 |
+
|
319 |
+
for filename, file_info in st.session_state.analysis_data['uploaded_files'].items():
|
320 |
+
report['ملخص_البيانات'][filename] = file_info['metadata']
|
321 |
+
|
322 |
+
if 'ai_insights' in st.session_state.analysis_data:
|
323 |
+
report['تحليل_الذكاء_الاصطناعي'] = st.session_state.analysis_data['ai_insights']
|
324 |
+
|
325 |
+
return report
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/document_analysis/analyzer.py
CHANGED
@@ -1,281 +1,434 @@
|
|
1 |
-
"""
|
2 |
-
وحدة تحليل المستندات لنظام إدارة المناقصات - Hybrid Face
|
3 |
-
"""
|
4 |
-
|
5 |
-
import os
|
6 |
-
import re
|
7 |
-
import logging
|
8 |
-
import threading
|
9 |
-
from pathlib import Path
|
10 |
-
import datetime
|
11 |
-
import json
|
12 |
-
|
13 |
-
# تهيئة السجل
|
14 |
-
logging.basicConfig(
|
15 |
-
level=logging.INFO,
|
16 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
17 |
-
)
|
18 |
-
logger = logging.getLogger('document_analysis')
|
19 |
-
|
20 |
-
class DocumentAnalyzer:
|
21 |
-
"""فئة تحليل المستندات"""
|
22 |
-
|
23 |
-
def __init__(self, config=None):
|
24 |
-
"""تهيئة محلل المستندات"""
|
25 |
-
self.config = config
|
26 |
-
self.analysis_in_progress = False
|
27 |
-
self.current_document = None
|
28 |
-
self.analysis_results = {}
|
29 |
-
|
30 |
-
# إنشاء مجلد المستندات إذا لم يكن موجوداً
|
31 |
-
if config and hasattr(config, 'DOCUMENTS_PATH'):
|
32 |
-
self.documents_path = Path(config.DOCUMENTS_PATH)
|
33 |
-
else:
|
34 |
-
self.documents_path = Path('data/documents')
|
35 |
-
|
36 |
-
if not self.documents_path.exists():
|
37 |
-
self.documents_path.mkdir(parents=True, exist_ok=True)
|
38 |
-
|
39 |
-
def analyze_document(self, document_path, document_type="tender", callback=None):
|
40 |
-
"""تحليل مستند"""
|
41 |
-
if self.analysis_in_progress:
|
42 |
-
logger.warning("هناك عملية تحليل جارية بالفعل")
|
43 |
-
return False
|
44 |
-
|
45 |
-
if not os.path.exists(document_path):
|
46 |
-
logger.error(f"المستند غير موجود: {document_path}")
|
47 |
-
return False
|
48 |
-
|
49 |
-
self.analysis_in_progress = True
|
50 |
-
self.current_document = document_path
|
51 |
-
self.analysis_results = {
|
52 |
-
"document_path": document_path,
|
53 |
-
"document_type": document_type,
|
54 |
-
"analysis_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
55 |
-
"status": "جاري التحليل",
|
56 |
-
"items": [],
|
57 |
-
"entities": [],
|
58 |
-
"dates": [],
|
59 |
-
"amounts": [],
|
60 |
-
"risks": []
|
61 |
-
}
|
62 |
-
|
63 |
-
# بدء التحليل في خيط منفصل
|
64 |
-
thread = threading.Thread(
|
65 |
-
target=self._analyze_document_thread,
|
66 |
-
args=(document_path, document_type, callback)
|
67 |
-
)
|
68 |
-
thread.daemon = True
|
69 |
-
thread.start()
|
70 |
-
|
71 |
-
return True
|
72 |
-
|
73 |
-
def _analyze_document_thread(self, document_path, document_type, callback):
|
74 |
-
"""خيط تحليل المستند"""
|
75 |
-
try:
|
76 |
-
# تحديد نوع المستند
|
77 |
-
file_extension = os.path.splitext(document_path)[1].lower()
|
78 |
-
|
79 |
-
if file_extension == '.pdf':
|
80 |
-
self._analyze_pdf(document_path, document_type)
|
81 |
-
elif file_extension == '.docx':
|
82 |
-
self._analyze_docx(document_path, document_type)
|
83 |
-
elif file_extension == '.xlsx':
|
84 |
-
self._analyze_xlsx(document_path, document_type)
|
85 |
-
elif file_extension == '.txt':
|
86 |
-
self._analyze_txt(document_path, document_type)
|
87 |
-
else:
|
88 |
-
logger.error(f"نوع المستند غير مدعوم: {file_extension}")
|
89 |
-
self.analysis_results["status"] = "فشل التحليل"
|
90 |
-
self.analysis_results["error"] = "نوع المستند غير مدعوم"
|
91 |
-
|
92 |
-
# تحديث حالة التحليل
|
93 |
-
if self.analysis_results["status"] != "فشل التحليل":
|
94 |
-
self.analysis_results["status"] = "اكتمل التحليل"
|
95 |
-
self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
96 |
-
|
97 |
-
logger.info(f"اكتمل تحليل المستند: {document_path}")
|
98 |
-
|
99 |
-
except Exception as e:
|
100 |
-
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
101 |
-
self.analysis_results["status"] = "فشل التحليل"
|
102 |
-
self.analysis_results["error"] = str(e)
|
103 |
-
|
104 |
-
finally:
|
105 |
-
self.analysis_in_progress = False
|
106 |
-
|
107 |
-
# استدعاء دالة الاستجابة إذا تم توفيرها
|
108 |
-
if callback and callable(callback):
|
109 |
-
callback(self.analysis_results)
|
110 |
-
|
111 |
-
def _analyze_pdf(self, document_path, document_type):
|
112 |
-
"""تحليل مستند PDF"""
|
113 |
-
try:
|
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 |
-
|
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 |
-
|
280 |
-
|
281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة تحليل المستندات لنظام إدارة المناقصات - Hybrid Face
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import re
|
7 |
+
import logging
|
8 |
+
import threading
|
9 |
+
from pathlib import Path
|
10 |
+
import datetime
|
11 |
+
import json
|
12 |
+
|
13 |
+
# تهيئة السجل
|
14 |
+
logging.basicConfig(
|
15 |
+
level=logging.INFO,
|
16 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
17 |
+
)
|
18 |
+
logger = logging.getLogger('document_analysis')
|
19 |
+
|
20 |
+
class DocumentAnalyzer:
|
21 |
+
"""فئة تحليل المستندات"""
|
22 |
+
|
23 |
+
def __init__(self, config=None):
|
24 |
+
"""تهيئة محلل المستندات"""
|
25 |
+
self.config = config
|
26 |
+
self.analysis_in_progress = False
|
27 |
+
self.current_document = None
|
28 |
+
self.analysis_results = {}
|
29 |
+
|
30 |
+
# إنشاء مجلد المستندات إذا لم يكن موجوداً
|
31 |
+
if config and hasattr(config, 'DOCUMENTS_PATH'):
|
32 |
+
self.documents_path = Path(config.DOCUMENTS_PATH)
|
33 |
+
else:
|
34 |
+
self.documents_path = Path('data/documents')
|
35 |
+
|
36 |
+
if not self.documents_path.exists():
|
37 |
+
self.documents_path.mkdir(parents=True, exist_ok=True)
|
38 |
+
|
39 |
+
def analyze_document(self, document_path, document_type="tender", callback=None):
|
40 |
+
"""تحليل مستند"""
|
41 |
+
if self.analysis_in_progress:
|
42 |
+
logger.warning("هناك عملية تحليل جارية بالفعل")
|
43 |
+
return False
|
44 |
+
|
45 |
+
if not os.path.exists(document_path):
|
46 |
+
logger.error(f"المستند غير موجود: {document_path}")
|
47 |
+
return False
|
48 |
+
|
49 |
+
self.analysis_in_progress = True
|
50 |
+
self.current_document = document_path
|
51 |
+
self.analysis_results = {
|
52 |
+
"document_path": document_path,
|
53 |
+
"document_type": document_type,
|
54 |
+
"analysis_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
55 |
+
"status": "جاري التحليل",
|
56 |
+
"items": [],
|
57 |
+
"entities": [],
|
58 |
+
"dates": [],
|
59 |
+
"amounts": [],
|
60 |
+
"risks": []
|
61 |
+
}
|
62 |
+
|
63 |
+
# بدء التحليل في خيط منفصل
|
64 |
+
thread = threading.Thread(
|
65 |
+
target=self._analyze_document_thread,
|
66 |
+
args=(document_path, document_type, callback)
|
67 |
+
)
|
68 |
+
thread.daemon = True
|
69 |
+
thread.start()
|
70 |
+
|
71 |
+
return True
|
72 |
+
|
73 |
+
def _analyze_document_thread(self, document_path, document_type, callback):
|
74 |
+
"""خيط تحليل المستند"""
|
75 |
+
try:
|
76 |
+
# تحديد نوع المستند
|
77 |
+
file_extension = os.path.splitext(document_path)[1].lower()
|
78 |
+
|
79 |
+
if file_extension == '.pdf':
|
80 |
+
self.analysis_results = self._analyze_pdf(document_path, document_type)
|
81 |
+
elif file_extension == '.docx':
|
82 |
+
self._analyze_docx(document_path, document_type)
|
83 |
+
elif file_extension == '.xlsx':
|
84 |
+
self._analyze_xlsx(document_path, document_type)
|
85 |
+
elif file_extension == '.txt':
|
86 |
+
self._analyze_txt(document_path, document_type)
|
87 |
+
else:
|
88 |
+
logger.error(f"نوع المستند غير مدعوم: {file_extension}")
|
89 |
+
self.analysis_results["status"] = "فشل التحليل"
|
90 |
+
self.analysis_results["error"] = "نوع المستند غير مدعوم"
|
91 |
+
|
92 |
+
# تحديث حالة التحليل
|
93 |
+
if self.analysis_results["status"] != "فشل التحليل":
|
94 |
+
self.analysis_results["status"] = "اكتمل التحليل"
|
95 |
+
self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
96 |
+
|
97 |
+
logger.info(f"اكتمل تحليل المستند: {document_path}")
|
98 |
+
|
99 |
+
except Exception as e:
|
100 |
+
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
101 |
+
self.analysis_results["status"] = "فشل التحليل"
|
102 |
+
self.analysis_results["error"] = str(e)
|
103 |
+
|
104 |
+
finally:
|
105 |
+
self.analysis_in_progress = False
|
106 |
+
|
107 |
+
# استدعاء دالة الاستجابة إذا تم توفيرها
|
108 |
+
if callback and callable(callback):
|
109 |
+
callback(self.analysis_results)
|
110 |
+
|
111 |
+
def _analyze_pdf(self, document_path, document_type):
|
112 |
+
"""تحليل مستند PDF باستخدام الذكاء الاصطناعي"""
|
113 |
+
try:
|
114 |
+
# استخراج النص من PDF
|
115 |
+
text = self._extract_text_from_pdf(document_path)
|
116 |
+
|
117 |
+
# تحليل متقدم للمستند
|
118 |
+
analysis = {
|
119 |
+
"file_info": {
|
120 |
+
"name": os.path.basename(document_path),
|
121 |
+
"type": "PDF",
|
122 |
+
"size": os.path.getsize(document_path),
|
123 |
+
"pages": self._count_pages(document_path),
|
124 |
+
"create_date": self._get_creation_date(document_path),
|
125 |
+
"modify_date": time.ctime(os.path.getmtime(document_path))
|
126 |
+
},
|
127 |
+
"content_analysis": {
|
128 |
+
"contract_terms": self._analyze_contract_terms(text),
|
129 |
+
"financial_analysis": self._analyze_financial_terms(text),
|
130 |
+
"legal_analysis": self._analyze_legal_terms(text),
|
131 |
+
"risk_analysis": self._analyze_risks(text),
|
132 |
+
"conditions_analysis": self._analyze_conditions(text),
|
133 |
+
"technical_specs": self._analyze_technical_specs(text),
|
134 |
+
"key_dates": self._extract_key_dates(text),
|
135 |
+
"important_figures": self._extract_figures(text),
|
136 |
+
"entities": self._extract_entities(text)
|
137 |
+
},
|
138 |
+
"statistical_analysis": {
|
139 |
+
"word_count": len(text.split()),
|
140 |
+
"unique_terms": self._analyze_unique_terms(text),
|
141 |
+
"topic_distribution": self._analyze_topics(text),
|
142 |
+
"complexity_score": self._calculate_complexity(text)
|
143 |
+
},
|
144 |
+
"compliance_check": {
|
145 |
+
"missing_sections": self._check_missing_sections(text),
|
146 |
+
"required_terms": self._check_required_terms(text),
|
147 |
+
"compliance_score": self._calculate_compliance_score(text)
|
148 |
+
},
|
149 |
+
"summary": self._generate_summary(text),
|
150 |
+
"recommendations": self._generate_recommendations(text),
|
151 |
+
"related_documents": self._find_related_documents(document_path),
|
152 |
+
"version_info": self._get_version_info(document_path)
|
153 |
+
}
|
154 |
+
|
155 |
+
# إضافة تحليل متخصص حسب نوع المستند
|
156 |
+
if document_type == "tender":
|
157 |
+
analysis["tender_analysis"] = self._analyze_tender_specifics(text)
|
158 |
+
elif document_type == "contract":
|
159 |
+
analysis["contract_analysis"] = self._analyze_contract_specifics(text)
|
160 |
+
elif document_type == "technical":
|
161 |
+
analysis["technical_analysis"] = self._analyze_technical_specifics(text)
|
162 |
+
|
163 |
+
return analysis
|
164 |
+
|
165 |
+
except Exception as e:
|
166 |
+
logger.error(f"خطأ في تحليل PDF: {str(e)}")
|
167 |
+
raise
|
168 |
+
|
169 |
+
def _extract_text_from_pdf(self, document_path):
|
170 |
+
"""استخراج النص من ملف PDF (تحتاج إلى مكتبة مثل PyPDF2 أو pdfplumber)"""
|
171 |
+
# Implementation using a PDF processing library like PyPDF2 or pdfplumber is needed here.
|
172 |
+
# This is a placeholder. Replace with actual PDF text extraction.
|
173 |
+
return "Placeholder text extracted from PDF"
|
174 |
+
|
175 |
+
def _analyze_contract_terms(self, text):
|
176 |
+
"""تحليل بنود العقد"""
|
177 |
+
# Implementation for contract term analysis is needed here. This is a placeholder.
|
178 |
+
return "Placeholder contract terms analysis"
|
179 |
+
|
180 |
+
def _analyze_financial_terms(self, text):
|
181 |
+
"""تحليل الجزء المالي"""
|
182 |
+
# Implementation for financial term analysis is needed here. This is a placeholder.
|
183 |
+
return "Placeholder financial terms analysis"
|
184 |
+
|
185 |
+
def _analyze_legal_terms(self, text):
|
186 |
+
"""تحليل القانوني للعقد"""
|
187 |
+
# Implementation for legal term analysis is needed here. This is a placeholder.
|
188 |
+
return "Placeholder legal terms analysis"
|
189 |
+
|
190 |
+
def _analyze_risks(self, text):
|
191 |
+
"""تحليل المخاطر"""
|
192 |
+
# Implementation for risk analysis is needed here. This is a placeholder.
|
193 |
+
return "Placeholder risk analysis"
|
194 |
+
|
195 |
+
def _analyze_conditions(self, text):
|
196 |
+
"""دراسة كراسة الشروط"""
|
197 |
+
# Implementation for conditions analysis is needed here. This is a placeholder.
|
198 |
+
return "Placeholder conditions analysis"
|
199 |
+
|
200 |
+
def _generate_summary(self, text):
|
201 |
+
"""توليد ملخص"""
|
202 |
+
# Implementation for summary generation is needed here. This is a placeholder.
|
203 |
+
return "Placeholder summary"
|
204 |
+
|
205 |
+
def _generate_recommendations(self, text):
|
206 |
+
"""توليد التوصيات"""
|
207 |
+
# Implementation for recommendation generation is needed here. This is a placeholder.
|
208 |
+
return "Placeholder recommendations"
|
209 |
+
|
210 |
+
|
211 |
+
|
212 |
+
def _analyze_docx(self, document_path, document_type):
|
213 |
+
"""تحليل مستند Word"""
|
214 |
+
try:
|
215 |
+
# محاكاة تحليل مستند Word
|
216 |
+
logger.info(f"تحليل مستند Word: {document_path}")
|
217 |
+
|
218 |
+
# في التطبيق الفعلي، سيتم استخدام مكتبة مثل python-docx
|
219 |
+
# لاستخراج النص من ملف Word وتحليله
|
220 |
+
|
221 |
+
# محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
|
222 |
+
# (مشابه لتحليل PDF)
|
223 |
+
self.analysis_results["items"] = [
|
224 |
+
{"id": 1, "name": "توريد معدات", "description": "توريد معدات المشروع", "unit": "مجموعة", "estimated_quantity": 10},
|
225 |
+
{"id": 2, "name": "تركيب المعدات", "description": "تركيب وتشغيل المعدات", "unit": "مجموعة", "estimated_quantity": 10},
|
226 |
+
{"id": 3, "name": "التدريب", "description": "تدريب الموظفين على استخدام المعدات", "unit": "يوم", "estimated_quantity": 20}
|
227 |
+
]
|
228 |
+
|
229 |
+
# محاكاة استخراج الكيانات والتواريخ والمبالغ والمخاطر
|
230 |
+
# (مشابه لتحليل PDF)
|
231 |
+
|
232 |
+
except Exception as e:
|
233 |
+
logger.error(f"خطأ في تحليل مستند Word: {str(e)}")
|
234 |
+
raise
|
235 |
+
|
236 |
+
def _analyze_xlsx(self, document_path, document_type):
|
237 |
+
"""تحليل مستند Excel"""
|
238 |
+
try:
|
239 |
+
# محاكاة تحليل مستند Excel
|
240 |
+
logger.info(f"تحليل مستند Excel: {document_path}")
|
241 |
+
|
242 |
+
# في التطبيق الفعلي، سيتم استخدام مكتبة مثل pandas أو openpyxl
|
243 |
+
# لاستخراج البيانات من ملف Excel وتحليلها
|
244 |
+
|
245 |
+
# محاكاة استخراج البنود
|
246 |
+
self.analysis_results["items"] = [
|
247 |
+
{"id": 1, "name": "بند 1", "description": "وصف البند 1", "unit": "وحدة", "estimated_quantity": 100},
|
248 |
+
{"id": 2, "name": "بند 2", "description": "وصف البند 2", "unit": "وحدة", "estimated_quantity": 200},
|
249 |
+
{"id": 3, "name": "بند 3", "description": "وصف البند 3", "unit": "وحدة", "estimated_quantity": 300}
|
250 |
+
]
|
251 |
+
|
252 |
+
# محاكاة استخراج المبالغ
|
253 |
+
self.analysis_results["amounts"] = [
|
254 |
+
{"type": "item_cost", "amount": 10000, "currency": "SAR", "description": "تكلفة البند 1"},
|
255 |
+
{"type": "item_cost", "amount": 20000, "currency": "SAR", "description": "تكلفة البند 2"},
|
256 |
+
{"type": "item_cost", "amount": 30000, "currency": "SAR", "description": "تكلفة البند 3"}
|
257 |
+
]
|
258 |
+
|
259 |
+
except Exception as e:
|
260 |
+
logger.error(f"خطأ في تحليل مستند Excel: {str(e)}")
|
261 |
+
raise
|
262 |
+
|
263 |
+
def _analyze_txt(self, document_path, document_type):
|
264 |
+
"""تحليل مستند نصي"""
|
265 |
+
try:
|
266 |
+
# محاكاة تحليل مستند نصي
|
267 |
+
logger.info(f"تحليل مستند نصي: {document_path}")
|
268 |
+
|
269 |
+
# في التطبيق الفعلي، سيتم قراءة الملف النصي وتحليله
|
270 |
+
|
271 |
+
# محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
|
272 |
+
# (مشابه للتحليلات الأخرى)
|
273 |
+
|
274 |
+
except Exception as e:
|
275 |
+
logger.error(f"خطأ في تحليل مستند نصي: {str(e)}")
|
276 |
+
raise
|
277 |
+
|
278 |
+
def get_analysis_status(self):
|
279 |
+
"""الحصول على حالة التحليل الحالي"""
|
280 |
+
if not self.analysis_in_progress:
|
281 |
+
if not self.analysis_results:
|
282 |
+
return {"status": "لا يوجد تحليل جارٍ"}
|
283 |
+
else:
|
284 |
+
return {"status": self.analysis_results.get("status", "غير معروف")}
|
285 |
+
|
286 |
+
return {
|
287 |
+
"status": "جاري التحليل",
|
288 |
+
"document_path": self.current_document,
|
289 |
+
"start_time": self.analysis_results.get("analysis_start_time")
|
290 |
+
}
|
291 |
+
|
292 |
+
def get_analysis_results(self):
|
293 |
+
"""الحصول على نتائج التحليل"""
|
294 |
+
return self.analysis_results
|
295 |
+
|
296 |
+
def export_analysis_results(self, output_path=None):
|
297 |
+
"""تصدير نتائج التحليل إلى ملف JSON"""
|
298 |
+
if not self.analysis_results:
|
299 |
+
logger.warning("لا توجد نتائج تحليل للتصدير")
|
300 |
+
return None
|
301 |
+
|
302 |
+
if not output_path:
|
303 |
+
# إنشاء اسم ملف افتراضي
|
304 |
+
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
305 |
+
filename = f"analysis_results_{timestamp}.json"
|
306 |
+
output_path = os.path.join(self.documents_path, filename)
|
307 |
+
|
308 |
+
try:
|
309 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
310 |
+
json.dump(self.analysis_results, f, ensure_ascii=False, indent=4)
|
311 |
+
|
312 |
+
logger.info(f"تم تصدير نتائج التحليل إلى: {output_path}")
|
313 |
+
return output_path
|
314 |
+
|
315 |
+
except Exception as e:
|
316 |
+
logger.error(f"خطأ في تصدير نتائج التحليل: {str(e)}")
|
317 |
+
return None
|
318 |
+
|
319 |
+
def import_analysis_results(self, input_path):
|
320 |
+
"""استيراد نتائج التحليل من ملف JSON"""
|
321 |
+
if not os.path.exists(input_path):
|
322 |
+
logger.error(f"ملف نتائج التحليل غير موجود: {input_path}")
|
323 |
+
return False
|
324 |
+
|
325 |
+
try:
|
326 |
+
with open(input_path, 'r', encoding='utf-8') as f:
|
327 |
+
self.analysis_results = json.load(f)
|
328 |
+
|
329 |
+
logger.info(f"تم استيراد نتائج التحليل من: {input_path}")
|
330 |
+
return True
|
331 |
+
|
332 |
+
except Exception as e:
|
333 |
+
logger.error(f"خطأ في استيراد نتائج التحليل: {str(e)}")
|
334 |
+
return False
|
335 |
+
|
336 |
+
def _count_pages(self, document_path):
|
337 |
+
"""حساب عدد صفحات المستند"""
|
338 |
+
try:
|
339 |
+
import PyPDF2
|
340 |
+
with open(document_path, 'rb') as file:
|
341 |
+
reader = PyPDF2.PdfReader(file)
|
342 |
+
return len(reader.pages)
|
343 |
+
except:
|
344 |
+
return 0
|
345 |
+
|
346 |
+
def _get_creation_date(self, document_path):
|
347 |
+
"""استخراج تاريخ إنشاء المستند"""
|
348 |
+
try:
|
349 |
+
import PyPDF2
|
350 |
+
with open(document_path, 'rb') as file:
|
351 |
+
reader = PyPDF2.PdfReader(file)
|
352 |
+
if '/CreationDate' in reader.metadata:
|
353 |
+
return reader.metadata['/CreationDate']
|
354 |
+
return "غير متوفر"
|
355 |
+
except:
|
356 |
+
return "غير متوفر"
|
357 |
+
|
358 |
+
def _analyze_technical_specs(self, text):
|
359 |
+
"""تحليل المواصفات الفنية"""
|
360 |
+
specs = {
|
361 |
+
"materials": self._extract_materials(text),
|
362 |
+
"measurements": self._extract_measurements(text),
|
363 |
+
"standards": self._extract_standards(text)
|
364 |
+
}
|
365 |
+
return specs
|
366 |
+
|
367 |
+
def _extract_key_dates(self, text):
|
368 |
+
"""استخراج التواريخ المهمة"""
|
369 |
+
import re
|
370 |
+
date_pattern = r'\d{1,2}[-/]\d{1,2}[-/]\d{2,4}'
|
371 |
+
dates = re.findall(date_pattern, text)
|
372 |
+
return list(set(dates))
|
373 |
+
|
374 |
+
def _extract_figures(self, text):
|
375 |
+
"""استخراج الأرقام والقيم المهمة"""
|
376 |
+
import re
|
377 |
+
# البحث عن القيم النقدية
|
378 |
+
currency_pattern = r'[\d,]+\.?\d*\s*(?:ريال|دولار|SAR|USD)'
|
379 |
+
currencies = re.findall(currency_pattern, text)
|
380 |
+
|
381 |
+
# البحث عن النسب المئوية
|
382 |
+
percentage_pattern = r'\d+\.?\d*\s*%'
|
383 |
+
percentages = re.findall(percentage_pattern, text)
|
384 |
+
|
385 |
+
return {
|
386 |
+
"currencies": currencies,
|
387 |
+
"percentages": percentages
|
388 |
+
}
|
389 |
+
|
390 |
+
def _analyze_unique_terms(self, text):
|
391 |
+
"""تحليل المصطلحات الفريدة"""
|
392 |
+
words = set(text.split())
|
393 |
+
return list(words)
|
394 |
+
|
395 |
+
def _calculate_complexity(self, text):
|
396 |
+
"""حساب مستوى تعقيد النص"""
|
397 |
+
words = text.split()
|
398 |
+
avg_word_length = sum(len(word) for word in words) / len(words)
|
399 |
+
sentences = text.split('.')
|
400 |
+
avg_sentence_length = len(words) / len(sentences)
|
401 |
+
|
402 |
+
# حساب درجة التعقيد (1-10)
|
403 |
+
complexity = min((avg_word_length * 0.5 + avg_sentence_length * 0.2), 10)
|
404 |
+
return round(complexity, 2)
|
405 |
+
|
406 |
+
def _check_missing_sections(self, text):
|
407 |
+
"""التحقق من الأقسام المفقودة"""
|
408 |
+
required_sections = [
|
409 |
+
"نطاق العمل",
|
410 |
+
"المواصفات الفنية",
|
411 |
+
"الشروط العامة",
|
412 |
+
"الضمانات",
|
413 |
+
"الغرامات",
|
414 |
+
"شروط الدفع"
|
415 |
+
]
|
416 |
+
|
417 |
+
missing = []
|
418 |
+
for section in required_sections:
|
419 |
+
if section not in text:
|
420 |
+
missing.append(section)
|
421 |
+
|
422 |
+
return missing
|
423 |
+
|
424 |
+
def _find_related_documents(self, document_path):
|
425 |
+
"""البحث عن المستندات المرتبطة"""
|
426 |
+
directory = os.path.dirname(document_path)
|
427 |
+
base_name = os.path.basename(document_path)
|
428 |
+
related = []
|
429 |
+
|
430 |
+
for file in os.listdir(directory):
|
431 |
+
if file != base_name and file.startswith(base_name.split('_')[0]):
|
432 |
+
related.append(file)
|
433 |
+
|
434 |
+
return related
|
modules/document_comparison/document_comparison_app.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
modules/project_management/project_management_app.py
CHANGED
@@ -1,666 +1,666 @@
|
|
1 |
-
"""
|
2 |
-
وحدة إدارة المشاريع - نظام تحليل المناقصات
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
import pandas as pd
|
7 |
-
import numpy as np
|
8 |
-
from datetime import datetime, timedelta
|
9 |
-
import os
|
10 |
-
import time
|
11 |
-
import io
|
12 |
-
import sys
|
13 |
-
from pathlib import Path
|
14 |
-
|
15 |
-
# إضافة مسار المشروع للنظام
|
16 |
-
sys.path.append(str(Path(__file__).parent.parent))
|
17 |
-
|
18 |
-
# استيراد محسن واجهة المستخدم
|
19 |
-
from styling.enhanced_ui import UIEnhancer
|
20 |
-
|
21 |
-
class ProjectsApp:
|
22 |
-
"""وحدة إدارة المشاريع"""
|
23 |
-
|
24 |
-
def __init__(self):
|
25 |
-
"""تهيئة وحدة إدارة المشاريع"""
|
26 |
-
self.ui = UIEnhancer(page_title="إدارة المشاريع - نظام تحليل المناقصات", page_icon="📋")
|
27 |
-
self.ui.apply_theme_colors()
|
28 |
-
|
29 |
-
# تهيئة البيانات المبدئية
|
30 |
-
if 'projects' not in st.session_state:
|
31 |
-
st.session_state.projects = self._generate_sample_projects()
|
32 |
-
|
33 |
-
def run(self):
|
34 |
-
"""تشغيل وحدة إدارة المشاريع"""
|
35 |
-
# إنشاء قائمة العناصر
|
36 |
-
menu_items = [
|
37 |
-
{"name": "لوحة المعلومات", "icon": "house"},
|
38 |
-
{"name": "المناقصات والعقود", "icon": "file-text"},
|
39 |
-
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
40 |
-
{"name": "نظام التسعير", "icon": "calculator"},
|
41 |
-
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
42 |
-
{"name": "الموارد والتكاليف", "icon": "people"},
|
43 |
-
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
44 |
-
{"name": "إدارة المشاريع", "icon": "kanban"},
|
45 |
-
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
46 |
-
{"name": "الجدول الزمني", "icon": "calendar3"},
|
47 |
-
{"name": "الإشعارات", "icon": "bell"},
|
48 |
-
{"name": "مقارنة المستندات", "icon": "files"},
|
49 |
-
{"name": "الترجمة", "icon": "translate"},
|
50 |
-
{"name": "المساعد الذكي", "icon": "robot"},
|
51 |
-
{"name": "التقارير", "icon": "bar-chart"},
|
52 |
-
{"name": "الإعدادات", "icon": "gear"}
|
53 |
-
]
|
54 |
-
|
55 |
-
# إنشاء الشريط الجانبي
|
56 |
-
selected = self.ui.create_sidebar(menu_items)
|
57 |
-
|
58 |
-
# إنشاء ترويسة الصفحة
|
59 |
-
self.ui.create_header("إدارة المشاريع", "إدارة ومتابعة المشاريع والمناقصات")
|
60 |
-
|
61 |
-
# عرض واجهة وحدة إدارة المشاريع
|
62 |
-
tabs = st.tabs([
|
63 |
-
"قائمة المشاريع",
|
64 |
-
"إضافة مشروع جديد",
|
65 |
-
"تفاصيل المشروع",
|
66 |
-
"متابعة المشاريع"
|
67 |
-
])
|
68 |
-
|
69 |
-
with tabs[0]:
|
70 |
-
self._render_projects_list_tab()
|
71 |
-
|
72 |
-
with tabs[1]:
|
73 |
-
self._render_add_project_tab()
|
74 |
-
|
75 |
-
with tabs[2]:
|
76 |
-
self._render_project_details_tab()
|
77 |
-
|
78 |
-
with tabs[3]:
|
79 |
-
self._render_projects_tracking_tab()
|
80 |
-
|
81 |
-
def _render_projects_list_tab(self):
|
82 |
-
"""عرض تبويب قائمة المشاريع"""
|
83 |
-
|
84 |
-
st.markdown("### قائمة المشاريع")
|
85 |
-
|
86 |
-
# فلترة المشاريع
|
87 |
-
col1, col2, col3 = st.columns(3)
|
88 |
-
|
89 |
-
with col1:
|
90 |
-
search_term = st.text_input("البحث في المشاريع", key="project_search")
|
91 |
-
|
92 |
-
with col2:
|
93 |
-
status_filter = st.multiselect(
|
94 |
-
"حالة المشروع",
|
95 |
-
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
96 |
-
default=["جديد", "قيد التسعير", "تم التقديم"],
|
97 |
-
key="project_status_filter"
|
98 |
-
)
|
99 |
-
|
100 |
-
with col3:
|
101 |
-
client_filter = st.multiselect(
|
102 |
-
"الجهة المالكة",
|
103 |
-
list(set([p['client'] for p in st.session_state.projects])),
|
104 |
-
key="project_client_filter"
|
105 |
-
)
|
106 |
-
|
107 |
-
# تطبيق الفلترة
|
108 |
-
filtered_projects = st.session_state.projects
|
109 |
-
|
110 |
-
if search_term:
|
111 |
-
filtered_projects = [p for p in filtered_projects if search_term.lower() in p['name'].lower() or search_term in p['number']]
|
112 |
-
|
113 |
-
if status_filter:
|
114 |
-
filtered_projects = [p for p in filtered_projects if p['status'] in status_filter]
|
115 |
-
|
116 |
-
if client_filter:
|
117 |
-
filtered_projects = [p for p in filtered_projects if p['client'] in client_filter]
|
118 |
-
|
119 |
-
# تحويل المشاريع المفلترة إلى DataFrame للعرض
|
120 |
-
if filtered_projects:
|
121 |
-
projects_df = pd.DataFrame(filtered_projects)
|
122 |
-
|
123 |
-
# اختيار وترتيب الأعمدة
|
124 |
-
display_columns = [
|
125 |
-
'name', 'number', 'client', 'location', 'status',
|
126 |
-
'submission_date', 'tender_type', 'created_at'
|
127 |
-
]
|
128 |
-
|
129 |
-
# تغيير أسماء الأعمدة للعرض
|
130 |
-
column_names = {
|
131 |
-
'name': 'اسم المشروع',
|
132 |
-
'number': 'رقم المناقصة',
|
133 |
-
'client': 'الجهة المالكة',
|
134 |
-
'location': 'الموقع',
|
135 |
-
'status': 'الحالة',
|
136 |
-
'submission_date': 'تاريخ التقديم',
|
137 |
-
'tender_type': 'نوع المناقصة',
|
138 |
-
'created_at': 'تاريخ الإنشاء'
|
139 |
-
}
|
140 |
-
|
141 |
-
display_df = projects_df[display_columns].rename(columns=column_names)
|
142 |
-
|
143 |
-
# تنسيق التواريخ
|
144 |
-
date_columns = ['تاريخ التقديم', 'تاريخ الإنشاء']
|
145 |
-
for col in date_columns:
|
146 |
-
if col in display_df.columns:
|
147 |
-
display_df[col] = pd.to_datetime(display_df[col]).dt.strftime('%Y-%m-%d')
|
148 |
-
|
149 |
-
# عرض الجدول
|
150 |
-
st.dataframe(display_df, use_container_width=True, hide_index=True)
|
151 |
-
|
152 |
-
# زر تصدير المشاريع
|
153 |
-
if st.button("تصدير المشاريع إلى Excel"):
|
154 |
-
# محاكاة التصدير
|
155 |
-
st.success("تم تصدير المشاريع بنجاح!")
|
156 |
-
else:
|
157 |
-
st.info("لا توجد مشاريع تطابق معايير البحث.")
|
158 |
-
|
159 |
-
def _render_add_project_tab(self):
|
160 |
-
"""عرض تبويب إضافة مشروع جديد"""
|
161 |
-
|
162 |
-
st.markdown("### إضافة مشروع جديد")
|
163 |
-
|
164 |
-
# نموذج إدخال بيانات المشروع
|
165 |
-
with st.form("new_project_form"):
|
166 |
-
col1, col2 = st.columns(2)
|
167 |
-
|
168 |
-
with col1:
|
169 |
-
project_name = st.text_input("اسم المشروع", key="new_project_name")
|
170 |
-
client = st.text_input("الجهة المالكة", key="new_project_client")
|
171 |
-
location = st.text_input("الموقع", key="new_project_location")
|
172 |
-
tender_type = st.selectbox(
|
173 |
-
"نوع المناقصة",
|
174 |
-
["عامة", "خاصة", "أمر مباشر"],
|
175 |
-
key="new_project_tender_type"
|
176 |
-
)
|
177 |
-
|
178 |
-
with col2:
|
179 |
-
tender_number = st.text_input("رقم المناقصة", key="new_project_number")
|
180 |
-
submission_date = st.date_input("تاريخ التقديم", key="new_project_submission_date")
|
181 |
-
pricing_method = st.selectbox(
|
182 |
-
"طريقة التسعير",
|
183 |
-
["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
|
184 |
-
key="new_project_pricing_method"
|
185 |
-
)
|
186 |
-
status = st.selectbox(
|
187 |
-
"حالة المشروع",
|
188 |
-
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
189 |
-
index=0,
|
190 |
-
key="new_project_status"
|
191 |
-
)
|
192 |
-
|
193 |
-
description = st.text_area("وصف المشروع", key="new_project_description")
|
194 |
-
|
195 |
-
submitted = st.form_submit_button("إضافة المشروع")
|
196 |
-
|
197 |
-
if submitted:
|
198 |
-
# التحقق من تعبئة الحقول الإلزامية
|
199 |
-
if not project_name or not tender_number or not client:
|
200 |
-
st.error("يرجى تعبئة جميع الحقول الإلزامية (اسم المشروع، رقم المناقصة، الجهة المالكة).")
|
201 |
-
else:
|
202 |
-
# إنشاء مشروع جديد
|
203 |
-
new_project = {
|
204 |
-
'id': len(st.session_state.projects) + 1,
|
205 |
-
'name': project_name,
|
206 |
-
'number': tender_number,
|
207 |
-
'client': client,
|
208 |
-
'location': location,
|
209 |
-
'description': description,
|
210 |
-
'status': status,
|
211 |
-
'tender_type': tender_type,
|
212 |
-
'pricing_method': pricing_method,
|
213 |
-
'submission_date': submission_date,
|
214 |
-
'created_at': datetime.now(),
|
215 |
-
'created_by_id': 1 # معرف المستخدم الحالي
|
216 |
-
}
|
217 |
-
|
218 |
-
# إضافة المشروع إلى قائمة المشاريع
|
219 |
-
st.session_state.projects.append(new_project)
|
220 |
-
|
221 |
-
# رسالة نجاح
|
222 |
-
st.success(f"تم إضافة المشروع [{project_name}] بنجاح!")
|
223 |
-
|
224 |
-
# تعيين المشروع الحالي
|
225 |
-
st.session_state.current_project = new_project
|
226 |
-
|
227 |
-
def _render_project_details_tab(self):
|
228 |
-
"""عرض تبويب تفاصيل المشروع"""
|
229 |
-
|
230 |
-
st.markdown("### تفاصيل المشروع")
|
231 |
-
|
232 |
-
# التحقق من وجود مشروع حالي
|
233 |
-
if 'current_project' not in st.session_state or st.session_state.current_project is None:
|
234 |
-
# إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع
|
235 |
-
project_names = [p['name'] for p in st.session_state.projects]
|
236 |
-
selected_project_name = st.selectbox("اختر المشروع", project_names)
|
237 |
-
|
238 |
-
if selected_project_name:
|
239 |
-
selected_project = next((p for p in st.session_state.projects if p['name'] == selected_project_name), None)
|
240 |
-
if selected_project:
|
241 |
-
st.session_state.current_project = selected_project
|
242 |
-
else:
|
243 |
-
st.warning("لم يتم العثور على المشروع المحدد.")
|
244 |
-
return
|
245 |
-
else:
|
246 |
-
st.info("يرجى اختيار مشروع لعرض تفاصيله.")
|
247 |
-
return
|
248 |
-
|
249 |
-
# عرض تفاصيل المشروع
|
250 |
-
project = st.session_state.current_project
|
251 |
-
|
252 |
-
# عرض معلومات المشروع الأساسية
|
253 |
-
col1, col2, col3 = st.columns(3)
|
254 |
-
|
255 |
-
with col1:
|
256 |
-
st.markdown(f"**اسم المشروع**: {project
|
257 |
-
st.markdown(f"**رقم المناقصة**: {project
|
258 |
-
st.markdown(f"**الجهة المالكة**: {project
|
259 |
-
|
260 |
-
with col2:
|
261 |
-
st.markdown(f"**الموقع**: {project['location']}")
|
262 |
-
st.markdown(f"**نوع المناقصة**: {project['tender_type']}")
|
263 |
-
st.markdown(f"**حالة المشروع**: {project['status']}")
|
264 |
-
|
265 |
-
with col3:
|
266 |
-
st.markdown(f"**طريقة التسعير**: {project['pricing_method']}")
|
267 |
-
st.markdown(f"**تاريخ التقديم**: {project['submission_date'].strftime('%Y-%m-%d') if isinstance(project['submission_date'], datetime) else project['submission_date']}")
|
268 |
-
st.markdown(f"**تاريخ الإنشاء**: {project['created_at'].strftime('%Y-%m-%d') if isinstance(project['created_at'], datetime) else project['created_at']}")
|
269 |
-
|
270 |
-
# عرض وصف المشروع
|
271 |
-
st.markdown("#### وصف المشروع")
|
272 |
-
st.text_area("", value=project.get('description', ''), disabled=True, height=100)
|
273 |
-
|
274 |
-
# عرض المستندات المرتبطة بالمشروع
|
275 |
-
st.markdown("#### مستندات المشروع")
|
276 |
-
|
277 |
-
if 'documents' in project and project['documents']:
|
278 |
-
docs_df = pd.DataFrame(project['documents'])
|
279 |
-
st.dataframe(docs_df, use_container_width=True, hide_index=True)
|
280 |
-
else:
|
281 |
-
st.info("لا توجد مستندات مرتبطة بهذا المشروع حاليًا.")
|
282 |
-
|
283 |
-
# زر إضافة مستندات
|
284 |
-
if st.button("إضافة مستندات"):
|
285 |
-
st.session_state.upload_documents = True
|
286 |
-
|
287 |
-
# واجهة تحميل المستندات
|
288 |
-
if 'upload_documents' in st.session_state and st.session_state.upload_documents:
|
289 |
-
st.markdown("#### تحميل مستندات جديدة")
|
290 |
-
|
291 |
-
uploaded_file = st.file_uploader("اختر ملفًا", type=['pdf', 'docx', 'xlsx', 'png', 'jpg', 'dwg'])
|
292 |
-
doc_type = st.selectbox("نوع المستند", ["كراسة شروط", "عقد", "مخططات", "جدول كميات", "مواصفات فنية", "تعديلات وملاحق"])
|
293 |
-
|
294 |
-
if uploaded_file and st.button("تحميل المستند"):
|
295 |
-
# محاكاة تحميل المستند
|
296 |
-
with st.spinner("جاري تحميل المستند..."):
|
297 |
-
time.sleep(2)
|
298 |
-
|
299 |
-
# إنشاء مستند جديد
|
300 |
-
new_document = {
|
301 |
-
'filename': uploaded_file.name,
|
302 |
-
'type': doc_type,
|
303 |
-
'upload_date': datetime.now().strftime('%Y-%m-%d'),
|
304 |
-
'size': f"{uploaded_file.size / 1024:.1f} KB"
|
305 |
-
}
|
306 |
-
|
307 |
-
# إضافة المستند إلى المشروع
|
308 |
-
if 'documents' not in project:
|
309 |
-
project['documents'] = []
|
310 |
-
|
311 |
-
project['documents'].append(new_document)
|
312 |
-
|
313 |
-
st.success(f"تم تحميل المستند [{uploaded_file.name}] بنجاح!")
|
314 |
-
st.session_state.upload_documents = False
|
315 |
-
st.experimental_rerun()
|
316 |
-
|
317 |
-
# عرض البنود والكميات
|
318 |
-
st.markdown("#### بنود وكميات المشروع")
|
319 |
-
|
320 |
-
if 'items' in project and project['items']:
|
321 |
-
items_df = pd.DataFrame(project['items'])
|
322 |
-
st.dataframe(items_df, use_container_width=True, hide_index=True)
|
323 |
-
|
324 |
-
# زر لتحويل البنود إلى وحدة التسعير
|
325 |
-
if st.button("تحويل البنود إلى وحدة التسعير"):
|
326 |
-
if 'manual_items' not in st.session_state:
|
327 |
-
st.session_state.manual_items = pd.DataFrame()
|
328 |
-
|
329 |
-
st.session_state.manual_items = items_df.copy()
|
330 |
-
st.success("تم تحويل البنود إلى وحدة التسعير بنجاح!")
|
331 |
-
else:
|
332 |
-
st.info("لا توجد بنود وكميات لهذا المشروع حاليًا.")
|
333 |
-
|
334 |
-
# زر استيراد البنود من وحدة تحليل المستندات
|
335 |
-
if st.button("استيراد البنود من تحليل المستندات"):
|
336 |
-
st.warning("ميزة استيراد البنود من تحليل المستندات قيد التطوير.")
|
337 |
-
|
338 |
-
# أزرار الإجراءات
|
339 |
-
col1, col2, col3 = st.columns(3)
|
340 |
-
|
341 |
-
with col1:
|
342 |
-
if st.button("تعديل المشروع"):
|
343 |
-
st.session_state.edit_project = True
|
344 |
-
st.experimental_rerun()
|
345 |
-
|
346 |
-
with col2:
|
347 |
-
if st.button("تصدير بيانات المشروع"):
|
348 |
-
st.success("تم تصدير بيانات المشروع بنجاح!")
|
349 |
-
|
350 |
-
with col3:
|
351 |
-
if st.button("إرسال للاعتماد"):
|
352 |
-
st.success("تم إرسال المشروع للاعتماد بنجاح!")
|
353 |
-
|
354 |
-
# نموذج تعديل المشروع
|
355 |
-
if 'edit_project' in st.session_state and st.session_state.edit_project:
|
356 |
-
st.markdown("#### تعديل المشروع")
|
357 |
-
|
358 |
-
with st.form("edit_project_form"):
|
359 |
-
col1, col2 = st.columns(2)
|
360 |
-
|
361 |
-
with col1:
|
362 |
-
project_name = st.text_input("اسم المشروع", value=project['name'])
|
363 |
-
client = st.text_input("الجهة المالكة", value=project['client'])
|
364 |
-
location = st.text_input("الموقع", value=project['location'])
|
365 |
-
tender_type = st.selectbox(
|
366 |
-
"نوع المناقصة",
|
367 |
-
["عامة", "خاصة", "أمر مباشر"],
|
368 |
-
index=["عامة", "خاصة", "أمر مباشر"].index(project['tender_type'])
|
369 |
-
)
|
370 |
-
|
371 |
-
with col2:
|
372 |
-
tender_number = st.text_input("رقم المناقصة", value=project['number'])
|
373 |
-
submission_date = st.date_input(
|
374 |
-
"تاريخ التقديم",
|
375 |
-
value=datetime.strptime(project['submission_date'], "%Y-%m-%d") if isinstance(project['submission_date'], str) else project['submission_date']
|
376 |
-
)
|
377 |
-
pricing_method = st.selectbox(
|
378 |
-
"طريقة التسعير",
|
379 |
-
["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
|
380 |
-
index=["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"].index(project['pricing_method'])
|
381 |
-
)
|
382 |
-
status = st.selectbox(
|
383 |
-
"حالة المشروع",
|
384 |
-
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
385 |
-
index=["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"].index(project['status'])
|
386 |
-
)
|
387 |
-
|
388 |
-
description = st.text_area("وصف المشروع", value=project.get('description', ''))
|
389 |
-
|
390 |
-
col1, col2 = st.columns(2)
|
391 |
-
|
392 |
-
with col1:
|
393 |
-
submit = st.form_submit_button("حفظ التعديلات")
|
394 |
-
|
395 |
-
with col2:
|
396 |
-
cancel = st.form_submit_button("إلغاء")
|
397 |
-
|
398 |
-
if submit:
|
399 |
-
# تحديث بيانات المشروع
|
400 |
-
project['name'] = project_name
|
401 |
-
project['number'] = tender_number
|
402 |
-
project['client'] = client
|
403 |
-
project['location'] = location
|
404 |
-
project['description'] = description
|
405 |
-
project['status'] = status
|
406 |
-
project['tender_type'] = tender_type
|
407 |
-
project['pricing_method'] = pricing_method
|
408 |
-
project['submission_date'] = submission_date
|
409 |
-
|
410 |
-
st.success("تم تحديث بيانات المشروع بنجاح!")
|
411 |
-
st.session_state.edit_project = False
|
412 |
-
st.experimental_rerun()
|
413 |
-
|
414 |
-
elif cancel:
|
415 |
-
st.session_state.edit_project = False
|
416 |
-
st.experimental_rerun()
|
417 |
-
|
418 |
-
def _render_projects_tracking_tab(self):
|
419 |
-
"""عرض تبويب متابعة المشاريع"""
|
420 |
-
|
421 |
-
st.markdown("### متابعة المشاريع")
|
422 |
-
|
423 |
-
# عرض إحصائيات المشاريع
|
424 |
-
col1, col2, col3, col4 = st.columns(4)
|
425 |
-
|
426 |
-
projects = st.session_state.projects
|
427 |
-
|
428 |
-
with col1:
|
429 |
-
total_projects = len(projects)
|
430 |
-
self.ui.create_metric_card("إجمالي المشاريع", str(total_projects), None, self.ui.COLORS['primary'])
|
431 |
-
|
432 |
-
with col2:
|
433 |
-
active_projects = len([p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]])
|
434 |
-
self.ui.create_metric_card("المشاريع النشطة", str(active_projects), None, self.ui.COLORS['success'])
|
435 |
-
|
436 |
-
with col3:
|
437 |
-
pending_submission = len([p for p in projects if p['status'] in ["جديد", "قيد التسعير"]])
|
438 |
-
self.ui.create_metric_card("مشاريع قيد التسعير", str(pending_submission), None, self.ui.COLORS['warning'])
|
439 |
-
|
440 |
-
with col4:
|
441 |
-
completed_projects = len([p for p in projects if p['status'] in ["منتهي"]])
|
442 |
-
self.ui.create_metric_card("المشاريع المنتهية", str(completed_projects), None, self.ui.COLORS['info'])
|
443 |
-
|
444 |
-
# عرض رسم بياني لحالة المشاريع
|
445 |
-
st.markdown("#### توزيع المشاريع حسب الحالة")
|
446 |
-
|
447 |
-
status_counts = {}
|
448 |
-
for p in projects:
|
449 |
-
status = p['status']
|
450 |
-
status_counts[status] = status_counts.get(status, 0) + 1
|
451 |
-
|
452 |
-
status_df = pd.DataFrame({
|
453 |
-
'الحالة': list(status_counts.keys()),
|
454 |
-
'عدد المشاريع': list(status_counts.values())
|
455 |
-
})
|
456 |
-
|
457 |
-
st.bar_chart(status_df.set_index('الحالة'))
|
458 |
-
|
459 |
-
# عرض المشاريع قيد المتابعة
|
460 |
-
st.markdown("#### المشاريع قيد المتابعة")
|
461 |
-
|
462 |
-
# عرض المشاريع النشطة المرتبة حسب تاريخ التقديم
|
463 |
-
active_projects_list = [p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]]
|
464 |
-
|
465 |
-
if active_projects_list:
|
466 |
-
# تحويل التواريخ إلى كائنات تاريخ إذا كانت نصوصًا
|
467 |
-
for p in active_projects_list:
|
468 |
-
if isinstance(p['submission_date'], str):
|
469 |
-
p['submission_date'] = datetime.strptime(p['submission_date'], "%Y-%m-%d")
|
470 |
-
|
471 |
-
# ترتيب المشاريع حسب تاريخ التقديم
|
472 |
-
active_projects_list.sort(key=lambda x: x['submission_date'])
|
473 |
-
|
474 |
-
# تحويل إلى DataFrame
|
475 |
-
active_df = pd.DataFrame(active_projects_list)
|
476 |
-
|
477 |
-
# اختيار وترتيب الأعمدة
|
478 |
-
display_columns = [
|
479 |
-
'name', 'number', 'client', 'status',
|
480 |
-
'submission_date', 'tender_type'
|
481 |
-
]
|
482 |
-
|
483 |
-
# تغيير أسماء الأعمدة
|
484 |
-
column_names = {
|
485 |
-
'name': 'اسم المشروع',
|
486 |
-
'number': 'رقم المناقصة',
|
487 |
-
'client': 'الجهة المالكة',
|
488 |
-
'status': 'الحالة',
|
489 |
-
'submission_date': 'تاريخ التقديم',
|
490 |
-
'tender_type': 'نوع المناقصة'
|
491 |
-
}
|
492 |
-
|
493 |
-
# تنسيق البيانات
|
494 |
-
display_df = active_df[display_columns].rename(columns=column_names)
|
495 |
-
display_df['تاريخ التقديم'] = pd.to_datetime(display_df['تاريخ التقديم']).dt.strftime('%Y-%m-%d')
|
496 |
-
|
497 |
-
# عرض الجدول
|
498 |
-
st.dataframe(display_df, use_container_width=True, hide_index=True)
|
499 |
-
else:
|
500 |
-
st.info("لا توجد مشاريع نشطة حاليًا.")
|
501 |
-
|
502 |
-
# عرض المشاريع المقبلة
|
503 |
-
st.markdown("#### المواعيد المقبلة")
|
504 |
-
|
505 |
-
upcoming_events = []
|
506 |
-
today = datetime.now().date()
|
507 |
-
|
508 |
-
for p in projects:
|
509 |
-
submission_date = p['submission_date']
|
510 |
-
if isinstance(submission_date, str):
|
511 |
-
submission_date = datetime.strptime(submission_date, "%Y-%m-%d").date()
|
512 |
-
elif isinstance(submission_date, datetime):
|
513 |
-
submission_date = submission_date.date()
|
514 |
-
|
515 |
-
# المشاريع التي موعد تقديمها خلال الأسبوعين القادمين
|
516 |
-
if today <= submission_date <= today + timedelta(days=14) and p['status'] in ["قيد التسعير"]:
|
517 |
-
days_left = (submission_date - today).days
|
518 |
-
upcoming_events.append({
|
519 |
-
'المشروع': p['name'],
|
520 |
-
'الحدث': 'موعد تقديم المناقصة',
|
521 |
-
'التاريخ': submission_date.strftime('%Y-%m-%d'),
|
522 |
-
'الأيام المتبقية': days_left
|
523 |
-
})
|
524 |
-
|
525 |
-
if upcoming_events:
|
526 |
-
events_df = pd.DataFrame(upcoming_events)
|
527 |
-
st.dataframe(events_df, use_container_width=True, hide_index=True)
|
528 |
-
else:
|
529 |
-
st.info("لا توجد مواعيد قريبة.")
|
530 |
-
|
531 |
-
def _generate_sample_projects(self):
|
532 |
-
"""توليد بيانات افتراضية للمشاريع"""
|
533 |
-
|
534 |
-
projects = [
|
535 |
-
{
|
536 |
-
'id': 1,
|
537 |
-
'name': "إنشاء مبنى مستشفى الولادة والأطفال بمنطقة الشرقية",
|
538 |
-
'number': "SHPD-2025-001",
|
539 |
-
'client': "وزارة الصحة",
|
540 |
-
'location': "الدمام، المنطقة الشرقية",
|
541 |
-
'description': "يشمل المشروع إنشاء وتجهيز مبنى مستشفى الولادة والأطفال بسعة 300 سرير، ويتكون المبنى من 4 طوابق بمساحة إجمالية 15,000 متر مربع.",
|
542 |
-
'status': "قيد التسعير",
|
543 |
-
'tender_type': "عامة",
|
544 |
-
'pricing_method': "قياسي",
|
545 |
-
'submission_date': (datetime.now() + timedelta(days=5)),
|
546 |
-
'created_at': datetime.now() - timedelta(days=10),
|
547 |
-
'created_by_id': 1,
|
548 |
-
'documents': [
|
549 |
-
{
|
550 |
-
'filename': "كراسة الشروط والمواصفات.pdf",
|
551 |
-
'type': "كراسة شروط",
|
552 |
-
'upload_date': (datetime.now() - timedelta(days=9)).strftime('%Y-%m-%d'),
|
553 |
-
'size': "5.2 MB"
|
554 |
-
},
|
555 |
-
{
|
556 |
-
'filename': "المخططات الهندسية.dwg",
|
557 |
-
'type': "مخططات",
|
558 |
-
'upload_date': (datetime.now() - timedelta(days=8)).strftime('%Y-%m-%d'),
|
559 |
-
'size': "25.7 MB"
|
560 |
-
},
|
561 |
-
{
|
562 |
-
'filename': "جدول الكميات.xlsx",
|
563 |
-
'type': "جدول كميات",
|
564 |
-
'upload_date': (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'),
|
565 |
-
'size': "1.8 MB"
|
566 |
-
}
|
567 |
-
],
|
568 |
-
'items': [
|
569 |
-
{
|
570 |
-
'رقم البند': "A1",
|
571 |
-
'وصف البند': "أعمال الحفر والردم",
|
572 |
-
'الوحدة': "م3",
|
573 |
-
'الكمية': 12500
|
574 |
-
},
|
575 |
-
{
|
576 |
-
'رقم البند': "A2",
|
577 |
-
'وصف البند': "أعمال الخرسانة المسلحة للأساسات",
|
578 |
-
'الوحدة': "م3",
|
579 |
-
'الكمية': 3500
|
580 |
-
},
|
581 |
-
{
|
582 |
-
'رقم البند': "A3",
|
583 |
-
'وصف البند': "أعمال حديد التسليح",
|
584 |
-
'الوحدة': "طن",
|
585 |
-
'الكمية': 450
|
586 |
-
}
|
587 |
-
]
|
588 |
-
},
|
589 |
-
{
|
590 |
-
'id': 2,
|
591 |
-
'name': "صيانة وتطوير طريق الملك عبدالله",
|
592 |
-
'number': "MOT-2025-042",
|
593 |
-
'client': "وزارة النقل",
|
594 |
-
'location': "الرياض، المنطقة الوسطى",
|
595 |
-
'description': "صيانة وتطوير طريق الملك عبدالله بطول 25 كم، ويشمل المشروع إعادة الرصف وتحسين الإنارة وتركيب اللوحات الإرشادية.",
|
596 |
-
'status': "تم التقديم",
|
597 |
-
'tender_type': "عامة",
|
598 |
-
'pricing_method': "غير متزن",
|
599 |
-
'submission_date': (datetime.now() - timedelta(days=15)),
|
600 |
-
'created_at': datetime.now() - timedelta(days=45),
|
601 |
-
'created_by_id': 1
|
602 |
-
},
|
603 |
-
{
|
604 |
-
'id': 3,
|
605 |
-
'name': "إنشاء محطة معالجة مياه الصرف الصحي",
|
606 |
-
'number': "SWPC-2025-007",
|
607 |
-
'client': "شركة المياه الوطنية",
|
608 |
-
'location': "جدة، المنطقة الغربية",
|
609 |
-
'description': "إنشاء محطة معالجة مياه الصرف الصحي بطاقة استيعابية 50,000 م3/يوم، مع جميع الأعمال المدنية والكهروميكانيكية.",
|
610 |
-
'status': "تمت الترسية",
|
611 |
-
'tender_type': "عامة",
|
612 |
-
'pricing_method': "قياسي",
|
613 |
-
'submission_date': (datetime.now() - timedelta(days=90)),
|
614 |
-
'created_at': datetime.now() - timedelta(days=120),
|
615 |
-
'created_by_id': 1
|
616 |
-
},
|
617 |
-
{
|
618 |
-
'id': 4,
|
619 |
-
'name': "إنشاء منتزه الملك سلمان",
|
620 |
-
'number': "RAM-2025-015",
|
621 |
-
'client': "أمانة منطقة الرياض",
|
622 |
-
'location': "الرياض، المنطقة الوسطى",
|
623 |
-
'description': "إنشاء منتزه الملك سلمان على مساحة 500,000 متر مربع، ويشمل المشروع أعمال التشجير والتنسيق والمسطحات المائية والمباني الخدمية.",
|
624 |
-
'status': "قيد التنفيذ",
|
625 |
-
'tender_type': "عامة",
|
626 |
-
'pricing_method': "قياسي",
|
627 |
-
'submission_date': (datetime.now() - timedelta(days=180)),
|
628 |
-
'created_at': datetime.now() - timedelta(days=210),
|
629 |
-
'created_by_id': 1
|
630 |
-
},
|
631 |
-
{
|
632 |
-
'id': 5,
|
633 |
-
'name': "إنشاء مبنى مختبرات كلية العلوم",
|
634 |
-
'number': "KSU-2025-032",
|
635 |
-
'client': "جامعة الملك سعود",
|
636 |
-
'location': "الرياض، المنطقة الوسطى",
|
637 |
-
'description': "إنشاء مبنى المختبرات الجديد لكلية العلوم بمساحة 8,000 متر مربع، ويتكون من 3 طوابق ويشمل تجهيز المعامل والمختبرات العلمية.",
|
638 |
-
'status': "جديد",
|
639 |
-
'tender_type': "خاصة",
|
640 |
-
'pricing_method': "تنافسي",
|
641 |
-
'submission_date': (datetime.now() + timedelta(days=10)),
|
642 |
-
'created_at': datetime.now() - timedelta(days=5),
|
643 |
-
'created_by_id': 1
|
644 |
-
},
|
645 |
-
{
|
646 |
-
'id': 6,
|
647 |
-
'name': "توريد وتركيب أنظمة الطاقة الشمسية",
|
648 |
-
'number': "SEC-2025-098",
|
649 |
-
'client': "الشركة السعودية للكهرباء",
|
650 |
-
'location': "تبوك، المنطقة الشمالية",
|
651 |
-
'description': "توريد وتركيب أنظمة الطاقة الشمسية بقدرة 5 ميجاوات، مع جميع الأعمال المدنية والكهربائية.",
|
652 |
-
'status': "جديد",
|
653 |
-
'tender_type': "عامة",
|
654 |
-
'pricing_method': "قياسي",
|
655 |
-
'submission_date': (datetime.now() + timedelta(days=20)),
|
656 |
-
'created_at': datetime.now() - timedelta(days=2),
|
657 |
-
'created_by_id': 1
|
658 |
-
}
|
659 |
-
]
|
660 |
-
|
661 |
-
return projects
|
662 |
-
|
663 |
-
# تشغيل التطبيق
|
664 |
-
if __name__ == "__main__":
|
665 |
-
projects_app = ProjectsApp()
|
666 |
-
projects_app.run()
|
|
|
1 |
+
"""
|
2 |
+
وحدة إدارة المشاريع - نظام تحليل المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
from datetime import datetime, timedelta
|
9 |
+
import os
|
10 |
+
import time
|
11 |
+
import io
|
12 |
+
import sys
|
13 |
+
from pathlib import Path
|
14 |
+
|
15 |
+
# إضافة مسار المشروع للنظام
|
16 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
17 |
+
|
18 |
+
# استيراد محسن واجهة المستخدم
|
19 |
+
from styling.enhanced_ui import UIEnhancer
|
20 |
+
|
21 |
+
class ProjectsApp:
|
22 |
+
"""وحدة إدارة المشاريع"""
|
23 |
+
|
24 |
+
def __init__(self):
|
25 |
+
"""تهيئة وحدة إدارة المشاريع"""
|
26 |
+
self.ui = UIEnhancer(page_title="إدارة المشاريع - نظام تحليل المناقصات", page_icon="📋")
|
27 |
+
self.ui.apply_theme_colors()
|
28 |
+
|
29 |
+
# تهيئة البيانات المبدئية
|
30 |
+
if 'projects' not in st.session_state:
|
31 |
+
st.session_state.projects = self._generate_sample_projects()
|
32 |
+
|
33 |
+
def run(self):
|
34 |
+
"""تشغيل وحدة إدارة المشاريع"""
|
35 |
+
# إنشاء قائمة العناصر
|
36 |
+
menu_items = [
|
37 |
+
{"name": "لوحة المعلومات", "icon": "house"},
|
38 |
+
{"name": "المناقصات والعقود", "icon": "file-text"},
|
39 |
+
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
40 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
41 |
+
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
42 |
+
{"name": "الموارد والتكاليف", "icon": "people"},
|
43 |
+
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
44 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
45 |
+
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
46 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
47 |
+
{"name": "الإشعارات", "icon": "bell"},
|
48 |
+
{"name": "مقارنة المستندات", "icon": "files"},
|
49 |
+
{"name": "الترجمة", "icon": "translate"},
|
50 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
51 |
+
{"name": "التقارير", "icon": "bar-chart"},
|
52 |
+
{"name": "الإعدادات", "icon": "gear"}
|
53 |
+
]
|
54 |
+
|
55 |
+
# إنشاء الشريط الجانبي
|
56 |
+
selected = self.ui.create_sidebar(menu_items)
|
57 |
+
|
58 |
+
# إنشاء ترويسة الصفحة
|
59 |
+
self.ui.create_header("إدارة المشاريع", "إدارة ومتابعة المشاريع والمناقصات")
|
60 |
+
|
61 |
+
# عرض واجهة وحدة إدارة المشاريع
|
62 |
+
tabs = st.tabs([
|
63 |
+
"قائمة المشاريع",
|
64 |
+
"إضافة مشروع جديد",
|
65 |
+
"تفاصيل المشروع",
|
66 |
+
"متابعة المشاريع"
|
67 |
+
])
|
68 |
+
|
69 |
+
with tabs[0]:
|
70 |
+
self._render_projects_list_tab()
|
71 |
+
|
72 |
+
with tabs[1]:
|
73 |
+
self._render_add_project_tab()
|
74 |
+
|
75 |
+
with tabs[2]:
|
76 |
+
self._render_project_details_tab()
|
77 |
+
|
78 |
+
with tabs[3]:
|
79 |
+
self._render_projects_tracking_tab()
|
80 |
+
|
81 |
+
def _render_projects_list_tab(self):
|
82 |
+
"""عرض تبويب قائمة المشاريع"""
|
83 |
+
|
84 |
+
st.markdown("### قائمة المشاريع")
|
85 |
+
|
86 |
+
# فلترة المشاريع
|
87 |
+
col1, col2, col3 = st.columns(3)
|
88 |
+
|
89 |
+
with col1:
|
90 |
+
search_term = st.text_input("البحث في المشاريع", key="project_search")
|
91 |
+
|
92 |
+
with col2:
|
93 |
+
status_filter = st.multiselect(
|
94 |
+
"حالة المشروع",
|
95 |
+
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
96 |
+
default=["جديد", "قيد التسعير", "تم التقديم"],
|
97 |
+
key="project_status_filter"
|
98 |
+
)
|
99 |
+
|
100 |
+
with col3:
|
101 |
+
client_filter = st.multiselect(
|
102 |
+
"الجهة المالكة",
|
103 |
+
list(set([p['client'] for p in st.session_state.projects])),
|
104 |
+
key="project_client_filter"
|
105 |
+
)
|
106 |
+
|
107 |
+
# تطبيق الفلترة
|
108 |
+
filtered_projects = st.session_state.projects
|
109 |
+
|
110 |
+
if search_term:
|
111 |
+
filtered_projects = [p for p in filtered_projects if search_term.lower() in p['name'].lower() or search_term in p['number']]
|
112 |
+
|
113 |
+
if status_filter:
|
114 |
+
filtered_projects = [p for p in filtered_projects if p['status'] in status_filter]
|
115 |
+
|
116 |
+
if client_filter:
|
117 |
+
filtered_projects = [p for p in filtered_projects if p['client'] in client_filter]
|
118 |
+
|
119 |
+
# تحويل المشاريع المفلترة إلى DataFrame للعرض
|
120 |
+
if filtered_projects:
|
121 |
+
projects_df = pd.DataFrame(filtered_projects)
|
122 |
+
|
123 |
+
# اختيار وترتيب الأعمدة
|
124 |
+
display_columns = [
|
125 |
+
'name', 'number', 'client', 'location', 'status',
|
126 |
+
'submission_date', 'tender_type', 'created_at'
|
127 |
+
]
|
128 |
+
|
129 |
+
# تغيير أسماء الأعمدة للعرض
|
130 |
+
column_names = {
|
131 |
+
'name': 'اسم المشروع',
|
132 |
+
'number': 'رقم المناقصة',
|
133 |
+
'client': 'الجهة المالكة',
|
134 |
+
'location': 'الموقع',
|
135 |
+
'status': 'الحالة',
|
136 |
+
'submission_date': 'تاريخ التقديم',
|
137 |
+
'tender_type': 'نوع المناقصة',
|
138 |
+
'created_at': 'تاريخ الإنشاء'
|
139 |
+
}
|
140 |
+
|
141 |
+
display_df = projects_df[display_columns].rename(columns=column_names)
|
142 |
+
|
143 |
+
# تنسيق التواريخ
|
144 |
+
date_columns = ['تاريخ التقديم', 'تاريخ الإنشاء']
|
145 |
+
for col in date_columns:
|
146 |
+
if col in display_df.columns:
|
147 |
+
display_df[col] = pd.to_datetime(display_df[col]).dt.strftime('%Y-%m-%d')
|
148 |
+
|
149 |
+
# عرض الجدول
|
150 |
+
st.dataframe(display_df, use_container_width=True, hide_index=True)
|
151 |
+
|
152 |
+
# زر تصدير المشاريع
|
153 |
+
if st.button("تصدير المشاريع إلى Excel"):
|
154 |
+
# محاكاة التصدير
|
155 |
+
st.success("تم تصدير المشاريع بنجاح!")
|
156 |
+
else:
|
157 |
+
st.info("لا توجد مشاريع تطابق معايير البحث.")
|
158 |
+
|
159 |
+
def _render_add_project_tab(self):
|
160 |
+
"""عرض تبويب إضافة مشروع جديد"""
|
161 |
+
|
162 |
+
st.markdown("### إضافة مشروع جديد")
|
163 |
+
|
164 |
+
# نموذج إدخال بيانات المشروع
|
165 |
+
with st.form("new_project_form"):
|
166 |
+
col1, col2 = st.columns(2)
|
167 |
+
|
168 |
+
with col1:
|
169 |
+
project_name = st.text_input("اسم المشروع", key="new_project_name")
|
170 |
+
client = st.text_input("الجهة المالكة", key="new_project_client")
|
171 |
+
location = st.text_input("الموقع", key="new_project_location")
|
172 |
+
tender_type = st.selectbox(
|
173 |
+
"نوع المناقصة",
|
174 |
+
["عامة", "خاصة", "أمر مباشر"],
|
175 |
+
key="new_project_tender_type"
|
176 |
+
)
|
177 |
+
|
178 |
+
with col2:
|
179 |
+
tender_number = st.text_input("رقم المناقصة", key="new_project_number")
|
180 |
+
submission_date = st.date_input("تاريخ التقديم", key="new_project_submission_date")
|
181 |
+
pricing_method = st.selectbox(
|
182 |
+
"طريقة التسعير",
|
183 |
+
["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
|
184 |
+
key="new_project_pricing_method"
|
185 |
+
)
|
186 |
+
status = st.selectbox(
|
187 |
+
"حالة المشروع",
|
188 |
+
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
189 |
+
index=0,
|
190 |
+
key="new_project_status"
|
191 |
+
)
|
192 |
+
|
193 |
+
description = st.text_area("وصف المشروع", key="new_project_description")
|
194 |
+
|
195 |
+
submitted = st.form_submit_button("إضافة المشروع")
|
196 |
+
|
197 |
+
if submitted:
|
198 |
+
# التحقق من تعبئة الحقول الإلزامية
|
199 |
+
if not project_name or not tender_number or not client:
|
200 |
+
st.error("يرجى تعبئة جميع الحقول الإلزامية (اسم المشروع، رقم المناقصة، الجهة المالكة).")
|
201 |
+
else:
|
202 |
+
# إنشاء مشروع جديد
|
203 |
+
new_project = {
|
204 |
+
'id': len(st.session_state.projects) + 1,
|
205 |
+
'name': project_name,
|
206 |
+
'number': tender_number,
|
207 |
+
'client': client,
|
208 |
+
'location': location,
|
209 |
+
'description': description,
|
210 |
+
'status': status,
|
211 |
+
'tender_type': tender_type,
|
212 |
+
'pricing_method': pricing_method,
|
213 |
+
'submission_date': submission_date,
|
214 |
+
'created_at': datetime.now(),
|
215 |
+
'created_by_id': 1 # معرف المستخدم الحالي
|
216 |
+
}
|
217 |
+
|
218 |
+
# إضافة المشروع إلى قائمة المشاريع
|
219 |
+
st.session_state.projects.append(new_project)
|
220 |
+
|
221 |
+
# رسالة نجاح
|
222 |
+
st.success(f"تم إضافة المشروع [{project_name}] بنجاح!")
|
223 |
+
|
224 |
+
# تعيين المشروع الحالي
|
225 |
+
st.session_state.current_project = new_project
|
226 |
+
|
227 |
+
def _render_project_details_tab(self):
|
228 |
+
"""عرض تبويب تفاصيل المشروع"""
|
229 |
+
|
230 |
+
st.markdown("### تفاصيل المشروع")
|
231 |
+
|
232 |
+
# التحقق من وجود مشروع حالي
|
233 |
+
if 'current_project' not in st.session_state or st.session_state.current_project is None:
|
234 |
+
# إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع
|
235 |
+
project_names = [p['name'] for p in st.session_state.projects]
|
236 |
+
selected_project_name = st.selectbox("اختر المشروع", project_names)
|
237 |
+
|
238 |
+
if selected_project_name:
|
239 |
+
selected_project = next((p for p in st.session_state.projects if p['name'] == selected_project_name), None)
|
240 |
+
if selected_project:
|
241 |
+
st.session_state.current_project = selected_project
|
242 |
+
else:
|
243 |
+
st.warning("لم يتم العثور على المشروع المحدد.")
|
244 |
+
return
|
245 |
+
else:
|
246 |
+
st.info("يرجى اختيار مشروع لعرض تفاصيله.")
|
247 |
+
return
|
248 |
+
|
249 |
+
# عرض تفاصيل المشروع
|
250 |
+
project = st.session_state.current_project
|
251 |
+
|
252 |
+
# عرض معلومات المشروع الأساسية
|
253 |
+
col1, col2, col3 = st.columns(3)
|
254 |
+
|
255 |
+
with col1:
|
256 |
+
st.markdown(f"**اسم المشروع**: {project.get('name', 'غير محدد')}")
|
257 |
+
st.markdown(f"**رقم المناقصة**: {project.get('tender_number', project.get('number', 'غير محدد'))}")
|
258 |
+
st.markdown(f"**الجهة المالكة**: {project.get('client', 'غير محدد')}")
|
259 |
+
|
260 |
+
with col2:
|
261 |
+
st.markdown(f"**الموقع**: {project['location']}")
|
262 |
+
st.markdown(f"**نوع المناقصة**: {project['tender_type']}")
|
263 |
+
st.markdown(f"**حالة المشروع**: {project['status']}")
|
264 |
+
|
265 |
+
with col3:
|
266 |
+
st.markdown(f"**طريقة التسعير**: {project['pricing_method']}")
|
267 |
+
st.markdown(f"**تاريخ التقديم**: {project['submission_date'].strftime('%Y-%m-%d') if isinstance(project['submission_date'], datetime) else project['submission_date']}")
|
268 |
+
st.markdown(f"**تاريخ الإنشاء**: {project['created_at'].strftime('%Y-%m-%d') if isinstance(project['created_at'], datetime) else project['created_at']}")
|
269 |
+
|
270 |
+
# عرض وصف المشروع
|
271 |
+
st.markdown("#### وصف المشروع")
|
272 |
+
st.text_area("", value=project.get('description', ''), disabled=True, height=100)
|
273 |
+
|
274 |
+
# عرض المستندات المرتبطة بالمشروع
|
275 |
+
st.markdown("#### مستندات المشروع")
|
276 |
+
|
277 |
+
if 'documents' in project and project['documents']:
|
278 |
+
docs_df = pd.DataFrame(project['documents'])
|
279 |
+
st.dataframe(docs_df, use_container_width=True, hide_index=True)
|
280 |
+
else:
|
281 |
+
st.info("لا توجد مستندات مرتبطة بهذا المشروع حاليًا.")
|
282 |
+
|
283 |
+
# زر إضافة مستندات
|
284 |
+
if st.button("إضافة مستندات"):
|
285 |
+
st.session_state.upload_documents = True
|
286 |
+
|
287 |
+
# واجهة تحميل المستندات
|
288 |
+
if 'upload_documents' in st.session_state and st.session_state.upload_documents:
|
289 |
+
st.markdown("#### تحميل مستندات جديدة")
|
290 |
+
|
291 |
+
uploaded_file = st.file_uploader("اختر ملفًا", type=['pdf', 'docx', 'xlsx', 'png', 'jpg', 'dwg'])
|
292 |
+
doc_type = st.selectbox("نوع المستند", ["كراسة شروط", "عقد", "مخططات", "جدول كميات", "مواصفات فنية", "تعديلات وملاحق"])
|
293 |
+
|
294 |
+
if uploaded_file and st.button("تحميل المستند"):
|
295 |
+
# محاكاة تحميل المستند
|
296 |
+
with st.spinner("جاري تحميل المستند..."):
|
297 |
+
time.sleep(2)
|
298 |
+
|
299 |
+
# إنشاء مستند جديد
|
300 |
+
new_document = {
|
301 |
+
'filename': uploaded_file.name,
|
302 |
+
'type': doc_type,
|
303 |
+
'upload_date': datetime.now().strftime('%Y-%m-%d'),
|
304 |
+
'size': f"{uploaded_file.size / 1024:.1f} KB"
|
305 |
+
}
|
306 |
+
|
307 |
+
# إضافة المستند إلى المشروع
|
308 |
+
if 'documents' not in project:
|
309 |
+
project['documents'] = []
|
310 |
+
|
311 |
+
project['documents'].append(new_document)
|
312 |
+
|
313 |
+
st.success(f"تم تحميل المستند [{uploaded_file.name}] بنجاح!")
|
314 |
+
st.session_state.upload_documents = False
|
315 |
+
st.experimental_rerun()
|
316 |
+
|
317 |
+
# عرض البنود والكميات
|
318 |
+
st.markdown("#### بنود وكميات المشروع")
|
319 |
+
|
320 |
+
if 'items' in project and project['items']:
|
321 |
+
items_df = pd.DataFrame(project['items'])
|
322 |
+
st.dataframe(items_df, use_container_width=True, hide_index=True)
|
323 |
+
|
324 |
+
# زر لتحويل البنود إلى وحدة التسعير
|
325 |
+
if st.button("تحويل البنود إلى وحدة التسعير"):
|
326 |
+
if 'manual_items' not in st.session_state:
|
327 |
+
st.session_state.manual_items = pd.DataFrame()
|
328 |
+
|
329 |
+
st.session_state.manual_items = items_df.copy()
|
330 |
+
st.success("تم تحويل البنود إلى وحدة التسعير بنجاح!")
|
331 |
+
else:
|
332 |
+
st.info("لا توجد بنود وكميات لهذا المشروع حاليًا.")
|
333 |
+
|
334 |
+
# زر استيراد البنود من وحدة تحليل المستندات
|
335 |
+
if st.button("استيراد البنود من تحليل المستندات"):
|
336 |
+
st.warning("ميزة استيراد البنود من تحليل المستندات قيد التطوير.")
|
337 |
+
|
338 |
+
# أزرار الإجراءات
|
339 |
+
col1, col2, col3 = st.columns(3)
|
340 |
+
|
341 |
+
with col1:
|
342 |
+
if st.button("تعديل المشروع"):
|
343 |
+
st.session_state.edit_project = True
|
344 |
+
st.experimental_rerun()
|
345 |
+
|
346 |
+
with col2:
|
347 |
+
if st.button("تصدير بيانات المشروع"):
|
348 |
+
st.success("تم تصدير بيانات المشروع بنجاح!")
|
349 |
+
|
350 |
+
with col3:
|
351 |
+
if st.button("إرسال للاعتماد"):
|
352 |
+
st.success("تم إرسال المشروع للاعتماد بنجاح!")
|
353 |
+
|
354 |
+
# نموذج تعديل المشروع
|
355 |
+
if 'edit_project' in st.session_state and st.session_state.edit_project:
|
356 |
+
st.markdown("#### تعديل المشروع")
|
357 |
+
|
358 |
+
with st.form("edit_project_form"):
|
359 |
+
col1, col2 = st.columns(2)
|
360 |
+
|
361 |
+
with col1:
|
362 |
+
project_name = st.text_input("اسم المشروع", value=project['name'])
|
363 |
+
client = st.text_input("الجهة المالكة", value=project['client'])
|
364 |
+
location = st.text_input("الموقع", value=project['location'])
|
365 |
+
tender_type = st.selectbox(
|
366 |
+
"نوع المناقصة",
|
367 |
+
["عامة", "خاصة", "أمر مباشر"],
|
368 |
+
index=["عامة", "خاصة", "أمر مباشر"].index(project['tender_type'])
|
369 |
+
)
|
370 |
+
|
371 |
+
with col2:
|
372 |
+
tender_number = st.text_input("رقم المناقصة", value=project['number'])
|
373 |
+
submission_date = st.date_input(
|
374 |
+
"تاريخ التقديم",
|
375 |
+
value=datetime.strptime(project['submission_date'], "%Y-%m-%d") if isinstance(project['submission_date'], str) else project['submission_date']
|
376 |
+
)
|
377 |
+
pricing_method = st.selectbox(
|
378 |
+
"طريقة التسعير",
|
379 |
+
["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
|
380 |
+
index=["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"].index(project['pricing_method'])
|
381 |
+
)
|
382 |
+
status = st.selectbox(
|
383 |
+
"حالة المشروع",
|
384 |
+
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
385 |
+
index=["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"].index(project['status'])
|
386 |
+
)
|
387 |
+
|
388 |
+
description = st.text_area("وصف المشروع", value=project.get('description', ''))
|
389 |
+
|
390 |
+
col1, col2 = st.columns(2)
|
391 |
+
|
392 |
+
with col1:
|
393 |
+
submit = st.form_submit_button("حفظ التعديلات")
|
394 |
+
|
395 |
+
with col2:
|
396 |
+
cancel = st.form_submit_button("إلغاء")
|
397 |
+
|
398 |
+
if submit:
|
399 |
+
# تحديث بيانات المشروع
|
400 |
+
project['name'] = project_name
|
401 |
+
project['number'] = tender_number
|
402 |
+
project['client'] = client
|
403 |
+
project['location'] = location
|
404 |
+
project['description'] = description
|
405 |
+
project['status'] = status
|
406 |
+
project['tender_type'] = tender_type
|
407 |
+
project['pricing_method'] = pricing_method
|
408 |
+
project['submission_date'] = submission_date
|
409 |
+
|
410 |
+
st.success("تم تحديث بيانات المشروع بنجاح!")
|
411 |
+
st.session_state.edit_project = False
|
412 |
+
st.experimental_rerun()
|
413 |
+
|
414 |
+
elif cancel:
|
415 |
+
st.session_state.edit_project = False
|
416 |
+
st.experimental_rerun()
|
417 |
+
|
418 |
+
def _render_projects_tracking_tab(self):
|
419 |
+
"""عرض تبويب متابعة المشاريع"""
|
420 |
+
|
421 |
+
st.markdown("### متابعة المشاريع")
|
422 |
+
|
423 |
+
# عرض إحصائيات المشاريع
|
424 |
+
col1, col2, col3, col4 = st.columns(4)
|
425 |
+
|
426 |
+
projects = st.session_state.projects
|
427 |
+
|
428 |
+
with col1:
|
429 |
+
total_projects = len(projects)
|
430 |
+
self.ui.create_metric_card("إجمالي المشاريع", str(total_projects), None, self.ui.COLORS['primary'])
|
431 |
+
|
432 |
+
with col2:
|
433 |
+
active_projects = len([p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]])
|
434 |
+
self.ui.create_metric_card("المشاريع النشطة", str(active_projects), None, self.ui.COLORS['success'])
|
435 |
+
|
436 |
+
with col3:
|
437 |
+
pending_submission = len([p for p in projects if p['status'] in ["جديد", "قيد التسعير"]])
|
438 |
+
self.ui.create_metric_card("مشاريع قيد التسعير", str(pending_submission), None, self.ui.COLORS['warning'])
|
439 |
+
|
440 |
+
with col4:
|
441 |
+
completed_projects = len([p for p in projects if p['status'] in ["منتهي"]])
|
442 |
+
self.ui.create_metric_card("المشاريع المنتهية", str(completed_projects), None, self.ui.COLORS['info'])
|
443 |
+
|
444 |
+
# عرض رسم بياني لحالة المشاريع
|
445 |
+
st.markdown("#### توزيع المشاريع حسب الحالة")
|
446 |
+
|
447 |
+
status_counts = {}
|
448 |
+
for p in projects:
|
449 |
+
status = p['status']
|
450 |
+
status_counts[status] = status_counts.get(status, 0) + 1
|
451 |
+
|
452 |
+
status_df = pd.DataFrame({
|
453 |
+
'الحالة': list(status_counts.keys()),
|
454 |
+
'عدد المشاريع': list(status_counts.values())
|
455 |
+
})
|
456 |
+
|
457 |
+
st.bar_chart(status_df.set_index('الحالة'))
|
458 |
+
|
459 |
+
# عرض المشاريع قيد المتابعة
|
460 |
+
st.markdown("#### المشاريع قيد المتابعة")
|
461 |
+
|
462 |
+
# عرض المشاريع النشطة المرتبة حسب تاريخ التقديم
|
463 |
+
active_projects_list = [p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]]
|
464 |
+
|
465 |
+
if active_projects_list:
|
466 |
+
# تحويل التواريخ إلى كائنات تاريخ إذا كانت نصوصًا
|
467 |
+
for p in active_projects_list:
|
468 |
+
if isinstance(p['submission_date'], str):
|
469 |
+
p['submission_date'] = datetime.strptime(p['submission_date'], "%Y-%m-%d")
|
470 |
+
|
471 |
+
# ترتيب المشاريع حسب تاريخ التقديم
|
472 |
+
active_projects_list.sort(key=lambda x: x['submission_date'])
|
473 |
+
|
474 |
+
# تحويل إلى DataFrame
|
475 |
+
active_df = pd.DataFrame(active_projects_list)
|
476 |
+
|
477 |
+
# اختيار وترتيب الأعمدة
|
478 |
+
display_columns = [
|
479 |
+
'name', 'number', 'client', 'status',
|
480 |
+
'submission_date', 'tender_type'
|
481 |
+
]
|
482 |
+
|
483 |
+
# تغيير أسماء الأعمدة
|
484 |
+
column_names = {
|
485 |
+
'name': 'اسم المشروع',
|
486 |
+
'number': 'رقم المناقصة',
|
487 |
+
'client': 'الجهة المالكة',
|
488 |
+
'status': 'الحالة',
|
489 |
+
'submission_date': 'تاريخ التقديم',
|
490 |
+
'tender_type': 'نوع المناقصة'
|
491 |
+
}
|
492 |
+
|
493 |
+
# تنسيق البيانات
|
494 |
+
display_df = active_df[display_columns].rename(columns=column_names)
|
495 |
+
display_df['تاريخ التقديم'] = pd.to_datetime(display_df['تاريخ التقديم']).dt.strftime('%Y-%m-%d')
|
496 |
+
|
497 |
+
# عرض الجدول
|
498 |
+
st.dataframe(display_df, use_container_width=True, hide_index=True)
|
499 |
+
else:
|
500 |
+
st.info("لا توجد مشاريع نشطة حاليًا.")
|
501 |
+
|
502 |
+
# عرض المشاريع المقبلة
|
503 |
+
st.markdown("#### المواعيد المقبلة")
|
504 |
+
|
505 |
+
upcoming_events = []
|
506 |
+
today = datetime.now().date()
|
507 |
+
|
508 |
+
for p in projects:
|
509 |
+
submission_date = p['submission_date']
|
510 |
+
if isinstance(submission_date, str):
|
511 |
+
submission_date = datetime.strptime(submission_date, "%Y-%m-%d").date()
|
512 |
+
elif isinstance(submission_date, datetime):
|
513 |
+
submission_date = submission_date.date()
|
514 |
+
|
515 |
+
# المشاريع التي موعد تقديمها خلال الأسبوعين القادمين
|
516 |
+
if today <= submission_date <= today + timedelta(days=14) and p['status'] in ["قيد التسعير"]:
|
517 |
+
days_left = (submission_date - today).days
|
518 |
+
upcoming_events.append({
|
519 |
+
'المشروع': p['name'],
|
520 |
+
'الحدث': 'موعد تقديم المناقصة',
|
521 |
+
'التاريخ': submission_date.strftime('%Y-%m-%d'),
|
522 |
+
'الأيام المتبقية': days_left
|
523 |
+
})
|
524 |
+
|
525 |
+
if upcoming_events:
|
526 |
+
events_df = pd.DataFrame(upcoming_events)
|
527 |
+
st.dataframe(events_df, use_container_width=True, hide_index=True)
|
528 |
+
else:
|
529 |
+
st.info("لا توجد مواعيد قريبة.")
|
530 |
+
|
531 |
+
def _generate_sample_projects(self):
|
532 |
+
"""توليد بيانات افتراضية للمشاريع"""
|
533 |
+
|
534 |
+
projects = [
|
535 |
+
{
|
536 |
+
'id': 1,
|
537 |
+
'name': "إنشاء مبنى مستشفى الولادة والأطفال بمنطقة الشرقية",
|
538 |
+
'number': "SHPD-2025-001",
|
539 |
+
'client': "وزارة الصحة",
|
540 |
+
'location': "الدمام، المنطقة الشرقية",
|
541 |
+
'description': "يشمل المشروع إنشاء وتجهيز مبنى مستشفى الولادة والأطفال بسعة 300 سرير، ويتكون المبنى من 4 طوابق بمساحة إجمالية 15,000 متر مربع.",
|
542 |
+
'status': "قيد التسعير",
|
543 |
+
'tender_type': "عامة",
|
544 |
+
'pricing_method': "قياسي",
|
545 |
+
'submission_date': (datetime.now() + timedelta(days=5)),
|
546 |
+
'created_at': datetime.now() - timedelta(days=10),
|
547 |
+
'created_by_id': 1,
|
548 |
+
'documents': [
|
549 |
+
{
|
550 |
+
'filename': "كراسة الشروط والمواصفات.pdf",
|
551 |
+
'type': "كراسة شروط",
|
552 |
+
'upload_date': (datetime.now() - timedelta(days=9)).strftime('%Y-%m-%d'),
|
553 |
+
'size': "5.2 MB"
|
554 |
+
},
|
555 |
+
{
|
556 |
+
'filename': "المخططات الهندسية.dwg",
|
557 |
+
'type': "مخططات",
|
558 |
+
'upload_date': (datetime.now() - timedelta(days=8)).strftime('%Y-%m-%d'),
|
559 |
+
'size': "25.7 MB"
|
560 |
+
},
|
561 |
+
{
|
562 |
+
'filename': "جدول الكميات.xlsx",
|
563 |
+
'type': "جدول كميات",
|
564 |
+
'upload_date': (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'),
|
565 |
+
'size': "1.8 MB"
|
566 |
+
}
|
567 |
+
],
|
568 |
+
'items': [
|
569 |
+
{
|
570 |
+
'رقم البند': "A1",
|
571 |
+
'وصف البند': "أعمال الحفر والردم",
|
572 |
+
'الوحدة': "م3",
|
573 |
+
'الكمية': 12500
|
574 |
+
},
|
575 |
+
{
|
576 |
+
'رقم البند': "A2",
|
577 |
+
'وصف البند': "أعمال الخرسانة المسلحة للأساسات",
|
578 |
+
'الوحدة': "م3",
|
579 |
+
'الكمية': 3500
|
580 |
+
},
|
581 |
+
{
|
582 |
+
'رقم البند': "A3",
|
583 |
+
'وصف البند': "أعمال حديد التسليح",
|
584 |
+
'الوحدة': "طن",
|
585 |
+
'الكمية': 450
|
586 |
+
}
|
587 |
+
]
|
588 |
+
},
|
589 |
+
{
|
590 |
+
'id': 2,
|
591 |
+
'name': "صيانة وتطوير طريق الملك عبدالله",
|
592 |
+
'number': "MOT-2025-042",
|
593 |
+
'client': "وزارة النقل",
|
594 |
+
'location': "الرياض، المنطقة الوسطى",
|
595 |
+
'description': "صيانة وتطوير طريق الملك عبدالله بطول 25 كم، ويشمل المشروع إعادة الرصف وتحسين الإنارة وتركيب اللوحات الإرشادية.",
|
596 |
+
'status': "تم التقديم",
|
597 |
+
'tender_type': "عامة",
|
598 |
+
'pricing_method': "غير متزن",
|
599 |
+
'submission_date': (datetime.now() - timedelta(days=15)),
|
600 |
+
'created_at': datetime.now() - timedelta(days=45),
|
601 |
+
'created_by_id': 1
|
602 |
+
},
|
603 |
+
{
|
604 |
+
'id': 3,
|
605 |
+
'name': "إنشاء محطة معالجة مياه الصرف الصحي",
|
606 |
+
'number': "SWPC-2025-007",
|
607 |
+
'client': "شركة المياه الوطنية",
|
608 |
+
'location': "جدة، المنطقة الغربية",
|
609 |
+
'description': "إنشاء محطة معالجة مياه الصرف الصحي بطاقة استيعابية 50,000 م3/يوم، مع جميع الأعمال المدنية والكهروميكانيكية.",
|
610 |
+
'status': "تمت الترسية",
|
611 |
+
'tender_type': "عامة",
|
612 |
+
'pricing_method': "قياسي",
|
613 |
+
'submission_date': (datetime.now() - timedelta(days=90)),
|
614 |
+
'created_at': datetime.now() - timedelta(days=120),
|
615 |
+
'created_by_id': 1
|
616 |
+
},
|
617 |
+
{
|
618 |
+
'id': 4,
|
619 |
+
'name': "إنشاء منتزه الملك سلمان",
|
620 |
+
'number': "RAM-2025-015",
|
621 |
+
'client': "أمانة منطقة الرياض",
|
622 |
+
'location': "الرياض، المنطقة الوسطى",
|
623 |
+
'description': "إنشاء منتزه الملك سلمان على مساحة 500,000 متر مربع، ويشمل المشروع أعمال التشجير والتنسيق والمسطحات المائية والمباني الخدمية.",
|
624 |
+
'status': "قيد التنفيذ",
|
625 |
+
'tender_type': "عامة",
|
626 |
+
'pricing_method': "قياسي",
|
627 |
+
'submission_date': (datetime.now() - timedelta(days=180)),
|
628 |
+
'created_at': datetime.now() - timedelta(days=210),
|
629 |
+
'created_by_id': 1
|
630 |
+
},
|
631 |
+
{
|
632 |
+
'id': 5,
|
633 |
+
'name': "إنشاء مبنى مختبرات كلية العلوم",
|
634 |
+
'number': "KSU-2025-032",
|
635 |
+
'client': "جامعة الملك سعود",
|
636 |
+
'location': "الرياض، المنطقة الوسطى",
|
637 |
+
'description': "إنشاء مبنى المختبرات الجديد لكلية العلوم بمساحة 8,000 متر مربع، ويتكون من 3 طوابق ويشمل تجهيز المعامل والمختبرات العلمية.",
|
638 |
+
'status': "جديد",
|
639 |
+
'tender_type': "خاصة",
|
640 |
+
'pricing_method': "تنافسي",
|
641 |
+
'submission_date': (datetime.now() + timedelta(days=10)),
|
642 |
+
'created_at': datetime.now() - timedelta(days=5),
|
643 |
+
'created_by_id': 1
|
644 |
+
},
|
645 |
+
{
|
646 |
+
'id': 6,
|
647 |
+
'name': "توريد وتركيب أنظمة الطاقة الشمسية",
|
648 |
+
'number': "SEC-2025-098",
|
649 |
+
'client': "الشركة السعودية للكهرباء",
|
650 |
+
'location': "تبوك، المنطقة الشمالية",
|
651 |
+
'description': "توريد وتركيب أنظمة الطاقة الشمسية بقدرة 5 ميجاوات، مع جميع الأعمال المدنية والكهربائية.",
|
652 |
+
'status': "جديد",
|
653 |
+
'tender_type': "عامة",
|
654 |
+
'pricing_method': "قياسي",
|
655 |
+
'submission_date': (datetime.now() + timedelta(days=20)),
|
656 |
+
'created_at': datetime.now() - timedelta(days=2),
|
657 |
+
'created_by_id': 1
|
658 |
+
}
|
659 |
+
]
|
660 |
+
|
661 |
+
return projects
|
662 |
+
|
663 |
+
# تشغيل التطبيق
|
664 |
+
if __name__ == "__main__":
|
665 |
+
projects_app = ProjectsApp()
|
666 |
+
projects_app.run()
|
modules/resources/resources_app.py
CHANGED
@@ -24,6 +24,13 @@ class ResourcesApp:
|
|
24 |
|
25 |
# تهيئة حالة الجلسة
|
26 |
if 'resources_data' not in st.session_state:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
# إنشاء بيانات افتراضية للموارد البشرية
|
28 |
np.random.seed(42)
|
29 |
|
@@ -207,6 +214,19 @@ class ResourcesApp:
|
|
207 |
|
208 |
st.markdown("<h1 class='module-title'>وحدة الموارد</h1>", unsafe_allow_html=True)
|
209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
210 |
tabs = st.tabs([
|
211 |
"لوحة المعلومات",
|
212 |
"الموارد البشرية",
|
@@ -251,25 +271,47 @@ class ResourcesApp:
|
|
251 |
|
252 |
col1, col2, col3, col4 = st.columns(4)
|
253 |
|
|
|
254 |
with col1:
|
255 |
total_employees = len(employees_df)
|
256 |
available_employees = len(employees_df[employees_df["متاح"] == True])
|
257 |
st.metric("الموظفون", f"{available_employees}/{total_employees}")
|
258 |
|
|
|
259 |
with col2:
|
260 |
total_equipment = len(equipment_df)
|
261 |
available_equipment = len(equipment_df[equipment_df["متاحة"] == True])
|
262 |
st.metric("المعدات", f"{available_equipment}/{total_equipment}")
|
263 |
|
|
|
264 |
with col3:
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
269 |
with col4:
|
270 |
-
|
271 |
-
|
272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
273 |
|
274 |
# عرض توزيع الموارد البشرية حسب القسم
|
275 |
st.markdown("#### توزيع الموارد البشرية حسب القسم")
|
@@ -380,20 +422,265 @@ class ResourcesApp:
|
|
380 |
def _render_human_resources_tab(self):
|
381 |
"""عرض تبويب الموارد البشرية"""
|
382 |
st.markdown("### الموارد البشرية")
|
383 |
-
|
384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
385 |
|
386 |
def _render_equipment_tab(self):
|
387 |
"""عرض تبويب المعدات"""
|
388 |
st.markdown("### المعدات")
|
389 |
-
|
390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
391 |
|
392 |
def _render_materials_tab(self):
|
393 |
"""عرض تبويب المواد"""
|
394 |
st.markdown("### المواد")
|
395 |
-
|
396 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
397 |
|
398 |
def _render_resource_allocation_tab(self):
|
399 |
"""عرض تبويب تخصيص الموارد"""
|
|
|
24 |
|
25 |
# تهيئة حالة الجلسة
|
26 |
if 'resources_data' not in st.session_state:
|
27 |
+
st.session_state.resources_data = {}
|
28 |
+
|
29 |
+
# جلب بيانات المشروع المحفوظ
|
30 |
+
if 'saved_pricing' in st.session_state:
|
31 |
+
self.saved_project = st.session_state.saved_pricing[-1] if st.session_state.saved_pricing else None
|
32 |
+
else:
|
33 |
+
self.saved_project = None
|
34 |
# إنشاء بيانات افتراضية للموارد البشرية
|
35 |
np.random.seed(42)
|
36 |
|
|
|
214 |
|
215 |
st.markdown("<h1 class='module-title'>وحدة الموارد</h1>", unsafe_allow_html=True)
|
216 |
|
217 |
+
# عرض بيانات المشروع المحفوظ
|
218 |
+
if self.saved_project:
|
219 |
+
with st.expander("تفاصيل المشروع المحفوظ", expanded=True):
|
220 |
+
st.markdown(f"**اسم المشروع:** {self.saved_project.get('project_name', 'غير محدد')}")
|
221 |
+
st.markdown(f"**إجمالي القيمة:** {self.saved_project.get('total_price', 0):,.2f} ريال")
|
222 |
+
st.markdown(f"**عدد البنود:** {len(self.saved_project.get('items', []))}")
|
223 |
+
|
224 |
+
# عرض جدول البنود
|
225 |
+
if 'items' in self.saved_project:
|
226 |
+
st.subheader("جدول الكميات")
|
227 |
+
df = pd.DataFrame(self.saved_project['items'])
|
228 |
+
st.dataframe(df)
|
229 |
+
|
230 |
tabs = st.tabs([
|
231 |
"لوحة المعلومات",
|
232 |
"الموارد البشرية",
|
|
|
271 |
|
272 |
col1, col2, col3, col4 = st.columns(4)
|
273 |
|
274 |
+
# الموظفون
|
275 |
with col1:
|
276 |
total_employees = len(employees_df)
|
277 |
available_employees = len(employees_df[employees_df["متاح"] == True])
|
278 |
st.metric("الموظفون", f"{available_employees}/{total_employees}")
|
279 |
|
280 |
+
# المعدات
|
281 |
with col2:
|
282 |
total_equipment = len(equipment_df)
|
283 |
available_equipment = len(equipment_df[equipment_df["متاحة"] == True])
|
284 |
st.metric("المعدات", f"{available_equipment}/{total_equipment}")
|
285 |
|
286 |
+
# المواد والبنود
|
287 |
with col3:
|
288 |
+
if self.saved_project and 'items' in self.saved_project:
|
289 |
+
total_items = len(self.saved_project['items'])
|
290 |
+
total_value = sum(item.get('total_price', 0) for item in self.saved_project['items'])
|
291 |
+
st.metric(
|
292 |
+
"إجمالي البنود",
|
293 |
+
f"{total_items} بند",
|
294 |
+
f"{total_value:,.0f} ريال"
|
295 |
+
)
|
296 |
+
else:
|
297 |
+
total_materials = len(materials_df)
|
298 |
+
low_stock_materials = len(materials_df[materials_df["الكمية المتاحة"] < 50])
|
299 |
+
st.metric("المواد", f"{total_materials}", f"-{low_stock_materials} منخفضة المخزون")
|
300 |
+
|
301 |
+
# تحليل التكاليف
|
302 |
with col4:
|
303 |
+
if self.saved_project and 'items' in self.saved_project:
|
304 |
+
items_df = pd.DataFrame(self.saved_project['items'])
|
305 |
+
if not items_df.empty:
|
306 |
+
avg_unit_price = items_df['unit_price'].mean() if 'unit_price' in items_df.columns else 0
|
307 |
+
st.metric(
|
308 |
+
"متوسط سعر الوحدة",
|
309 |
+
f"{avg_unit_price:,.2f} ريال"
|
310 |
+
)
|
311 |
+
else:
|
312 |
+
total_projects = len(projects_df)
|
313 |
+
active_projects = len(projects_df[projects_df["الحالة"] == "قيد التنفيذ"])
|
314 |
+
st.metric("المشاريع النشطة", f"{active_projects}/{total_projects}")
|
315 |
|
316 |
# عرض توزيع الموارد البشرية حسب القسم
|
317 |
st.markdown("#### توزيع الموارد البشرية حسب القسم")
|
|
|
422 |
def _render_human_resources_tab(self):
|
423 |
"""عرض تبويب الموارد البشرية"""
|
424 |
st.markdown("### الموارد البشرية")
|
425 |
+
|
426 |
+
# إضافة موظف جديد
|
427 |
+
with st.expander("إضافة موظف جديد"):
|
428 |
+
employee_form = st.form("add_employee_form")
|
429 |
+
with employee_form:
|
430 |
+
employee_data = {}
|
431 |
+
employee_data['name'] = st.text_input("الاسم")
|
432 |
+
employee_data['department'] = st.selectbox("القسم", [
|
433 |
+
"الهندسة",
|
434 |
+
"المشتريات",
|
435 |
+
"المالية",
|
436 |
+
"الموارد البشرية",
|
437 |
+
"تقنية المعلومات",
|
438 |
+
"التسويق",
|
439 |
+
"المشاريع",
|
440 |
+
"الجودة والسلامة",
|
441 |
+
"المكتب الفني"
|
442 |
+
], key="employee_department_selectbox")
|
443 |
+
employee_data['position'] = st.selectbox("المنصب", [
|
444 |
+
"مدير مشروع",
|
445 |
+
"مهندس مدني",
|
446 |
+
"مهندس معماري",
|
447 |
+
"مهندس ميكانيكي",
|
448 |
+
"مهندس كهربائي",
|
449 |
+
"مهندس مساحة",
|
450 |
+
"فني مدني",
|
451 |
+
"فني كهرباء",
|
452 |
+
"فني مساحة",
|
453 |
+
"مراقب",
|
454 |
+
"سائق معدات",
|
455 |
+
"عامل مهني",
|
456 |
+
"عامل عادي"
|
457 |
+
], key="employee_position_selectbox")
|
458 |
+
|
459 |
+
cols = st.columns(3)
|
460 |
+
with cols[0]:
|
461 |
+
employee_data['monthly_salary'] = st.number_input("الراتب الشهري", min_value=0)
|
462 |
+
with cols[1]:
|
463 |
+
employee_data['experience_years'] = st.number_input("سنوات الخبرة", min_value=0)
|
464 |
+
with cols[2]:
|
465 |
+
employee_data['availability'] = st.selectbox("الحالة", ["متاح", "غير متاح"], key="employee_availability_selectbox")
|
466 |
+
|
467 |
+
employee_data['skills'] = st.multiselect("المهارات", [
|
468 |
+
"إدارة مشاريع",
|
469 |
+
"تصميم إنشائي",
|
470 |
+
"تخطيط وجدولة",
|
471 |
+
"حساب كميات",
|
472 |
+
"AutoCAD",
|
473 |
+
"Revit",
|
474 |
+
"Primavera",
|
475 |
+
"MS Project",
|
476 |
+
"تقدير تكاليف",
|
477 |
+
"مراقبة جودة",
|
478 |
+
"إدارة سلامة"
|
479 |
+
])
|
480 |
+
|
481 |
+
employee_data['certifications'] = st.multiselect("الشهادات", [
|
482 |
+
"PMP",
|
483 |
+
"عضوية الهيئة السعودية للمهندسين",
|
484 |
+
"OSHA",
|
485 |
+
"NEBOSH",
|
486 |
+
"AutoCAD Certified",
|
487 |
+
"Revit Certified"
|
488 |
+
])
|
489 |
+
|
490 |
+
if st.form_submit_button("إضافة"):
|
491 |
+
employees_df = st.session_state.resources_data["employees"]
|
492 |
+
new_id = f"EMP-{len(employees_df) + 1:03d}"
|
493 |
+
employee_data['id'] = new_id
|
494 |
+
employees_df.loc[len(employees_df)] = employee_data
|
495 |
+
st.success("تمت إضافة الموظف بنجاح")
|
496 |
+
|
497 |
+
# عرض الموظفين الحاليين
|
498 |
+
employees_df = st.session_state.resources_data["employees"]
|
499 |
+
|
500 |
+
# فلترة الموظفين
|
501 |
+
col1, col2, col3 = st.columns(3)
|
502 |
+
with col1:
|
503 |
+
dept_filter = st.selectbox("تصفية حسب القسم", ["الكل"] + list(employees_df["القسم"].unique()), key="employee_department_filter")
|
504 |
+
with col2:
|
505 |
+
position_filter = st.selectbox("تصفية حسب المنصب", ["الكل"] + list(employees_df["المنصب"].unique()), key="employee_position_filter")
|
506 |
+
with col3:
|
507 |
+
availability_filter = st.selectbox("تصفية حسب التوفر", ["الكل", "متاح", "غير متاح"], key="employee_availability_filter")
|
508 |
+
|
509 |
+
filtered_df = employees_df.copy()
|
510 |
+
if dept_filter != "الكل":
|
511 |
+
filtered_df = filtered_df[filtered_df["القسم"] == dept_filter]
|
512 |
+
if position_filter != "الكل":
|
513 |
+
filtered_df = filtered_df[filtered_df["المنصب"] == position_filter]
|
514 |
+
if availability_filter != "الكل":
|
515 |
+
filtered_df = filtered_df[filtered_df["متاح"] == (availability_filter == "متاح")]
|
516 |
+
|
517 |
+
st.dataframe(filtered_df, use_container_width=True)
|
518 |
|
519 |
def _render_equipment_tab(self):
|
520 |
"""عرض تبويب المعدات"""
|
521 |
st.markdown("### المعدات")
|
522 |
+
|
523 |
+
# إضافة معدة جديدة
|
524 |
+
with st.expander("إضافة معدة جديدة"):
|
525 |
+
equipment_form = st.form("add_equipment_form")
|
526 |
+
with equipment_form:
|
527 |
+
equipment_data = {}
|
528 |
+
equipment_data['name'] = st.text_input("اسم المعدة")
|
529 |
+
equipment_data['category'] = st.selectbox("الفئة", [
|
530 |
+
"معدات الحفر والردم",
|
531 |
+
"معدات النقل",
|
532 |
+
"معدات الرفع",
|
533 |
+
"معدات الخرسانة",
|
534 |
+
"معدات الطرق",
|
535 |
+
"معدات الصرف الصحي",
|
536 |
+
"معدات السيول والكباري",
|
537 |
+
"معدات الضغط والتثبيت",
|
538 |
+
"معدات التوليد والطاقة",
|
539 |
+
"معدات القياس والمساحة"
|
540 |
+
], key="equipment_category_selectbox")
|
541 |
+
equipment_data['brand'] = st.text_input("الماركة")
|
542 |
+
equipment_data['model'] = st.text_input("الموديل")
|
543 |
+
equipment_data['capacity'] = st.text_input("السعة/القدرة")
|
544 |
+
equipment_data['production_rate'] = st.text_input("معدل الإنتاجية")
|
545 |
+
|
546 |
+
cols = st.columns(4)
|
547 |
+
with cols[0]:
|
548 |
+
equipment_data['hourly_cost'] = st.number_input("التكلفة بالساعة", min_value=0)
|
549 |
+
with cols[1]:
|
550 |
+
equipment_data['daily_cost'] = st.number_input("التكلفة باليوم", min_value=0)
|
551 |
+
with cols[2]:
|
552 |
+
equipment_data['weekly_cost'] = st.number_input("التكلفة بالأسبوع", min_value=0)
|
553 |
+
with cols[3]:
|
554 |
+
equipment_data['monthly_cost'] = st.number_input("التكلفة بالشهر", min_value=0)
|
555 |
+
|
556 |
+
equipment_data['fuel_consumption'] = st.text_input("استهلاك الوقود")
|
557 |
+
equipment_data['maintenance_period'] = st.text_input("فترة الصيانة")
|
558 |
+
equipment_data['maintenance_cost'] = st.number_input("تكلفة الصيانة", min_value=0)
|
559 |
+
equipment_data['operator_required'] = st.checkbox("تتطلب مشغل")
|
560 |
+
equipment_data['description'] = st.text_area("الوصف")
|
561 |
+
|
562 |
+
if st.form_submit_button("إضافة"):
|
563 |
+
equipment_df = st.session_state.resources_data["equipment"]
|
564 |
+
new_id = f"EQP-{len(equipment_df) + 1:03d}"
|
565 |
+
equipment_data['id'] = new_id
|
566 |
+
equipment_df.loc[len(equipment_df)] = equipment_data
|
567 |
+
st.success("تمت إضافة المعدة بنجاح")
|
568 |
+
|
569 |
+
# عرض المعدات الحالية
|
570 |
+
equipment_df = st.session_state.resources_data["equipment"]
|
571 |
+
|
572 |
+
# فلترة المعدات
|
573 |
+
col1, col2 = st.columns(2)
|
574 |
+
with col1:
|
575 |
+
category_filter = st.selectbox("تصفية حسب الفئة", ["الكل"] + list(equipment_df["النوع"].unique()), key="equipment_category_filter")
|
576 |
+
with col2:
|
577 |
+
availability_filter = st.selectbox("تصفية حسب التوفر", ["الكل", "متاح", "غير متاح"], key="equipment_availability_filter")
|
578 |
+
|
579 |
+
filtered_df = equipment_df.copy()
|
580 |
+
if category_filter != "الكل":
|
581 |
+
filtered_df = filtered_df[filtered_df["النوع"] == category_filter]
|
582 |
+
if availability_filter != "الكل":
|
583 |
+
filtered_df = filtered_df[filtered_df["متاحة"] == (availability_filter == "متاح")]
|
584 |
+
|
585 |
+
st.dataframe(filtered_df, use_container_width=True)
|
586 |
+
|
587 |
+
# إحصائيات المعدات
|
588 |
+
st.markdown("### إحصائيات المعدات")
|
589 |
+
col1, col2, col3 = st.columns(3)
|
590 |
+
|
591 |
+
with col1:
|
592 |
+
total_equipment = len(equipment_df)
|
593 |
+
available_equipment = len(equipment_df[equipment_df["متاحة"] == True])
|
594 |
+
st.metric("إجمالي المعدات", f"{available_equipment}/{total_equipment}")
|
595 |
+
|
596 |
+
with col2:
|
597 |
+
avg_daily_cost = equipment_df["التكلفة اليومية"].mean()
|
598 |
+
st.metric("متوسط التكلفة اليومية", f"{avg_daily_cost:.2f} ريال")
|
599 |
+
|
600 |
+
with col3:
|
601 |
+
maintenance_equipment = len(equipment_df[equipment_df["الموقع"] == "في الصيانة"])
|
602 |
+
st.metric("المعدات في الصيانة", maintenance_equipment)
|
603 |
|
604 |
def _render_materials_tab(self):
|
605 |
"""عرض تبويب المواد"""
|
606 |
st.markdown("### المواد")
|
607 |
+
|
608 |
+
# إضافة مادة جديدة
|
609 |
+
with st.expander("إضافة مادة جديدة"):
|
610 |
+
material_form = st.form("add_material_form")
|
611 |
+
with material_form:
|
612 |
+
material_data = {}
|
613 |
+
material_data['name'] = st.text_input("اسم المادة")
|
614 |
+
material_data['category'] = st.selectbox("الفئة", [
|
615 |
+
"مواد الخرسانة",
|
616 |
+
"مواد البناء",
|
617 |
+
"مواد الطرق",
|
618 |
+
"مواد الصرف الصحي",
|
619 |
+
"مواد العزل",
|
620 |
+
"مواد التشطيبات",
|
621 |
+
"مواد كهربائية",
|
622 |
+
"موا�� ميكانيكية",
|
623 |
+
"مواد الري والزراعة"
|
624 |
+
], key="material_category_selectbox")
|
625 |
+
|
626 |
+
material_data['unit'] = st.selectbox("وحدة القياس", [
|
627 |
+
"م3", "م2", "متر طولي", "طن", "كجم", "لتر", "قطعة"
|
628 |
+
], key="material_unit_selectbox")
|
629 |
+
|
630 |
+
cols = st.columns(4)
|
631 |
+
with cols[0]:
|
632 |
+
material_data['quantity'] = st.number_input("الكمية المتاحة", min_value=0)
|
633 |
+
with cols[1]:
|
634 |
+
material_data['unit_cost'] = st.number_input("تكلفة الوحدة", min_value=0)
|
635 |
+
with cols[2]:
|
636 |
+
material_data['supplier'] = st.text_input("المورد")
|
637 |
+
with cols[3]:
|
638 |
+
material_data['lead_time'] = st.number_input("مدة التوريد (يوم)", min_value=1)
|
639 |
+
|
640 |
+
material_data['specifications'] = st.text_area("المواصفات")
|
641 |
+
material_data['notes'] = st.text_area("ملاحظات")
|
642 |
+
|
643 |
+
if st.form_submit_button("إضافة"):
|
644 |
+
materials_df = st.session_state.resources_data["materials"]
|
645 |
+
new_id = f"MAT-{len(materials_df) + 1:03d}"
|
646 |
+
material_data['id'] = new_id
|
647 |
+
materials_df.loc[len(materials_df)] = material_data
|
648 |
+
st.success("تمت إضافة المادة بنجاح")
|
649 |
+
|
650 |
+
# عرض المواد الحالية
|
651 |
+
materials_df = st.session_state.resources_data["materials"]
|
652 |
+
|
653 |
+
# فلترة المواد
|
654 |
+
col1, col2 = st.columns(2)
|
655 |
+
with col1:
|
656 |
+
category_filter = st.selectbox("تصفية حسب الفئة", ["الكل"] + list(materials_df["اسم المادة"].unique()), key="material_category_filter")
|
657 |
+
with col2:
|
658 |
+
supplier_filter = st.selectbox("تصفية حسب المورد", ["الكل"] + list(materials_df["المورد"].unique()), key="material_supplier_filter")
|
659 |
+
|
660 |
+
filtered_df = materials_df.copy()
|
661 |
+
if category_filter != "الكل":
|
662 |
+
filtered_df = filtered_df[filtered_df["اسم المادة"] == category_filter]
|
663 |
+
if supplier_filter != "الكل":
|
664 |
+
filtered_df = filtered_df[filtered_df["المورد"] == supplier_filter]
|
665 |
+
|
666 |
+
st.dataframe(filtered_df, use_container_width=True)
|
667 |
+
|
668 |
+
# إحصائيات المواد
|
669 |
+
st.markdown("### إحصائيات المواد")
|
670 |
+
col1, col2, col3 = st.columns(3)
|
671 |
+
|
672 |
+
with col1:
|
673 |
+
total_materials = len(materials_df)
|
674 |
+
low_stock_materials = len(materials_df[materials_df["الكمية المتاحة"] < 50])
|
675 |
+
st.metric("إجمالي المواد", total_materials, f"-{low_stock_materials} منخفضة المخزون")
|
676 |
+
|
677 |
+
with col2:
|
678 |
+
total_value = (materials_df["الكمية المتاحة"] * materials_df["تكلفة الوحدة"]).sum()
|
679 |
+
st.metric("القيمة الإجمالية للمخزون", f"{total_value:,.2f} ريال")
|
680 |
+
|
681 |
+
with col3:
|
682 |
+
avg_lead_time = materials_df["مدة التوريد (يوم)"].mean()
|
683 |
+
st.metric("متوسط مدة التوريد", f"{avg_lead_time:.1f} يوم")
|
684 |
|
685 |
def _render_resource_allocation_tab(self):
|
686 |
"""عرض تبويب تخصيص الموارد"""
|
modules/risk_analysis/risk_analyzer.py
CHANGED
@@ -195,21 +195,21 @@ class RiskAnalyzer:
|
|
195 |
|
196 |
class RiskAnalysisApp:
|
197 |
"""تطبيق تحليل المخاطر"""
|
198 |
-
|
199 |
def __init__(self):
|
200 |
"""تهيئة تطبيق تحليل المخاطر"""
|
201 |
self.ui = UIEnhancer(page_title="تحليل المخاطر - نظام تحليل المناقصات", page_icon="⚠️")
|
202 |
self.ui.apply_theme_colors()
|
203 |
self.risk_analyzer = RiskAnalyzer()
|
204 |
-
|
205 |
# تهيئة بيانات المشاريع
|
206 |
if 'projects' not in st.session_state:
|
207 |
st.session_state.projects = self._generate_sample_projects()
|
208 |
-
|
209 |
# تهيئة نتائج التحليل
|
210 |
if 'risk_analysis_results' not in st.session_state:
|
211 |
st.session_state.risk_analysis_results = {}
|
212 |
-
|
213 |
def run(self):
|
214 |
"""تشغيل تطبيق تحليل المخاطر"""
|
215 |
# إنشاء قائمة العناصر
|
@@ -231,13 +231,13 @@ class RiskAnalysisApp:
|
|
231 |
{"name": "التقارير", "icon": "bar-chart"},
|
232 |
{"name": "الإعدادات", "icon": "gear"}
|
233 |
]
|
234 |
-
|
235 |
# إنشاء الشريط الجانبي
|
236 |
selected = self.ui.create_sidebar(menu_items)
|
237 |
-
|
238 |
# إنشاء ترويسة الصفحة
|
239 |
self.ui.create_header("تحليل المخاطر", "تحديد وتقييم وإدارة مخاطر المشاريع")
|
240 |
-
|
241 |
# عرض واجهة تحليل المخاطر
|
242 |
tabs = st.tabs([
|
243 |
"لوحة المعلومات",
|
@@ -246,36 +246,36 @@ class RiskAnalysisApp:
|
|
246 |
"مصفوفة المخاطر",
|
247 |
"استراتيجيات التخفيف"
|
248 |
])
|
249 |
-
|
250 |
with tabs[0]:
|
251 |
self._render_dashboard_tab()
|
252 |
-
|
253 |
with tabs[1]:
|
254 |
self._render_analysis_tab()
|
255 |
-
|
256 |
with tabs[2]:
|
257 |
self._render_risk_register_tab()
|
258 |
-
|
259 |
with tabs[3]:
|
260 |
self._render_risk_matrix_tab()
|
261 |
-
|
262 |
with tabs[4]:
|
263 |
self._render_mitigation_strategies_tab()
|
264 |
-
|
265 |
def _render_dashboard_tab(self):
|
266 |
"""عرض تبويب لوحة المعلومات"""
|
267 |
-
|
268 |
st.markdown("### لوحة معلومات تحليل المخاطر")
|
269 |
-
|
270 |
# عرض إحصائيات المخاطر
|
271 |
col1, col2, col3, col4 = st.columns(4)
|
272 |
-
|
273 |
# الحصول على إحصائيات المخاطر
|
274 |
total_risks = 0
|
275 |
high_risks = 0
|
276 |
medium_risks = 0
|
277 |
low_risks = 0
|
278 |
-
|
279 |
for project_id, results in st.session_state.risk_analysis_results.items():
|
280 |
if "identified_risks" in results:
|
281 |
project_risks = results["identified_risks"]
|
@@ -283,25 +283,25 @@ class RiskAnalysisApp:
|
|
283 |
high_risks += len([r for r in project_risks if r["risk_score"] >= 6])
|
284 |
medium_risks += len([r for r in project_risks if 3 <= r["risk_score"] < 6])
|
285 |
low_risks += len([r for r in project_risks if r["risk_score"] < 3])
|
286 |
-
|
287 |
with col1:
|
288 |
self.ui.create_metric_card("إجمالي المخاطر", str(total_risks), None, self.ui.COLORS['primary'])
|
289 |
-
|
290 |
with col2:
|
291 |
self.ui.create_metric_card("مخاطر عالية", str(high_risks), None, self.ui.COLORS['danger'])
|
292 |
-
|
293 |
with col3:
|
294 |
self.ui.create_metric_card("مخاطر متوسطة", str(medium_risks), None, self.ui.COLORS['warning'])
|
295 |
-
|
296 |
with col4:
|
297 |
self.ui.create_metric_card("مخاطر منخفضة", str(low_risks), None, self.ui.COLORS['success'])
|
298 |
-
|
299 |
# عرض توزيع المخاطر حسب الفئة
|
300 |
st.markdown("#### توزيع المخاطر حسب الفئة")
|
301 |
-
|
302 |
# جمع بيانات توزيع المخاطر
|
303 |
category_distribution = {}
|
304 |
-
|
305 |
for project_id, results in st.session_state.risk_analysis_results.items():
|
306 |
if "identified_risks" in results:
|
307 |
for risk in results["identified_risks"]:
|
@@ -309,24 +309,24 @@ class RiskAnalysisApp:
|
|
309 |
if category not in category_distribution:
|
310 |
category_distribution[category] = 0
|
311 |
category_distribution[category] += 1
|
312 |
-
|
313 |
if category_distribution:
|
314 |
# تحويل البيانات إلى DataFrame
|
315 |
category_df = pd.DataFrame({
|
316 |
'الفئة': list(category_distribution.keys()),
|
317 |
'عدد المخاطر': list(category_distribution.values())
|
318 |
})
|
319 |
-
|
320 |
# عرض الرسم البياني
|
321 |
st.bar_chart(category_df.set_index('الفئة'))
|
322 |
else:
|
323 |
st.info("لا توجد بيانات كافية لعرض توزيع المخاطر.")
|
324 |
-
|
325 |
# عرض المشاريع ذات المخاطر العالية
|
326 |
st.markdown("#### المشاريع ذات المخاطر العالية")
|
327 |
-
|
328 |
high_risk_projects = []
|
329 |
-
|
330 |
for project_id, results in st.session_state.risk_analysis_results.items():
|
331 |
if "identified_risks" in results:
|
332 |
project_high_risks = len([r for r in results["identified_risks"] if r["risk_score"] >= 6])
|
@@ -340,78 +340,80 @@ class RiskAnalysisApp:
|
|
340 |
'الجهة المالكة': project["client"],
|
341 |
'عدد المخاطر العالية': project_high_risks
|
342 |
})
|
343 |
-
|
344 |
if high_risk_projects:
|
345 |
high_risk_df = pd.DataFrame(high_risk_projects)
|
346 |
st.dataframe(high_risk_df, use_container_width=True, hide_index=True)
|
347 |
else:
|
348 |
st.info("لا توجد مشاريع ذات مخاطر عالية حاليًا.")
|
349 |
-
|
350 |
def _render_analysis_tab(self):
|
351 |
"""عرض تبويب تحليل المخاطر"""
|
352 |
-
|
353 |
st.markdown("### تحليل مخاطر المشروع")
|
354 |
-
|
355 |
# اختيار المشروع
|
356 |
project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
|
357 |
selected_project = st.selectbox("اختر المشروع", project_options)
|
358 |
-
|
359 |
if selected_project:
|
360 |
# استخراج معرف المشروع من الاختيار
|
361 |
project_index = project_options.index(selected_project)
|
362 |
project = st.session_state.projects[project_index]
|
363 |
project_id = project["id"]
|
364 |
-
|
365 |
# عرض معلومات المشروع
|
366 |
col1, col2 = st.columns(2)
|
367 |
-
|
368 |
with col1:
|
369 |
st.markdown(f"**اسم المشروع**: {project['name']}")
|
370 |
st.markdown(f"**رقم المناقصة**: {project['number']}")
|
371 |
st.markdown(f"**الجهة المالكة**: {project['client']}")
|
372 |
-
|
373 |
with col2:
|
374 |
-
st.markdown(f"
|
375 |
-
|
376 |
-
st.markdown(f"
|
377 |
-
|
|
|
|
|
378 |
# زر بدء التحليل
|
379 |
if st.button("بدء تحليل المخاطر"):
|
380 |
with st.spinner("جاري تحليل مخاطر المشروع..."):
|
381 |
# محاكاة وقت المعالجة
|
382 |
import time
|
383 |
time.sleep(2)
|
384 |
-
|
385 |
# إجراء تحليل المخاطر (Using the new RiskAnalyzer)
|
386 |
results = self.risk_analyzer.render_risk_analysis(project)
|
387 |
-
|
388 |
# تخزين النتائج في حالة الجلسة
|
389 |
st.session_state.risk_analysis_results[str(project_id)] = {"analysis_results": results}
|
390 |
-
|
391 |
st.success("تم الانتهاء من تحليل المخاطر بنجاح!")
|
392 |
-
st.
|
393 |
-
|
394 |
-
|
395 |
def _render_risk_register_tab(self):
|
396 |
"""عرض تبويب سجل المخاطر"""
|
397 |
-
|
398 |
st.markdown("### سجل المخاطر")
|
399 |
-
|
400 |
# اختيار المشروع
|
401 |
project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
|
402 |
project_options.insert(0, "جميع المشاريع")
|
403 |
selected_project_option = st.selectbox("اختر المشروع", project_options, key="risk_register_project")
|
404 |
-
|
405 |
# جمع المخاطر المحددة
|
406 |
all_risks = []
|
407 |
-
|
408 |
if selected_project_option == "جميع المشاريع":
|
409 |
# جمع المخاطر من جميع المشاريع
|
410 |
for project_id, results in st.session_state.risk_analysis_results.items():
|
411 |
if "analysis_results" in results:
|
412 |
project = next((p for p in st.session_state.projects if p["id"] == int(project_id)), None)
|
413 |
project_name = project["name"] if project else f"مشروع {project_id}"
|
414 |
-
|
415 |
for index, row in results["analysis_results"].iterrows():
|
416 |
risk_copy = row.to_dict()
|
417 |
risk_copy["project_name"] = project_name
|
@@ -421,7 +423,7 @@ class RiskAnalysisApp:
|
|
421 |
project_index = project_options.index(selected_project_option) - 1 # -1 لأننا أضفنا "جميع المشاريع" في البداية
|
422 |
project = st.session_state.projects[project_index]
|
423 |
project_id = project["id"]
|
424 |
-
|
425 |
# جمع المخاطر من المشروع المحدد
|
426 |
if str(project_id) in st.session_state.risk_analysis_results:
|
427 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
@@ -430,54 +432,54 @@ class RiskAnalysisApp:
|
|
430 |
risk_copy = row.to_dict()
|
431 |
risk_copy["project_name"] = project["name"]
|
432 |
all_risks.append(risk_copy)
|
433 |
-
|
434 |
# فلترة المخاطر
|
435 |
col1, col2, col3 = st.columns(3)
|
436 |
-
|
437 |
with col1:
|
438 |
category_filter = st.multiselect(
|
439 |
"فئة المخاطر",
|
440 |
list(set(risk["category"] for risk in all_risks)) if all_risks else [],
|
441 |
key="risk_register_category"
|
442 |
)
|
443 |
-
|
444 |
with col2:
|
445 |
probability_filter = st.multiselect(
|
446 |
"الاحتمالية",
|
447 |
list(set(risk["probability"] for risk in all_risks)) if all_risks else [],
|
448 |
key="risk_register_probability"
|
449 |
)
|
450 |
-
|
451 |
with col3:
|
452 |
impact_filter = st.multiselect(
|
453 |
"التأثير",
|
454 |
list(set(risk["impact"] for risk in all_risks)) if all_risks else [],
|
455 |
key="risk_register_impact"
|
456 |
)
|
457 |
-
|
458 |
# تطبيق الفلترة
|
459 |
filtered_risks = all_risks
|
460 |
-
|
461 |
if category_filter:
|
462 |
filtered_risks = [risk for risk in filtered_risks if risk["category"] in category_filter]
|
463 |
-
|
464 |
if probability_filter:
|
465 |
filtered_risks = [risk for risk in filtered_risks if risk["probability"] in probability_filter]
|
466 |
-
|
467 |
if impact_filter:
|
468 |
filtered_risks = [risk for risk in filtered_risks if risk["impact"] in impact_filter]
|
469 |
-
|
470 |
# عرض سجل المخاطر
|
471 |
if filtered_risks:
|
472 |
# تحويل المخاطر إلى DataFrame
|
473 |
risk_df = pd.DataFrame(filtered_risks)
|
474 |
-
|
475 |
# ترتيب المخاطر حسب درجة المخاطرة (تنازليًا)
|
476 |
risk_df = risk_df.sort_values(by='risk_score', ascending=False)
|
477 |
-
|
478 |
# عرض الجدول
|
479 |
st.dataframe(risk_df, use_container_width=True, hide_index=True)
|
480 |
-
|
481 |
# زر تصدير سجل المخاطر
|
482 |
if st.button("تصدير سجل المخاطر"):
|
483 |
with st.spinner("جاري تصدير سجل المخاطر..."):
|
@@ -486,26 +488,26 @@ class RiskAnalysisApp:
|
|
486 |
st.success("تم تصدير سجل المخاطر بنجاح!")
|
487 |
else:
|
488 |
st.info("لا توجد مخاطر تطابق معايير البحث أو لم يتم إجراء تحليل للمخاطر بعد.")
|
489 |
-
|
490 |
def _render_risk_matrix_tab(self):
|
491 |
"""عرض تبويب مصفوفة المخاطر"""
|
492 |
-
|
493 |
st.markdown("### مصفوفة المخاطر")
|
494 |
-
|
495 |
# اختيار المشروع
|
496 |
project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
|
497 |
selected_project = st.selectbox("اختر المشروع", project_options, key="risk_matrix_project")
|
498 |
-
|
499 |
if selected_project:
|
500 |
# استخراج معرف المشروع من الاختيار
|
501 |
project_index = project_options.index(selected_project)
|
502 |
project = st.session_state.projects[project_index]
|
503 |
project_id = project["id"]
|
504 |
-
|
505 |
# التحقق من وجود نتائج تحليل للمشروع
|
506 |
if str(project_id) in st.session_state.risk_analysis_results:
|
507 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
508 |
-
|
509 |
if "analysis_results" in results:
|
510 |
risks_df = results["analysis_results"]
|
511 |
self.risk_analyzer._render_risk_matrix(risks_df)
|
@@ -513,26 +515,26 @@ class RiskAnalysisApp:
|
|
513 |
st.warning("لم يتم العثور على مصفوفة المخاطر للمشروع المحدد.")
|
514 |
else:
|
515 |
st.warning("لم يتم إجراء تحليل للمخاطر لهذا المشروع بعد.")
|
516 |
-
|
517 |
def _render_mitigation_strategies_tab(self):
|
518 |
"""عرض تبويب استراتيجيات التخفيف"""
|
519 |
-
|
520 |
st.markdown("### استراتيجيات التخفيف من المخاطر")
|
521 |
-
|
522 |
# اختيار المشروع
|
523 |
project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
|
524 |
selected_project = st.selectbox("اختر المشروع", project_options, key="mitigation_project")
|
525 |
-
|
526 |
if selected_project:
|
527 |
# استخراج معرف المشروع من الاختيار
|
528 |
project_index = project_options.index(selected_project)
|
529 |
project = st.session_state.projects[project_index]
|
530 |
project_id = project["id"]
|
531 |
-
|
532 |
# التحقق من وجود نتائج تحليل للمشروع
|
533 |
if str(project_id) in st.session_state.risk_analysis_results:
|
534 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
535 |
-
|
536 |
if "analysis_results" in results and not results["analysis_results"].empty:
|
537 |
risks_df = results["analysis_results"]
|
538 |
self.risk_analyzer._render_risk_response_plan(risks_df)
|
@@ -540,10 +542,10 @@ class RiskAnalysisApp:
|
|
540 |
st.warning("لم يتم العثور على استراتيجيات تخفيف للمشروع المحدد.")
|
541 |
else:
|
542 |
st.warning("لم يتم إجراء تحليل للمخاطر لهذا المشروع بعد.")
|
543 |
-
|
544 |
def _generate_sample_projects(self):
|
545 |
"""توليد بيانات افتراضية للمشاريع"""
|
546 |
-
|
547 |
return [
|
548 |
{
|
549 |
'id': 1,
|
|
|
195 |
|
196 |
class RiskAnalysisApp:
|
197 |
"""تطبيق تحليل المخاطر"""
|
198 |
+
|
199 |
def __init__(self):
|
200 |
"""تهيئة تطبيق تحليل المخاطر"""
|
201 |
self.ui = UIEnhancer(page_title="تحليل المخاطر - نظام تحليل المناقصات", page_icon="⚠️")
|
202 |
self.ui.apply_theme_colors()
|
203 |
self.risk_analyzer = RiskAnalyzer()
|
204 |
+
|
205 |
# تهيئة بيانات المشاريع
|
206 |
if 'projects' not in st.session_state:
|
207 |
st.session_state.projects = self._generate_sample_projects()
|
208 |
+
|
209 |
# تهيئة نتائج التحليل
|
210 |
if 'risk_analysis_results' not in st.session_state:
|
211 |
st.session_state.risk_analysis_results = {}
|
212 |
+
|
213 |
def run(self):
|
214 |
"""تشغيل تطبيق تحليل المخاطر"""
|
215 |
# إنشاء قائمة العناصر
|
|
|
231 |
{"name": "التقارير", "icon": "bar-chart"},
|
232 |
{"name": "الإعدادات", "icon": "gear"}
|
233 |
]
|
234 |
+
|
235 |
# إنشاء الشريط الجانبي
|
236 |
selected = self.ui.create_sidebar(menu_items)
|
237 |
+
|
238 |
# إنشاء ترويسة الصفحة
|
239 |
self.ui.create_header("تحليل المخاطر", "تحديد وتقييم وإدارة مخاطر المشاريع")
|
240 |
+
|
241 |
# عرض واجهة تحليل المخاطر
|
242 |
tabs = st.tabs([
|
243 |
"لوحة المعلومات",
|
|
|
246 |
"مصفوفة المخاطر",
|
247 |
"استراتيجيات التخفيف"
|
248 |
])
|
249 |
+
|
250 |
with tabs[0]:
|
251 |
self._render_dashboard_tab()
|
252 |
+
|
253 |
with tabs[1]:
|
254 |
self._render_analysis_tab()
|
255 |
+
|
256 |
with tabs[2]:
|
257 |
self._render_risk_register_tab()
|
258 |
+
|
259 |
with tabs[3]:
|
260 |
self._render_risk_matrix_tab()
|
261 |
+
|
262 |
with tabs[4]:
|
263 |
self._render_mitigation_strategies_tab()
|
264 |
+
|
265 |
def _render_dashboard_tab(self):
|
266 |
"""عرض تبويب لوحة المعلومات"""
|
267 |
+
|
268 |
st.markdown("### لوحة معلومات تحليل المخاطر")
|
269 |
+
|
270 |
# عرض إحصائيات المخاطر
|
271 |
col1, col2, col3, col4 = st.columns(4)
|
272 |
+
|
273 |
# الحصول على إحصائيات المخاطر
|
274 |
total_risks = 0
|
275 |
high_risks = 0
|
276 |
medium_risks = 0
|
277 |
low_risks = 0
|
278 |
+
|
279 |
for project_id, results in st.session_state.risk_analysis_results.items():
|
280 |
if "identified_risks" in results:
|
281 |
project_risks = results["identified_risks"]
|
|
|
283 |
high_risks += len([r for r in project_risks if r["risk_score"] >= 6])
|
284 |
medium_risks += len([r for r in project_risks if 3 <= r["risk_score"] < 6])
|
285 |
low_risks += len([r for r in project_risks if r["risk_score"] < 3])
|
286 |
+
|
287 |
with col1:
|
288 |
self.ui.create_metric_card("إجمالي المخاطر", str(total_risks), None, self.ui.COLORS['primary'])
|
289 |
+
|
290 |
with col2:
|
291 |
self.ui.create_metric_card("مخاطر عالية", str(high_risks), None, self.ui.COLORS['danger'])
|
292 |
+
|
293 |
with col3:
|
294 |
self.ui.create_metric_card("مخاطر متوسطة", str(medium_risks), None, self.ui.COLORS['warning'])
|
295 |
+
|
296 |
with col4:
|
297 |
self.ui.create_metric_card("مخاطر منخفضة", str(low_risks), None, self.ui.COLORS['success'])
|
298 |
+
|
299 |
# عرض توزيع المخاطر حسب الفئة
|
300 |
st.markdown("#### توزيع المخاطر حسب الفئة")
|
301 |
+
|
302 |
# جمع بيانات توزيع المخاطر
|
303 |
category_distribution = {}
|
304 |
+
|
305 |
for project_id, results in st.session_state.risk_analysis_results.items():
|
306 |
if "identified_risks" in results:
|
307 |
for risk in results["identified_risks"]:
|
|
|
309 |
if category not in category_distribution:
|
310 |
category_distribution[category] = 0
|
311 |
category_distribution[category] += 1
|
312 |
+
|
313 |
if category_distribution:
|
314 |
# تحويل البيانات إلى DataFrame
|
315 |
category_df = pd.DataFrame({
|
316 |
'الفئة': list(category_distribution.keys()),
|
317 |
'عدد المخاطر': list(category_distribution.values())
|
318 |
})
|
319 |
+
|
320 |
# عرض الرسم البياني
|
321 |
st.bar_chart(category_df.set_index('الفئة'))
|
322 |
else:
|
323 |
st.info("لا توجد بيانات كافية لعرض توزيع المخاطر.")
|
324 |
+
|
325 |
# عرض المشاريع ذات المخاطر العالية
|
326 |
st.markdown("#### المشاريع ذات المخاطر العالية")
|
327 |
+
|
328 |
high_risk_projects = []
|
329 |
+
|
330 |
for project_id, results in st.session_state.risk_analysis_results.items():
|
331 |
if "identified_risks" in results:
|
332 |
project_high_risks = len([r for r in results["identified_risks"] if r["risk_score"] >= 6])
|
|
|
340 |
'الجهة المالكة': project["client"],
|
341 |
'عدد المخاطر العالية': project_high_risks
|
342 |
})
|
343 |
+
|
344 |
if high_risk_projects:
|
345 |
high_risk_df = pd.DataFrame(high_risk_projects)
|
346 |
st.dataframe(high_risk_df, use_container_width=True, hide_index=True)
|
347 |
else:
|
348 |
st.info("لا توجد مشاريع ذات مخاطر عالية حاليًا.")
|
349 |
+
|
350 |
def _render_analysis_tab(self):
|
351 |
"""عرض تبويب تحليل المخاطر"""
|
352 |
+
|
353 |
st.markdown("### تحليل مخاطر المشروع")
|
354 |
+
|
355 |
# اختيار المشروع
|
356 |
project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
|
357 |
selected_project = st.selectbox("اختر المشروع", project_options)
|
358 |
+
|
359 |
if selected_project:
|
360 |
# استخراج معرف المشروع من الاختيار
|
361 |
project_index = project_options.index(selected_project)
|
362 |
project = st.session_state.projects[project_index]
|
363 |
project_id = project["id"]
|
364 |
+
|
365 |
# عرض معلومات المشروع
|
366 |
col1, col2 = st.columns(2)
|
367 |
+
|
368 |
with col1:
|
369 |
st.markdown(f"**اسم المشروع**: {project['name']}")
|
370 |
st.markdown(f"**رقم المناقصة**: {project['number']}")
|
371 |
st.markdown(f"**الجهة المالكة**: {project['client']}")
|
372 |
+
|
373 |
with col2:
|
374 |
+
st.markdown(f"**موقع المشروع**: {project['location']}")
|
375 |
+
project_type = project.get('type', project.get('tender_type', 'غير محدد'))
|
376 |
+
st.markdown(f"**نوع المشروع**: {project_type}")
|
377 |
+
complexity = project.get('complexity', 'غير محدد')
|
378 |
+
st.markdown(f"**مستوى التعقيد**: {complexity}")
|
379 |
+
|
380 |
# زر بدء التحليل
|
381 |
if st.button("بدء تحليل المخاطر"):
|
382 |
with st.spinner("جاري تحليل مخاطر المشروع..."):
|
383 |
# محاكاة وقت المعالجة
|
384 |
import time
|
385 |
time.sleep(2)
|
386 |
+
|
387 |
# إجراء تحليل المخاطر (Using the new RiskAnalyzer)
|
388 |
results = self.risk_analyzer.render_risk_analysis(project)
|
389 |
+
|
390 |
# تخزين النتائج في حالة الجلسة
|
391 |
st.session_state.risk_analysis_results[str(project_id)] = {"analysis_results": results}
|
392 |
+
|
393 |
st.success("تم الانتهاء من تحليل المخاطر بنجاح!")
|
394 |
+
st.rerun()
|
395 |
+
|
396 |
+
|
397 |
def _render_risk_register_tab(self):
|
398 |
"""عرض تبويب سجل المخاطر"""
|
399 |
+
|
400 |
st.markdown("### سجل المخاطر")
|
401 |
+
|
402 |
# اختيار المشروع
|
403 |
project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
|
404 |
project_options.insert(0, "جميع المشاريع")
|
405 |
selected_project_option = st.selectbox("اختر المشروع", project_options, key="risk_register_project")
|
406 |
+
|
407 |
# جمع المخاطر المحددة
|
408 |
all_risks = []
|
409 |
+
|
410 |
if selected_project_option == "جميع المشاريع":
|
411 |
# جمع المخاطر من جميع المشاريع
|
412 |
for project_id, results in st.session_state.risk_analysis_results.items():
|
413 |
if "analysis_results" in results:
|
414 |
project = next((p for p in st.session_state.projects if p["id"] == int(project_id)), None)
|
415 |
project_name = project["name"] if project else f"مشروع {project_id}"
|
416 |
+
|
417 |
for index, row in results["analysis_results"].iterrows():
|
418 |
risk_copy = row.to_dict()
|
419 |
risk_copy["project_name"] = project_name
|
|
|
423 |
project_index = project_options.index(selected_project_option) - 1 # -1 لأننا أضفنا "جميع المشاريع" في البداية
|
424 |
project = st.session_state.projects[project_index]
|
425 |
project_id = project["id"]
|
426 |
+
|
427 |
# جمع المخاطر من المشروع المحدد
|
428 |
if str(project_id) in st.session_state.risk_analysis_results:
|
429 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
|
|
432 |
risk_copy = row.to_dict()
|
433 |
risk_copy["project_name"] = project["name"]
|
434 |
all_risks.append(risk_copy)
|
435 |
+
|
436 |
# فلترة المخاطر
|
437 |
col1, col2, col3 = st.columns(3)
|
438 |
+
|
439 |
with col1:
|
440 |
category_filter = st.multiselect(
|
441 |
"فئة المخاطر",
|
442 |
list(set(risk["category"] for risk in all_risks)) if all_risks else [],
|
443 |
key="risk_register_category"
|
444 |
)
|
445 |
+
|
446 |
with col2:
|
447 |
probability_filter = st.multiselect(
|
448 |
"الاحتمالية",
|
449 |
list(set(risk["probability"] for risk in all_risks)) if all_risks else [],
|
450 |
key="risk_register_probability"
|
451 |
)
|
452 |
+
|
453 |
with col3:
|
454 |
impact_filter = st.multiselect(
|
455 |
"التأثير",
|
456 |
list(set(risk["impact"] for risk in all_risks)) if all_risks else [],
|
457 |
key="risk_register_impact"
|
458 |
)
|
459 |
+
|
460 |
# تطبيق الفلترة
|
461 |
filtered_risks = all_risks
|
462 |
+
|
463 |
if category_filter:
|
464 |
filtered_risks = [risk for risk in filtered_risks if risk["category"] in category_filter]
|
465 |
+
|
466 |
if probability_filter:
|
467 |
filtered_risks = [risk for risk in filtered_risks if risk["probability"] in probability_filter]
|
468 |
+
|
469 |
if impact_filter:
|
470 |
filtered_risks = [risk for risk in filtered_risks if risk["impact"] in impact_filter]
|
471 |
+
|
472 |
# عرض سجل المخاطر
|
473 |
if filtered_risks:
|
474 |
# تحويل المخاطر إلى DataFrame
|
475 |
risk_df = pd.DataFrame(filtered_risks)
|
476 |
+
|
477 |
# ترتيب المخاطر حسب درجة المخاطرة (تنازليًا)
|
478 |
risk_df = risk_df.sort_values(by='risk_score', ascending=False)
|
479 |
+
|
480 |
# عرض الجدول
|
481 |
st.dataframe(risk_df, use_container_width=True, hide_index=True)
|
482 |
+
|
483 |
# زر تصدير سجل المخاطر
|
484 |
if st.button("تصدير سجل المخاطر"):
|
485 |
with st.spinner("جاري تصدير سجل المخاطر..."):
|
|
|
488 |
st.success("تم تصدير سجل المخاطر بنجاح!")
|
489 |
else:
|
490 |
st.info("لا توجد مخاطر تطابق معايير البحث أو لم يتم إجراء تحليل للمخاطر بعد.")
|
491 |
+
|
492 |
def _render_risk_matrix_tab(self):
|
493 |
"""عرض تبويب مصفوفة المخاطر"""
|
494 |
+
|
495 |
st.markdown("### مصفوفة المخاطر")
|
496 |
+
|
497 |
# اختيار المشروع
|
498 |
project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
|
499 |
selected_project = st.selectbox("اختر المشروع", project_options, key="risk_matrix_project")
|
500 |
+
|
501 |
if selected_project:
|
502 |
# استخراج معرف المشروع من الاختيار
|
503 |
project_index = project_options.index(selected_project)
|
504 |
project = st.session_state.projects[project_index]
|
505 |
project_id = project["id"]
|
506 |
+
|
507 |
# التحقق من وجود نتائج تحليل للمشروع
|
508 |
if str(project_id) in st.session_state.risk_analysis_results:
|
509 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
510 |
+
|
511 |
if "analysis_results" in results:
|
512 |
risks_df = results["analysis_results"]
|
513 |
self.risk_analyzer._render_risk_matrix(risks_df)
|
|
|
515 |
st.warning("لم يتم العثور على مصفوفة المخاطر للمشروع المحدد.")
|
516 |
else:
|
517 |
st.warning("لم يتم إجراء تحليل للمخاطر لهذا المشروع بعد.")
|
518 |
+
|
519 |
def _render_mitigation_strategies_tab(self):
|
520 |
"""عرض تبويب استراتيجيات التخفيف"""
|
521 |
+
|
522 |
st.markdown("### استراتيجيات التخفيف من المخاطر")
|
523 |
+
|
524 |
# اختيار المشروع
|
525 |
project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
|
526 |
selected_project = st.selectbox("اختر المشروع", project_options, key="mitigation_project")
|
527 |
+
|
528 |
if selected_project:
|
529 |
# استخراج معرف المشروع من الاختيار
|
530 |
project_index = project_options.index(selected_project)
|
531 |
project = st.session_state.projects[project_index]
|
532 |
project_id = project["id"]
|
533 |
+
|
534 |
# التحقق من وجود نتائج تحليل للمشروع
|
535 |
if str(project_id) in st.session_state.risk_analysis_results:
|
536 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
537 |
+
|
538 |
if "analysis_results" in results and not results["analysis_results"].empty:
|
539 |
risks_df = results["analysis_results"]
|
540 |
self.risk_analyzer._render_risk_response_plan(risks_df)
|
|
|
542 |
st.warning("لم يتم العثور على استراتيجيات تخفيف للمشروع المحدد.")
|
543 |
else:
|
544 |
st.warning("لم يتم إجراء تحليل للمخاطر لهذا المشروع بعد.")
|
545 |
+
|
546 |
def _generate_sample_projects(self):
|
547 |
"""توليد بيانات افتراضية للمشاريع"""
|
548 |
+
|
549 |
return [
|
550 |
{
|
551 |
'id': 1,
|
modules/scheduling/schedule_app.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
|
|
|
2 |
import streamlit as st
|
3 |
import pandas as pd
|
4 |
import plotly.figure_factory as ff
|
@@ -152,17 +153,34 @@ class ScheduleApp:
|
|
152 |
return
|
153 |
|
154 |
st.subheader("تفاصيل المشروع")
|
155 |
-
|
|
|
|
|
|
|
156 |
with col1:
|
157 |
st.write(f"اسم المشروع: {project['project_name']}")
|
158 |
-
st.write(f"
|
|
|
|
|
159 |
with col2:
|
|
|
|
|
|
|
|
|
|
|
160 |
project['project_duration'] = st.number_input(
|
161 |
"مدة المشروع (بالأيام)",
|
162 |
min_value=30,
|
163 |
max_value=1800,
|
164 |
value=project.get('project_duration', 180)
|
165 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
166 |
|
167 |
self._generate_and_display_schedule(project)
|
168 |
|
@@ -179,46 +197,137 @@ class ScheduleApp:
|
|
179 |
|
180 |
def _initialize_schedule_items(self, project):
|
181 |
project['schedule_items'] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
for item in project['items']:
|
183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
184 |
schedule_item = {
|
185 |
-
'Task':
|
186 |
-
'Start':
|
187 |
-
'Finish':
|
188 |
-
'Duration':
|
189 |
-
'Dependencies': '',
|
190 |
'Progress': 0,
|
191 |
-
'Resource': ''
|
|
|
192 |
}
|
193 |
project['schedule_items'].append(schedule_item)
|
|
|
194 |
|
195 |
def _edit_schedule(self, schedule_items):
|
196 |
-
|
197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
198 |
column_config={
|
199 |
-
"Task":
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
"
|
205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
},
|
|
|
207 |
use_container_width=True,
|
208 |
-
hide_index=True
|
|
|
209 |
)
|
|
|
|
|
210 |
|
211 |
def _display_gantt_chart(self, schedule_items):
|
212 |
st.subheader("مخطط جانت")
|
213 |
df = pd.DataFrame(schedule_items)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
214 |
fig = ff.create_gantt(
|
215 |
df,
|
216 |
-
colors=
|
217 |
-
|
218 |
-
|
219 |
-
},
|
220 |
-
index_col='Resource',
|
221 |
-
show_colorbar=True,
|
222 |
group_tasks=True,
|
223 |
showgrid_x=True,
|
224 |
showgrid_y=True
|
@@ -236,14 +345,74 @@ class ScheduleApp:
|
|
236 |
def _display_progress_report(self, schedule_items):
|
237 |
st.subheader("تقرير تقدم المشروع")
|
238 |
df = pd.DataFrame(schedule_items)
|
239 |
-
avg_progress = df['Progress'].mean()
|
240 |
|
241 |
col1, col2, col3 = st.columns(3)
|
242 |
with col1:
|
243 |
st.metric("متوسط نسبة الإنجاز", f"{avg_progress:.1f}%")
|
244 |
with col2:
|
245 |
-
completed_tasks = len(df[df['Progress'] == 100])
|
246 |
st.metric("البنود المكتملة", f"{completed_tasks} من {len(df)}")
|
247 |
with col3:
|
248 |
-
not_started = len(df[df['Progress'] == 0])
|
249 |
st.metric("البنود غير المبدوءة", not_started)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
|
2 |
+
import os
|
3 |
import streamlit as st
|
4 |
import pandas as pd
|
5 |
import plotly.figure_factory as ff
|
|
|
153 |
return
|
154 |
|
155 |
st.subheader("تفاصيل المشروع")
|
156 |
+
|
157 |
+
# عرض التفاصيل الأساسية
|
158 |
+
col1, col2, col3 = st.columns(3)
|
159 |
+
|
160 |
with col1:
|
161 |
st.write(f"اسم المشروع: {project['project_name']}")
|
162 |
+
st.write(f"رقم المشروع: {project.get('project_number', 'غير محدد')}")
|
163 |
+
st.write(f"العميل: {project.get('client', 'غير محدد')}")
|
164 |
+
|
165 |
with col2:
|
166 |
+
st.write(f"إجمالي القيمة: {project['total_price']:,.2f} ريال")
|
167 |
+
st.write(f"الموقع: {project.get('location', 'غير محدد')}")
|
168 |
+
st.write(f"الحالة: {project.get('status', 'قيد التنفيذ')}")
|
169 |
+
|
170 |
+
with col3:
|
171 |
project['project_duration'] = st.number_input(
|
172 |
"مدة المشروع (بالأيام)",
|
173 |
min_value=30,
|
174 |
max_value=1800,
|
175 |
value=project.get('project_duration', 180)
|
176 |
)
|
177 |
+
st.write(f"تاريخ البدء: {project.get('start_date', 'غير محدد')}")
|
178 |
+
st.write(f"تاريخ الانتهاء المتوقع: {project.get('end_date', 'غير محدد')}")
|
179 |
+
|
180 |
+
# عرض وصف المشروع إذا كان متوفراً
|
181 |
+
if project.get('description'):
|
182 |
+
st.write("وصف المشروع:")
|
183 |
+
st.write(project['description'])
|
184 |
|
185 |
self._generate_and_display_schedule(project)
|
186 |
|
|
|
197 |
|
198 |
def _initialize_schedule_items(self, project):
|
199 |
project['schedule_items'] = []
|
200 |
+
start_date = datetime.now()
|
201 |
+
total_project_price = project.get('total_price', 0)
|
202 |
+
|
203 |
+
if total_project_price == 0:
|
204 |
+
total_project_price = sum(item.get('total_price', 0) for item in project['items'])
|
205 |
+
project['total_price'] = total_project_price
|
206 |
+
|
207 |
+
current_date = start_date
|
208 |
for item in project['items']:
|
209 |
+
item_price = item.get('total_price', 0)
|
210 |
+
if total_project_price > 0:
|
211 |
+
duration = max(1, int((item_price / total_project_price) * project['project_duration']))
|
212 |
+
else:
|
213 |
+
duration = max(1, int(project['project_duration'] / len(project['items'])))
|
214 |
+
|
215 |
+
end_date = current_date + timedelta(days=duration)
|
216 |
+
|
217 |
+
# تجميع المواد المطلوبة من تحليل البند
|
218 |
+
materials_list = []
|
219 |
+
if 'materials' in item:
|
220 |
+
for material in item['materials']:
|
221 |
+
materials_list.append(f"{material['name']} - {material['quantity']} {material['unit']}")
|
222 |
+
|
223 |
+
# إنشاء وصف تفصيلي للبند من جدول الكميات
|
224 |
+
# البحث باستخدام وصف البند
|
225 |
+
boq_item = next((x for x in project.get('items', [])
|
226 |
+
if x.get('description', '').strip().lower() == item.get('description', '').strip().lower()
|
227 |
+
or (x.get('code', '') == item.get('code', '') if item.get('code') else False)), None)
|
228 |
+
|
229 |
+
if boq_item:
|
230 |
+
task_description = f"البند: {boq_item.get('code', '')} - {boq_item.get('description', '')}\n"
|
231 |
+
task_description += f"الكمية: {boq_item.get('quantity', 0)} {boq_item.get('unit', '')}\n"
|
232 |
+
task_description += f"سعر الوحدة: {boq_item.get('unit_price', 0):,.2f} ريال\n"
|
233 |
+
task_description += f"الإجمالي: {boq_item.get('total_price', 0):,.2f} ريال"
|
234 |
+
else:
|
235 |
+
task_description = f"{item.get('code', '')} - {item.get('description', '')}"
|
236 |
+
|
237 |
schedule_item = {
|
238 |
+
'Task': task_description,
|
239 |
+
'Start': current_date.strftime('%Y-%m-%d'),
|
240 |
+
'Finish': end_date.strftime('%Y-%m-%d'),
|
241 |
+
'Duration': duration,
|
|
|
242 |
'Progress': 0,
|
243 |
+
'Resource': item.get('category', 'الموارد الأساسية'),
|
244 |
+
'materials': ', '.join(materials_list) if materials_list else ''
|
245 |
}
|
246 |
project['schedule_items'].append(schedule_item)
|
247 |
+
current_date = end_date
|
248 |
|
249 |
def _edit_schedule(self, schedule_items):
|
250 |
+
df = pd.DataFrame(schedule_items)
|
251 |
+
|
252 |
+
# تحويل التواريخ من نص إلى تاريخ
|
253 |
+
df['Start'] = pd.to_datetime(df['Start'])
|
254 |
+
df['Finish'] = pd.to_datetime(df['Finish'])
|
255 |
+
|
256 |
+
# إضافة عمود للمواد المطلوبة
|
257 |
+
if 'materials' not in df.columns:
|
258 |
+
df['materials'] = ''
|
259 |
+
|
260 |
+
# حساب المدة بين تاريخ البداية والنهاية
|
261 |
+
df['Duration'] = (pd.to_datetime(df['Finish']) - pd.to_datetime(df['Start'])).dt.days
|
262 |
+
|
263 |
+
edited_df = st.data_editor(
|
264 |
+
df,
|
265 |
column_config={
|
266 |
+
"Task": st.column_config.TextColumn(
|
267 |
+
"البند",
|
268 |
+
width="large",
|
269 |
+
help="وصف البند"
|
270 |
+
),
|
271 |
+
"Start": st.column_config.DatetimeColumn(
|
272 |
+
"تاريخ البداية",
|
273 |
+
format="YYYY-MM-DD",
|
274 |
+
step=86400
|
275 |
+
),
|
276 |
+
"Finish": st.column_config.DatetimeColumn(
|
277 |
+
"تاريخ النهاية",
|
278 |
+
format="YYYY-MM-DD",
|
279 |
+
step=86400
|
280 |
+
),
|
281 |
+
"Duration": st.column_config.NumberColumn(
|
282 |
+
"المدة (أيام)",
|
283 |
+
min_value=1,
|
284 |
+
format="%d"
|
285 |
+
),
|
286 |
+
"Progress": st.column_config.ProgressColumn(
|
287 |
+
"نسبة الإنجاز",
|
288 |
+
min_value=0,
|
289 |
+
max_value=100,
|
290 |
+
format="%d%%"
|
291 |
+
),
|
292 |
+
"Resource": st.column_config.TextColumn(
|
293 |
+
"الموارد",
|
294 |
+
width="medium"
|
295 |
+
),
|
296 |
+
"materials": st.column_config.TextColumn(
|
297 |
+
"المواد المطلوبة",
|
298 |
+
width="large"
|
299 |
+
)
|
300 |
},
|
301 |
+
num_rows="dynamic",
|
302 |
use_container_width=True,
|
303 |
+
hide_index=True,
|
304 |
+
key="schedule_editor"
|
305 |
)
|
306 |
+
|
307 |
+
return edited_df
|
308 |
|
309 |
def _display_gantt_chart(self, schedule_items):
|
310 |
st.subheader("مخطط جانت")
|
311 |
df = pd.DataFrame(schedule_items)
|
312 |
+
|
313 |
+
# تحويل التواريخ إلى datetime
|
314 |
+
df['Start'] = pd.to_datetime(df['Start'])
|
315 |
+
df['Finish'] = pd.to_datetime(df['Finish'])
|
316 |
+
|
317 |
+
# تأكد من أن كل القيم في عمود Resource هي نصوص
|
318 |
+
df['Resource'] = df['Resource'].astype(str)
|
319 |
+
|
320 |
+
# إنشاء رقم تسلسلي كمؤشر
|
321 |
+
df['ID'] = range(len(df))
|
322 |
+
|
323 |
+
# تحديد الألوان للمهام
|
324 |
+
colors = ['rgb(46, 137, 205)', 'rgb(82, 189, 97)', 'rgb(241, 196, 15)', 'rgb(231, 76, 60)']
|
325 |
+
|
326 |
fig = ff.create_gantt(
|
327 |
df,
|
328 |
+
colors=colors,
|
329 |
+
index_col='ID',
|
330 |
+
show_colorbar=False, # إخفاء شريط الألوان
|
|
|
|
|
|
|
331 |
group_tasks=True,
|
332 |
showgrid_x=True,
|
333 |
showgrid_y=True
|
|
|
345 |
def _display_progress_report(self, schedule_items):
|
346 |
st.subheader("تقرير تقدم المشروع")
|
347 |
df = pd.DataFrame(schedule_items)
|
348 |
+
avg_progress = df['Progress'].mean() if 'Progress' in df.columns else 0
|
349 |
|
350 |
col1, col2, col3 = st.columns(3)
|
351 |
with col1:
|
352 |
st.metric("متوسط نسبة الإنجاز", f"{avg_progress:.1f}%")
|
353 |
with col2:
|
354 |
+
completed_tasks = len(df[df['Progress'] == 100]) if 'Progress' in df.columns else 0
|
355 |
st.metric("البنود المكتملة", f"{completed_tasks} من {len(df)}")
|
356 |
with col3:
|
357 |
+
not_started = len(df[df['Progress'] == 0]) if 'Progress' in df.columns else len(df)
|
358 |
st.metric("البنود غير المبدوءة", not_started)
|
359 |
+
|
360 |
+
# أزرار الحفظ والتصدير
|
361 |
+
col1, col2 = st.columns(2)
|
362 |
+
with col1:
|
363 |
+
if st.button("💾 حفظ الجدول الزمني", key="save_schedule_btn"):
|
364 |
+
self._save_schedule_to_db(df)
|
365 |
+
st.success("تم حفظ الجدول الزمني بنجاح!")
|
366 |
+
|
367 |
+
with col2:
|
368 |
+
if st.button("📊 تصدير إلى Excel", key="export_schedule_btn"):
|
369 |
+
self._export_schedule_to_excel(df)
|
370 |
+
|
371 |
+
def _save_schedule_to_db(self, df):
|
372 |
+
"""حفظ الجدول الزمني في قاعدة البيانات"""
|
373 |
+
try:
|
374 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
375 |
+
schedule_data = {
|
376 |
+
'timestamp': timestamp,
|
377 |
+
'project_name': st.session_state.current_project.get('name', 'مشروع جديد'),
|
378 |
+
'schedule_items': df.to_dict('records')
|
379 |
+
}
|
380 |
+
|
381 |
+
if 'saved_schedules' not in st.session_state:
|
382 |
+
st.session_state.saved_schedules = []
|
383 |
+
|
384 |
+
st.session_state.saved_schedules.append(schedule_data)
|
385 |
+
|
386 |
+
except Exception as e:
|
387 |
+
st.error(f"حدث خطأ أثناء حفظ الجدول الزمني: {str(e)}")
|
388 |
+
|
389 |
+
def _export_schedule_to_excel(self, df):
|
390 |
+
"""تصدير الجدول الزمني إلى ملف Excel"""
|
391 |
+
try:
|
392 |
+
export_path = "data/exports"
|
393 |
+
os.makedirs(export_path, exist_ok=True)
|
394 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
395 |
+
excel_file = f"{export_path}/schedule_{timestamp}.xlsx"
|
396 |
+
|
397 |
+
# إنشاء كاتب Excel
|
398 |
+
with pd.ExcelWriter(excel_file, engine='openpyxl') as writer:
|
399 |
+
df.to_excel(writer, index=False, sheet_name='الجدول الزمني')
|
400 |
+
worksheet = writer.sheets['الجدول الزمني']
|
401 |
+
|
402 |
+
# إضافة معلومات الملخص
|
403 |
+
worksheet['A1'] = f"اسم المشروع: {st.session_state.current_project.get('name', 'مشروع جديد')}"
|
404 |
+
worksheet['A2'] = f"تاريخ التصدير: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
405 |
+
|
406 |
+
# توفير زر التحميل
|
407 |
+
with open(excel_file, 'rb') as f:
|
408 |
+
excel_data = f.read()
|
409 |
+
st.download_button(
|
410 |
+
label="تحميل ملف Excel",
|
411 |
+
data=excel_data,
|
412 |
+
file_name=f"schedule_{timestamp}.xlsx",
|
413 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
414 |
+
)
|
415 |
+
st.success("تم تصدير الجدول الزمني بنجاح!")
|
416 |
+
|
417 |
+
except Exception as e:
|
418 |
+
st.error(f"حدث خطأ أثناء تصدير الجدول الزمني: {str(e)}")
|
requirements.txt
CHANGED
@@ -34,4 +34,24 @@ pytest==7.4.0
|
|
34 |
pytest-cov==4.1.0
|
35 |
reportlab==4.0.8
|
36 |
rouge-score==0.1.2
|
37 |
-
python-Levenshtein==0.25.1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
pytest-cov==4.1.0
|
35 |
reportlab==4.0.8
|
36 |
rouge-score==0.1.2
|
37 |
+
python-Levenshtein==0.25.1
|
38 |
+
ezdxf>=1.0.0
|
39 |
+
ifcopenshell>=0.7.0
|
40 |
+
opencv-python>=4.8.0
|
41 |
+
pytesseract>=0.3.10
|
42 |
+
numpy>=1.26.0
|
43 |
+
Pillow>=10.0.0
|
44 |
+
shapely>=2.0.0
|
45 |
+
matplotlib>=3.8.0
|
46 |
+
scikit-image>=0.21.0
|
47 |
+
tensorflow>=2.15.0
|
48 |
+
torch>=2.1.0
|
49 |
+
transformers>=4.36.0
|
50 |
+
shapely>=2.0.0
|
51 |
+
pillow>=10.0.0
|
52 |
+
opencv-python
|
53 |
+
pandas
|
54 |
+
plotly
|
55 |
+
pytesseract
|
56 |
+
shapely
|
57 |
+
pdf2image
|
styling/enhanced_ui.py
CHANGED
@@ -11,7 +11,7 @@ import os
|
|
11 |
|
12 |
class UIEnhancer:
|
13 |
"""فئة لتحسين واجهة المستخدم وتوحيد التصميم عبر النظام"""
|
14 |
-
|
15 |
# ألوان النظام
|
16 |
COLORS = {
|
17 |
'primary': '#1E88E5', # أزرق
|
@@ -27,7 +27,7 @@ class UIEnhancer:
|
|
27 |
'text': '#212121', # أسود
|
28 |
'border': '#E0E0E0' # رمادي حدود
|
29 |
}
|
30 |
-
|
31 |
# أحجام الخطوط
|
32 |
FONT_SIZES = {
|
33 |
'xs': '0.75rem',
|
@@ -40,34 +40,34 @@ class UIEnhancer:
|
|
40 |
'4xl': '2.25rem',
|
41 |
'5xl': '3rem'
|
42 |
}
|
43 |
-
|
44 |
def __init__(self, page_title="نظام تحليل المناقصات", page_icon="📊"):
|
45 |
"""تهيئة محسن واجهة المستخدم"""
|
46 |
self.page_title = page_title
|
47 |
self.page_icon = page_icon
|
48 |
self.theme_mode = "light" # الوضع الافتراضي هو الوضع الفاتح
|
49 |
-
|
50 |
# تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
|
51 |
if 'theme' not in st.session_state:
|
52 |
st.session_state.theme = 'light'
|
53 |
-
|
54 |
def apply_global_styles(self):
|
55 |
"""تطبيق التنسيقات العامة على الصفحة"""
|
56 |
# تعريف CSS العام
|
57 |
css = f"""
|
58 |
@import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700&display=swap');
|
59 |
-
|
60 |
* {{
|
61 |
font-family: 'Tajawal', sans-serif;
|
62 |
direction: rtl;
|
63 |
}}
|
64 |
-
|
65 |
h1, h2, h3, h4, h5, h6 {{
|
66 |
font-family: 'Tajawal', sans-serif;
|
67 |
font-weight: 700;
|
68 |
color: {self.COLORS['dark']};
|
69 |
}}
|
70 |
-
|
71 |
.module-title {{
|
72 |
color: {self.COLORS['primary']};
|
73 |
font-size: {self.FONT_SIZES['3xl']};
|
@@ -75,11 +75,11 @@ class UIEnhancer:
|
|
75 |
border-bottom: 2px solid {self.COLORS['primary']};
|
76 |
padding-bottom: 0.5rem;
|
77 |
}}
|
78 |
-
|
79 |
.stTabs [data-baseweb="tab-list"] {{
|
80 |
gap: 2px;
|
81 |
}}
|
82 |
-
|
83 |
.stTabs [data-baseweb="tab"] {{
|
84 |
height: 50px;
|
85 |
white-space: pre-wrap;
|
@@ -89,82 +89,86 @@ class UIEnhancer:
|
|
89 |
padding-top: 10px;
|
90 |
padding-bottom: 10px;
|
91 |
}}
|
92 |
-
|
93 |
.stTabs [aria-selected="true"] {{
|
94 |
background-color: {self.COLORS['primary']};
|
95 |
color: white;
|
96 |
}}
|
97 |
-
|
98 |
div[data-testid="stSidebarNav"] li div a span {{
|
99 |
direction: rtl;
|
100 |
text-align: right;
|
101 |
font-family: 'Tajawal', sans-serif;
|
102 |
}}
|
103 |
-
|
104 |
div[data-testid="stSidebarNav"] {{
|
105 |
background-color: {self.COLORS['light']};
|
106 |
}}
|
107 |
-
|
108 |
div[data-testid="stSidebarNav"] li div {{
|
109 |
margin-right: 0;
|
110 |
margin-left: auto;
|
111 |
}}
|
112 |
-
|
113 |
div[data-testid="stSidebarNav"] li div a {{
|
114 |
padding-right: 10px;
|
115 |
padding-left: 0;
|
116 |
}}
|
117 |
-
|
118 |
div[data-testid="stSidebarNav"] li div a:hover {{
|
119 |
background-color: {self.COLORS['primary'] + '20'};
|
120 |
}}
|
121 |
-
|
122 |
div[data-testid="stSidebarNav"] li div[aria-selected="true"] {{
|
123 |
background-color: {self.COLORS['primary'] + '40'};
|
124 |
}}
|
125 |
-
|
126 |
div[data-testid="stSidebarNav"] li div[aria-selected="true"] a span {{
|
127 |
color: {self.COLORS['primary']};
|
128 |
font-weight: 500;
|
129 |
}}
|
130 |
-
|
131 |
.metric-card {{
|
132 |
background-color: white;
|
133 |
border-radius: 10px;
|
134 |
padding: 20px;
|
135 |
-
box-shadow: 0 4px
|
136 |
text-align: center;
|
137 |
-
transition:
|
|
|
|
|
138 |
}}
|
139 |
-
|
140 |
.metric-card:hover {{
|
141 |
transform: translateY(-5px);
|
|
|
|
|
142 |
}}
|
143 |
-
|
144 |
.metric-value {{
|
145 |
font-size: 2.5rem;
|
146 |
font-weight: 700;
|
147 |
margin: 10px 0;
|
148 |
}}
|
149 |
-
|
150 |
.metric-label {{
|
151 |
font-size: 1rem;
|
152 |
color: #666;
|
153 |
}}
|
154 |
-
|
155 |
.metric-change {{
|
156 |
font-size: 0.9rem;
|
157 |
margin-top: 5px;
|
158 |
}}
|
159 |
-
|
160 |
.metric-change-positive {{
|
161 |
color: {self.COLORS['success']};
|
162 |
}}
|
163 |
-
|
164 |
.metric-change-negative {{
|
165 |
color: {self.COLORS['danger']};
|
166 |
}}
|
167 |
-
|
168 |
.custom-button {{
|
169 |
background-color: {self.COLORS['primary']};
|
170 |
color: white;
|
@@ -175,11 +179,11 @@ class UIEnhancer:
|
|
175 |
cursor: pointer;
|
176 |
transition: background-color 0.3s ease;
|
177 |
}}
|
178 |
-
|
179 |
.custom-button:hover {{
|
180 |
background-color: {self.COLORS['secondary']};
|
181 |
}}
|
182 |
-
|
183 |
.custom-card {{
|
184 |
background-color: white;
|
185 |
border-radius: 10px;
|
@@ -187,59 +191,54 @@ class UIEnhancer:
|
|
187 |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
188 |
margin-bottom: 20px;
|
189 |
}}
|
190 |
-
|
191 |
.header-container {{
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
margin-bottom: 2rem;
|
196 |
-
|
197 |
-
border-bottom: 1px solid {self.COLORS['border']};
|
198 |
}}
|
199 |
-
|
200 |
.header-title {{
|
201 |
-
color:
|
202 |
-
font-size:
|
203 |
margin: 0;
|
|
|
204 |
}}
|
205 |
-
|
206 |
.header-subtitle {{
|
207 |
-
color:
|
208 |
-
font-size:
|
209 |
-
margin: 0;
|
210 |
-
}}
|
211 |
-
|
212 |
-
.header-actions {{
|
213 |
-
display: flex;
|
214 |
-
gap: 10px;
|
215 |
}}
|
216 |
-
|
217 |
/* تنسيق الجداول */
|
218 |
div[data-testid="stTable"] table {{
|
219 |
width: 100%;
|
220 |
border-collapse: collapse;
|
221 |
}}
|
222 |
-
|
223 |
div[data-testid="stTable"] thead tr th {{
|
224 |
background-color: {self.COLORS['primary']};
|
225 |
color: white;
|
226 |
text-align: right;
|
227 |
padding: 12px;
|
228 |
}}
|
229 |
-
|
230 |
div[data-testid="stTable"] tbody tr:nth-child(even) {{
|
231 |
background-color: {self.COLORS['light']};
|
232 |
}}
|
233 |
-
|
234 |
div[data-testid="stTable"] tbody tr:hover {{
|
235 |
background-color: {self.COLORS['primary'] + '10'};
|
236 |
}}
|
237 |
-
|
238 |
div[data-testid="stTable"] tbody tr td {{
|
239 |
padding: 10px;
|
240 |
text-align: right;
|
241 |
}}
|
242 |
-
|
243 |
/* تنسيق النماذج */
|
244 |
div[data-testid="stForm"] {{
|
245 |
background-color: white;
|
@@ -247,18 +246,18 @@ class UIEnhancer:
|
|
247 |
padding: 20px;
|
248 |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
249 |
}}
|
250 |
-
|
251 |
button[kind="primaryFormSubmit"] {{
|
252 |
background-color: {self.COLORS['primary']};
|
253 |
color: white;
|
254 |
}}
|
255 |
-
|
256 |
button[kind="secondaryFormSubmit"] {{
|
257 |
background-color: {self.COLORS['light']};
|
258 |
color: {self.COLORS['dark']};
|
259 |
border: 1px solid {self.COLORS['border']};
|
260 |
}}
|
261 |
-
|
262 |
/* تنسيق الرسوم البيانية */
|
263 |
div[data-testid="stVegaLiteChart"] {{
|
264 |
background-color: white;
|
@@ -267,10 +266,10 @@ class UIEnhancer:
|
|
267 |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
268 |
}}
|
269 |
"""
|
270 |
-
|
271 |
# تطبيق CSS
|
272 |
st.markdown(f'<style>{css}</style>', unsafe_allow_html=True)
|
273 |
-
|
274 |
def apply_theme_colors(self):
|
275 |
"""تطبيق ألوان السمة الحالية"""
|
276 |
# تحديد ألوان السمة بناءً على الوضع
|
@@ -282,7 +281,7 @@ class UIEnhancer:
|
|
282 |
self.COLORS['background'] = '#FFFFFF'
|
283 |
self.COLORS['text'] = '#212121'
|
284 |
self.COLORS['border'] = '#E0E0E0'
|
285 |
-
|
286 |
# تطبيق CSS للسمة
|
287 |
theme_css = f"""
|
288 |
body {{
|
@@ -290,18 +289,18 @@ class UIEnhancer:
|
|
290 |
color: {self.COLORS['text']};
|
291 |
}}
|
292 |
"""
|
293 |
-
|
294 |
st.markdown(f'<style>{theme_css}</style>', unsafe_allow_html=True)
|
295 |
-
|
296 |
def toggle_theme(self):
|
297 |
"""تبديل وضع السمة بين الفاتح والداكن"""
|
298 |
if self.theme_mode == "light":
|
299 |
self.theme_mode = "dark"
|
300 |
else:
|
301 |
self.theme_mode = "light"
|
302 |
-
|
303 |
self.apply_theme_colors()
|
304 |
-
|
305 |
def create_sidebar(self, menu_items):
|
306 |
"""إنشاء الشريط الجانبي مع قائمة العناصر"""
|
307 |
with st.sidebar:
|
@@ -314,7 +313,7 @@ class UIEnhancer:
|
|
314 |
""",
|
315 |
unsafe_allow_html=True
|
316 |
)
|
317 |
-
|
318 |
# إضافة معلومات المستخدم
|
319 |
st.markdown(
|
320 |
f"""
|
@@ -323,14 +322,14 @@ class UIEnhancer:
|
|
323 |
م
|
324 |
</div>
|
325 |
<p style="margin-top: 10px; font-weight: bold;">مهندس تامر الجوهري</p>
|
326 |
-
<p style="margin-top: -15px; font-size: 0.8rem; color: #666;">مدير تقنية معلومات المنطقة
|
327 |
</div>
|
328 |
""",
|
329 |
unsafe_allow_html=True
|
330 |
)
|
331 |
-
|
332 |
st.divider()
|
333 |
-
|
334 |
# إنشاء القائمة
|
335 |
selected = st.radio(
|
336 |
"القائمة الرئيسية",
|
@@ -338,9 +337,9 @@ class UIEnhancer:
|
|
338 |
format_func=lambda x: x,
|
339 |
label_visibility="collapsed"
|
340 |
)
|
341 |
-
|
342 |
st.divider()
|
343 |
-
|
344 |
# إضافة معلومات النظام
|
345 |
st.markdown(
|
346 |
"""
|
@@ -351,37 +350,36 @@ class UIEnhancer:
|
|
351 |
""",
|
352 |
unsafe_allow_html=True
|
353 |
)
|
354 |
-
|
355 |
return selected
|
356 |
-
|
357 |
-
def create_header(self, title, subtitle=None, show_actions=True):
|
358 |
-
"""إنشاء ترويسة الصفحة"""
|
359 |
-
# إنشاء معرفات فريدة للأزرار
|
360 |
-
add_button_key = f"add_button_{title}"
|
361 |
-
update_button_key = f"update_button_{title}"
|
362 |
-
|
363 |
-
col1, col2 = st.columns([3, 1])
|
364 |
-
|
365 |
-
with col1:
|
366 |
-
st.markdown(f'<h1 class="header-title">{title}</h1>', unsafe_allow_html=True)
|
367 |
-
if subtitle:
|
368 |
-
st.markdown(f'<p class="header-subtitle">{subtitle}</p>', unsafe_allow_html=True)
|
369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
370 |
if show_actions:
|
|
|
|
|
|
|
371 |
with col2:
|
372 |
-
|
373 |
-
|
374 |
-
st.button("إضافة جديد", key=add_button_key)
|
375 |
-
with col2_2:
|
376 |
-
st.button("تحديث", key=update_button_key)
|
377 |
-
|
378 |
st.divider()
|
379 |
-
|
380 |
def create_metric_card(self, label, value, change=None, color=None):
|
381 |
"""إنشاء بطاقة مقياس"""
|
382 |
if color is None:
|
383 |
color = self.COLORS['primary']
|
384 |
-
|
385 |
change_html = ""
|
386 |
if change is not None:
|
387 |
if change.startswith("+"):
|
@@ -393,9 +391,9 @@ class UIEnhancer:
|
|
393 |
else:
|
394 |
change_class = ""
|
395 |
change_icon = ""
|
396 |
-
|
397 |
change_html = f'<div class="metric-change {change_class}">{change_icon} {change}</div>'
|
398 |
-
|
399 |
st.markdown(
|
400 |
f"""
|
401 |
<div class="metric-card" style="border-top: 4px solid {color};">
|
@@ -406,12 +404,12 @@ class UIEnhancer:
|
|
406 |
""",
|
407 |
unsafe_allow_html=True
|
408 |
)
|
409 |
-
|
410 |
def create_card(self, title, content, color=None):
|
411 |
"""إنشاء بطاقة عامة"""
|
412 |
if color is None:
|
413 |
color = self.COLORS['primary']
|
414 |
-
|
415 |
st.markdown(
|
416 |
f"""
|
417 |
<div class="custom-card" style="border-top: 4px solid {color};">
|
@@ -421,39 +419,39 @@ class UIEnhancer:
|
|
421 |
""",
|
422 |
unsafe_allow_html=True
|
423 |
)
|
424 |
-
|
425 |
def create_button(self, label, color=None, icon=None, key=None):
|
426 |
"""إنشاء زر مخصص"""
|
427 |
if color is None:
|
428 |
color = self.COLORS['primary']
|
429 |
-
|
430 |
# إنشاء معرف فريد للزر إذا لم يتم توفيره
|
431 |
if key is None:
|
432 |
key = f"button_{label}_{hash(label)}"
|
433 |
-
|
434 |
icon_html = f"{icon} " if icon else ""
|
435 |
-
|
436 |
return st.button(
|
437 |
f"{icon_html}{label}",
|
438 |
key=key
|
439 |
)
|
440 |
-
|
441 |
def create_tabs(self, tab_names):
|
442 |
"""إنشاء تبويبات"""
|
443 |
return st.tabs(tab_names)
|
444 |
-
|
445 |
def create_expander(self, title, expanded=False, key=None):
|
446 |
"""إنشاء عنصر قابل للتوسيع"""
|
447 |
# إنشاء معرف فريد للعنصر إذا لم يتم توفيره
|
448 |
if key is None:
|
449 |
key = f"expander_{title}_{hash(title)}"
|
450 |
-
|
451 |
return st.expander(title, expanded=expanded, key=key)
|
452 |
-
|
453 |
def create_data_table(self, data, use_container_width=True, hide_index=True):
|
454 |
"""إنشاء جدول بيانات"""
|
455 |
return st.dataframe(data, use_container_width=use_container_width, hide_index=hide_index)
|
456 |
-
|
457 |
def create_chart(self, chart_type, data, **kwargs):
|
458 |
"""إنشاء رسم بياني"""
|
459 |
if chart_type == "bar":
|
@@ -464,119 +462,119 @@ class UIEnhancer:
|
|
464 |
return st.area_chart(data, **kwargs)
|
465 |
else:
|
466 |
return st.bar_chart(data, **kwargs)
|
467 |
-
|
468 |
def create_form(self, title, key=None):
|
469 |
"""إنشاء نموذج"""
|
470 |
# إنشاء معرف فريد للنموذج إذا لم يتم توفيره
|
471 |
if key is None:
|
472 |
key = f"form_{title}_{hash(title)}"
|
473 |
-
|
474 |
return st.form(key=key)
|
475 |
-
|
476 |
def create_file_uploader(self, label, types=None, key=None):
|
477 |
"""إنشاء أداة رفع الملفات"""
|
478 |
# إنشاء معرف فريد لأداة رفع الملفات إذا لم يتم توفيره
|
479 |
if key is None:
|
480 |
key = f"file_uploader_{label}_{hash(label)}"
|
481 |
-
|
482 |
return st.file_uploader(label, type=types, key=key)
|
483 |
-
|
484 |
def create_date_input(self, label, value=None, key=None):
|
485 |
"""إنشاء حقل إدخال تاريخ"""
|
486 |
# إنشاء معرف فريد لحقل إدخال التاريخ إذا لم يتم توفيره
|
487 |
if key is None:
|
488 |
key = f"date_input_{label}_{hash(label)}"
|
489 |
-
|
490 |
return st.date_input(label, value=value, key=key)
|
491 |
-
|
492 |
def create_select_box(self, label, options, index=0, key=None):
|
493 |
"""إنشاء قائمة منسدلة"""
|
494 |
# إنشاء معرف فريد للقائمة المنسدلة إذا لم يتم توفيره
|
495 |
if key is None:
|
496 |
key = f"select_box_{label}_{hash(label)}"
|
497 |
-
|
498 |
return st.selectbox(label, options, index=index, key=key)
|
499 |
-
|
500 |
def create_multi_select(self, label, options, default=None, key=None):
|
501 |
"""إنشاء قائمة اختيار متعدد"""
|
502 |
# إنشاء معرف فريد لقائمة الاختيار المتعدد إذا لم يتم توفيره
|
503 |
if key is None:
|
504 |
key = f"multi_select_{label}_{hash(label)}"
|
505 |
-
|
506 |
return st.multiselect(label, options, default=default, key=key)
|
507 |
-
|
508 |
def create_slider(self, label, min_value, max_value, value=None, step=1, key=None):
|
509 |
"""إنشاء شريط تمرير"""
|
510 |
# إنشاء معرف فريد لشريط التمرير إذا لم يتم توفيره
|
511 |
if key is None:
|
512 |
key = f"slider_{label}_{hash(label)}"
|
513 |
-
|
514 |
return st.slider(label, min_value=min_value, max_value=max_value, value=value, step=step, key=key)
|
515 |
-
|
516 |
def create_text_input(self, label, value="", key=None):
|
517 |
"""إنشاء حقل إدخال نصي"""
|
518 |
# إنشاء معرف فريد لحقل الإدخال النصي إذا لم يتم توفيره
|
519 |
if key is None:
|
520 |
key = f"text_input_{label}_{hash(label)}"
|
521 |
-
|
522 |
return st.text_input(label, value=value, key=key)
|
523 |
-
|
524 |
def create_text_area(self, label, value="", height=None, key=None):
|
525 |
"""إنشاء منطقة نص"""
|
526 |
# إنشاء معرف فريد لمنطقة النص إذا لم يتم توفيره
|
527 |
if key is None:
|
528 |
key = f"text_area_{label}_{hash(label)}"
|
529 |
-
|
530 |
return st.text_area(label, value=value, height=height, key=key)
|
531 |
-
|
532 |
def create_number_input(self, label, min_value=None, max_value=None, value=0, step=1, key=None):
|
533 |
"""إنشاء حقل إدخال رقمي"""
|
534 |
# إنشاء معرف فريد لحقل الإدخال الرقمي إذا لم يتم توفيره
|
535 |
if key is None:
|
536 |
key = f"number_input_{label}_{hash(label)}"
|
537 |
-
|
538 |
return st.number_input(label, min_value=min_value, max_value=max_value, value=value, step=step, key=key)
|
539 |
-
|
540 |
def create_checkbox(self, label, value=False, key=None):
|
541 |
"""إنشاء خانة اختيار"""
|
542 |
# إنشاء معرف فريد لخانة الاختيار إذا لم يتم توفيره
|
543 |
if key is None:
|
544 |
key = f"checkbox_{label}_{hash(label)}"
|
545 |
-
|
546 |
return st.checkbox(label, value=value, key=key)
|
547 |
-
|
548 |
def create_radio(self, label, options, index=0, key=None):
|
549 |
"""إنشاء أزرار راديو"""
|
550 |
# إنشاء معرف فريد لأزرار الراديو إذا لم يتم توفيره
|
551 |
if key is None:
|
552 |
key = f"radio_{label}_{hash(label)}"
|
553 |
-
|
554 |
return st.radio(label, options, index=index, key=key)
|
555 |
-
|
556 |
def create_progress_bar(self, value, key=None):
|
557 |
"""إنشاء شريط تقدم"""
|
558 |
# إنشاء معرف فريد لشريط التقدم إذا لم يتم توفيره
|
559 |
if key is None:
|
560 |
key = f"progress_bar_{value}_{hash(str(value))}"
|
561 |
-
|
562 |
return st.progress(value, key=key)
|
563 |
-
|
564 |
def create_spinner(self, text="جاري التحميل..."):
|
565 |
"""إنشاء مؤشر تحميل"""
|
566 |
return st.spinner(text)
|
567 |
-
|
568 |
def create_success_message(self, message):
|
569 |
"""إنشاء رسالة نجاح"""
|
570 |
return st.success(message)
|
571 |
-
|
572 |
def create_error_message(self, message):
|
573 |
"""إنشاء رسالة خطأ"""
|
574 |
return st.error(message)
|
575 |
-
|
576 |
def create_warning_message(self, message):
|
577 |
"""إنشاء رسالة تحذير"""
|
578 |
return st.warning(message)
|
579 |
-
|
580 |
def create_info_message(self, message):
|
581 |
"""إنشاء رسالة معلومات"""
|
582 |
-
return st.info(message)
|
|
|
11 |
|
12 |
class UIEnhancer:
|
13 |
"""فئة لتحسين واجهة المستخدم وتوحيد التصميم عبر النظام"""
|
14 |
+
|
15 |
# ألوان النظام
|
16 |
COLORS = {
|
17 |
'primary': '#1E88E5', # أزرق
|
|
|
27 |
'text': '#212121', # أسود
|
28 |
'border': '#E0E0E0' # رمادي حدود
|
29 |
}
|
30 |
+
|
31 |
# أحجام الخطوط
|
32 |
FONT_SIZES = {
|
33 |
'xs': '0.75rem',
|
|
|
40 |
'4xl': '2.25rem',
|
41 |
'5xl': '3rem'
|
42 |
}
|
43 |
+
|
44 |
def __init__(self, page_title="نظام تحليل المناقصات", page_icon="📊"):
|
45 |
"""تهيئة محسن واجهة المستخدم"""
|
46 |
self.page_title = page_title
|
47 |
self.page_icon = page_icon
|
48 |
self.theme_mode = "light" # الوضع الافتراضي هو الوضع الفاتح
|
49 |
+
|
50 |
# تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
|
51 |
if 'theme' not in st.session_state:
|
52 |
st.session_state.theme = 'light'
|
53 |
+
|
54 |
def apply_global_styles(self):
|
55 |
"""تطبيق التنسيقات العامة على الصفحة"""
|
56 |
# تعريف CSS العام
|
57 |
css = f"""
|
58 |
@import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700&display=swap');
|
59 |
+
|
60 |
* {{
|
61 |
font-family: 'Tajawal', sans-serif;
|
62 |
direction: rtl;
|
63 |
}}
|
64 |
+
|
65 |
h1, h2, h3, h4, h5, h6 {{
|
66 |
font-family: 'Tajawal', sans-serif;
|
67 |
font-weight: 700;
|
68 |
color: {self.COLORS['dark']};
|
69 |
}}
|
70 |
+
|
71 |
.module-title {{
|
72 |
color: {self.COLORS['primary']};
|
73 |
font-size: {self.FONT_SIZES['3xl']};
|
|
|
75 |
border-bottom: 2px solid {self.COLORS['primary']};
|
76 |
padding-bottom: 0.5rem;
|
77 |
}}
|
78 |
+
|
79 |
.stTabs [data-baseweb="tab-list"] {{
|
80 |
gap: 2px;
|
81 |
}}
|
82 |
+
|
83 |
.stTabs [data-baseweb="tab"] {{
|
84 |
height: 50px;
|
85 |
white-space: pre-wrap;
|
|
|
89 |
padding-top: 10px;
|
90 |
padding-bottom: 10px;
|
91 |
}}
|
92 |
+
|
93 |
.stTabs [aria-selected="true"] {{
|
94 |
background-color: {self.COLORS['primary']};
|
95 |
color: white;
|
96 |
}}
|
97 |
+
|
98 |
div[data-testid="stSidebarNav"] li div a span {{
|
99 |
direction: rtl;
|
100 |
text-align: right;
|
101 |
font-family: 'Tajawal', sans-serif;
|
102 |
}}
|
103 |
+
|
104 |
div[data-testid="stSidebarNav"] {{
|
105 |
background-color: {self.COLORS['light']};
|
106 |
}}
|
107 |
+
|
108 |
div[data-testid="stSidebarNav"] li div {{
|
109 |
margin-right: 0;
|
110 |
margin-left: auto;
|
111 |
}}
|
112 |
+
|
113 |
div[data-testid="stSidebarNav"] li div a {{
|
114 |
padding-right: 10px;
|
115 |
padding-left: 0;
|
116 |
}}
|
117 |
+
|
118 |
div[data-testid="stSidebarNav"] li div a:hover {{
|
119 |
background-color: {self.COLORS['primary'] + '20'};
|
120 |
}}
|
121 |
+
|
122 |
div[data-testid="stSidebarNav"] li div[aria-selected="true"] {{
|
123 |
background-color: {self.COLORS['primary'] + '40'};
|
124 |
}}
|
125 |
+
|
126 |
div[data-testid="stSidebarNav"] li div[aria-selected="true"] a span {{
|
127 |
color: {self.COLORS['primary']};
|
128 |
font-weight: 500;
|
129 |
}}
|
130 |
+
|
131 |
.metric-card {{
|
132 |
background-color: white;
|
133 |
border-radius: 10px;
|
134 |
padding: 20px;
|
135 |
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
136 |
text-align: center;
|
137 |
+
transition: all 0.3s ease;
|
138 |
+
border: 2px solid transparent;
|
139 |
+
cursor: pointer;
|
140 |
}}
|
141 |
+
|
142 |
.metric-card:hover {{
|
143 |
transform: translateY(-5px);
|
144 |
+
border-color: {self.COLORS['primary']};
|
145 |
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
146 |
}}
|
147 |
+
|
148 |
.metric-value {{
|
149 |
font-size: 2.5rem;
|
150 |
font-weight: 700;
|
151 |
margin: 10px 0;
|
152 |
}}
|
153 |
+
|
154 |
.metric-label {{
|
155 |
font-size: 1rem;
|
156 |
color: #666;
|
157 |
}}
|
158 |
+
|
159 |
.metric-change {{
|
160 |
font-size: 0.9rem;
|
161 |
margin-top: 5px;
|
162 |
}}
|
163 |
+
|
164 |
.metric-change-positive {{
|
165 |
color: {self.COLORS['success']};
|
166 |
}}
|
167 |
+
|
168 |
.metric-change-negative {{
|
169 |
color: {self.COLORS['danger']};
|
170 |
}}
|
171 |
+
|
172 |
.custom-button {{
|
173 |
background-color: {self.COLORS['primary']};
|
174 |
color: white;
|
|
|
179 |
cursor: pointer;
|
180 |
transition: background-color 0.3s ease;
|
181 |
}}
|
182 |
+
|
183 |
.custom-button:hover {{
|
184 |
background-color: {self.COLORS['secondary']};
|
185 |
}}
|
186 |
+
|
187 |
.custom-card {{
|
188 |
background-color: white;
|
189 |
border-radius: 10px;
|
|
|
191 |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
192 |
margin-bottom: 20px;
|
193 |
}}
|
194 |
+
|
195 |
.header-container {{
|
196 |
+
background: linear-gradient(135deg, {self.COLORS['primary']}, {self.COLORS['secondary']});
|
197 |
+
padding: 2rem;
|
198 |
+
border-radius: 15px;
|
199 |
margin-bottom: 2rem;
|
200 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
|
201 |
}}
|
202 |
+
|
203 |
.header-title {{
|
204 |
+
color: #ffffff;
|
205 |
+
font-size: 2rem;
|
206 |
margin: 0;
|
207 |
+
font-weight: 700;
|
208 |
}}
|
209 |
+
|
210 |
.header-subtitle {{
|
211 |
+
color: rgba(255, 255, 255, 0.9);
|
212 |
+
font-size: 1.1rem;
|
213 |
+
margin: 0.5rem 0 0 0;
|
|
|
|
|
|
|
|
|
|
|
214 |
}}
|
215 |
+
|
216 |
/* تنسيق الجداول */
|
217 |
div[data-testid="stTable"] table {{
|
218 |
width: 100%;
|
219 |
border-collapse: collapse;
|
220 |
}}
|
221 |
+
|
222 |
div[data-testid="stTable"] thead tr th {{
|
223 |
background-color: {self.COLORS['primary']};
|
224 |
color: white;
|
225 |
text-align: right;
|
226 |
padding: 12px;
|
227 |
}}
|
228 |
+
|
229 |
div[data-testid="stTable"] tbody tr:nth-child(even) {{
|
230 |
background-color: {self.COLORS['light']};
|
231 |
}}
|
232 |
+
|
233 |
div[data-testid="stTable"] tbody tr:hover {{
|
234 |
background-color: {self.COLORS['primary'] + '10'};
|
235 |
}}
|
236 |
+
|
237 |
div[data-testid="stTable"] tbody tr td {{
|
238 |
padding: 10px;
|
239 |
text-align: right;
|
240 |
}}
|
241 |
+
|
242 |
/* تنسيق النماذج */
|
243 |
div[data-testid="stForm"] {{
|
244 |
background-color: white;
|
|
|
246 |
padding: 20px;
|
247 |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
248 |
}}
|
249 |
+
|
250 |
button[kind="primaryFormSubmit"] {{
|
251 |
background-color: {self.COLORS['primary']};
|
252 |
color: white;
|
253 |
}}
|
254 |
+
|
255 |
button[kind="secondaryFormSubmit"] {{
|
256 |
background-color: {self.COLORS['light']};
|
257 |
color: {self.COLORS['dark']};
|
258 |
border: 1px solid {self.COLORS['border']};
|
259 |
}}
|
260 |
+
|
261 |
/* تنسيق الرسوم البيانية */
|
262 |
div[data-testid="stVegaLiteChart"] {{
|
263 |
background-color: white;
|
|
|
266 |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
267 |
}}
|
268 |
"""
|
269 |
+
|
270 |
# تطبيق CSS
|
271 |
st.markdown(f'<style>{css}</style>', unsafe_allow_html=True)
|
272 |
+
|
273 |
def apply_theme_colors(self):
|
274 |
"""تطبيق ألوان السمة الحالية"""
|
275 |
# تحديد ألوان السمة بناءً على الوضع
|
|
|
281 |
self.COLORS['background'] = '#FFFFFF'
|
282 |
self.COLORS['text'] = '#212121'
|
283 |
self.COLORS['border'] = '#E0E0E0'
|
284 |
+
|
285 |
# تطبيق CSS للسمة
|
286 |
theme_css = f"""
|
287 |
body {{
|
|
|
289 |
color: {self.COLORS['text']};
|
290 |
}}
|
291 |
"""
|
292 |
+
|
293 |
st.markdown(f'<style>{theme_css}</style>', unsafe_allow_html=True)
|
294 |
+
|
295 |
def toggle_theme(self):
|
296 |
"""تبديل وضع السمة بين الفاتح والداكن"""
|
297 |
if self.theme_mode == "light":
|
298 |
self.theme_mode = "dark"
|
299 |
else:
|
300 |
self.theme_mode = "light"
|
301 |
+
|
302 |
self.apply_theme_colors()
|
303 |
+
|
304 |
def create_sidebar(self, menu_items):
|
305 |
"""إنشاء الشريط الجانبي مع قائمة العناصر"""
|
306 |
with st.sidebar:
|
|
|
313 |
""",
|
314 |
unsafe_allow_html=True
|
315 |
)
|
316 |
+
|
317 |
# إضافة معلومات المستخدم
|
318 |
st.markdown(
|
319 |
f"""
|
|
|
322 |
م
|
323 |
</div>
|
324 |
<p style="margin-top: 10px; font-weight: bold;">مهندس تامر الجوهري</p>
|
325 |
+
<p style="margin-top: -15px; font-size: 0.8rem; color: #666;">مدير تقنية معلومات المنطقة الشمالية</p>
|
326 |
</div>
|
327 |
""",
|
328 |
unsafe_allow_html=True
|
329 |
)
|
330 |
+
|
331 |
st.divider()
|
332 |
+
|
333 |
# إنشاء القائمة
|
334 |
selected = st.radio(
|
335 |
"القائمة الرئيسية",
|
|
|
337 |
format_func=lambda x: x,
|
338 |
label_visibility="collapsed"
|
339 |
)
|
340 |
+
|
341 |
st.divider()
|
342 |
+
|
343 |
# إضافة معلومات النظام
|
344 |
st.markdown(
|
345 |
"""
|
|
|
350 |
""",
|
351 |
unsafe_allow_html=True
|
352 |
)
|
353 |
+
|
354 |
return selected
|
355 |
+
|
356 |
+
def create_header(self, title, subtitle=None, show_actions=True, icon=None):
|
357 |
+
"""إنشاء ترويسة الصفحة مع أيقونة وتصميم محسن"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
358 |
|
359 |
+
st.markdown(
|
360 |
+
f"""
|
361 |
+
<div class="header-container">
|
362 |
+
<h1 class="header-title">{title}</h1>
|
363 |
+
{f'<p class="header-subtitle">{subtitle}</p>' if subtitle else ''}
|
364 |
+
</div>
|
365 |
+
""",
|
366 |
+
unsafe_allow_html=True
|
367 |
+
)
|
368 |
+
|
369 |
if show_actions:
|
370 |
+
col1, col2 = st.columns([3, 1])
|
371 |
+
with col1:
|
372 |
+
st.button("إضافة جديد", key=f"add_button_{title}", use_container_width=True)
|
373 |
with col2:
|
374 |
+
st.button("تحديث", key=f"update_button_{title}", use_container_width=True)
|
375 |
+
|
|
|
|
|
|
|
|
|
376 |
st.divider()
|
377 |
+
|
378 |
def create_metric_card(self, label, value, change=None, color=None):
|
379 |
"""إنشاء بطاقة مقياس"""
|
380 |
if color is None:
|
381 |
color = self.COLORS['primary']
|
382 |
+
|
383 |
change_html = ""
|
384 |
if change is not None:
|
385 |
if change.startswith("+"):
|
|
|
391 |
else:
|
392 |
change_class = ""
|
393 |
change_icon = ""
|
394 |
+
|
395 |
change_html = f'<div class="metric-change {change_class}">{change_icon} {change}</div>'
|
396 |
+
|
397 |
st.markdown(
|
398 |
f"""
|
399 |
<div class="metric-card" style="border-top: 4px solid {color};">
|
|
|
404 |
""",
|
405 |
unsafe_allow_html=True
|
406 |
)
|
407 |
+
|
408 |
def create_card(self, title, content, color=None):
|
409 |
"""إنشاء بطاقة عامة"""
|
410 |
if color is None:
|
411 |
color = self.COLORS['primary']
|
412 |
+
|
413 |
st.markdown(
|
414 |
f"""
|
415 |
<div class="custom-card" style="border-top: 4px solid {color};">
|
|
|
419 |
""",
|
420 |
unsafe_allow_html=True
|
421 |
)
|
422 |
+
|
423 |
def create_button(self, label, color=None, icon=None, key=None):
|
424 |
"""إنشاء زر مخصص"""
|
425 |
if color is None:
|
426 |
color = self.COLORS['primary']
|
427 |
+
|
428 |
# إنشاء معرف فريد للزر إذا لم يتم توفيره
|
429 |
if key is None:
|
430 |
key = f"button_{label}_{hash(label)}"
|
431 |
+
|
432 |
icon_html = f"{icon} " if icon else ""
|
433 |
+
|
434 |
return st.button(
|
435 |
f"{icon_html}{label}",
|
436 |
key=key
|
437 |
)
|
438 |
+
|
439 |
def create_tabs(self, tab_names):
|
440 |
"""إنشاء تبويبات"""
|
441 |
return st.tabs(tab_names)
|
442 |
+
|
443 |
def create_expander(self, title, expanded=False, key=None):
|
444 |
"""إنشاء عنصر قابل للتوسيع"""
|
445 |
# إنشاء معرف فريد للعنصر إذا لم يتم توفيره
|
446 |
if key is None:
|
447 |
key = f"expander_{title}_{hash(title)}"
|
448 |
+
|
449 |
return st.expander(title, expanded=expanded, key=key)
|
450 |
+
|
451 |
def create_data_table(self, data, use_container_width=True, hide_index=True):
|
452 |
"""إنشاء جدول بيانات"""
|
453 |
return st.dataframe(data, use_container_width=use_container_width, hide_index=hide_index)
|
454 |
+
|
455 |
def create_chart(self, chart_type, data, **kwargs):
|
456 |
"""إنشاء رسم بياني"""
|
457 |
if chart_type == "bar":
|
|
|
462 |
return st.area_chart(data, **kwargs)
|
463 |
else:
|
464 |
return st.bar_chart(data, **kwargs)
|
465 |
+
|
466 |
def create_form(self, title, key=None):
|
467 |
"""إنشاء نموذج"""
|
468 |
# إنشاء معرف فريد للنموذج إذا لم يتم توفيره
|
469 |
if key is None:
|
470 |
key = f"form_{title}_{hash(title)}"
|
471 |
+
|
472 |
return st.form(key=key)
|
473 |
+
|
474 |
def create_file_uploader(self, label, types=None, key=None):
|
475 |
"""إنشاء أداة رفع الملفات"""
|
476 |
# إنشاء معرف فريد لأداة رفع الملفات إذا لم يتم توفيره
|
477 |
if key is None:
|
478 |
key = f"file_uploader_{label}_{hash(label)}"
|
479 |
+
|
480 |
return st.file_uploader(label, type=types, key=key)
|
481 |
+
|
482 |
def create_date_input(self, label, value=None, key=None):
|
483 |
"""إنشاء حقل إدخال تاريخ"""
|
484 |
# إنشاء معرف فريد لحقل إدخال التاريخ إذا لم يتم توفيره
|
485 |
if key is None:
|
486 |
key = f"date_input_{label}_{hash(label)}"
|
487 |
+
|
488 |
return st.date_input(label, value=value, key=key)
|
489 |
+
|
490 |
def create_select_box(self, label, options, index=0, key=None):
|
491 |
"""إنشاء قائمة منسدلة"""
|
492 |
# إنشاء معرف فريد للقائمة المنسدلة إذا لم يتم توفيره
|
493 |
if key is None:
|
494 |
key = f"select_box_{label}_{hash(label)}"
|
495 |
+
|
496 |
return st.selectbox(label, options, index=index, key=key)
|
497 |
+
|
498 |
def create_multi_select(self, label, options, default=None, key=None):
|
499 |
"""إنشاء قائمة اختيار متعدد"""
|
500 |
# إنشاء معرف فريد لقائمة الاختيار المتعدد إذا لم يتم توفيره
|
501 |
if key is None:
|
502 |
key = f"multi_select_{label}_{hash(label)}"
|
503 |
+
|
504 |
return st.multiselect(label, options, default=default, key=key)
|
505 |
+
|
506 |
def create_slider(self, label, min_value, max_value, value=None, step=1, key=None):
|
507 |
"""إنشاء شريط تمرير"""
|
508 |
# إنشاء معرف فريد لشريط التمرير إذا لم يتم توفيره
|
509 |
if key is None:
|
510 |
key = f"slider_{label}_{hash(label)}"
|
511 |
+
|
512 |
return st.slider(label, min_value=min_value, max_value=max_value, value=value, step=step, key=key)
|
513 |
+
|
514 |
def create_text_input(self, label, value="", key=None):
|
515 |
"""إنشاء حقل إدخال نصي"""
|
516 |
# إنشاء معرف فريد لحقل الإدخال النصي إذا لم يتم توفيره
|
517 |
if key is None:
|
518 |
key = f"text_input_{label}_{hash(label)}"
|
519 |
+
|
520 |
return st.text_input(label, value=value, key=key)
|
521 |
+
|
522 |
def create_text_area(self, label, value="", height=None, key=None):
|
523 |
"""إنشاء منطقة نص"""
|
524 |
# إنشاء معرف فريد لمنطقة النص إذا لم يتم توفيره
|
525 |
if key is None:
|
526 |
key = f"text_area_{label}_{hash(label)}"
|
527 |
+
|
528 |
return st.text_area(label, value=value, height=height, key=key)
|
529 |
+
|
530 |
def create_number_input(self, label, min_value=None, max_value=None, value=0, step=1, key=None):
|
531 |
"""إنشاء حقل إدخال رقمي"""
|
532 |
# إنشاء معرف فريد لحقل الإدخال الرقمي إذا لم يتم توفيره
|
533 |
if key is None:
|
534 |
key = f"number_input_{label}_{hash(label)}"
|
535 |
+
|
536 |
return st.number_input(label, min_value=min_value, max_value=max_value, value=value, step=step, key=key)
|
537 |
+
|
538 |
def create_checkbox(self, label, value=False, key=None):
|
539 |
"""إنشاء خانة اختيار"""
|
540 |
# إنشاء معرف فريد لخانة الاختيار إذا لم يتم توفيره
|
541 |
if key is None:
|
542 |
key = f"checkbox_{label}_{hash(label)}"
|
543 |
+
|
544 |
return st.checkbox(label, value=value, key=key)
|
545 |
+
|
546 |
def create_radio(self, label, options, index=0, key=None):
|
547 |
"""إنشاء أزرار راديو"""
|
548 |
# إنشاء معرف فريد لأزرار الراديو إذا لم يتم توفيره
|
549 |
if key is None:
|
550 |
key = f"radio_{label}_{hash(label)}"
|
551 |
+
|
552 |
return st.radio(label, options, index=index, key=key)
|
553 |
+
|
554 |
def create_progress_bar(self, value, key=None):
|
555 |
"""إنشاء شريط تقدم"""
|
556 |
# إنشاء معرف فريد لشريط التقدم إذا لم يتم توفيره
|
557 |
if key is None:
|
558 |
key = f"progress_bar_{value}_{hash(str(value))}"
|
559 |
+
|
560 |
return st.progress(value, key=key)
|
561 |
+
|
562 |
def create_spinner(self, text="جاري التحميل..."):
|
563 |
"""إنشاء مؤشر تحميل"""
|
564 |
return st.spinner(text)
|
565 |
+
|
566 |
def create_success_message(self, message):
|
567 |
"""إنشاء رسالة نجاح"""
|
568 |
return st.success(message)
|
569 |
+
|
570 |
def create_error_message(self, message):
|
571 |
"""إنشاء رسالة خطأ"""
|
572 |
return st.error(message)
|
573 |
+
|
574 |
def create_warning_message(self, message):
|
575 |
"""إنشاء رسالة تحذير"""
|
576 |
return st.warning(message)
|
577 |
+
|
578 |
def create_info_message(self, message):
|
579 |
"""إنشاء رسالة معلومات"""
|
580 |
+
return st.info(message)
|