Upload 34 files
Browse files- README.md +164 -386
- app.py +168 -0
- assets/images/dashboard.png +0 -0
- assets/images/logo.png +0 -0
- config.py +229 -0
- database/db_connector.py +323 -0
- database/models.py +626 -0
- docs/architecture.md +136 -0
- docs/missing_modules_analysis.md +88 -0
- docs/pricing_module_design.md +714 -0
- docs/pricing_module_requirements.md +155 -0
- modules/ai_assistant/ai_app.py +1067 -0
- modules/ai_assistant/assistant.py +444 -0
- modules/data_analysis/data_analysis_app.py +1022 -0
- modules/document_analysis/analyzer.py +281 -0
- modules/document_analysis/document_app.py +887 -0
- modules/document_comparison/document_comparison_app.py +1003 -0
- modules/maps/maps_app.py +456 -0
- modules/notifications/notifications_app.py +672 -0
- modules/pricing/price_analyzer.py +1695 -0
- modules/pricing/pricing_app.py +1760 -0
- modules/pricing/pricing_engine.py +430 -0
- modules/project_management/project_management_app.py +666 -0
- modules/resources/resources_app.py +1706 -0
- modules/risk_analysis/risk_analyzer.py +585 -0
- modules/translation/translation_app.py +936 -0
- requirements.txt +60 -0
- styling/charts.py +282 -0
- styling/enhanced_ui.py +513 -0
- styling/icons.py +609 -0
- styling/theme.py +541 -0
- tests/test_app.py +601 -0
- tests/test_integrated_system.py +96 -0
- tests/test_ui.py +413 -0
README.md
CHANGED
@@ -1,418 +1,196 @@
|
|
1 |
-
|
2 |
-
license: mit
|
3 |
-
title: نظام تحليل العقود والمناقصات بالذكاء الاصطناعي
|
4 |
-
sdk: streamlit
|
5 |
-
emoji: 📊
|
6 |
-
colorFrom: green
|
7 |
-
colorTo: green
|
8 |
-
sdk_version: 1.43.2
|
9 |
-
---
|
10 |
-
# نظام تحليل العقود والمناقصات | WAHBi-AI v2
|
11 |
-
## شركة شبه الجزيرة للمقاولات
|
12 |
-
|
13 |
-
<div align="center">
|
14 |
-
<img src="https://imgg.io/images/2025/03/23/96dc2ab758cdd5e3267ff8221c2becc2.jpg" alt="شعار النظام" width="200"/>
|
15 |
-
<p>
|
16 |
-
<strong>هذا النظام خاص بشركة شبه الجزيرة للمقاولات، جميع الحقوق محفوظة 2025</strong>
|
17 |
-
</p>
|
18 |
-
<p>
|
19 |
-
<img src="https://img.shields.io/badge/إصدار-2.0.0-blue" alt="Version 2.0.0" />
|
20 |
-
<img src="https://img.shields.io/badge/الحالة-تطوير%20نشط-success" alt="Status: Active Development" />
|
21 |
-
<img src="https://img.shields.io/badge/اللغة-العربية-green" alt="Language: Arabic" />
|
22 |
-
<img src="https://img.shields.io/badge/AI-Claude%20+%20GPT--4o-purple" alt="AI: Claude + GPT-4o" />
|
23 |
-
</p>
|
24 |
-
</div>
|
25 |
-
|
26 |
-
## نبذة عامة
|
27 |
-
|
28 |
-
نظام **WAHBi-AI** هو منصة متكاملة مدعومة بالذكاء الاصطناعي متخصصة في تحليل العقود والمناقصات باللغة العربية. يوفر النظام حلاً شاملاً لدراسة المشاريع الهندسية والمقاولات، مع التركيز على تسعير البنود بدقة عالية، وتحليل المخاطر التعاقدية، وتقييم فرص المشاريع الجديدة.
|
29 |
-
|
30 |
-
يتميز النظام بواجهة مستخدم متطورة باللغة العربية، مدعومة بأحدث تقنيات الذكاء الاصطناعي (Claude 3.7 Sonnet و GPT-4o) لمعالجة وتحليل وثائق المناقصات والعقود المعقدة. كما يتضمن نظام Streamlit المتكامل لحساب التكاليف وإدارة الموارد.
|
31 |
-
|
32 |
-
### فريق التطوير
|
33 |
-
|
34 |
-
- **مطور النظام**: م. تامر الجوهري
|
35 |
-
- **مدير التطوير**: م. بدر وهبي
|
36 |
-
- **المستشار الفني**: م. إسلام عيسى - مدير المكتب الفني (القصيم)
|
37 |
-
- **مدير تكنولوجيا المعلومات**: م. إرشاد يعقوب
|
38 |
-
|
39 |
-
## المتطلبات التقنية
|
40 |
-
|
41 |
-
### متطلبات النظام
|
42 |
-
|
43 |
-
- **نظام التشغيل**: Windows 10/11، MacOS 12+، Linux Ubuntu 20.04+
|
44 |
-
- **المعالج**: Intel Core i5 أو ما يعادله (8 أنوية على الأقل)
|
45 |
-
- **الذاكرة**: 16GB RAM (يوصى بـ 32GB للمشاريع الكبيرة)
|
46 |
-
- **مساحة التخزين**: 10GB للنظام + مساحة إضافية للمستندات والبيانات
|
47 |
-
- **الشاشة**: دقة 1920×1080 أو أعلى
|
48 |
-
|
49 |
-
### متطلبات البرمجيات
|
50 |
|
51 |
-
|
52 |
-
|
53 |
-
- **قاعدة البيانات**: SQLite (للنشر المحلي) أو MySQL 8.0+ (للنشر المؤسسي)
|
54 |
-
- **متصفح**: Chrome 90+، Firefox 88+، Edge 90+
|
55 |
|
56 |
-
##
|
57 |
-
|
58 |
-
### 1. تثبيت بيئة Hybrid Face
|
59 |
-
|
60 |
-
```bash
|
61 |
-
# تثبيت بيئة Hybrid Face
|
62 |
-
curl -sSL https://get.hybridface.io/install.sh | bash
|
63 |
-
|
64 |
-
# تفعيل البيئة
|
65 |
-
source ~/.hybridface/bin/activate
|
66 |
-
```
|
67 |
|
68 |
-
###
|
|
|
69 |
|
70 |
-
|
71 |
-
|
72 |
-
git clone https://gitlab.peninsula-contracting.com/ai-systems/tender-analysis-system.git
|
73 |
-
cd tender-analysis-system
|
74 |
-
```
|
75 |
|
76 |
-
### 3.
|
|
|
77 |
|
78 |
-
|
79 |
-
|
80 |
-
pip install -r requirements.txt
|
81 |
|
82 |
-
|
83 |
-
|
84 |
-
```
|
85 |
|
86 |
-
###
|
|
|
87 |
|
88 |
-
|
89 |
-
|
90 |
-
python setup_db.py --mode=local
|
91 |
|
92 |
-
|
93 |
-
|
94 |
-
```
|
95 |
|
96 |
-
|
97 |
|
98 |
-
|
99 |
-
|
100 |
-
|
|
|
101 |
|
102 |
-
|
103 |
-
|
104 |
-
|
|
|
|
|
105 |
|
106 |
-
|
107 |
|
108 |
-
|
109 |
-
|
110 |
-
hface run --app=tender_system --port=8501
|
111 |
-
```
|
112 |
|
113 |
-
|
114 |
-
|
115 |
-
### النظام الأساسي
|
116 |
-
النظام مبني على هيكلية حديثة تتكون من:
|
117 |
-
- **واجهة مستخدم متطورة**: مطورة باستخدام React.js + TypeScript مع واجهة عربية سلسة
|
118 |
-
- **خادم API قوي**: مبني بلغة Node.js مع Express
|
119 |
-
- **تطبيق تحليل البيانات**: مطور بلغة Python مع Streamlit
|
120 |
-
- **قاعدة بيانات متكاملة**: تعتمد على PostgreSQL مع دعم Supabase
|
121 |
-
- **نماذج ذكاء اصطناعي**: متكاملة مع Claude و GPT-4o
|
122 |
-
|
123 |
-
### مكونات النظام
|
124 |
-
|
125 |
-
```bash
|
126 |
-
tender-analysis-system/
|
127 |
-
├── app.py # نقطة الدخول الرئيسية للتطبيق
|
128 |
-
├── config/ # إعدادات النظام
|
129 |
-
│ ├── settings.py # إعدادات عامة للتطبيق
|
130 |
-
│ ├── ai_models_config.py # إعدادات نماذج الذكاء الاصطناعي
|
131 |
-
│ └── logging_config.py # إعدادات التسجيل والتتبع
|
132 |
-
│
|
133 |
-
├── database/ # وحدة قاعدة البيانات
|
134 |
-
│ ├── db_connector.py # اتصال قاعدة البيانات
|
135 |
-
│ ├── orm/ # وحدة تحديد العلاقات
|
136 |
-
│ │ ├── models.py # نماذج البيانات
|
137 |
-
│ │ └── schemas.py # مخططات البيانات
|
138 |
-
│ └── migrations/ # تحديثات قاعدة البيانات
|
139 |
-
│
|
140 |
-
├── modules/ # وحدات النظام الوظيفية
|
141 |
-
│ ├── ai_assistant/ # المساعد الذكي
|
142 |
-
│ │ ├── assistant_app.py # واجهة المساعد
|
143 |
-
│ │ └── ai_assistant.py # محرك المساعد الذكي
|
144 |
-
│ │
|
145 |
-
│ ├── document_comparison/ # مقارنة المستندات
|
146 |
-
│ │ ├── comparison_app.py # واجهة المقارنة
|
147 |
-
│ │ └── document_comparator.py # محرك المقارنة
|
148 |
-
│ │
|
149 |
-
│ ├── pricing/ # وحدة التسعير والتكاليف
|
150 |
-
│ │ ├── pricing_app.py # واجهة التسعير
|
151 |
-
│ │ ├── construction_calculator.py # حاسبة مواد البناء
|
152 |
-
│ │ └── services/ # خدمات التسعير
|
153 |
-
│ │ ├── templates_catalog/ # كتالوج البنود النموذجية
|
154 |
-
│ │ ├── materials_pricing/ # تسعير المواد
|
155 |
-
│ │ └── labor_rates/ # معدلات العمالة
|
156 |
-
│ │
|
157 |
-
│ ├── resources/ # إدارة الموارد
|
158 |
-
│ │ ├── resources_app.py # واجهة الموارد
|
159 |
-
│ │ └── resource_manager.py # مدير الموارد
|
160 |
-
│ │
|
161 |
-
│ ├── risk_assessment/ # تقييم المخاطر
|
162 |
-
│ │ ├── risk_assessment_app.py # واجهة تقييم المخاطر
|
163 |
-
│ │ └── contract_risk_analyzer.py # محلل مخاطر العقود
|
164 |
-
│ │
|
165 |
-
│ ├── projects/ # إدارة المشاريع
|
166 |
-
│ │ ├── projects_app.py # واجهة المشاريع
|
167 |
-
│ │ └── projects_management.py # إدارة المشاريع
|
168 |
-
│ │
|
169 |
-
│ ├── maps/ # الخرائط التفاعلية
|
170 |
-
│ │ ├── maps_app.py # واجهة الخرائط
|
171 |
-
│ │ └── interactive_map.py # خريطة تفاعلية
|
172 |
-
│ │
|
173 |
-
│ ├── notifications/ # نظام الإشعارات
|
174 |
-
│ │ ├── notifications_app.py # واجهة الإشعارات
|
175 |
-
│ │ └── smart_notifications.py # الإشعارات الذكية
|
176 |
-
│ │
|
177 |
-
│ ├── ai_finetuning/ # تحسين نماذج الذكاء الاصطناعي
|
178 |
-
│ │ ├── finetuning_app.py # واجهة التحسين
|
179 |
-
│ │ └── model_finetuning.py # محرك تحسين النماذج
|
180 |
-
│ │
|
181 |
-
│ ├── voice_narration/ # التعليق الصوتي
|
182 |
-
│ │ ├── voice_narration_app.py # واجهة التعليق الصوتي
|
183 |
-
│ │ └── voice_over_system.py # نظام التعليق الصوتي
|
184 |
-
│ │
|
185 |
-
│ ├── achievements/ # نظام الإنجازات
|
186 |
-
│ │ ├── achievements_app.py # واجهة الإنجازات
|
187 |
-
│ │ └── achievement_system.py # نظام الإنجازات
|
188 |
-
│ │
|
189 |
-
│ └── project_tracker/ # متتبع المشاريع
|
190 |
-
│ ├── tracker_app.py # واجهة المتابعة
|
191 |
-
│ └── status_tracker.py # متتبع الحالة
|
192 |
-
│
|
193 |
-
├── utils/ # أدوات مساعدة
|
194 |
-
│ ├── helpers/ # دوال مساعدة
|
195 |
-
│ │ ├── __init__.py # ملف التهيئة
|
196 |
-
│ │ └── utils.py # وظائف مساعدة
|
197 |
-
│ │
|
198 |
-
│ └── components/ # مكونات واجهة المستخدم
|
199 |
-
│ ├── sidebar.py # الشريط الجانبي
|
200 |
-
│ └── about_system.py # صفحة حول النظام
|
201 |
-
│
|
202 |
-
├── static/ # ملفات ثابتة
|
203 |
-
│ ├── css/ # أنماط CSS
|
204 |
-
│ ├── js/ # سكربتات JavaScript
|
205 |
-
│ ├── images/ # الصور والشعارات
|
206 |
-
│ └── templates/ # قوالب HTML
|
207 |
-
│
|
208 |
-
├── tests/ # اختبارات النظام
|
209 |
-
│ ├── unit/ # اختبارات الوحدات
|
210 |
-
│ └── integration/ # اختبارات التكامل
|
211 |
-
│
|
212 |
-
└── docs/ # التوثيق
|
213 |
-
├── user_manual.md # دليل المستخدم
|
214 |
-
├── api_reference.md # مرجع API
|
215 |
-
├── developer_guide.md # دليل المطور
|
216 |
-
└── deployment_guide.md # دليل النشر
|
217 |
```
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
| المكون | الوصف |
|
222 |
-
|--------|-------|
|
223 |
-
| **المساعد الذكي** | يوفر واجهة محادثة ذكية للاستعلام عن المناقصات والعقود باللغة العربية باستخدام Claude و GPT-4o |
|
224 |
-
| **مقارنة المستندات** | يحلل الاختلافات بين الإصدارات المختلفة للعقود والاتفاقيات بدقة عالية |
|
225 |
-
| **وحدة التسعير** | تقدم نظاماً شاملاً لحساب تكاليف البنود مع إدارة المواد والعمالة والمعدات |
|
226 |
-
| **تقييم المخاطر** | تحدد وتحلل المخاطر في العقود والمناقصات باستخدام خوارزميات متقدمة للذكاء الاصطناعي |
|
227 |
-
| **إدارة المشاريع** | تتبع حالة المناقصات والمشاريع المرساة مع مؤشرات الأداء الرئيسية |
|
228 |
-
| **الخرائط التفاعلية** | تقدم تمثي��اً مرئياً لمواقع المشاريع مع معلومات جغرافية مفصلة |
|
229 |
-
| **نظام الإشعارات** | يدير تنبيهات ذكية للمستخدمين حسب أدوارهم واهتماماتهم |
|
230 |
-
|
231 |
-
## الوحدات الرئيسية
|
232 |
-
|
233 |
-
| الوحدة | الوصف |
|
234 |
-
|--------|-------|
|
235 |
-
| **تحليل المستندات** | تحليل كراسات الشروط والعقود باستخدام معالجة اللغة العربية الطبيعية |
|
236 |
-
| **التسعير المتكامل** | نموذج شامل للتسعير يربط بين مختلف وحدات النظام |
|
237 |
-
| **الموارد والتكاليف** | إدارة بيانات المواد والمعدات والعمالة وتكاليفها |
|
238 |
-
| **تحليل المخاطر** | تقييم المخاطر التعاقدية والمالية والفنية |
|
239 |
-
| **إدارة المشاريع** | متابعة المناقصات والمشاريع المرساة |
|
240 |
-
| **التقارير والتحليلات** | تقارير وتحليلات متقدمة لدعم اتخاذ القرار |
|
241 |
-
| **الذكاء الاصطناعي** | محرك الذكاء الاصطناعي لمختلف وظائف النظام |
|
242 |
-
|
243 |
-
## المزايا الرئيسية للنظام
|
244 |
-
|
245 |
-
### 1. تحليل المستندات باللغة العربية
|
246 |
-
- معالجة متقدمة للغة العربية في كراسات الشروط والعقود
|
247 |
-
- استخراج البنود الرئيسية والشروط الخاصة تلقائيًا
|
248 |
-
- تحديد بنود التكاليف والغرامات والضمانات
|
249 |
-
- مقارنة أتوماتيكية بين الإصدارات المختلفة للمستندات
|
250 |
-
|
251 |
-
### 2. حاسبة التكاليف المتكاملة
|
252 |
-
- تحليل تفصيلي لتكاليف البنود
|
253 |
-
- قاعدة بيانات متكاملة للمواد والعمالة والمعدات
|
254 |
-
- نظام طلب تسعير للمواد والمعدات
|
255 |
-
- حساب تلقائي للمصاريف الإدارية ونسب الربح
|
256 |
-
|
257 |
-
### 3. إدارة الموارد والمشاريع
|
258 |
-
- تتبع استخدام المواد والمعدات عبر المشاريع
|
259 |
-
- جدولة زمنية متكاملة للمشاريع القائمة والمتوقعة
|
260 |
-
- خرائط تفاعلية لمواقع المشاريع
|
261 |
-
- تتبع مؤشرات الأداء الرئيسية للمشاريع
|
262 |
-
|
263 |
-
### 4. نظام الإشعارات الذكية
|
264 |
-
- تنبيهات آلية للمواعيد النهائية والمهام
|
265 |
-
- متابعة حالة المناقصات والردود على الاستفسارات
|
266 |
-
- تنبيهات أتوماتيكية لتغييرات الأسعار والمواد
|
267 |
-
- إشعارات مخصصة لكل مستخدم حسب دوره ومسؤولياته
|
268 |
-
|
269 |
-
### 5. المساعد الذكي المتكامل
|
270 |
-
- محادثة تفاعلية بالعربية مع المساعد الذكي
|
271 |
-
- ملخصات تلقائية للمناقصات والعقود
|
272 |
-
- اقتراحات ذكية لتحسين العروض التقنية والمالية
|
273 |
-
- إجابات فورية على الاستفسارات المتعلقة بالمشاريع
|
274 |
-
|
275 |
-
### 6. التحليل المالي والمخاطر
|
276 |
-
- تقييم ذكي للمخاطر التعاقدية والمالية
|
277 |
-
- تحليل الربحية وتوقعات التدفق النقدي
|
278 |
-
- دراسة متقدمة للأسعار غير المتوازنة
|
279 |
-
- تقارير شاملة عن صحة المشاريع المالية
|
280 |
-
|
281 |
-
## استخدام النظام
|
282 |
-
|
283 |
-
### 1. تسجيل الدخول
|
284 |
-
|
285 |
-
```bash
|
286 |
-
# تسجيل الدخول إلى النظام
|
287 |
-
hface login --username=USER --password=PASS
|
288 |
```
|
289 |
|
290 |
-
###
|
291 |
-
|
292 |
-
```bash
|
293 |
-
# إنشاء مشروع جديد
|
294 |
-
hface project create --name="اسم المشروع" --client="اسم العميل"
|
295 |
-
|
296 |
-
# استيراد كراسة شروط
|
297 |
-
hface document import --project-id=123 --type=tender --file=path/to/document.pdf
|
298 |
```
|
299 |
-
|
300 |
-
### 3. تشغيل التحليل والتسعير
|
301 |
-
|
302 |
-
```bash
|
303 |
-
# تحليل مستندات المناقصة
|
304 |
-
hface analyze --project-id=123 --mode=full
|
305 |
-
|
306 |
-
# تشغيل التسعير الشامل
|
307 |
-
hface pricing --project-id=123 --strategy=comprehensive
|
308 |
```
|
309 |
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
- 🔄 تحسين حاسبة التكاليف المتقدمة
|
322 |
-
- 🔄 إضافة نظام طلب تسعير المواد والمعدات
|
323 |
-
- 🔄 تح��ين واجهة المستخدم وتجربة الاستخدام
|
324 |
-
|
325 |
-
### المرحلة الثالثة (2-3 أسابيع)
|
326 |
-
- نظام حساب المحتوى المحلي
|
327 |
-
- تحليل التسعير غير المتوازن
|
328 |
-
- إضافة نظام الخرائط التفاعلي للمواقع
|
329 |
-
- تطوير نظام الإشعارات الذكية
|
330 |
-
|
331 |
-
### المرحلة الرابعة (3-4 أسابيع)
|
332 |
-
- نظام متكامل لإدارة المشاريع والمواقع
|
333 |
-
- تحسين دقة التحليل من خلال تدريب النماذج
|
334 |
-
- إضافة تحليل متقدم للمخاطر والفرص
|
335 |
-
- تطوير نظام التقارير الشاملة
|
336 |
-
|
337 |
-
### المرحلة الخامسة (نهائية)
|
338 |
-
- تطبيق للهواتف الذكية للوصول للنظام من المواقع
|
339 |
-
- دعم التحليل ثلاثي الأبعاد للمواقع والكميات
|
340 |
-
- تكامل مع برامج التصميم وحساب الكميات
|
341 |
-
- نظام التنبؤ بالفرص المستقبلية
|
342 |
-
|
343 |
-
## نقل قاعدة البيانات إلى Supabase
|
344 |
-
|
345 |
-
يمكن نقل قاعدة البيانات من Replit PostgreSQL إلى Supabase باتباع الخطوات التالية:
|
346 |
-
|
347 |
-
### 1. تصدير البيانات من Replit
|
348 |
-
```bash
|
349 |
-
# تصدير هيكل قاعدة البيانات
|
350 |
-
pg_dump -s -f schema.sql $DATABASE_URL
|
351 |
-
|
352 |
-
# تصدير البيانات
|
353 |
-
pg_dump -a -f data.sql $DATABASE_URL
|
354 |
```
|
355 |
-
|
356 |
-
### 2. إنشاء مشروع في Supabase
|
357 |
-
- قم بإنشاء حساب على [Supabase](https://supabase.com) إذا لم يكن لديك حساب
|
358 |
-
- أنشئ مشروعًا جديدًا واختر اسمًا مناسبًا (مثل "wahbi-ai")
|
359 |
-
- احصل على معلومات الاتصال من لوحة التحكم (Settings > Database)
|
360 |
-
|
361 |
-
### 3. استيراد البيانات إلى Supabase
|
362 |
-
```bash
|
363 |
-
# استيراد هيكل قاعدة البيانات
|
364 |
-
psql [SUPABASE_CONNECTION_STRING] -f schema.sql
|
365 |
-
|
366 |
-
# استيراد البيانات
|
367 |
-
psql [SUPABASE_CONNECTION_STRING] -f data.sql
|
368 |
```
|
369 |
|
370 |
-
###
|
371 |
-
قم بتحديث ملف `.env` بمعلومات اتصال Supabase:
|
372 |
-
```
|
373 |
-
DATABASE_URL=postgresql://postgres:[PASSWORD]@db.[PROJECT_ID].supabase.co:5432/postgres
|
374 |
-
PGHOST=db.[PROJECT_ID].supabase.co
|
375 |
-
PGPORT=5432
|
376 |
-
PGUSER=postgres
|
377 |
-
PGPASSWORD=[PASSWORD]
|
378 |
-
PGDATABASE=postgres
|
379 |
```
|
380 |
-
|
381 |
-
### 5. اختبار الاتصال
|
382 |
-
تحقق من الاتصال بقاعدة البيانات الجديدة:
|
383 |
-
```python
|
384 |
-
import psycopg2
|
385 |
-
from dotenv import load_dotenv
|
386 |
-
import os
|
387 |
-
|
388 |
-
load_dotenv()
|
389 |
-
|
390 |
-
connection = psycopg2.connect(os.getenv("DATABASE_URL"))
|
391 |
-
cursor = connection.cursor()
|
392 |
-
cursor.execute("SELECT NOW()")
|
393 |
-
result = cursor.fetchone()
|
394 |
-
print("Connected to Supabase database:", result)
|
395 |
-
cursor.close()
|
396 |
-
connection.close()
|
397 |
```
|
398 |
|
399 |
-
##
|
400 |
-
|
401 |
-
### المستندات والتوثيق
|
402 |
-
- **دليل المستخدم**: `/docs/user_manual.md`
|
403 |
-
- **التوثيق التقني**: `/docs/technical_docs.md`
|
404 |
-
- **واجهة المساعدة التفاعلية**: متاحة داخل التطبيق عبر زر "المساعدة"
|
405 |
-
|
406 |
-
### الدعم الفني
|
407 |
-
للحصول على الدعم الفني، يرجى التواصل مع:
|
408 |
|
409 |
-
|
410 |
-
-
|
411 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
412 |
|
413 |
---
|
414 |
|
415 |
-
|
416 |
-
<strong>شركة شبه الجزيرة للمقاولات - نظام WAHBi-AI لتحليل المناقصات</strong><br>
|
417 |
-
جميع الحقوق محفوظة © 2025
|
418 |
-
</div>
|
|
|
1 |
+
# نظام إدارة المناقصات
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
+
## نظرة عامة
|
4 |
+
نظام إدارة المناقصات هو تطبيق متكامل مصمم لمساعدة الشركات والمؤسسات في إدارة عمليات المناقصات والعطاءات بكفاءة عالية. يوفر النظام مجموعة من الوحدات المتكاملة التي تغطي جميع جوانب إدارة المناقصات، بدءاً من تحليل المستندات وحتى إعداد التقارير النهائية.
|
|
|
|
|
5 |
|
6 |
+
## الوحدات الرئيسية
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
+
### 1. تحليل المستندات
|
9 |
+
تمكن هذه الوحدة من استخراج المعلومات الرئيسية من مستندات المناقصات بشكل آلي، مما يوفر الوقت والجهد ويقلل من الأخطاء البشرية.
|
10 |
|
11 |
+
### 2. التسعير
|
12 |
+
توفر هذه الوحدة أدوات متقدمة لإعداد جداول الكميات وتسعير بنود المناقصة، مع إمكانية تحليل الأسعار ومقارنتها بالأسعار المرجعية.
|
|
|
|
|
|
|
13 |
|
14 |
+
### 3. الموارد
|
15 |
+
تساعد في إدارة الموارد البشرية والمعدات والمواد اللازمة لتنفيذ المشاريع، مع إمكانية تخصيص الموارد للمشاريع المختلفة.
|
16 |
|
17 |
+
### 4. تحليل المخاطر
|
18 |
+
تم��ن من تحديد وتقييم المخاطر المحتملة في المناقصات والمشاريع، مع اقتراح إجراءات للتخفيف من هذه المخاطر.
|
|
|
19 |
|
20 |
+
### 5. إدارة المشاريع
|
21 |
+
توفر أدوات لمتابعة تنفيذ المشاريع وإدارة المهام والجداول الزمنية، مع إمكانية تتبع التقدم في العمل.
|
|
|
22 |
|
23 |
+
### 6. تحليل البيانات
|
24 |
+
تمكن من تحليل بيانات المناقصات واستخراج المؤشرات والاتجاهات، مما يساعد في اتخاذ القرارات المستقبلية.
|
25 |
|
26 |
+
### 7. التقارير
|
27 |
+
توفر إمكانية إعداد التقارير الفنية والمالية المختلفة، مع خيارات متعددة للتصدير والمشاركة.
|
|
|
28 |
|
29 |
+
### 8. الذكاء الاصطناعي
|
30 |
+
تستخدم تقنيات الذكاء الاصطناعي في تحليل المناقصات واقتراح الحلول المناسبة، مع إمكانية التعلم من البيانات السابقة.
|
|
|
31 |
|
32 |
+
## متطلبات النظام
|
33 |
|
34 |
+
### متطلبات البرمجيات
|
35 |
+
- Python 3.8 أو أحدث
|
36 |
+
- Streamlit 1.32.0 أو أحدث
|
37 |
+
- المكتبات المذكورة في ملف requirements.txt
|
38 |
|
39 |
+
### متطلبات الأجهزة
|
40 |
+
- معالج: Intel Core i5 أو ما يعادله
|
41 |
+
- ذاكرة: 8 GB RAM أو أكثر
|
42 |
+
- مساحة تخزين: 1 GB من المساحة الحرة
|
43 |
+
- اتصال بالإنترنت (لبعض الوظائف)
|
44 |
|
45 |
+
## التثبيت والإعداد
|
46 |
|
47 |
+
### 1. تثبيت Python
|
48 |
+
تأكد من تثبيت Python 3.8 أو أحدث على جهازك. يمكنك تنزيله من [الموقع الرسمي](https://www.python.org/downloads/).
|
|
|
|
|
49 |
|
50 |
+
### 2. تنزيل النظام
|
51 |
+
قم بتنزيل أو استنساخ مستودع النظام:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
```
|
53 |
+
git clone https://github.com/your-username/tender-management-system.git
|
54 |
+
cd tender-management-system
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
```
|
56 |
|
57 |
+
### 3. إنشاء بيئة افتراضية (اختياري ولكن موصى به)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
```
|
59 |
+
python -m venv venv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
```
|
61 |
|
62 |
+
### 4. تفعيل البيئة الافتراضية
|
63 |
+
- في Windows:
|
64 |
+
```
|
65 |
+
venv\Scripts\activate
|
66 |
+
```
|
67 |
+
- في macOS/Linux:
|
68 |
+
```
|
69 |
+
source venv/bin/activate
|
70 |
+
```
|
71 |
+
|
72 |
+
### 5. تثبيت المتطلبات
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
```
|
74 |
+
pip install -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
```
|
76 |
|
77 |
+
### 6. تشغيل التطبيق
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
```
|
79 |
+
streamlit run app.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
```
|
81 |
|
82 |
+
## استخدام النظام
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
|
84 |
+
### تسجيل الدخول
|
85 |
+
- اسم المستخدم الافتراضي: admin
|
86 |
+
- كلمة المرور الافتراضية: admin
|
87 |
+
|
88 |
+
### التنقل بين الوحدات
|
89 |
+
يمكنك التنقل بين وحدات النظام المختلفة باستخدام القائمة الجانبية.
|
90 |
+
|
91 |
+
### تغيير الإعدادات
|
92 |
+
يمكنك تغيير إعدادات النظام مثل اللغة والسمة من خلال قسم الإعدادات في القائمة الجانبية.
|
93 |
+
|
94 |
+
## وحدة التسعير
|
95 |
+
|
96 |
+
### الميزات الرئيسية
|
97 |
+
- إنشاء وتحرير جداول الكميات
|
98 |
+
- تسعير البنود باستخدام قاعدة بيانات الأسعار المرجعية
|
99 |
+
- تحليل التكاليف وحساب الهوامش
|
100 |
+
- مقارنة الأسعار مع المنافسين
|
101 |
+
- تصدير جداول الكميات بتنسيقات مختلفة
|
102 |
+
|
103 |
+
### كيفية الاستخدام
|
104 |
+
1. انتقل إلى وحدة التسعير من القائمة الجانبية
|
105 |
+
2. اختر المناقصة التي تريد العمل عليها أو أنشئ مناقصة جديدة
|
106 |
+
3. قم بإضافة بنود جدول الكميات أو استيرادها من ملف خارجي
|
107 |
+
4. قم بتسعير البنود يدوياً أو باستخدام الأسعار المرجعية
|
108 |
+
5. قم بتحليل التكاليف وحساب الهوامش
|
109 |
+
6. قم بتصدير جدول الكميات النهائي
|
110 |
+
|
111 |
+
## وحدة الذكاء الاصطناعي
|
112 |
+
|
113 |
+
### الميزات الرئيسية
|
114 |
+
- تحليل مستندات المناقصات باستخدام معالجة اللغة الطبيعية
|
115 |
+
- اقتراح الأسعار المناسبة بناءً على البيانات السابقة
|
116 |
+
- تقييم فرص الفوز بالمناقصات
|
117 |
+
- تحليل المنافسين واستراتيجياتهم
|
118 |
+
- توليد تقارير وملخصات ذكية
|
119 |
+
|
120 |
+
### كيفية الاستخدام
|
121 |
+
1. انتقل إلى وحدة الذكاء الاصطناعي من القائمة الجانبية
|
122 |
+
2. قم بتحميل مستندات المناقصة للتحليل
|
123 |
+
3. اختر نوع التحليل الذي تريد إجراءه
|
124 |
+
4. راجع النتائج والتوصيات
|
125 |
+
5. قم بتصدير التقارير والملخصات
|
126 |
+
|
127 |
+
## وحدة تحليل البيانات
|
128 |
+
|
129 |
+
### الميزات الرئيسية
|
130 |
+
- عرض لوحات معلومات تفاعلية للمناقصات
|
131 |
+
- تحليل اتجاهات الأسعار والتكاليف
|
132 |
+
- تحليل أداء المنافسين
|
133 |
+
- تحليل نسب النجاح والفشل
|
134 |
+
- تصدير التقارير والرسوم البيانية
|
135 |
+
|
136 |
+
### كيفية الاستخدام
|
137 |
+
1. انتقل إلى وحدة تحليل البيانات من القائمة الجانبية
|
138 |
+
2. اختر نوع التحليل الذي تريد إجراءه
|
139 |
+
3. حدد نطاق البيانات والفترة الزمنية
|
140 |
+
4. قم بتخصيص الرسوم البيانية والتقارير
|
141 |
+
5. قم بتصدير النتائج بالتنسيق المطلوب
|
142 |
+
|
143 |
+
## وحدة الموارد
|
144 |
+
|
145 |
+
### الميزات الرئيسية
|
146 |
+
- إدارة الموارد البشرية والمعدات والمواد
|
147 |
+
- تخصيص الموارد للمشاريع المختلفة
|
148 |
+
- تتبع توافر الموارد واستخدامها
|
149 |
+
- تخطيط الاحتياجات المستقبلية من الموارد
|
150 |
+
- تحليل تكاليف الموارد وكفاءتها
|
151 |
+
|
152 |
+
### كيفية الاستخدام
|
153 |
+
1. انتقل إلى وحدة الموارد من القائمة الجانبية
|
154 |
+
2. استعرض الموارد المتاحة (الموظفين، المعدات، المواد)
|
155 |
+
3. قم بتخصيص الموارد للمشاريع
|
156 |
+
4. قم بتحليل استخدام الموارد وتكاليفها
|
157 |
+
5. قم بتخطيط الاحتياجات المستقبلية من الموارد
|
158 |
+
|
159 |
+
## استكشاف الأخطاء وإصلاحها
|
160 |
+
|
161 |
+
### مشاكل تسجيل الدخول
|
162 |
+
- تأكد من إدخال اسم المستخدم وكلمة المرور بشكل صحيح
|
163 |
+
- تأكد من تفعيل لوحة المفاتيح باللغة الصحيحة
|
164 |
+
- إذا نسيت كلمة المرور، اتصل بمسؤول النظام
|
165 |
+
|
166 |
+
### مشاكل في عرض البيانات
|
167 |
+
- تأكد من اتصالك بالإنترنت إذا كانت البيانات تُجلب من مصدر خارجي
|
168 |
+
- قم بتحديث الصفحة أو إعادة تشغيل التطبيق
|
169 |
+
- تأكد من وجود البيانات المطلوبة في قاعدة البيانات
|
170 |
+
|
171 |
+
### مشاكل في تشغيل التطبيق
|
172 |
+
- تأكد من تثبيت جميع المتطلبات بشكل صحيح
|
173 |
+
- تأكد من استخدام إصدار Python المناسب
|
174 |
+
- تحقق من وجود جميع الملفات والمجلدات المطلوبة
|
175 |
+
|
176 |
+
## الدعم والتواصل
|
177 |
+
|
178 |
+
للحصول على المساعدة أو الإبلاغ عن مشكلة، يرجى التواصل معنا عبر:
|
179 |
+
- البريد الإلكتروني: [email protected]
|
180 |
+
- الهاتف: +966 12 345 6789
|
181 |
+
- الموقع الإلكتروني: www.tender-system.com/support
|
182 |
+
|
183 |
+
## الترخيص
|
184 |
+
|
185 |
+
هذا النظام مرخص بموجب رخصة MIT. راجع ملف LICENSE للحصول على مزيد من المعلومات.
|
186 |
+
|
187 |
+
## شكر وتقدير
|
188 |
+
|
189 |
+
نشكر جميع المساهمين في تطوير هذا النظام، ونخص بالذكر:
|
190 |
+
- فريق تطوير البرمجيات
|
191 |
+
- فريق اختبار الجودة
|
192 |
+
- المستخدمين الذين قدموا ملاحظات قيمة
|
193 |
|
194 |
---
|
195 |
|
196 |
+
© 2025 نظام إدارة المناقصات. جميع الحقوق محفوظة.
|
|
|
|
|
|
app.py
ADDED
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import sys
|
3 |
+
from pathlib import Path
|
4 |
+
|
5 |
+
# إضافة مسار المشروع للنظام
|
6 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
7 |
+
|
8 |
+
# استيراد الوحدات
|
9 |
+
from modules.document_analysis.document_app import DocumentAnalysisApp
|
10 |
+
from modules.pricing.pricing_app import PricingApp
|
11 |
+
from modules.resources.resources_app import ResourcesApp
|
12 |
+
from modules.risk_analysis.risk_analyzer import RiskAnalysisApp
|
13 |
+
from modules.project_management.project_management_app import ProjectsApp
|
14 |
+
from modules.maps.maps_app import MapsApp
|
15 |
+
from modules.notifications.notifications_app import NotificationsApp
|
16 |
+
from modules.document_comparison.document_comparison_app import DocumentComparisonApp
|
17 |
+
from modules.translation.translation_app import TranslationApp
|
18 |
+
from modules.ai_assistant.ai_app import AIAssistantApp
|
19 |
+
from modules.data_analysis.data_analysis_app import DataAnalysisApp
|
20 |
+
from styling.enhanced_ui import UIEnhancer
|
21 |
+
|
22 |
+
# تكوين الصفحة
|
23 |
+
st.set_page_config(
|
24 |
+
page_title="نظام تحليل المناقصات",
|
25 |
+
page_icon="📊",
|
26 |
+
layout="wide",
|
27 |
+
initial_sidebar_state="expanded",
|
28 |
+
menu_items={
|
29 |
+
'Get Help': 'https://www.example.com/help',
|
30 |
+
'Report a bug': "https://www.example.com/bug",
|
31 |
+
'About': "### نظام تحليل المناقصات\nالإصدار 2.0.0"
|
32 |
+
}
|
33 |
+
)
|
34 |
+
|
35 |
+
# تطبيق التنسيق العام
|
36 |
+
ui_enhancer = UIEnhancer(page_title="نظام تحليل المناقصات", page_icon="📊")
|
37 |
+
ui_enhancer.apply_global_styles()
|
38 |
+
|
39 |
+
# إنشاء قائمة العناصر
|
40 |
+
menu_items = [
|
41 |
+
{"name": "لوحة المعلومات", "icon": "house"},
|
42 |
+
{"name": "المناقصات والعقود", "icon": "file-text"},
|
43 |
+
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
44 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
45 |
+
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
46 |
+
{"name": "الموارد والتكاليف", "icon": "people"},
|
47 |
+
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
48 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
49 |
+
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
50 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
51 |
+
{"name": "الإشعارات", "icon": "bell"},
|
52 |
+
{"name": "مقارنة المستندات", "icon": "files"},
|
53 |
+
{"name": "الترجمة", "icon": "translate"},
|
54 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
55 |
+
{"name": "تحليل البيانات", "icon": "bar-chart"},
|
56 |
+
{"name": "الإعدادات", "icon": "gear"}
|
57 |
+
]
|
58 |
+
|
59 |
+
# إنشاء الشريط الجانبي
|
60 |
+
selected = ui_enhancer.create_sidebar(menu_items)
|
61 |
+
|
62 |
+
# تحديد الوحدة المطلوبة بناءً على اختيار المستخدم
|
63 |
+
if selected == "لوحة المعلومات":
|
64 |
+
ui_enhancer.create_header("لوحة المعلومات", "نظرة عامة على المناقصات والمشاريع")
|
65 |
+
|
66 |
+
# عرض لوحة المعلومات
|
67 |
+
col1, col2, col3 = st.columns(3)
|
68 |
+
|
69 |
+
with col1:
|
70 |
+
ui_enhancer.create_metric_card("المناقصات النشطة", "12", "+3", ui_enhancer.COLORS['primary'])
|
71 |
+
|
72 |
+
with col2:
|
73 |
+
ui_enhancer.create_metric_card("المشاريع قيد التنفيذ", "8", "+1", ui_enhancer.COLORS['success'])
|
74 |
+
|
75 |
+
with col3:
|
76 |
+
ui_enhancer.create_metric_card("المناقصات المقدمة", "24", "+5", ui_enhancer.COLORS['info'])
|
77 |
+
|
78 |
+
# عرض الإشعارات الأخيرة
|
79 |
+
st.markdown("### الإشعارات الأخيرة")
|
80 |
+
|
81 |
+
notifications = [
|
82 |
+
{"title": "موعد تقديم مناقصة", "project": "إنشاء مبنى مستشفى الولادة والأطفال", "date": "2025-04-05", "priority": "عالية"},
|
83 |
+
{"title": "تحديث مستندات", "project": "صيانة وتطوير طريق الملك عبدالله", "date": "2025-03-28", "priority": "متوسطة"},
|
84 |
+
{"title": "اجتماع مراجعة التسعير", "project": "إنشاء محطة معالجة مياه الصرف الصحي", "date": "2025-03-25", "priority": "عالية"}
|
85 |
+
]
|
86 |
+
|
87 |
+
for notification in notifications:
|
88 |
+
with st.container():
|
89 |
+
col1, col2 = st.columns([4, 1])
|
90 |
+
with col1:
|
91 |
+
st.markdown(f"**{notification['title']}** - {notification['project']}")
|
92 |
+
st.caption(f"التاريخ: {notification['date']} | الأولوية: {notification['priority']}")
|
93 |
+
with col2:
|
94 |
+
st.button("عرض", key=f"view_{notification['title']}")
|
95 |
+
st.divider()
|
96 |
+
|
97 |
+
elif selected == "تحليل المستندات":
|
98 |
+
document_app = DocumentAnalysisApp()
|
99 |
+
document_app.run()
|
100 |
+
|
101 |
+
elif selected == "نظام التسعير":
|
102 |
+
pricing_app = PricingApp()
|
103 |
+
pricing_app.run()
|
104 |
+
|
105 |
+
elif selected == "الموارد والتكاليف":
|
106 |
+
resources_app = ResourcesApp()
|
107 |
+
resources_app.run()
|
108 |
+
|
109 |
+
elif selected == "تحليل المخاطر":
|
110 |
+
risk_app = RiskAnalysisApp()
|
111 |
+
risk_app.run()
|
112 |
+
|
113 |
+
elif selected == "إدارة المشاريع":
|
114 |
+
projects_app = ProjectsApp()
|
115 |
+
projects_app.run()
|
116 |
+
|
117 |
+
elif selected == "الخرائط والمواقع":
|
118 |
+
maps_app = MapsApp()
|
119 |
+
maps_app.run()
|
120 |
+
|
121 |
+
elif selected == "الإشعارات":
|
122 |
+
notifications_app = NotificationsApp()
|
123 |
+
notifications_app.run()
|
124 |
+
|
125 |
+
elif selected == "مقارنة المستندات":
|
126 |
+
document_comparison_app = DocumentComparisonApp()
|
127 |
+
document_comparison_app.run()
|
128 |
+
|
129 |
+
elif selected == "الترجمة":
|
130 |
+
translation_app = TranslationApp()
|
131 |
+
translation_app.run()
|
132 |
+
|
133 |
+
elif selected == "المساعد الذكي":
|
134 |
+
ai_app = AIAssistantApp()
|
135 |
+
ai_app.run()
|
136 |
+
|
137 |
+
elif selected == "تحليل البيانات":
|
138 |
+
data_analysis_app = DataAnalysisApp()
|
139 |
+
data_analysis_app.run()
|
140 |
+
|
141 |
+
elif selected == "الإعدادات":
|
142 |
+
ui_enhancer.create_header("الإعدادات", "إعدادات النظام والحساب")
|
143 |
+
|
144 |
+
# عرض إعدادات النظام
|
145 |
+
st.markdown("### إعدادات النظام")
|
146 |
+
|
147 |
+
tabs = st.tabs(["إعدادات عامة", "الواجهة", "الأمان", "مفاتيح API"])
|
148 |
+
|
149 |
+
with tabs[0]:
|
150 |
+
st.checkbox("تفعيل الإشعارات", value=True)
|
151 |
+
st.checkbox("حفظ تلقائي للبيانات", value=True)
|
152 |
+
st.selectbox("اللغة", ["العربية", "English"])
|
153 |
+
st.selectbox("المنطقة الزمنية", ["توقيت الرياض (GMT+3)", "توقيت جرينتش (GMT)"])
|
154 |
+
|
155 |
+
with tabs[1]:
|
156 |
+
st.radio("النمط", ["فاتح", "داكن", "تلقائي (حسب نظام التشغيل)"])
|
157 |
+
st.slider("حجم الخط", 12, 20, 16)
|
158 |
+
st.color_picker("لون التمييز", "#1E88E5")
|
159 |
+
|
160 |
+
with tabs[2]:
|
161 |
+
st.checkbox("تفعيل المصادقة الثنائية", value=False)
|
162 |
+
st.number_input("مدة الجلسة (دقائق)", min_value=5, max_value=120, value=30)
|
163 |
+
st.button("تغيير كلمة المرور")
|
164 |
+
|
165 |
+
with tabs[3]:
|
166 |
+
st.text_input("مفتاح OpenAI API", type="password")
|
167 |
+
st.text_input("مفتاح Google Maps API", type="password")
|
168 |
+
st.button("حفظ مفاتيح API")
|
assets/images/dashboard.png
ADDED
![]() |
assets/images/logo.png
ADDED
![]() |
config.py
ADDED
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
ملف الإعدادات لنظام إدارة المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import json
|
7 |
+
from pathlib import Path
|
8 |
+
|
9 |
+
class AppConfig:
|
10 |
+
"""فئة إعدادات التطبيق"""
|
11 |
+
|
12 |
+
def __init__(self):
|
13 |
+
"""تهيئة الإعدادات"""
|
14 |
+
# المسارات الأساسية
|
15 |
+
self.app_dir = os.path.dirname(os.path.abspath(__file__))
|
16 |
+
self.assets_dir = os.path.join(self.app_dir, "assets")
|
17 |
+
self.data_dir = os.path.join(self.app_dir, "data")
|
18 |
+
|
19 |
+
# إنشاء المجلدات إذا لم تكن موجودة
|
20 |
+
Path(self.assets_dir).mkdir(parents=True, exist_ok=True)
|
21 |
+
Path(self.data_dir).mkdir(parents=True, exist_ok=True)
|
22 |
+
|
23 |
+
# مسارات الأصول
|
24 |
+
self.icons_dir = os.path.join(self.assets_dir, "icons")
|
25 |
+
self.images_dir = os.path.join(self.assets_dir, "images")
|
26 |
+
self.fonts_dir = os.path.join(self.assets_dir, "fonts")
|
27 |
+
|
28 |
+
# إنشاء مجلدات الأصول إذا لم تكن موجودة
|
29 |
+
Path(self.icons_dir).mkdir(parents=True, exist_ok=True)
|
30 |
+
Path(self.images_dir).mkdir(parents=True, exist_ok=True)
|
31 |
+
Path(self.fonts_dir).mkdir(parents=True, exist_ok=True)
|
32 |
+
|
33 |
+
# مسارات البيانات
|
34 |
+
self.database_file = os.path.join(self.data_dir, "database.db")
|
35 |
+
self.settings_file = os.path.join(self.data_dir, "settings.json")
|
36 |
+
self.charts_dir = os.path.join(self.data_dir, "charts")
|
37 |
+
|
38 |
+
# إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
|
39 |
+
Path(self.charts_dir).mkdir(parents=True, exist_ok=True)
|
40 |
+
|
41 |
+
# تحميل الإعدادات
|
42 |
+
self.settings = self._load_settings()
|
43 |
+
|
44 |
+
def _load_settings(self):
|
45 |
+
"""تحميل الإعدادات من ملف JSON"""
|
46 |
+
default_settings = {
|
47 |
+
"app": {
|
48 |
+
"name": "نظام إدارة المناقصات",
|
49 |
+
"version": "1.0.0",
|
50 |
+
"language": "ar",
|
51 |
+
"theme": "light",
|
52 |
+
"font": "Cairo",
|
53 |
+
"font_size": 12
|
54 |
+
},
|
55 |
+
"database": {
|
56 |
+
"type": "sqlite",
|
57 |
+
"path": self.database_file
|
58 |
+
},
|
59 |
+
"ui": {
|
60 |
+
"window_width": 1200,
|
61 |
+
"window_height": 800,
|
62 |
+
"sidebar_width": 250
|
63 |
+
},
|
64 |
+
"notifications": {
|
65 |
+
"enabled": True,
|
66 |
+
"email_enabled": True,
|
67 |
+
"email_server": "smtp.example.com",
|
68 |
+
"email_port": 587,
|
69 |
+
"email_username": "",
|
70 |
+
"email_password": ""
|
71 |
+
},
|
72 |
+
"reports": {
|
73 |
+
"default_format": "pdf",
|
74 |
+
"default_path": os.path.join(self.data_dir, "reports")
|
75 |
+
},
|
76 |
+
"backup": {
|
77 |
+
"auto_backup": True,
|
78 |
+
"backup_frequency": "weekly",
|
79 |
+
"backup_path": os.path.join(self.data_dir, "backups"),
|
80 |
+
"max_backups": 10
|
81 |
+
}
|
82 |
+
}
|
83 |
+
|
84 |
+
# إذا كان ملف الإعدادات موجودًا، قم بتحميله
|
85 |
+
if os.path.exists(self.settings_file):
|
86 |
+
try:
|
87 |
+
with open(self.settings_file, "r", encoding="utf-8") as f:
|
88 |
+
settings = json.load(f)
|
89 |
+
|
90 |
+
# دمج الإعدادات المحملة مع الإعدادات الافتراضية
|
91 |
+
self._merge_settings(default_settings, settings)
|
92 |
+
return default_settings
|
93 |
+
except Exception as e:
|
94 |
+
print(f"خطأ في تحميل الإعدادات: {str(e)}")
|
95 |
+
return default_settings
|
96 |
+
else:
|
97 |
+
# إنشاء ملف الإعدادات الافتراضية
|
98 |
+
self._save_settings(default_settings)
|
99 |
+
return default_settings
|
100 |
+
|
101 |
+
def _merge_settings(self, default_settings, loaded_settings):
|
102 |
+
"""دمج الإعدادات المحملة مع الإعدادات الافتراضية"""
|
103 |
+
for key, value in loaded_settings.items():
|
104 |
+
if key in default_settings:
|
105 |
+
if isinstance(value, dict) and isinstance(default_settings[key], dict):
|
106 |
+
self._merge_settings(default_settings[key], value)
|
107 |
+
else:
|
108 |
+
default_settings[key] = value
|
109 |
+
|
110 |
+
def _save_settings(self, settings=None):
|
111 |
+
"""حفظ الإعدادات إلى ملف JSON"""
|
112 |
+
if settings is None:
|
113 |
+
settings = self.settings
|
114 |
+
|
115 |
+
try:
|
116 |
+
with open(self.settings_file, "w", encoding="utf-8") as f:
|
117 |
+
json.dump(settings, f, ensure_ascii=False, indent=4)
|
118 |
+
return True
|
119 |
+
except Exception as e:
|
120 |
+
print(f"خطأ في حفظ الإعدادات: {str(e)}")
|
121 |
+
return False
|
122 |
+
|
123 |
+
def get_setting(self, section, key, default=None):
|
124 |
+
"""الحصول على قي��ة إعداد معين"""
|
125 |
+
try:
|
126 |
+
return self.settings[section][key]
|
127 |
+
except KeyError:
|
128 |
+
return default
|
129 |
+
|
130 |
+
def set_setting(self, section, key, value):
|
131 |
+
"""تعيين قيمة إعداد معين"""
|
132 |
+
if section not in self.settings:
|
133 |
+
self.settings[section] = {}
|
134 |
+
|
135 |
+
self.settings[section][key] = value
|
136 |
+
self._save_settings()
|
137 |
+
|
138 |
+
def get_app_name(self):
|
139 |
+
"""الحصول على اسم التطبيق"""
|
140 |
+
return self.get_setting("app", "name", "نظام إدارة المناقصات")
|
141 |
+
|
142 |
+
def get_app_version(self):
|
143 |
+
"""الحصول على إصدار التطبيق"""
|
144 |
+
return self.get_setting("app", "version", "1.0.0")
|
145 |
+
|
146 |
+
def get_language(self):
|
147 |
+
"""الحصول على لغة التطبيق"""
|
148 |
+
return self.get_setting("app", "language", "ar")
|
149 |
+
|
150 |
+
def set_language(self, language):
|
151 |
+
"""تعيين لغة التطبيق"""
|
152 |
+
self.set_setting("app", "language", language)
|
153 |
+
|
154 |
+
def get_theme(self):
|
155 |
+
"""الحصول على نمط التطبيق"""
|
156 |
+
return self.get_setting("app", "theme", "light")
|
157 |
+
|
158 |
+
def set_theme(self, theme):
|
159 |
+
"""تعيين نمط التطبيق"""
|
160 |
+
self.set_setting("app", "theme", theme)
|
161 |
+
|
162 |
+
def get_font(self):
|
163 |
+
"""الحصول على خط التطبيق"""
|
164 |
+
return self.get_setting("app", "font", "Cairo")
|
165 |
+
|
166 |
+
def set_font(self, font):
|
167 |
+
"""تعيين خط التطبيق"""
|
168 |
+
self.set_setting("app", "font", font)
|
169 |
+
|
170 |
+
def get_font_size(self):
|
171 |
+
"""الحصول على حجم خط التطبيق"""
|
172 |
+
return self.get_setting("app", "font_size", 12)
|
173 |
+
|
174 |
+
def set_font_size(self, font_size):
|
175 |
+
"""تعيين حجم خط التطبيق"""
|
176 |
+
self.set_setting("app", "font_size", font_size)
|
177 |
+
|
178 |
+
def get_window_size(self):
|
179 |
+
"""الحصول على حجم نافذة التطبيق"""
|
180 |
+
width = self.get_setting("ui", "window_width", 1200)
|
181 |
+
height = self.get_setting("ui", "window_height", 800)
|
182 |
+
return (width, height)
|
183 |
+
|
184 |
+
def set_window_size(self, width, height):
|
185 |
+
"""تعيين حجم نافذة التطبيق"""
|
186 |
+
self.set_setting("ui", "window_width", width)
|
187 |
+
self.set_setting("ui", "window_height", height)
|
188 |
+
|
189 |
+
def get_sidebar_width(self):
|
190 |
+
"""الحصول على عرض الشريط الجانبي"""
|
191 |
+
return self.get_setting("ui", "sidebar_width", 250)
|
192 |
+
|
193 |
+
def set_sidebar_width(self, width):
|
194 |
+
"""تعيين عرض الشريط الجانبي"""
|
195 |
+
self.set_setting("ui", "sidebar_width", width)
|
196 |
+
|
197 |
+
def get_database_config(self):
|
198 |
+
"""الحصول على إعدادات قاعدة البيانات"""
|
199 |
+
return self.settings.get("database", {
|
200 |
+
"type": "sqlite",
|
201 |
+
"path": self.database_file
|
202 |
+
})
|
203 |
+
|
204 |
+
def get_notifications_config(self):
|
205 |
+
"""الحصول على إعدادات الإشعارات"""
|
206 |
+
return self.settings.get("notifications", {
|
207 |
+
"enabled": True,
|
208 |
+
"email_enabled": True,
|
209 |
+
"email_server": "smtp.example.com",
|
210 |
+
"email_port": 587,
|
211 |
+
"email_username": "",
|
212 |
+
"email_password": ""
|
213 |
+
})
|
214 |
+
|
215 |
+
def get_reports_config(self):
|
216 |
+
"""الحصول على إعدادات التقارير"""
|
217 |
+
return self.settings.get("reports", {
|
218 |
+
"default_format": "pdf",
|
219 |
+
"default_path": os.path.join(self.data_dir, "reports")
|
220 |
+
})
|
221 |
+
|
222 |
+
def get_backup_config(self):
|
223 |
+
"""الحصول على إعدادات النسخ الاحتياطي"""
|
224 |
+
return self.settings.get("backup", {
|
225 |
+
"auto_backup": True,
|
226 |
+
"backup_frequency": "weekly",
|
227 |
+
"backup_path": os.path.join(self.data_dir, "backups"),
|
228 |
+
"max_backups": 10
|
229 |
+
})
|
database/db_connector.py
ADDED
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
موصل قاعدة البيانات لنظام إدارة المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import sqlite3
|
7 |
+
import logging
|
8 |
+
|
9 |
+
logger = logging.getLogger('tender_system.database')
|
10 |
+
|
11 |
+
class DatabaseConnector:
|
12 |
+
"""فئة موصل قاعدة البيانات"""
|
13 |
+
|
14 |
+
def __init__(self, config):
|
15 |
+
"""تهيئة موصل قاعدة البيانات"""
|
16 |
+
self.config = config
|
17 |
+
self.db_config = config.get_database_config()
|
18 |
+
self.db_path = self.db_config.get('path')
|
19 |
+
self.connection = None
|
20 |
+
self.cursor = None
|
21 |
+
|
22 |
+
# إنشاء قاعدة البيانات إذا لم تكن موجودة
|
23 |
+
self._initialize_database()
|
24 |
+
|
25 |
+
def _initialize_database(self):
|
26 |
+
"""تهيئة قاعدة البيانات"""
|
27 |
+
try:
|
28 |
+
# التأكد من وجود المجلد
|
29 |
+
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
|
30 |
+
|
31 |
+
# إنشاء الاتصال
|
32 |
+
self.connection = sqlite3.connect(self.db_path)
|
33 |
+
self.cursor = self.connection.cursor()
|
34 |
+
|
35 |
+
# إنشاء الجداول إذا لم تكن موجودة
|
36 |
+
self._create_tables()
|
37 |
+
|
38 |
+
# إضافة بيانات افتراضية إذا كانت قاعدة البيانات فارغة
|
39 |
+
self._add_default_data()
|
40 |
+
|
41 |
+
logger.info(f"تم تهيئة قاعدة البيانات بنجاح: {self.db_path}")
|
42 |
+
except Exception as e:
|
43 |
+
logger.error(f"خطأ في تهيئة قاعدة البيانات: {str(e)}")
|
44 |
+
raise
|
45 |
+
|
46 |
+
def _create_tables(self):
|
47 |
+
"""إنشاء جداول قاعدة البيانات"""
|
48 |
+
# جدول المستخدمين
|
49 |
+
self.cursor.execute('''
|
50 |
+
CREATE TABLE IF NOT EXISTS users (
|
51 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
52 |
+
username TEXT UNIQUE NOT NULL,
|
53 |
+
password TEXT NOT NULL,
|
54 |
+
full_name TEXT NOT NULL,
|
55 |
+
email TEXT UNIQUE NOT NULL,
|
56 |
+
role TEXT NOT NULL,
|
57 |
+
status TEXT NOT NULL,
|
58 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
59 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
60 |
+
)
|
61 |
+
''')
|
62 |
+
|
63 |
+
# جدول المشاريع
|
64 |
+
self.cursor.execute('''
|
65 |
+
CREATE TABLE IF NOT EXISTS projects (
|
66 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
67 |
+
name TEXT NOT NULL,
|
68 |
+
client TEXT NOT NULL,
|
69 |
+
description TEXT,
|
70 |
+
start_date TEXT,
|
71 |
+
end_date TEXT,
|
72 |
+
status TEXT NOT NULL,
|
73 |
+
created_by INTEGER,
|
74 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
75 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
76 |
+
FOREIGN KEY (created_by) REFERENCES users (id)
|
77 |
+
)
|
78 |
+
''')
|
79 |
+
|
80 |
+
# جدول المستندات
|
81 |
+
self.cursor.execute('''
|
82 |
+
CREATE TABLE IF NOT EXISTS documents (
|
83 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
84 |
+
project_id INTEGER,
|
85 |
+
name TEXT NOT NULL,
|
86 |
+
file_path TEXT NOT NULL,
|
87 |
+
document_type TEXT NOT NULL,
|
88 |
+
description TEXT,
|
89 |
+
uploaded_by INTEGER,
|
90 |
+
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
91 |
+
FOREIGN KEY (project_id) REFERENCES projects (id),
|
92 |
+
FOREIGN KEY (uploaded_by) REFERENCES users (id)
|
93 |
+
)
|
94 |
+
''')
|
95 |
+
|
96 |
+
# جدول بنود التسعير
|
97 |
+
self.cursor.execute('''
|
98 |
+
CREATE TABLE IF NOT EXISTS pricing_items (
|
99 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
100 |
+
project_id INTEGER,
|
101 |
+
item_number TEXT NOT NULL,
|
102 |
+
description TEXT NOT NULL,
|
103 |
+
unit TEXT NOT NULL,
|
104 |
+
quantity REAL NOT NULL,
|
105 |
+
unit_price REAL NOT NULL,
|
106 |
+
total_price REAL NOT NULL,
|
107 |
+
created_by INTEGER,
|
108 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
109 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
110 |
+
FOREIGN KEY (project_id) REFERENCES projects (id),
|
111 |
+
FOREIGN KEY (created_by) REFERENCES users (id)
|
112 |
+
)
|
113 |
+
''')
|
114 |
+
|
115 |
+
# جدول الموارد البشرية
|
116 |
+
self.cursor.execute('''
|
117 |
+
CREATE TABLE IF NOT EXISTS human_resources (
|
118 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
119 |
+
name TEXT NOT NULL,
|
120 |
+
position TEXT NOT NULL,
|
121 |
+
daily_cost REAL NOT NULL,
|
122 |
+
skills TEXT,
|
123 |
+
status TEXT NOT NULL,
|
124 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
125 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
126 |
+
)
|
127 |
+
''')
|
128 |
+
|
129 |
+
# جدول المعدات
|
130 |
+
self.cursor.execute('''
|
131 |
+
CREATE TABLE IF NOT EXISTS equipment (
|
132 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
133 |
+
name TEXT NOT NULL,
|
134 |
+
type TEXT NOT NULL,
|
135 |
+
daily_cost REAL NOT NULL,
|
136 |
+
status TEXT NOT NULL,
|
137 |
+
location TEXT,
|
138 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
139 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
140 |
+
)
|
141 |
+
''')
|
142 |
+
|
143 |
+
# جدول المواد
|
144 |
+
self.cursor.execute('''
|
145 |
+
CREATE TABLE IF NOT EXISTS materials (
|
146 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
147 |
+
name TEXT NOT NULL,
|
148 |
+
unit TEXT NOT NULL,
|
149 |
+
quantity REAL NOT NULL,
|
150 |
+
unit_price REAL NOT NULL,
|
151 |
+
supplier TEXT,
|
152 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
153 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
154 |
+
)
|
155 |
+
''')
|
156 |
+
|
157 |
+
# جدول المخاطر
|
158 |
+
self.cursor.execute('''
|
159 |
+
CREATE TABLE IF NOT EXISTS risks (
|
160 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
161 |
+
project_id INTEGER,
|
162 |
+
name TEXT NOT NULL,
|
163 |
+
category TEXT NOT NULL,
|
164 |
+
probability TEXT NOT NULL,
|
165 |
+
impact TEXT NOT NULL,
|
166 |
+
risk_level TEXT NOT NULL,
|
167 |
+
mitigation_strategy TEXT,
|
168 |
+
created_by INTEGER,
|
169 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
170 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
171 |
+
FOREIGN KEY (project_id) REFERENCES projects (id),
|
172 |
+
FOREIGN KEY (created_by) REFERENCES users (id)
|
173 |
+
)
|
174 |
+
''')
|
175 |
+
|
176 |
+
# جدول التقارير
|
177 |
+
self.cursor.execute('''
|
178 |
+
CREATE TABLE IF NOT EXISTS reports (
|
179 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
180 |
+
name TEXT NOT NULL,
|
181 |
+
project_id INTEGER,
|
182 |
+
report_type TEXT NOT NULL,
|
183 |
+
period TEXT,
|
184 |
+
file_path TEXT,
|
185 |
+
created_by INTEGER,
|
186 |
+
status TEXT NOT NULL,
|
187 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
188 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
189 |
+
FOREIGN KEY (project_id) REFERENCES projects (id),
|
190 |
+
FOREIGN KEY (created_by) REFERENCES users (id)
|
191 |
+
)
|
192 |
+
''')
|
193 |
+
|
194 |
+
# حفظ التغييرات
|
195 |
+
self.connection.commit()
|
196 |
+
|
197 |
+
def _add_default_data(self):
|
198 |
+
"""إضافة بيانات افتراضية"""
|
199 |
+
# التحقق من وجود مستخدمين
|
200 |
+
self.cursor.execute("SELECT COUNT(*) FROM users")
|
201 |
+
user_count = self.cursor.fetchone()[0]
|
202 |
+
|
203 |
+
if user_count == 0:
|
204 |
+
# إضافة مستخدم افتراضي (admin/admin)
|
205 |
+
self.cursor.execute('''
|
206 |
+
INSERT INTO users (username, password, full_name, email, role, status)
|
207 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
208 |
+
''', ('admin', 'admin', 'مدير النظام', '[email protected]', 'مدير', 'نشط'))
|
209 |
+
|
210 |
+
# إضافة مستخدمين إضافيين
|
211 |
+
self.cursor.execute('''
|
212 |
+
INSERT INTO users (username, password, full_name, email, role, status)
|
213 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
214 |
+
''', ('user1', 'password', 'أحمد محمد', '[email protected]', 'مستخدم', 'نشط'))
|
215 |
+
|
216 |
+
self.cursor.execute('''
|
217 |
+
INSERT INTO users (username, password, full_name, email, role, status)
|
218 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
219 |
+
''', ('user2', 'password', 'سارة أحمد', '[email protected]', 'مستخدم', 'نشط'))
|
220 |
+
|
221 |
+
# حفظ التغييرات
|
222 |
+
self.connection.commit()
|
223 |
+
|
224 |
+
logger.info("تم إضافة بيانات المستخدمين الافتراضية")
|
225 |
+
|
226 |
+
# التحقق من وجود مشاريع
|
227 |
+
self.cursor.execute("SELECT COUNT(*) FROM projects")
|
228 |
+
project_count = self.cursor.fetchone()[0]
|
229 |
+
|
230 |
+
if project_count == 0:
|
231 |
+
# إضافة مشاريع افتراضية
|
232 |
+
self.cursor.execute('''
|
233 |
+
INSERT INTO projects (name, client, description, start_date, end_date, status, created_by)
|
234 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
235 |
+
''', ('مشروع تطوير الطريق السريع', 'وزارة النقل', 'مشروع تطوير وتوسعة الطريق السريع', '2025-01-15', '2025-12-31', 'نشط', 1))
|
236 |
+
|
237 |
+
self.cursor.execute('''
|
238 |
+
INSERT INTO projects (name, client, description, start_date, end_date, status, created_by)
|
239 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
240 |
+
''', ('مشروع بناء المدرسة الثانوية', 'وزارة التعليم', 'مشروع بناء مدرسة ثانوية جديدة', '2025-02-01', '2025-08-30', 'نشط', 1))
|
241 |
+
|
242 |
+
self.cursor.execute('''
|
243 |
+
INSERT INTO projects (name, client, description, start_date, end_date, status, created_by)
|
244 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
245 |
+
''', ('مشروع تجديد المستشفى', 'وزارة الصحة', 'مشروع تجديد وتطوير المستشفى', '2024-10-15', '2025-03-15', 'مكتمل', 1))
|
246 |
+
|
247 |
+
# حفظ التغييرات
|
248 |
+
self.connection.commit()
|
249 |
+
|
250 |
+
logger.info("تم إضافة بيانات المشاريع الافتراضية")
|
251 |
+
|
252 |
+
def execute_query(self, query, params=None):
|
253 |
+
"""تنفيذ استعلام"""
|
254 |
+
try:
|
255 |
+
if params:
|
256 |
+
self.cursor.execute(query, params)
|
257 |
+
else:
|
258 |
+
self.cursor.execute(query)
|
259 |
+
|
260 |
+
self.connection.commit()
|
261 |
+
return self.cursor
|
262 |
+
except Exception as e:
|
263 |
+
logger.error(f"خطأ في تنفيذ الاستعلام: {str(e)}")
|
264 |
+
self.connection.rollback()
|
265 |
+
raise
|
266 |
+
|
267 |
+
def fetch_one(self, query, params=None):
|
268 |
+
"""جلب صف واحد"""
|
269 |
+
cursor = self.execute_query(query, params)
|
270 |
+
return cursor.fetchone()
|
271 |
+
|
272 |
+
def fetch_all(self, query, params=None):
|
273 |
+
"""جلب جميع الصفوف"""
|
274 |
+
cursor = self.execute_query(query, params)
|
275 |
+
return cursor.fetchall()
|
276 |
+
|
277 |
+
def insert(self, table, data):
|
278 |
+
"""إدراج بيانات"""
|
279 |
+
columns = ', '.join(data.keys())
|
280 |
+
placeholders = ', '.join(['?' for _ in data])
|
281 |
+
query = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
|
282 |
+
|
283 |
+
try:
|
284 |
+
self.cursor.execute(query, list(data.values()))
|
285 |
+
self.connection.commit()
|
286 |
+
return self.cursor.lastrowid
|
287 |
+
except Exception as e:
|
288 |
+
logger.error(f"خطأ في إدراج البيانات: {str(e)}")
|
289 |
+
self.connection.rollback()
|
290 |
+
raise
|
291 |
+
|
292 |
+
def update(self, table, data, condition):
|
293 |
+
"""تحديث بيانات"""
|
294 |
+
set_clause = ', '.join([f"{column} = ?" for column in data.keys()])
|
295 |
+
query = f"UPDATE {table} SET {set_clause} WHERE {condition}"
|
296 |
+
|
297 |
+
try:
|
298 |
+
self.cursor.execute(query, list(data.values()))
|
299 |
+
self.connection.commit()
|
300 |
+
return self.cursor.rowcount
|
301 |
+
except Exception as e:
|
302 |
+
logger.error(f"خطأ في تحديث البيانات: {str(e)}")
|
303 |
+
self.connection.rollback()
|
304 |
+
raise
|
305 |
+
|
306 |
+
def delete(self, table, condition):
|
307 |
+
"""حذف بيانات"""
|
308 |
+
query = f"DELETE FROM {table} WHERE {condition}"
|
309 |
+
|
310 |
+
try:
|
311 |
+
self.cursor.execute(query)
|
312 |
+
self.connection.commit()
|
313 |
+
return self.cursor.rowcount
|
314 |
+
except Exception as e:
|
315 |
+
logger.error(f"خطأ في حذف البيانات: {str(e)}")
|
316 |
+
self.connection.rollback()
|
317 |
+
raise
|
318 |
+
|
319 |
+
def close(self):
|
320 |
+
"""إغلاق الاتصال"""
|
321 |
+
if self.connection:
|
322 |
+
self.connection.close()
|
323 |
+
logger.info("تم إغلاق الاتصال بقاعدة البيانات")
|
database/models.py
ADDED
@@ -0,0 +1,626 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
نماذج البيانات لنظام إدارة المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import sqlite3
|
6 |
+
import logging
|
7 |
+
from datetime import datetime
|
8 |
+
|
9 |
+
logger = logging.getLogger('tender_system.models')
|
10 |
+
|
11 |
+
class User:
|
12 |
+
"""نموذج المستخدم"""
|
13 |
+
|
14 |
+
def __init__(self, id=None, username=None, password=None, full_name=None, email=None, role=None, status=None):
|
15 |
+
"""تهيئة نموذج المستخدم"""
|
16 |
+
self.id = id
|
17 |
+
self.username = username
|
18 |
+
self.password = password
|
19 |
+
self.full_name = full_name
|
20 |
+
self.email = email
|
21 |
+
self.role = role
|
22 |
+
self.status = status
|
23 |
+
self.created_at = None
|
24 |
+
self.updated_at = None
|
25 |
+
|
26 |
+
@staticmethod
|
27 |
+
def authenticate(username, password, db):
|
28 |
+
"""مصادقة المستخدم"""
|
29 |
+
try:
|
30 |
+
query = "SELECT * FROM users WHERE username = ? AND password = ? AND status = 'نشط'"
|
31 |
+
result = db.fetch_one(query, (username, password))
|
32 |
+
|
33 |
+
if result:
|
34 |
+
user = User()
|
35 |
+
user.id = result[0]
|
36 |
+
user.username = result[1]
|
37 |
+
user.password = result[2]
|
38 |
+
user.full_name = result[3]
|
39 |
+
user.email = result[4]
|
40 |
+
user.role = result[5]
|
41 |
+
user.status = result[6]
|
42 |
+
user.created_at = result[7]
|
43 |
+
user.updated_at = result[8]
|
44 |
+
|
45 |
+
return user
|
46 |
+
|
47 |
+
return None
|
48 |
+
except Exception as e:
|
49 |
+
logger.error(f"خطأ في مصادقة المستخدم: {str(e)}")
|
50 |
+
return None
|
51 |
+
|
52 |
+
@staticmethod
|
53 |
+
def get_by_id(user_id, db):
|
54 |
+
"""الحصول على المستخدم بواسطة المعرف"""
|
55 |
+
try:
|
56 |
+
query = "SELECT * FROM users WHERE id = ?"
|
57 |
+
result = db.fetch_one(query, (user_id,))
|
58 |
+
|
59 |
+
if result:
|
60 |
+
user = User()
|
61 |
+
user.id = result[0]
|
62 |
+
user.username = result[1]
|
63 |
+
user.password = result[2]
|
64 |
+
user.full_name = result[3]
|
65 |
+
user.email = result[4]
|
66 |
+
user.role = result[5]
|
67 |
+
user.status = result[6]
|
68 |
+
user.created_at = result[7]
|
69 |
+
user.updated_at = result[8]
|
70 |
+
|
71 |
+
return user
|
72 |
+
|
73 |
+
return None
|
74 |
+
except Exception as e:
|
75 |
+
logger.error(f"خطأ في الحصول على المستخدم: {str(e)}")
|
76 |
+
return None
|
77 |
+
|
78 |
+
@staticmethod
|
79 |
+
def get_all(db):
|
80 |
+
"""الحصول على جميع المستخدمين"""
|
81 |
+
try:
|
82 |
+
query = "SELECT * FROM users"
|
83 |
+
results = db.fetch_all(query)
|
84 |
+
|
85 |
+
users = []
|
86 |
+
for result in results:
|
87 |
+
user = User()
|
88 |
+
user.id = result[0]
|
89 |
+
user.username = result[1]
|
90 |
+
user.password = result[2]
|
91 |
+
user.full_name = result[3]
|
92 |
+
user.email = result[4]
|
93 |
+
user.role = result[5]
|
94 |
+
user.status = result[6]
|
95 |
+
user.created_at = result[7]
|
96 |
+
user.updated_at = result[8]
|
97 |
+
|
98 |
+
users.append(user)
|
99 |
+
|
100 |
+
return users
|
101 |
+
except Exception as e:
|
102 |
+
logger.error(f"خطأ في الحصول على المستخدمين: {str(e)}")
|
103 |
+
return []
|
104 |
+
|
105 |
+
def save(self, db):
|
106 |
+
"""حفظ المستخدم"""
|
107 |
+
try:
|
108 |
+
if self.id:
|
109 |
+
# تحديث مستخدم موجود
|
110 |
+
data = {
|
111 |
+
'username': self.username,
|
112 |
+
'password': self.password,
|
113 |
+
'full_name': self.full_name,
|
114 |
+
'email': self.email,
|
115 |
+
'role': self.role,
|
116 |
+
'status': self.status,
|
117 |
+
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
118 |
+
}
|
119 |
+
|
120 |
+
db.update('users', data, f"id = {self.id}")
|
121 |
+
return self.id
|
122 |
+
else:
|
123 |
+
# إنشاء مستخدم جديد
|
124 |
+
data = {
|
125 |
+
'username': self.username,
|
126 |
+
'password': self.password,
|
127 |
+
'full_name': self.full_name,
|
128 |
+
'email': self.email,
|
129 |
+
'role': self.role,
|
130 |
+
'status': self.status
|
131 |
+
}
|
132 |
+
|
133 |
+
self.id = db.insert('users', data)
|
134 |
+
return self.id
|
135 |
+
except Exception as e:
|
136 |
+
logger.error(f"خطأ في حفظ المستخدم: {str(e)}")
|
137 |
+
return None
|
138 |
+
|
139 |
+
def delete(self, db):
|
140 |
+
"""حذف المستخدم"""
|
141 |
+
try:
|
142 |
+
if self.id:
|
143 |
+
db.delete('users', f"id = {self.id}")
|
144 |
+
return True
|
145 |
+
|
146 |
+
return False
|
147 |
+
except Exception as e:
|
148 |
+
logger.error(f"خطأ في حذف المستخدم: {str(e)}")
|
149 |
+
return False
|
150 |
+
|
151 |
+
|
152 |
+
class Project:
|
153 |
+
"""نموذج المشروع"""
|
154 |
+
|
155 |
+
def __init__(self, id=None, name=None, client=None, description=None, start_date=None, end_date=None, status=None, created_by=None):
|
156 |
+
"""تهيئة نموذج المشروع"""
|
157 |
+
self.id = id
|
158 |
+
self.name = name
|
159 |
+
self.client = client
|
160 |
+
self.description = description
|
161 |
+
self.start_date = start_date
|
162 |
+
self.end_date = end_date
|
163 |
+
self.status = status
|
164 |
+
self.created_by = created_by
|
165 |
+
self.created_at = None
|
166 |
+
self.updated_at = None
|
167 |
+
|
168 |
+
@staticmethod
|
169 |
+
def get_by_id(project_id, db):
|
170 |
+
"""الحصول على المشروع بواسطة المعرف"""
|
171 |
+
try:
|
172 |
+
query = "SELECT * FROM projects WHERE id = ?"
|
173 |
+
result = db.fetch_one(query, (project_id,))
|
174 |
+
|
175 |
+
if result:
|
176 |
+
project = Project()
|
177 |
+
project.id = result[0]
|
178 |
+
project.name = result[1]
|
179 |
+
project.client = result[2]
|
180 |
+
project.description = result[3]
|
181 |
+
project.start_date = result[4]
|
182 |
+
project.end_date = result[5]
|
183 |
+
project.status = result[6]
|
184 |
+
project.created_by = result[7]
|
185 |
+
project.created_at = result[8]
|
186 |
+
project.updated_at = result[9]
|
187 |
+
|
188 |
+
return project
|
189 |
+
|
190 |
+
return None
|
191 |
+
except Exception as e:
|
192 |
+
logger.error(f"خطأ في الحصول على المشروع: {str(e)}")
|
193 |
+
return None
|
194 |
+
|
195 |
+
@staticmethod
|
196 |
+
def get_all(db):
|
197 |
+
"""الحصول على جميع المشاريع"""
|
198 |
+
try:
|
199 |
+
query = "SELECT * FROM projects"
|
200 |
+
results = db.fetch_all(query)
|
201 |
+
|
202 |
+
projects = []
|
203 |
+
for result in results:
|
204 |
+
project = Project()
|
205 |
+
project.id = result[0]
|
206 |
+
project.name = result[1]
|
207 |
+
project.client = result[2]
|
208 |
+
project.description = result[3]
|
209 |
+
project.start_date = result[4]
|
210 |
+
project.end_date = result[5]
|
211 |
+
project.status = result[6]
|
212 |
+
project.created_by = result[7]
|
213 |
+
project.created_at = result[8]
|
214 |
+
project.updated_at = result[9]
|
215 |
+
|
216 |
+
projects.append(project)
|
217 |
+
|
218 |
+
return projects
|
219 |
+
except Exception as e:
|
220 |
+
logger.error(f"خطأ في الحصول على المشاريع: {str(e)}")
|
221 |
+
return []
|
222 |
+
|
223 |
+
def save(self, db):
|
224 |
+
"""حفظ المشروع"""
|
225 |
+
try:
|
226 |
+
if self.id:
|
227 |
+
# تحديث مشروع موجود
|
228 |
+
data = {
|
229 |
+
'name': self.name,
|
230 |
+
'client': self.client,
|
231 |
+
'description': self.description,
|
232 |
+
'start_date': self.start_date,
|
233 |
+
'end_date': self.end_date,
|
234 |
+
'status': self.status,
|
235 |
+
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
236 |
+
}
|
237 |
+
|
238 |
+
db.update('projects', data, f"id = {self.id}")
|
239 |
+
return self.id
|
240 |
+
else:
|
241 |
+
# إنشاء مشروع جديد
|
242 |
+
data = {
|
243 |
+
'name': self.name,
|
244 |
+
'client': self.client,
|
245 |
+
'description': self.description,
|
246 |
+
'start_date': self.start_date,
|
247 |
+
'end_date': self.end_date,
|
248 |
+
'status': self.status,
|
249 |
+
'created_by': self.created_by
|
250 |
+
}
|
251 |
+
|
252 |
+
self.id = db.insert('projects', data)
|
253 |
+
return self.id
|
254 |
+
except Exception as e:
|
255 |
+
logger.error(f"خطأ في حفظ المشروع: {str(e)}")
|
256 |
+
return None
|
257 |
+
|
258 |
+
def delete(self, db):
|
259 |
+
"""حذف المشروع"""
|
260 |
+
try:
|
261 |
+
if self.id:
|
262 |
+
db.delete('projects', f"id = {self.id}")
|
263 |
+
return True
|
264 |
+
|
265 |
+
return False
|
266 |
+
except Exception as e:
|
267 |
+
logger.error(f"خطأ في حذف المشروع: {str(e)}")
|
268 |
+
return False
|
269 |
+
|
270 |
+
|
271 |
+
class Document:
|
272 |
+
"""نموذج المستند"""
|
273 |
+
|
274 |
+
def __init__(self, id=None, project_id=None, name=None, file_path=None, document_type=None, description=None, uploaded_by=None):
|
275 |
+
"""تهيئة نموذج المستند"""
|
276 |
+
self.id = id
|
277 |
+
self.project_id = project_id
|
278 |
+
self.name = name
|
279 |
+
self.file_path = file_path
|
280 |
+
self.document_type = document_type
|
281 |
+
self.description = description
|
282 |
+
self.uploaded_by = uploaded_by
|
283 |
+
self.uploaded_at = None
|
284 |
+
|
285 |
+
@staticmethod
|
286 |
+
def get_by_id(document_id, db):
|
287 |
+
"""الحصول على المستند بواسط�� المعرف"""
|
288 |
+
try:
|
289 |
+
query = "SELECT * FROM documents WHERE id = ?"
|
290 |
+
result = db.fetch_one(query, (document_id,))
|
291 |
+
|
292 |
+
if result:
|
293 |
+
document = Document()
|
294 |
+
document.id = result[0]
|
295 |
+
document.project_id = result[1]
|
296 |
+
document.name = result[2]
|
297 |
+
document.file_path = result[3]
|
298 |
+
document.document_type = result[4]
|
299 |
+
document.description = result[5]
|
300 |
+
document.uploaded_by = result[6]
|
301 |
+
document.uploaded_at = result[7]
|
302 |
+
|
303 |
+
return document
|
304 |
+
|
305 |
+
return None
|
306 |
+
except Exception as e:
|
307 |
+
logger.error(f"خطأ في الحصول على المستند: {str(e)}")
|
308 |
+
return None
|
309 |
+
|
310 |
+
@staticmethod
|
311 |
+
def get_by_project(project_id, db):
|
312 |
+
"""الحصول على المستندات بواسطة معرف المشروع"""
|
313 |
+
try:
|
314 |
+
query = "SELECT * FROM documents WHERE project_id = ?"
|
315 |
+
results = db.fetch_all(query, (project_id,))
|
316 |
+
|
317 |
+
documents = []
|
318 |
+
for result in results:
|
319 |
+
document = Document()
|
320 |
+
document.id = result[0]
|
321 |
+
document.project_id = result[1]
|
322 |
+
document.name = result[2]
|
323 |
+
document.file_path = result[3]
|
324 |
+
document.document_type = result[4]
|
325 |
+
document.description = result[5]
|
326 |
+
document.uploaded_by = result[6]
|
327 |
+
document.uploaded_at = result[7]
|
328 |
+
|
329 |
+
documents.append(document)
|
330 |
+
|
331 |
+
return documents
|
332 |
+
except Exception as e:
|
333 |
+
logger.error(f"خطأ في الحصول على المستندات: {str(e)}")
|
334 |
+
return []
|
335 |
+
|
336 |
+
def save(self, db):
|
337 |
+
"""حفظ المستند"""
|
338 |
+
try:
|
339 |
+
if self.id:
|
340 |
+
# تحديث مستند موجود
|
341 |
+
data = {
|
342 |
+
'project_id': self.project_id,
|
343 |
+
'name': self.name,
|
344 |
+
'file_path': self.file_path,
|
345 |
+
'document_type': self.document_type,
|
346 |
+
'description': self.description
|
347 |
+
}
|
348 |
+
|
349 |
+
db.update('documents', data, f"id = {self.id}")
|
350 |
+
return self.id
|
351 |
+
else:
|
352 |
+
# إنشاء مستند جديد
|
353 |
+
data = {
|
354 |
+
'project_id': self.project_id,
|
355 |
+
'name': self.name,
|
356 |
+
'file_path': self.file_path,
|
357 |
+
'document_type': self.document_type,
|
358 |
+
'description': self.description,
|
359 |
+
'uploaded_by': self.uploaded_by
|
360 |
+
}
|
361 |
+
|
362 |
+
self.id = db.insert('documents', data)
|
363 |
+
return self.id
|
364 |
+
except Exception as e:
|
365 |
+
logger.error(f"خطأ في حفظ المستند: {str(e)}")
|
366 |
+
return None
|
367 |
+
|
368 |
+
def delete(self, db):
|
369 |
+
"""حذف المستند"""
|
370 |
+
try:
|
371 |
+
if self.id:
|
372 |
+
db.delete('documents', f"id = {self.id}")
|
373 |
+
return True
|
374 |
+
|
375 |
+
return False
|
376 |
+
except Exception as e:
|
377 |
+
logger.error(f"خطأ في حذف المستند: {str(e)}")
|
378 |
+
return False
|
379 |
+
|
380 |
+
|
381 |
+
class PricingItem:
|
382 |
+
"""نموذج بند التسعير"""
|
383 |
+
|
384 |
+
def __init__(self, id=None, project_id=None, item_number=None, description=None, unit=None, quantity=None, unit_price=None, total_price=None, created_by=None):
|
385 |
+
"""تهيئة نموذج بند التسعير"""
|
386 |
+
self.id = id
|
387 |
+
self.project_id = project_id
|
388 |
+
self.item_number = item_number
|
389 |
+
self.description = description
|
390 |
+
self.unit = unit
|
391 |
+
self.quantity = quantity
|
392 |
+
self.unit_price = unit_price
|
393 |
+
self.total_price = total_price
|
394 |
+
self.created_by = created_by
|
395 |
+
self.created_at = None
|
396 |
+
self.updated_at = None
|
397 |
+
|
398 |
+
@staticmethod
|
399 |
+
def get_by_project(project_id, db):
|
400 |
+
"""الحصول على بنود التسعير بواسطة معرف المشروع"""
|
401 |
+
try:
|
402 |
+
query = "SELECT * FROM pricing_items WHERE project_id = ?"
|
403 |
+
results = db.fetch_all(query, (project_id,))
|
404 |
+
|
405 |
+
items = []
|
406 |
+
for result in results:
|
407 |
+
item = PricingItem()
|
408 |
+
item.id = result[0]
|
409 |
+
item.project_id = result[1]
|
410 |
+
item.item_number = result[2]
|
411 |
+
item.description = result[3]
|
412 |
+
item.unit = result[4]
|
413 |
+
item.quantity = result[5]
|
414 |
+
item.unit_price = result[6]
|
415 |
+
item.total_price = result[7]
|
416 |
+
item.created_by = result[8]
|
417 |
+
item.created_at = result[9]
|
418 |
+
item.updated_at = result[10]
|
419 |
+
|
420 |
+
items.append(item)
|
421 |
+
|
422 |
+
return items
|
423 |
+
except Exception as e:
|
424 |
+
logger.error(f"خطأ في الحصول على بنود التسعير: {str(e)}")
|
425 |
+
return []
|
426 |
+
|
427 |
+
def save(self, db):
|
428 |
+
"""حفظ بند التسعير"""
|
429 |
+
try:
|
430 |
+
if self.id:
|
431 |
+
# تحديث بند موجود
|
432 |
+
data = {
|
433 |
+
'project_id': self.project_id,
|
434 |
+
'item_number': self.item_number,
|
435 |
+
'description': self.description,
|
436 |
+
'unit': self.unit,
|
437 |
+
'quantity': self.quantity,
|
438 |
+
'unit_price': self.unit_price,
|
439 |
+
'total_price': self.total_price,
|
440 |
+
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
441 |
+
}
|
442 |
+
|
443 |
+
db.update('pricing_items', data, f"id = {self.id}")
|
444 |
+
return self.id
|
445 |
+
else:
|
446 |
+
# إنشاء بند جديد
|
447 |
+
data = {
|
448 |
+
'project_id': self.project_id,
|
449 |
+
'item_number': self.item_number,
|
450 |
+
'description': self.description,
|
451 |
+
'unit': self.unit,
|
452 |
+
'quantity': self.quantity,
|
453 |
+
'unit_price': self.unit_price,
|
454 |
+
'total_price': self.total_price,
|
455 |
+
'created_by': self.created_by
|
456 |
+
}
|
457 |
+
|
458 |
+
self.id = db.insert('pricing_items', data)
|
459 |
+
return self.id
|
460 |
+
except Exception as e:
|
461 |
+
logger.error(f"خطأ في حفظ بند التسعير: {str(e)}")
|
462 |
+
return None
|
463 |
+
|
464 |
+
|
465 |
+
class Risk:
|
466 |
+
"""نموذج المخاطرة"""
|
467 |
+
|
468 |
+
def __init__(self, id=None, project_id=None, name=None, category=None, probability=None, impact=None, risk_level=None, mitigation_strategy=None, created_by=None):
|
469 |
+
"""تهيئة نموذج المخاطرة"""
|
470 |
+
self.id = id
|
471 |
+
self.project_id = project_id
|
472 |
+
self.name = name
|
473 |
+
self.category = category
|
474 |
+
self.probability = probability
|
475 |
+
self.impact = impact
|
476 |
+
self.risk_level = risk_level
|
477 |
+
self.mitigation_strategy = mitigation_strategy
|
478 |
+
self.created_by = created_by
|
479 |
+
self.created_at = None
|
480 |
+
self.updated_at = None
|
481 |
+
|
482 |
+
@staticmethod
|
483 |
+
def get_by_project(project_id, db):
|
484 |
+
"""الحصول على المخاطر بواسطة معرف المشروع"""
|
485 |
+
try:
|
486 |
+
query = "SELECT * FROM risks WHERE project_id = ?"
|
487 |
+
results = db.fetch_all(query, (project_id,))
|
488 |
+
|
489 |
+
risks = []
|
490 |
+
for result in results:
|
491 |
+
risk = Risk()
|
492 |
+
risk.id = result[0]
|
493 |
+
risk.project_id = result[1]
|
494 |
+
risk.name = result[2]
|
495 |
+
risk.category = result[3]
|
496 |
+
risk.probability = result[4]
|
497 |
+
risk.impact = result[5]
|
498 |
+
risk.risk_level = result[6]
|
499 |
+
risk.mitigation_strategy = result[7]
|
500 |
+
risk.created_by = result[8]
|
501 |
+
risk.created_at = result[9]
|
502 |
+
risk.updated_at = result[10]
|
503 |
+
|
504 |
+
risks.append(risk)
|
505 |
+
|
506 |
+
return risks
|
507 |
+
except Exception as e:
|
508 |
+
logger.error(f"خطأ في الحصول على المخاطر: {str(e)}")
|
509 |
+
return []
|
510 |
+
|
511 |
+
def save(self, db):
|
512 |
+
"""حفظ المخاطرة"""
|
513 |
+
try:
|
514 |
+
if self.id:
|
515 |
+
# تحديث مخاطرة موجودة
|
516 |
+
data = {
|
517 |
+
'project_id': self.project_id,
|
518 |
+
'name': self.name,
|
519 |
+
'category': self.category,
|
520 |
+
'probability': self.probability,
|
521 |
+
'impact': self.impact,
|
522 |
+
'risk_level': self.risk_level,
|
523 |
+
'mitigation_strategy': self.mitigation_strategy,
|
524 |
+
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
525 |
+
}
|
526 |
+
|
527 |
+
db.update('risks', data, f"id = {self.id}")
|
528 |
+
return self.id
|
529 |
+
else:
|
530 |
+
# إنشاء مخاطرة جديدة
|
531 |
+
data = {
|
532 |
+
'project_id': self.project_id,
|
533 |
+
'name': self.name,
|
534 |
+
'category': self.category,
|
535 |
+
'probability': self.probability,
|
536 |
+
'impact': self.impact,
|
537 |
+
'risk_level': self.risk_level,
|
538 |
+
'mitigation_strategy': self.mitigation_strategy,
|
539 |
+
'created_by': self.created_by
|
540 |
+
}
|
541 |
+
|
542 |
+
self.id = db.insert('risks', data)
|
543 |
+
return self.id
|
544 |
+
except Exception as e:
|
545 |
+
logger.error(f"خطأ في حفظ المخاطرة: {str(e)}")
|
546 |
+
return None
|
547 |
+
|
548 |
+
|
549 |
+
class Report:
|
550 |
+
"""نموذج التقرير"""
|
551 |
+
|
552 |
+
def __init__(self, id=None, name=None, project_id=None, report_type=None, period=None, file_path=None, created_by=None, status=None):
|
553 |
+
"""تهيئة نموذج التقرير"""
|
554 |
+
self.id = id
|
555 |
+
self.name = name
|
556 |
+
self.project_id = project_id
|
557 |
+
self.report_type = report_type
|
558 |
+
self.period = period
|
559 |
+
self.file_path = file_path
|
560 |
+
self.created_by = created_by
|
561 |
+
self.status = status
|
562 |
+
self.created_at = None
|
563 |
+
self.updated_at = None
|
564 |
+
|
565 |
+
@staticmethod
|
566 |
+
def get_by_project(project_id, db):
|
567 |
+
"""الحصول على التقارير بواسطة معرف المشروع"""
|
568 |
+
try:
|
569 |
+
query = "SELECT * FROM reports WHERE project_id = ?"
|
570 |
+
results = db.fetch_all(query, (project_id,))
|
571 |
+
|
572 |
+
reports = []
|
573 |
+
for result in results:
|
574 |
+
report = Report()
|
575 |
+
report.id = result[0]
|
576 |
+
report.name = result[1]
|
577 |
+
report.project_id = result[2]
|
578 |
+
report.report_type = result[3]
|
579 |
+
report.period = result[4]
|
580 |
+
report.file_path = result[5]
|
581 |
+
report.created_by = result[6]
|
582 |
+
report.status = result[7]
|
583 |
+
report.created_at = result[8]
|
584 |
+
report.updated_at = result[9]
|
585 |
+
|
586 |
+
reports.append(report)
|
587 |
+
|
588 |
+
return reports
|
589 |
+
except Exception as e:
|
590 |
+
logger.error(f"خطأ في الحصول على التقارير: {str(e)}")
|
591 |
+
return []
|
592 |
+
|
593 |
+
def save(self, db):
|
594 |
+
"""حفظ التقرير"""
|
595 |
+
try:
|
596 |
+
if self.id:
|
597 |
+
# تحديث تقرير موجود
|
598 |
+
data = {
|
599 |
+
'name': self.name,
|
600 |
+
'project_id': self.project_id,
|
601 |
+
'report_type': self.report_type,
|
602 |
+
'period': self.period,
|
603 |
+
'file_path': self.file_path,
|
604 |
+
'status': self.status,
|
605 |
+
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
606 |
+
}
|
607 |
+
|
608 |
+
db.update('reports', data, f"id = {self.id}")
|
609 |
+
return self.id
|
610 |
+
else:
|
611 |
+
# إنشاء تقرير جديد
|
612 |
+
data = {
|
613 |
+
'name': self.name,
|
614 |
+
'project_id': self.project_id,
|
615 |
+
'report_type': self.report_type,
|
616 |
+
'period': self.period,
|
617 |
+
'file_path': self.file_path,
|
618 |
+
'created_by': self.created_by,
|
619 |
+
'status': self.status
|
620 |
+
}
|
621 |
+
|
622 |
+
self.id = db.insert('reports', data)
|
623 |
+
return self.id
|
624 |
+
except Exception as e:
|
625 |
+
logger.error(f"خطأ في حفظ التقرير: {str(e)}")
|
626 |
+
return None
|
docs/architecture.md
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# هيكلية النظام المحسنة لنظام إدارة المناقصات
|
2 |
+
|
3 |
+
## نظرة عامة
|
4 |
+
هذا المستند يوضح هيكلية النظام المحسنة لنظام إدارة المناقصات، والذي يتضمن الوحدات التالية:
|
5 |
+
- وحدة التسعير المتكاملة
|
6 |
+
- وحدة الذكاء الاصطناعي
|
7 |
+
- وحدة تحليل البيانات
|
8 |
+
- وحدة الموارد
|
9 |
+
|
10 |
+
## هيكلية المجلدات
|
11 |
+
|
12 |
+
```
|
13 |
+
tender_system/
|
14 |
+
├── app.py # نقطة الدخول الرئيسية للتطبيق
|
15 |
+
├── config.py # إعدادات التطبيق
|
16 |
+
├── requirements.txt # متطلبات المكتبات
|
17 |
+
├── README.md # توثيق النظام
|
18 |
+
├── assets/ # الأصول الثابتة
|
19 |
+
│ ├── images/ # الصور
|
20 |
+
│ ├── icons/ # الأيقونات
|
21 |
+
│ └── fonts/ # الخطوط
|
22 |
+
├── data/ # البيانات
|
23 |
+
│ ├── templates/ # قوالب البيانات
|
24 |
+
│ └── charts/ # بيانات الرسوم البيانية
|
25 |
+
├── database/ # قاعدة البيانات
|
26 |
+
│ ├── db_connector.py # موصل قاعدة البيانات
|
27 |
+
│ └── models.py # نماذج البيانات
|
28 |
+
├── modules/ # وحدات النظام
|
29 |
+
│ ├── pricing/ # وحدة التسعير
|
30 |
+
│ │ ├── pricing_app.py # تطبيق التسعير
|
31 |
+
│ │ └── services/ # خدمات التسعير
|
32 |
+
│ │ ├── standard_pricing.py
|
33 |
+
│ │ ├── unbalanced_pricing.py
|
34 |
+
│ │ ├── local_content_calculator.py
|
35 |
+
│ │ ├── price_prediction.py
|
36 |
+
│ │ ├── construction_cost_calculator.py
|
37 |
+
│ │ └── construction_templates.py
|
38 |
+
│ ├── ai_assistant/ # وحدة الذكاء الاصطناعي
|
39 |
+
│ │ ├── ai_app.py # تطبيق الذكاء الاصطناعي
|
40 |
+
│ │ └── services/ # خدمات الذكاء الاصطناعي
|
41 |
+
│ │ ├── openai_service.py
|
42 |
+
│ │ ├── anthropic_service.py
|
43 |
+
│ │ ├── local_llm_service.py
|
44 |
+
│ │ └── prompt_templates.py
|
45 |
+
│ ├── document_analysis/ # وحدة تحليل المستندات
|
46 |
+
│ │ ├── document_app.py # تطبيق تحليل المستندات
|
47 |
+
│ │ └── services/ # خدمات تحليل المستندات
|
48 |
+
│ │ ├── text_extractor.py
|
49 |
+
│ │ ├── item_extractor.py
|
50 |
+
│ │ └── document_parser.py
|
51 |
+
│ ├── resources/ # وحدة الموارد
|
52 |
+
│ │ ├── resources_app.py # تطبيق الموارد
|
53 |
+
│ │ └── services/ # خدمات الموارد
|
54 |
+
│ │ ├── material_manager.py
|
55 |
+
│ │ ├── labor_manager.py
|
56 |
+
│ │ ├── equipment_manager.py
|
57 |
+
│ │ └── subcontractor_manager.py
|
58 |
+
│ ├── project_management/ # وحدة إدارة المشاريع
|
59 |
+
│ │ └── project_app.py # تطبيق إدارة المشاريع
|
60 |
+
│ └── reports/ # وحدة التقارير
|
61 |
+
│ └── reports_app.py # تطبيق التقارير
|
62 |
+
├── styling/ # التنسيق
|
63 |
+
│ ├── theme.py # سمات التطبيق
|
64 |
+
│ ├── icons.py # أيقونات التطبيق
|
65 |
+
│ └── charts.py # تنسيق الرسوم البيانية
|
66 |
+
├── utils/ # أدوات مساعدة
|
67 |
+
│ ├── excel_handler.py # معالج ملفات Excel
|
68 |
+
│ ├── pdf_handler.py # معالج ملفات PDF
|
69 |
+
│ ├── helpers.py # دوال مساعدة
|
70 |
+
│ └── auth.py # المصادقة
|
71 |
+
└── tests/ # اختبارات
|
72 |
+
├── test_pricing.py # اختبارات وحدة التسعير
|
73 |
+
├── test_ai.py # اختبارات وحدة الذكاء الاصطناعي
|
74 |
+
├── test_document.py # اختبارات وحدة تحليل المستندات
|
75 |
+
└── test_resources.py # اختبارات وحدة الموارد
|
76 |
+
```
|
77 |
+
|
78 |
+
## تفاصيل الوحدات
|
79 |
+
|
80 |
+
### 1. وحدة التسعير المتكاملة
|
81 |
+
- **الوظائف الرئيسية**:
|
82 |
+
- إنشاء تسعير جديد
|
83 |
+
- تحليل سعر البند
|
84 |
+
- نموذج التسعير الشامل
|
85 |
+
- التسعير غير المتزن
|
86 |
+
- المحتوى المحلي
|
87 |
+
- حاسبة تكاليف البناء
|
88 |
+
- الأدوات المساعدة
|
89 |
+
|
90 |
+
### 2. وحدة الذكاء الاصطناعي
|
91 |
+
- **الوظائف ال��ئيسية**:
|
92 |
+
- تحليل المستندات باستخدام الذكاء الاصطناعي
|
93 |
+
- توليد توصيات ذكية للتسعير
|
94 |
+
- تحليل المخاطر باستخدام الذكاء الاصطناعي
|
95 |
+
- مساعد المحادثة الذكي
|
96 |
+
- تلخيص المستندات
|
97 |
+
- استخراج المعلومات الرئيسية
|
98 |
+
|
99 |
+
### 3. وحدة تحليل البيانات
|
100 |
+
- **الوظائف الرئيسية**:
|
101 |
+
- استخراج النصوص من المستندات
|
102 |
+
- استخراج الجداول والبنود
|
103 |
+
- تحليل المستندات
|
104 |
+
- تحويل المستندات إلى بيانات منظمة
|
105 |
+
- تحليل الصور والمخططات
|
106 |
+
|
107 |
+
### 4. وحدة الموارد
|
108 |
+
- **الوظائف الرئيسية**:
|
109 |
+
- إدارة المواد
|
110 |
+
- إدارة العمالة
|
111 |
+
- إدارة المعدات
|
112 |
+
- إدارة المقاولين من الباطن
|
113 |
+
- تحليل تكاليف الموارد
|
114 |
+
- تخطيط الموارد
|
115 |
+
|
116 |
+
## واجهة المستخدم
|
117 |
+
- تستخدم إطار عمل Streamlit لبناء واجهة مستخدم تفاعلية
|
118 |
+
- تدعم اللغة العربية بشكل كامل
|
119 |
+
- تتضمن تبويبات لكل وحدة من وحدات النظام
|
120 |
+
- تدعم الوضعين الفاتح والداكن
|
121 |
+
- تتضمن رسومات بيانية تفاعلية باستخدام Plotly
|
122 |
+
|
123 |
+
## تكامل الوحدات
|
124 |
+
- تتكامل وحدة التسعير مع وحدة الموارد لاستخدام بيانات الأسعار
|
125 |
+
- تتكامل وحدة تحليل البيانات مع وحدة التسعير لاستخراج بنود المناقصة
|
126 |
+
- تتكامل وحدة الذكاء الاصطناعي مع جميع الوحدات لتقديم توصيات ذكية
|
127 |
+
- تتكامل جميع الوحدات مع قاعدة البيانات المركزية
|
128 |
+
|
129 |
+
## التقنيات المستخدمة
|
130 |
+
- **لغة البرمجة**: Python
|
131 |
+
- **إطار عمل واجهة المستخدم**: Streamlit
|
132 |
+
- **معالجة البيانات**: Pandas, NumPy
|
133 |
+
- **الرسوم البيانية**: Plotly, Matplotlib
|
134 |
+
- **الذكاء الاصطناعي**: OpenAI API, Anthropic API, Transformers
|
135 |
+
- **معالجة المستندات**: PyPDF2, python-docx, pdf2image
|
136 |
+
- **قاعدة البيانات**: SQLAlchemy
|
docs/missing_modules_analysis.md
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# تحليل الوحدات الناقصة والميزات المطلوبة
|
2 |
+
|
3 |
+
## الوحدات الناقصة في النظام الحالي
|
4 |
+
|
5 |
+
بناءً على تحليل دليل المستخدم والمتطلبات المقدمة، تم تحديد الوحدات والميزات التالية التي يجب إضافتها أو تحسينها في النظام:
|
6 |
+
|
7 |
+
### 1. وحدة الخرائط والمواقع
|
8 |
+
- خرائط تفاعلية لمواقع المشاريع
|
9 |
+
- عرض المشاريع على الخريطة بناءً على الموقع الجغرافي
|
10 |
+
- إمكانية تحديد المسافات والمناطق
|
11 |
+
|
12 |
+
### 2. وحدة الإشعارات الذكية
|
13 |
+
- نظام إشعارات متكامل للتنبيهات المهمة
|
14 |
+
- إشعارات للمواعيد النهائية والمهام
|
15 |
+
- إشعارات لتغييرات حالة المناقصات والمشاريع
|
16 |
+
|
17 |
+
### 3. وحدة مقارنة المستندات
|
18 |
+
- أدوات متطورة لمقارنة المستندات
|
19 |
+
- تحديد الاختلافات بين إصدارات المستندات
|
20 |
+
- تحليل التغييرات في الشروط والبنود
|
21 |
+
|
22 |
+
### 4. وحدة الترجمة
|
23 |
+
- ترجمة المستندات والنصوص بين اللغات
|
24 |
+
- دعم خاص للترجمة بين العربية والإنجليزية
|
25 |
+
- ترجمة المصطلحات الفنية والقانونية
|
26 |
+
|
27 |
+
### 5. تحسين وحدة الذكاء الاصطناعي
|
28 |
+
- تكامل مع نماذج الذكاء الاصطناعي المتقدمة
|
29 |
+
- العمل في بيئة هجينة (محلية وسحابية)
|
30 |
+
- طلب مفاتيح API من قسم الأمان
|
31 |
+
|
32 |
+
### 6. تحسين وحدة التسعير المتكامل
|
33 |
+
- تطوير نظام تسعير شامل ومتكامل
|
34 |
+
- تحسين واجهة المستخدم وسهولة الاستخدام
|
35 |
+
- إضافة ميزات تحليل الأسعار المتقدمة
|
36 |
+
|
37 |
+
### 7. تحسين وحدة إدارة المشاريع
|
38 |
+
- تطوير وحدة متكاملة لإدارة المشاريع المرساة
|
39 |
+
- متابعة تنفيذ المشاريع ومراحلها
|
40 |
+
- إدارة الموارد والجداول الزمنية للمشاريع
|
41 |
+
|
42 |
+
## تحسينات الواجهة المرئية المطلوبة
|
43 |
+
|
44 |
+
### 1. تحسين التصميم العام
|
45 |
+
- تطوير واجهة مستخدم أكثر احترافية
|
46 |
+
- تحسين الألوان والتباين
|
47 |
+
- تنسيق متناسق بين جميع الوحدات
|
48 |
+
|
49 |
+
### 2. تحسين القوائم والتنقل
|
50 |
+
- تطوير شريط قوائم رئيسي متكامل
|
51 |
+
- تحسين تجربة التنقل بين الوحدات
|
52 |
+
- إضافة اختصارات للوظائف الأكثر استخداماً
|
53 |
+
|
54 |
+
### 3. تحسين عرض الشعار والهوية
|
55 |
+
- تحسين عرض شعار الشركة
|
56 |
+
- تطبيق هوية بصرية موحدة في جميع أجزاء النظام
|
57 |
+
- إضافة خيارات تخصيص الواجهة
|
58 |
+
|
59 |
+
### 4. تحسين لوحات المعلومات
|
60 |
+
- تطوير لوحات معلومات تفاعلية وجذابة
|
61 |
+
- تحسين عرض المؤشرات والإحصائيات
|
62 |
+
- إضافة رسوم بيانية متقدمة
|
63 |
+
|
64 |
+
## متطلبات التكامل والاتساق
|
65 |
+
|
66 |
+
### 1. تكامل الوحدات
|
67 |
+
- ضمان التكامل السلس بين جميع وحدات النظام
|
68 |
+
- مشاركة البيانات بين الوحدات المختلفة
|
69 |
+
- واجهة مستخدم موحدة عبر جميع الوحدات
|
70 |
+
|
71 |
+
### 2. اتساق البيانات
|
72 |
+
- ضمان اتساق البيانات بين جميع أجزاء النظام
|
73 |
+
- تزامن البيانات بين الوحدات المختلفة
|
74 |
+
- منع تكرار البيانات وتضاربها
|
75 |
+
|
76 |
+
### 3. أداء النظام
|
77 |
+
- تحسين سرعة استجابة النظام
|
78 |
+
- تحسين كفاءة استخدام الموارد
|
79 |
+
- تحسين تجربة المستخدم العامة
|
80 |
+
|
81 |
+
## الاعتماديات والمتطلبات الفنية
|
82 |
+
|
83 |
+
يجب تحديث ملف المتطلبات ليشمل جميع المكتبات اللازمة للوحدات الجديدة والمحسنة، بما في ذلك:
|
84 |
+
- مكتبات الخرائط والتحليل المكاني
|
85 |
+
- مكتبات الترجمة ومعالجة اللغات
|
86 |
+
- مكتبات الذكاء الاصطناعي والتعلم الآلي
|
87 |
+
- مكتبات الرسوم البيانية والتصور
|
88 |
+
- مكتبات واجهة المستخدم المتقدمة
|
docs/pricing_module_design.md
ADDED
@@ -0,0 +1,714 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# تصميم وحدة التسعير المتكاملة وتحليل الأسعار
|
2 |
+
|
3 |
+
## هيكل قاعدة البيانات
|
4 |
+
|
5 |
+
### جدول فئات البنود (pricing_categories)
|
6 |
+
```sql
|
7 |
+
CREATE TABLE pricing_categories (
|
8 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
9 |
+
name TEXT NOT NULL,
|
10 |
+
description TEXT,
|
11 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
12 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
13 |
+
);
|
14 |
+
```
|
15 |
+
|
16 |
+
### جدول وحدات القياس (measurement_units)
|
17 |
+
```sql
|
18 |
+
CREATE TABLE measurement_units (
|
19 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
20 |
+
name TEXT NOT NULL,
|
21 |
+
symbol TEXT NOT NULL,
|
22 |
+
description TEXT,
|
23 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
24 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
25 |
+
);
|
26 |
+
```
|
27 |
+
|
28 |
+
### جدول بنود التسعير الأساسية (pricing_items_base)
|
29 |
+
```sql
|
30 |
+
CREATE TABLE pricing_items_base (
|
31 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
32 |
+
code TEXT NOT NULL,
|
33 |
+
name TEXT NOT NULL,
|
34 |
+
description TEXT,
|
35 |
+
category_id INTEGER,
|
36 |
+
unit_id INTEGER,
|
37 |
+
base_price REAL NOT NULL,
|
38 |
+
last_updated_date TEXT,
|
39 |
+
price_source TEXT,
|
40 |
+
notes TEXT,
|
41 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
42 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
43 |
+
FOREIGN KEY (category_id) REFERENCES pricing_categories (id),
|
44 |
+
FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
|
45 |
+
);
|
46 |
+
```
|
47 |
+
|
48 |
+
### جدول تاريخ أسعار البنود (pricing_items_history)
|
49 |
+
```sql
|
50 |
+
CREATE TABLE pricing_items_history (
|
51 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
52 |
+
base_item_id INTEGER,
|
53 |
+
price REAL NOT NULL,
|
54 |
+
price_date TEXT NOT NULL,
|
55 |
+
price_source TEXT,
|
56 |
+
notes TEXT,
|
57 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
58 |
+
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id)
|
59 |
+
);
|
60 |
+
```
|
61 |
+
|
62 |
+
### جدول بنود التسعير للمشاريع (project_pricing_items)
|
63 |
+
```sql
|
64 |
+
CREATE TABLE project_pricing_items (
|
65 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
66 |
+
project_id INTEGER,
|
67 |
+
base_item_id INTEGER,
|
68 |
+
item_number TEXT NOT NULL,
|
69 |
+
description TEXT NOT NULL,
|
70 |
+
unit_id INTEGER,
|
71 |
+
quantity REAL NOT NULL,
|
72 |
+
unit_price REAL NOT NULL,
|
73 |
+
total_price REAL NOT NULL,
|
74 |
+
direct_cost REAL,
|
75 |
+
indirect_cost REAL,
|
76 |
+
profit_margin REAL,
|
77 |
+
risk_factor REAL,
|
78 |
+
notes TEXT,
|
79 |
+
created_by INTEGER,
|
80 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
81 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
82 |
+
FOREIGN KEY (project_id) REFERENCES projects (id),
|
83 |
+
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id),
|
84 |
+
FOREIGN KEY (unit_id) REFERENCES measurement_units (id),
|
85 |
+
FOREIGN KEY (created_by) REFERENCES users (id)
|
86 |
+
);
|
87 |
+
```
|
88 |
+
|
89 |
+
### جدول مكونات بنود التسعير (pricing_item_components)
|
90 |
+
```sql
|
91 |
+
CREATE TABLE pricing_item_components (
|
92 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
93 |
+
pricing_item_id INTEGER,
|
94 |
+
component_type TEXT NOT NULL, -- 'material', 'labor', 'equipment', 'subcontractor', 'other'
|
95 |
+
component_name TEXT NOT NULL,
|
96 |
+
unit_id INTEGER,
|
97 |
+
quantity REAL NOT NULL,
|
98 |
+
unit_price REAL NOT NULL,
|
99 |
+
total_price REAL NOT NULL,
|
100 |
+
notes TEXT,
|
101 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
102 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
103 |
+
FOREIGN KEY (pricing_item_id) REFERENCES project_pricing_items (id),
|
104 |
+
FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
|
105 |
+
);
|
106 |
+
```
|
107 |
+
|
108 |
+
### جدول عوامل التعديل (adjustment_factors)
|
109 |
+
```sql
|
110 |
+
CREATE TABLE adjustment_factors (
|
111 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
112 |
+
name TEXT NOT NULL,
|
113 |
+
description TEXT,
|
114 |
+
factor_type TEXT NOT NULL, -- 'inflation', 'location', 'risk', 'market', 'other'
|
115 |
+
value REAL NOT NULL,
|
116 |
+
start_date TEXT,
|
117 |
+
end_date TEXT,
|
118 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
119 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
120 |
+
);
|
121 |
+
```
|
122 |
+
|
123 |
+
### جدول نماذج التسعير (pricing_templates)
|
124 |
+
```sql
|
125 |
+
CREATE TABLE pricing_templates (
|
126 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
127 |
+
name TEXT NOT NULL,
|
128 |
+
description TEXT,
|
129 |
+
created_by INTEGER,
|
130 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
131 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
132 |
+
FOREIGN KEY (created_by) REFERENCES users (id)
|
133 |
+
);
|
134 |
+
```
|
135 |
+
|
136 |
+
### جدول بنود نماذج التسعير (pricing_template_items)
|
137 |
+
```sql
|
138 |
+
CREATE TABLE pricing_template_items (
|
139 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
140 |
+
template_id INTEGER,
|
141 |
+
base_item_id INTEGER,
|
142 |
+
item_number TEXT NOT NULL,
|
143 |
+
description TEXT NOT NULL,
|
144 |
+
unit_id INTEGER,
|
145 |
+
unit_price REAL NOT NULL,
|
146 |
+
notes TEXT,
|
147 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
148 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
149 |
+
FOREIGN KEY (template_id) REFERENCES pricing_templates (id),
|
150 |
+
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id),
|
151 |
+
FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
|
152 |
+
);
|
153 |
+
```
|
154 |
+
|
155 |
+
### جدول تنبؤات الأسعار (price_forecasts)
|
156 |
+
```sql
|
157 |
+
CREATE TABLE price_forecasts (
|
158 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
159 |
+
base_item_id INTEGER,
|
160 |
+
forecast_date TEXT NOT NULL,
|
161 |
+
forecast_price REAL NOT NULL,
|
162 |
+
forecast_model TEXT,
|
163 |
+
confidence_level REAL,
|
164 |
+
scenario TEXT, -- 'optimistic', 'baseline', 'pessimistic'
|
165 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
166 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
167 |
+
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id)
|
168 |
+
);
|
169 |
+
```
|
170 |
+
|
171 |
+
## هيكل الكلاسات
|
172 |
+
|
173 |
+
### 1. مدير التسعير (PricingManager)
|
174 |
+
```python
|
175 |
+
class PricingManager:
|
176 |
+
"""فئة مدير التسعير الرئيسية"""
|
177 |
+
|
178 |
+
def __init__(self, db_connector, config):
|
179 |
+
"""تهيئة مدير التسعير"""
|
180 |
+
self.db = db_connector
|
181 |
+
self.config = config
|
182 |
+
self.item_manager = PricingItemManager(db_connector)
|
183 |
+
self.cost_calculator = CostCalculator(db_connector)
|
184 |
+
self.price_analyzer = PriceAnalyzer(db_connector)
|
185 |
+
self.price_forecaster = PriceForecaster(db_connector)
|
186 |
+
self.report_generator = PricingReportGenerator(db_connector)
|
187 |
+
|
188 |
+
def initialize_database(self):
|
189 |
+
"""تهيئة قاعدة البيانات للتسعير"""
|
190 |
+
# إنشاء الجداول إذا لم تكن موجودة
|
191 |
+
pass
|
192 |
+
|
193 |
+
def load_default_data(self):
|
194 |
+
"""تحميل البيانات الافتراضية"""
|
195 |
+
# تحميل الفئات ووحدات القياس الافتراضية
|
196 |
+
pass
|
197 |
+
|
198 |
+
def get_project_pricing_summary(self, project_id):
|
199 |
+
"""الحصول على ملخص التسعير للمشروع"""
|
200 |
+
pass
|
201 |
+
|
202 |
+
def import_pricing_data(self, file_path, import_type):
|
203 |
+
"""استيراد بيانات التسعير من ملف خارجي"""
|
204 |
+
pass
|
205 |
+
|
206 |
+
def export_pricing_data(self, project_id, export_type, file_path):
|
207 |
+
"""تصدير بيانات التسعير إلى ملف خارجي"""
|
208 |
+
pass
|
209 |
+
```
|
210 |
+
|
211 |
+
### 2. مدير بنود التسعير (PricingItemManager)
|
212 |
+
```python
|
213 |
+
class PricingItemManager:
|
214 |
+
"""فئة إدارة بنود التسعير"""
|
215 |
+
|
216 |
+
def __init__(self, db_connector):
|
217 |
+
"""تهيئة مدير بنود التسعير"""
|
218 |
+
self.db = db_connector
|
219 |
+
|
220 |
+
def get_all_base_items(self, filters=None):
|
221 |
+
"""الحصول على جميع البنود الأساسية"""
|
222 |
+
pass
|
223 |
+
|
224 |
+
def get_base_item_by_id(self, item_id):
|
225 |
+
"""الحصول على بند أساسي بواسطة المعرف"""
|
226 |
+
pass
|
227 |
+
|
228 |
+
def add_base_item(self, item_data):
|
229 |
+
"""إضافة بند أساسي جديد"""
|
230 |
+
pass
|
231 |
+
|
232 |
+
def update_base_item(self, item_id, item_data):
|
233 |
+
"""تحديث بند أساسي"""
|
234 |
+
pass
|
235 |
+
|
236 |
+
def delete_base_item(self, item_id):
|
237 |
+
"""حذف بند أساسي"""
|
238 |
+
pass
|
239 |
+
|
240 |
+
def get_project_items(self, project_id):
|
241 |
+
"""الحصول على بنود المشروع"""
|
242 |
+
pass
|
243 |
+
|
244 |
+
def add_project_item(self, project_id, item_data):
|
245 |
+
"""إضافة بند للمشروع"""
|
246 |
+
pass
|
247 |
+
|
248 |
+
def update_project_item(self, item_id, item_data):
|
249 |
+
"""تحديث بند المشروع"""
|
250 |
+
pass
|
251 |
+
|
252 |
+
def delete_project_item(self, item_id):
|
253 |
+
"""حذف بند المشروع"""
|
254 |
+
pass
|
255 |
+
|
256 |
+
def get_item_components(self, item_id):
|
257 |
+
"""الحصول على مكونات البند"""
|
258 |
+
pass
|
259 |
+
|
260 |
+
def add_item_component(self, item_id, component_data):
|
261 |
+
"""إضافة مكون للبند"""
|
262 |
+
pass
|
263 |
+
|
264 |
+
def update_item_component(self, component_id, component_data):
|
265 |
+
"""تحديث مكون البند"""
|
266 |
+
pass
|
267 |
+
|
268 |
+
def delete_item_component(self, component_id):
|
269 |
+
"""حذف مكون البند"""
|
270 |
+
pass
|
271 |
+
|
272 |
+
def get_categories(self):
|
273 |
+
"""الحصول على فئات البنود"""
|
274 |
+
pass
|
275 |
+
|
276 |
+
def get_measurement_units(self):
|
277 |
+
"""الحصول على وحدات القياس"""
|
278 |
+
pass
|
279 |
+
|
280 |
+
def get_templates(self):
|
281 |
+
"""الحصول على نماذج التسعير"""
|
282 |
+
pass
|
283 |
+
|
284 |
+
def get_template_items(self, template_id):
|
285 |
+
"""الحصول على بنود النموذج"""
|
286 |
+
pass
|
287 |
+
|
288 |
+
def apply_template_to_project(self, project_id, template_id):
|
289 |
+
"""تطبيق نموذج على مشروع"""
|
290 |
+
pass
|
291 |
+
```
|
292 |
+
|
293 |
+
### 3. حاسبة التكاليف (CostCalculator)
|
294 |
+
```python
|
295 |
+
class CostCalculator:
|
296 |
+
"""فئة حساب التكاليف"""
|
297 |
+
|
298 |
+
def __init__(self, db_connector):
|
299 |
+
"""تهيئة حاسبة التكاليف"""
|
300 |
+
self.db = db_connector
|
301 |
+
|
302 |
+
def calculate_direct_costs(self, project_id):
|
303 |
+
"""حساب التكاليف المباشرة"""
|
304 |
+
pass
|
305 |
+
|
306 |
+
def calculate_indirect_costs(self, project_id, indirect_cost_percentage):
|
307 |
+
"""حساب التكاليف غير المباشرة"""
|
308 |
+
pass
|
309 |
+
|
310 |
+
def calculate_profit_margin(self, project_id, profit_percentage):
|
311 |
+
"""حساب هامش الربح"""
|
312 |
+
pass
|
313 |
+
|
314 |
+
def calculate_risk_contingency(self, project_id, risk_factors):
|
315 |
+
"""حساب احتياطي المخاطر"""
|
316 |
+
pass
|
317 |
+
|
318 |
+
def calculate_taxes_and_fees(self, project_id, tax_rates):
|
319 |
+
"""حساب الضرائب والرسوم"""
|
320 |
+
pass
|
321 |
+
|
322 |
+
def calculate_total_cost(self, project_id):
|
323 |
+
"""حساب التكلفة الإجمالية"""
|
324 |
+
pass
|
325 |
+
|
326 |
+
def calculate_unit_rates(self, project_id):
|
327 |
+
"""حساب معدلات الوحدات"""
|
328 |
+
pass
|
329 |
+
|
330 |
+
def apply_adjustment_factors(self, project_id, factors):
|
331 |
+
"""تطبيق عوامل التعديل"""
|
332 |
+
pass
|
333 |
+
|
334 |
+
def calculate_cost_breakdown(self, project_id):
|
335 |
+
"""حساب تفصيل التكاليف"""
|
336 |
+
pass
|
337 |
+
```
|
338 |
+
|
339 |
+
### 4. محلل الأسعار (PriceAnalyzer)
|
340 |
+
```python
|
341 |
+
class PriceAnalyzer:
|
342 |
+
"""فئة تحليل الأسعار"""
|
343 |
+
|
344 |
+
def __init__(self, db_connector):
|
345 |
+
"""تهيئة محلل الأسعار"""
|
346 |
+
self.db = db_connector
|
347 |
+
|
348 |
+
def get_price_history(self, item_id):
|
349 |
+
"""الحصول على تاريخ الأسعار"""
|
350 |
+
pass
|
351 |
+
|
352 |
+
def analyze_price_trends(self, item_id, start_date, end_date):
|
353 |
+
"""تحليل اتجاهات الأسعار"""
|
354 |
+
pass
|
355 |
+
|
356 |
+
def compare_prices(self, items, date=None):
|
357 |
+
"""مقارنة الأسعار"""
|
358 |
+
pass
|
359 |
+
|
360 |
+
def calculate_price_volatility(self, item_id, period):
|
361 |
+
"""حساب تقلب الأسعار"""
|
362 |
+
pass
|
363 |
+
|
364 |
+
def perform_sensitivity_analysis(self, project_id, variable_items, ranges):
|
365 |
+
"""إجراء تحليل الحساسية"""
|
366 |
+
pass
|
367 |
+
|
368 |
+
def analyze_price_correlations(self, items):
|
369 |
+
"""تحليل ارتباطات الأسعار"""
|
370 |
+
pass
|
371 |
+
|
372 |
+
def compare_with_market_prices(self, items):
|
373 |
+
"""مقارنة مع أسعار السوق"""
|
374 |
+
pass
|
375 |
+
|
376 |
+
def analyze_cost_drivers(self, project_id):
|
377 |
+
"""تحليل محركات التكلفة"""
|
378 |
+
pass
|
379 |
+
|
380 |
+
def generate_price_analysis_charts(self, analysis_type, params):
|
381 |
+
"""إنشاء رسوم بيانية لتحليل الأسعار"""
|
382 |
+
pass
|
383 |
+
```
|
384 |
+
|
385 |
+
### 5. متنبئ الأسعار (PriceForecaster)
|
386 |
+
```python
|
387 |
+
class PriceForecaster:
|
388 |
+
"""فئة التنبؤ بالأسعار"""
|
389 |
+
|
390 |
+
def __init__(self, db_connector):
|
391 |
+
"""تهيئة متنبئ الأسعار"""
|
392 |
+
self.db = db_connector
|
393 |
+
|
394 |
+
def forecast_price(self, item_id, forecast_date, model_type='arima'):
|
395 |
+
"""التنبؤ بالسعر"""
|
396 |
+
pass
|
397 |
+
|
398 |
+
def generate_price_scenarios(self, item_id, forecast_date):
|
399 |
+
"""إنشاء سيناريوهات الأسعار"""
|
400 |
+
pass
|
401 |
+
|
402 |
+
def calculate_inflation_impact(self, project_id, inflation_rate, duration):
|
403 |
+
"""حساب تأثير التضخم"""
|
404 |
+
pass
|
405 |
+
|
406 |
+
def forecast_project_costs(self, project_id, forecast_date):
|
407 |
+
"""التنبؤ بتكاليف المشروع"""
|
408 |
+
pass
|
409 |
+
|
410 |
+
def evaluate_forecast_accuracy(self, item_id):
|
411 |
+
"""تقييم دقة التنبؤ"""
|
412 |
+
pass
|
413 |
+
|
414 |
+
def generate_forecast_charts(self, item_id, forecast_date):
|
415 |
+
"""إنشاء رسوم بيانية للتنبؤ"""
|
416 |
+
pass
|
417 |
+
```
|
418 |
+
|
419 |
+
### 6. مولد تقارير التسعير (PricingReportGenerator)
|
420 |
+
```python
|
421 |
+
class PricingReportGenerator:
|
422 |
+
"""فئة إنشاء تقارير التسعير"""
|
423 |
+
|
424 |
+
def __init__(self, db_connector):
|
425 |
+
"""تهيئة مولد تقارير التسعير"""
|
426 |
+
self.db = db_connector
|
427 |
+
|
428 |
+
def generate_cost_summary_report(self, project_id):
|
429 |
+
"""إنشاء تقرير ملخص التكاليف"""
|
430 |
+
pass
|
431 |
+
|
432 |
+
def generate_detailed_items_report(self, project_id):
|
433 |
+
"""إنشاء تقرير تفصيلي للبنود"""
|
434 |
+
pass
|
435 |
+
|
436 |
+
def generate_price_comparison_report(self, items, parameters):
|
437 |
+
"""إنشاء تقرير مقارنة الأسعار"""
|
438 |
+
pass
|
439 |
+
|
440 |
+
def generate_sensitivity_analysis_report(self, project_id, parameters):
|
441 |
+
"""إنشاء تقرير تحليل الحساسية"""
|
442 |
+
pass
|
443 |
+
|
444 |
+
def generate_price_forecast_report(self, items, forecast_date):
|
445 |
+
"""إنشاء تقرير التنبؤ بالأسعار"""
|
446 |
+
pass
|
447 |
+
|
448 |
+
def generate_price_risk_report(self, project_id):
|
449 |
+
"""إنشاء تقرير مخاطر الأسعار"""
|
450 |
+
pass
|
451 |
+
|
452 |
+
def export_report_to_pdf(self, report_data, file_path):
|
453 |
+
"""تصدير التقرير إلى PDF"""
|
454 |
+
pass
|
455 |
+
|
456 |
+
def export_report_to_excel(self, report_data, file_path):
|
457 |
+
"""تصدير التقرير إلى Excel"""
|
458 |
+
pass
|
459 |
+
```
|
460 |
+
|
461 |
+
## تصميم واجهة المستخدم
|
462 |
+
|
463 |
+
### 1. الشاشة الرئيسية لوحدة التسعير
|
464 |
+
|
465 |
+
```
|
466 |
+
+--------------------------------------------------+
|
467 |
+
| وحدة التسعير |
|
468 |
+
+--------------------------------------------------+
|
469 |
+
| |
|
470 |
+
| +----------------+ +----------------------+ |
|
471 |
+
| | المناقصات | | إحصائيات التسعير | |
|
472 |
+
| | الحالية | | | |
|
473 |
+
| | | | | |
|
474 |
+
| | | | | |
|
475 |
+
| | | | | |
|
476 |
+
| +----------------+ +----------------------+ |
|
477 |
+
| |
|
478 |
+
| +----------------+ +----------------------+ |
|
479 |
+
| | الوصول | | آخر التحديثات | |
|
480 |
+
| | السريع | | | |
|
481 |
+
| | | | | |
|
482 |
+
| | | | | |
|
483 |
+
| | | | | |
|
484 |
+
| +----------------+ +----------------------+ |
|
485 |
+
| |
|
486 |
+
+--------------------------------------------------+
|
487 |
+
```
|
488 |
+
|
489 |
+
### 2. شاشة إدارة بنود التسعير
|
490 |
+
|
491 |
+
```
|
492 |
+
+--------------------------------------------------+
|
493 |
+
| إدارة بنود التسعير |
|
494 |
+
+--------------------------------------------------+
|
495 |
+
| بحث: [ ] [تصفية▼] [تصدير] |
|
496 |
+
+--------------------------------------------------+
|
497 |
+
| # | الكود | الوصف | الوحدة | الكمية | السعر | المجموع |
|
498 |
+
+--------------------------------------------------+
|
499 |
+
| 1 | | | | | | |
|
500 |
+
| 2 | | | | | | |
|
501 |
+
| 3 | | | | | | |
|
502 |
+
| 4 | | | | | | |
|
503 |
+
| 5 | | | | | | |
|
504 |
+
+--------------------------------------------------+
|
505 |
+
| [إضافة بند] [حذف المحدد] [استيراد من Excel] |
|
506 |
+
+--------------------------------------------------+
|
507 |
+
| المجموع الكلي: |
|
508 |
+
+--------------------------------------------------+
|
509 |
+
```
|
510 |
+
|
511 |
+
### 3. شاشة تفاصيل البند
|
512 |
+
|
513 |
+
```
|
514 |
+
+--------------------------------------------------+
|
515 |
+
| تفاصيل البند |
|
516 |
+
+--------------------------------------------------+
|
517 |
+
| الكود: [ ] الوصف: [ ] |
|
518 |
+
| الفئة: [ ▼] الوحدة: [ ▼] |
|
519 |
+
+--------------------------------------------------+
|
520 |
+
| مكونات البند: |
|
521 |
+
+--------------------------------------------------+
|
522 |
+
| النوع | الوصف | الوحدة | الكمية | السعر | المجموع |
|
523 |
+
+--------------------------------------------------+
|
524 |
+
| مواد | | | | | |
|
525 |
+
| عمالة | | | | | |
|
526 |
+
| معدات | | | | | |
|
527 |
+
| أخرى | | | | | |
|
528 |
+
+--------------------------------------------------+
|
529 |
+
| [إضافة مكون] [حذف المحدد] |
|
530 |
+
+--------------------------------------------------+
|
531 |
+
| التكلفة المباشرة: |
|
532 |
+
| التكلفة غير المباشرة: |
|
533 |
+
| هامش الربح: |
|
534 |
+
| احتياطي المخاطر: |
|
535 |
+
| السعر النهائي: |
|
536 |
+
+--------------------------------------------------+
|
537 |
+
| [حفظ] [إلغاء] |
|
538 |
+
+--------------------------------------------------+
|
539 |
+
```
|
540 |
+
|
541 |
+
### 4. شاشة تحليل الأسعار
|
542 |
+
|
543 |
+
```
|
544 |
+
+--------------------------------------------------+
|
545 |
+
| تحليل الأسعار |
|
546 |
+
+--------------------------------------------------+
|
547 |
+
| [اختيار البند▼] [الفترة الزمنية▼] [تحليل] |
|
548 |
+
+--------------------------------------------------+
|
549 |
+
| |
|
550 |
+
| |
|
551 |
+
| |
|
552 |
+
| (رسم بياني للأسعار) |
|
553 |
+
| |
|
554 |
+
| |
|
555 |
+
| |
|
556 |
+
+--------------------------------------------------+
|
557 |
+
| إحصائيات: |
|
558 |
+
| - متوسط السعر: |
|
559 |
+
| - أعلى سعر: |
|
560 |
+
| - أدنى سعر: |
|
561 |
+
| - معدل التغير: |
|
562 |
+
| - التقلب: |
|
563 |
+
+--------------------------------------------------+
|
564 |
+
| [مقارنة مع بنود أخرى] [تصدير التحليل] |
|
565 |
+
+--------------------------------------------------+
|
566 |
+
```
|
567 |
+
|
568 |
+
### 5. ش��شة التنبؤ بالأسعار
|
569 |
+
|
570 |
+
```
|
571 |
+
+--------------------------------------------------+
|
572 |
+
| التنبؤ بالأسعار |
|
573 |
+
+--------------------------------------------------+
|
574 |
+
| [اختيار البند▼] [تاريخ التنبؤ] [نموذج التنبؤ▼] |
|
575 |
+
+--------------------------------------------------+
|
576 |
+
| |
|
577 |
+
| |
|
578 |
+
| |
|
579 |
+
| (رسم بياني للتنبؤ بالأسعار) |
|
580 |
+
| |
|
581 |
+
| |
|
582 |
+
| |
|
583 |
+
+--------------------------------------------------+
|
584 |
+
| السيناريوهات: |
|
585 |
+
| - متفائل: |
|
586 |
+
| - متوسط: |
|
587 |
+
| - متشائم: |
|
588 |
+
+--------------------------------------------------+
|
589 |
+
| عوامل التأثير: |
|
590 |
+
| - التضخم: |
|
591 |
+
| - تغيرات السوق: |
|
592 |
+
| - العوامل الموسمية: |
|
593 |
+
+--------------------------------------------------+
|
594 |
+
| [تطبيق على المشروع] [تصدير التنبؤ] |
|
595 |
+
+--------------------------------------------------+
|
596 |
+
```
|
597 |
+
|
598 |
+
### 6. شاشة تحليل الحساسية
|
599 |
+
|
600 |
+
```
|
601 |
+
+--------------------------------------------------+
|
602 |
+
| تحليل الحساسية |
|
603 |
+
+--------------------------------------------------+
|
604 |
+
| المشروع: [ ▼] |
|
605 |
+
+--------------------------------------------------+
|
606 |
+
| المتغيرات: |
|
607 |
+
| [✓] أسعار المواد الخام (±20%) |
|
608 |
+
| [✓] تكلفة العمالة (±15%) |
|
609 |
+
| [✓] تكلفة المعدات (±10%) |
|
610 |
+
| [ ] المصاريف العامة (±5%) |
|
611 |
+
+--------------------------------------------------+
|
612 |
+
| |
|
613 |
+
| |
|
614 |
+
| (رسم بياني لتحليل الحساسية) |
|
615 |
+
| |
|
616 |
+
| |
|
617 |
+
| |
|
618 |
+
+--------------------------------------------------+
|
619 |
+
| النتائج: |
|
620 |
+
| - أكثر العوامل تأثيراً: |
|
621 |
+
| - نطاق التغير المتوقع: |
|
622 |
+
| - توصيات: |
|
623 |
+
+--------------------------------------------------+
|
624 |
+
| [تحديث التحليل] [تصدير النتائج] |
|
625 |
+
+--------------------------------------------------+
|
626 |
+
```
|
627 |
+
|
628 |
+
### 7. شاشة التقارير
|
629 |
+
|
630 |
+
```
|
631 |
+
+--------------------------------------------------+
|
632 |
+
| التقارير |
|
633 |
+
+--------------------------------------------------+
|
634 |
+
| [نوع التقرير▼] [المشروع▼] [إنشاء تقرير] |
|
635 |
+
+--------------------------------------------------+
|
636 |
+
| التقارير المتاحة: |
|
637 |
+
| |
|
638 |
+
| ○ ملخص التكاليف |
|
639 |
+
| ○ تفصيل البنود |
|
640 |
+
| ○ مقارنة الأسعار |
|
641 |
+
| ○ تحليل الحساسية |
|
642 |
+
| ○ التنبؤ بالأسعار |
|
643 |
+
| ○ مخاطر الأسعار |
|
644 |
+
| |
|
645 |
+
+--------------------------------------------------+
|
646 |
+
| خيارات التقرير: |
|
647 |
+
| |
|
648 |
+
| [✓] تضمين الرسوم البيانية |
|
649 |
+
| [✓] تضمين التوصيات |
|
650 |
+
| [ ] تضمين البيانات التفصيلية |
|
651 |
+
| |
|
652 |
+
+--------------------------------------------------+
|
653 |
+
| [PDF] [Excel] [طباعة] |
|
654 |
+
+--------------------------------------------------+
|
655 |
+
```
|
656 |
+
|
657 |
+
## تكامل النظام
|
658 |
+
|
659 |
+
### 1. تكامل مع وحدة تحليل المستندات
|
660 |
+
- استخراج بنود التسعير من وثائق المناقصة
|
661 |
+
- تحديد الكميات والمواصفات من المستندات
|
662 |
+
- مقارنة البنود المستخرجة مع قاعدة البيانات
|
663 |
+
|
664 |
+
### 2. تكامل مع وحدة تحليل المخاطر
|
665 |
+
- ��حديد المخاطر المرتبطة بالتسعير
|
666 |
+
- تقييم تأثير المخاطر على التكاليف
|
667 |
+
- تحديد احتياطي المخاطر المناسب
|
668 |
+
|
669 |
+
### 3. تكامل مع وحدة إدارة المشاريع
|
670 |
+
- متابعة التكاليف الفعلية مقابل المخططة
|
671 |
+
- تحديث التنبؤات بناءً على بيانات المشروع الفعلية
|
672 |
+
- تحليل انحرافات التكاليف
|
673 |
+
|
674 |
+
### 4. تكامل مع وحدة التقارير
|
675 |
+
- إنشاء تقارير متكاملة تشمل بيانات التسعير
|
676 |
+
- دمج تحليلات التسعير في تقارير المشروع
|
677 |
+
- توفير لوحات معلومات متكاملة
|
678 |
+
|
679 |
+
## خطة التنفيذ التفصيلية
|
680 |
+
|
681 |
+
### المرحلة 1: إعداد البنية التحتية (3 أيام)
|
682 |
+
- تصميم وإنشاء جداول قاعدة البيانات
|
683 |
+
- إعداد هيكل الملفات والمجلدات
|
684 |
+
- تهيئة البيئة التطويرية
|
685 |
+
|
686 |
+
### المرحلة 2: تنفيذ الوظائف الأساسية (5 أيام)
|
687 |
+
- تنفيذ فئة مدير التسعير
|
688 |
+
- تنفيذ فئة مدير بنود التسعير
|
689 |
+
- تنفيذ فئة حاسبة التكاليف
|
690 |
+
- إنشاء واجهات المستخدم الأساسية
|
691 |
+
|
692 |
+
### المرحلة 3: تنفيذ وظائف التحليل (7 أيام)
|
693 |
+
- تنفيذ فئة محلل الأسعار
|
694 |
+
- تنفيذ فئة متنبئ الأسعار
|
695 |
+
- إنشاء الرسوم البيانية والتحليلات
|
696 |
+
- تنفيذ واجهات المستخدم للتحليل
|
697 |
+
|
698 |
+
### المرحلة 4: تنفيذ التقارير والتكامل (5 أيام)
|
699 |
+
- تنفيذ فئة مولد تقارير التسعير
|
700 |
+
- تكامل مع الوحدات الأخرى
|
701 |
+
- إنشاء واجهات المستخدم للتقارير
|
702 |
+
- اختبار التكامل
|
703 |
+
|
704 |
+
### المرحلة 5: الاختبار والتحسين (3 أيام)
|
705 |
+
- اختبار جميع الوظائف
|
706 |
+
- تحسين الأداء
|
707 |
+
- إصلاح الأخطاء
|
708 |
+
- تحسين واجهة المستخدم
|
709 |
+
|
710 |
+
### المرحلة 6: التوثيق والتسليم (2 أيام)
|
711 |
+
- إعداد وثائق المستخدم
|
712 |
+
- إعداد وثائق المطور
|
713 |
+
- تجهيز النسخة النهائية
|
714 |
+
- تسليم النظام
|
docs/pricing_module_requirements.md
ADDED
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# متطلبات وحدة التسعير المتكاملة وتحليل الأسعار
|
2 |
+
|
3 |
+
## الهدف
|
4 |
+
تطوير وحدة متكاملة للتسعير وتحليل الأسعار في نظام إدارة المناقصات لتمكين المستخدمين من إدارة عمليات التسعير بشكل أكثر دقة وفعالية، وتوفير أدوات تحليلية متقدمة لاتخاذ قرارات أفضل.
|
5 |
+
|
6 |
+
## المتطلبات الوظيفية
|
7 |
+
|
8 |
+
### 1. إدارة بنود التسعير
|
9 |
+
- إنشاء وتحرير وحذف بنود التسعير
|
10 |
+
- تصنيف البنود حسب الفئات (مواد، عمالة، معدات، مصاريف عامة)
|
11 |
+
- دعم الوحدات المختلفة (متر مربع، متر مكعب، عدد، طن، إلخ)
|
12 |
+
- إمكانية استيراد بنود التسعير من ملفات Excel
|
13 |
+
- إمكانية تصدير بنود التسعير إلى ملفات Excel
|
14 |
+
|
15 |
+
### 2. حساب التكاليف
|
16 |
+
- حساب تكلفة المواد المباشرة
|
17 |
+
- حساب تكلفة العمالة المباشرة
|
18 |
+
- حساب تكلفة المعدات
|
19 |
+
- حساب المصاريف العامة والإدارية
|
20 |
+
- حساب هامش الربح
|
21 |
+
- حساب الضرائب والرسوم
|
22 |
+
|
23 |
+
### 3. تحليل الأسعار
|
24 |
+
- مقارنة الأسعار التاريخية للبنود
|
25 |
+
- تحليل تغيرات الأسعار عبر الزمن
|
26 |
+
- تحليل حساسية الأسعار للمتغيرات المختلفة
|
27 |
+
- تحليل المخاطر المرتبطة بتغيرات الأسعار
|
28 |
+
- مقارنة الأسعار مع أسعار السوق
|
29 |
+
- تحليل تأثير تغير أسعار المواد الخام على التكلفة الإجمالية
|
30 |
+
|
31 |
+
### 4. التنبؤ بالأسعار
|
32 |
+
- التنبؤ بتغيرات الأسعار المستقبلية
|
33 |
+
- حساب معدلات التضخم المتوقعة
|
34 |
+
- تقدير تأثير العوامل الاقتصادية على الأسعار
|
35 |
+
- إنشاء سيناريوهات مختلفة للأسعار (متفائل، متوسط، متشائم)
|
36 |
+
|
37 |
+
### 5. تقارير التسعير
|
38 |
+
- تقرير ملخص التكاليف
|
39 |
+
- تقرير تفصيلي للبنود
|
40 |
+
- تقرير مقارنة الأسعار
|
41 |
+
- تقرير تحليل الحساسية
|
42 |
+
- تقرير التنبؤ بالأسعار
|
43 |
+
- تقرير المخاطر المرتبطة بالأسعار
|
44 |
+
|
45 |
+
### 6. لوحة معلومات التسعير
|
46 |
+
- عرض مؤشرات الأداء الرئيسية للتسعير
|
47 |
+
- عرض الرسوم البيانية لتحليل الأسعار
|
48 |
+
- عرض تنبيهات لتغيرات الأسعار الكبيرة
|
49 |
+
- عرض مقارنات مع المناقصات السابقة
|
50 |
+
|
51 |
+
## المتطلبات غير الوظيفية
|
52 |
+
|
53 |
+
### 1. الأداء
|
54 |
+
- سرعة استجابة عالية عند التعامل مع كميات كبيرة من البيانات
|
55 |
+
- قدرة على معالجة آلاف البنود في المناقصة الواحدة
|
56 |
+
|
57 |
+
### 2. قابلية الاستخدام
|
58 |
+
- واجهة مستخدم بديهية وسهلة الاستخدام
|
59 |
+
- إمكانية تخصيص العرض حسب احتياجات المستخدم
|
60 |
+
- توفير أدوات مساعدة وشروحات للمستخدمين
|
61 |
+
|
62 |
+
### 3. التكامل
|
63 |
+
- تكامل مع وحدة تحليل المستندات لاستخراج بنود التسعير من وثائق المناقصة
|
64 |
+
- تكامل مع وحدة تحليل المخاطر لتقييم مخاطر التسعير
|
65 |
+
- تكامل مع وحدة إدارة المشاريع لمتابعة التكاليف الفعلية مقابل المخططة
|
66 |
+
|
67 |
+
### 4. الأمان
|
68 |
+
- تحديد صلاحيات الوصول لبيانات التسعير
|
69 |
+
- تسجيل جميع التغييرات على بيانات التسعير
|
70 |
+
- حماية البيانات الحساسة المتعلقة بالتسعير
|
71 |
+
|
72 |
+
## التقنيات المقترحة
|
73 |
+
|
74 |
+
### 1. تخزين البيانات
|
75 |
+
- استخدام قاعدة بيانات SQLite لتخزين بيانات التسعير
|
76 |
+
- تصميم جداول مناسبة لتخزين البنود والتكاليف والأسعار التاريخية
|
77 |
+
|
78 |
+
### 2. تحليل البيانات
|
79 |
+
- استخدام مكتبة Pandas لمعالجة وتحليل بيانات الأسعار
|
80 |
+
- استخدام مكتبة NumPy للعمليات الحسابية المتقدمة
|
81 |
+
- استخدام مكتبة SciPy للتحليل الإحصائي
|
82 |
+
|
83 |
+
### 3. التنبؤ بالأسعار
|
84 |
+
- استخدام مكتبة Statsmodels للنماذج الإحصائية
|
85 |
+
- استخدام مكتبة Prophet للتنبؤ بالسلاسل الزمنية
|
86 |
+
- استخدام مكتبة scikit-learn لنماذج التعلم الآلي
|
87 |
+
|
88 |
+
### 4. العرض المرئي
|
89 |
+
- استخدام مكتبة Matplotlib لإنشاء الرسوم البيانية الأساسية
|
90 |
+
- استخدام مكتبة Seaborn للرسوم البيانية الإحصائية المتقدمة
|
91 |
+
- استخدام مكتبة Plotly للرسوم البيانية التفاعلية
|
92 |
+
|
93 |
+
## الواجهة المقترح��
|
94 |
+
|
95 |
+
### 1. الشاشة الرئيسية لوحدة التسعير
|
96 |
+
- قائمة بالمناقصات الحالية
|
97 |
+
- ملخص لإحصائيات التسعير
|
98 |
+
- الوصول السريع للوظائف الشائعة
|
99 |
+
|
100 |
+
### 2. شاشة إدارة بنود التسعير
|
101 |
+
- جدول لعرض وتحرير البنود
|
102 |
+
- أدوات للتصفية والبحث
|
103 |
+
- أزرار للإضافة والحذف والاستيراد والتصدير
|
104 |
+
|
105 |
+
### 3. شاشة حساب التكاليف
|
106 |
+
- نموذج لإدخال معلومات التكاليف
|
107 |
+
- عرض ملخص للتكاليف حسب الفئات
|
108 |
+
- حاسبة تفاعلية للتكاليف
|
109 |
+
|
110 |
+
### 4. شاشة تحليل الأسعار
|
111 |
+
- رسوم بيانية لتحليل الأسعار
|
112 |
+
- أدوات للمقارنة والتحليل
|
113 |
+
- خيارات لتخصيص التحليل
|
114 |
+
|
115 |
+
### 5. شاشة التنبؤ بالأسعار
|
116 |
+
- نماذج للتنبؤ بالأسعار
|
117 |
+
- عرض السيناريوهات المختلفة
|
118 |
+
- تحليل الحساسية للمتغيرات
|
119 |
+
|
120 |
+
### 6. شاشة تقارير التسعير
|
121 |
+
- قائمة بالتقارير المتاحة
|
122 |
+
- خيارات لتخصيص التقارير
|
123 |
+
- أدوات لتصدير التقارير
|
124 |
+
|
125 |
+
## خطة التنفيذ
|
126 |
+
|
127 |
+
### المرحلة 1: تصميم قاعدة البيانات وهيكل الوحدة
|
128 |
+
- تصميم جداول قاعدة البيانات
|
129 |
+
- تصميم هيكل الكلاسات والوظائف
|
130 |
+
- تصميم واجهة المستخدم
|
131 |
+
|
132 |
+
### المرحلة 2: تنفيذ إدارة بنود التسعير وحساب التكاليف
|
133 |
+
- تنفيذ وظائف إدارة البنود
|
134 |
+
- تنفيذ وظائف حساب التكاليف
|
135 |
+
- تنفيذ واجهة المستخدم لهذه الوظائف
|
136 |
+
|
137 |
+
### المرحلة 3: تنفيذ تحليل الأسعار والتنبؤ
|
138 |
+
- تنفيذ وظائف تحليل الأسعار
|
139 |
+
- تنفيذ وظائف التنبؤ بالأسعار
|
140 |
+
- تنفيذ واجهة المستخدم لهذه الوظائف
|
141 |
+
|
142 |
+
### المرحلة 4: تنفيذ التقارير ولوحة المعلومات
|
143 |
+
- تنفيذ وظائف إنشاء التقارير
|
144 |
+
- تنفيذ لوحة معلومات التسعير
|
145 |
+
- تنفيذ واجهة المستخدم لهذه الوظائف
|
146 |
+
|
147 |
+
### المرحلة 5: التكامل والاختبار
|
148 |
+
- تكامل الوحدة مع النظام الحالي
|
149 |
+
- اختبار الوظائف والأداء
|
150 |
+
- تصحيح الأخطاء وتحسين الأداء
|
151 |
+
|
152 |
+
### المرحلة 6: التوثيق والتسليم
|
153 |
+
- توثيق الوحدة وكيفية استخدامها
|
154 |
+
- إعداد أمثلة ودروس تعليمية
|
155 |
+
- تسليم الوحدة للمستخدم النهائي
|
modules/ai_assistant/ai_app.py
ADDED
@@ -0,0 +1,1067 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة الذكاء الاصطناعي - التطبيق الرئيسي
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import matplotlib.pyplot as plt
|
9 |
+
import plotly.express as px
|
10 |
+
import plotly.graph_objects as go
|
11 |
+
from datetime import datetime
|
12 |
+
import time
|
13 |
+
import io
|
14 |
+
import os
|
15 |
+
import json
|
16 |
+
import base64
|
17 |
+
from pathlib import Path
|
18 |
+
|
19 |
+
class AIAssistantApp:
|
20 |
+
"""وحدة الذكاء الاصطناعي"""
|
21 |
+
|
22 |
+
def __init__(self):
|
23 |
+
"""تهيئة وحدة الذكاء الاصطناعي"""
|
24 |
+
|
25 |
+
# تهيئة حالة الجلسة
|
26 |
+
if 'chat_history' not in st.session_state:
|
27 |
+
st.session_state.chat_history = [
|
28 |
+
{
|
29 |
+
'role': 'assistant',
|
30 |
+
'content': 'مرحباً! أنا مساعدك الذكي لإدارة المناقصات. كيف يمكنني مساعدتك اليوم؟'
|
31 |
+
}
|
32 |
+
]
|
33 |
+
|
34 |
+
if 'document_summaries' not in st.session_state:
|
35 |
+
st.session_state.document_summaries = [
|
36 |
+
{
|
37 |
+
'id': 1,
|
38 |
+
'title': 'كراسة الشروط والمواصفات - مشروع إنشاء مبنى إداري',
|
39 |
+
'date': '2024-03-15',
|
40 |
+
'summary': 'تتضمن كراسة الشروط والمواصفات لمشروع إنشاء مبنى إداري متطلبات المشروع وشروط التنفيذ والمواصفات الفنية للأعمال المطلوبة. يتكون المبنى من 5 طوابق بمساحة إجمالية 5000 متر مربع. تشمل الأعمال الأساسية: الأعمال الإنشائية، الأعمال المعمارية، الأعمال الكهربائية، الأعمال الميكانيكية، وأعمال التشطيبات.',
|
41 |
+
'key_points': [
|
42 |
+
'مدة التنفيذ: 18 شهراً',
|
43 |
+
'قيمة الضمان الابتدائي: 2% من قيمة العطاء',
|
44 |
+
'قيمة الضمان النهائي: 5% من قيمة العقد',
|
45 |
+
'غرامة التأخير: 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%',
|
46 |
+
'شروط الدفع: دفعات شهرية حسب نسبة الإنجاز'
|
47 |
+
],
|
48 |
+
'entities': {
|
49 |
+
'الجهة المالكة': 'وزارة المالية',
|
50 |
+
'موقع المشروع': 'الرياض - حي العليا',
|
51 |
+
'رقم المناقصة': 'T-2024-001',
|
52 |
+
'تاريخ الطرح': '2024-03-01',
|
53 |
+
'تاريخ الإقفال': '2024-04-15'
|
54 |
+
}
|
55 |
+
},
|
56 |
+
{
|
57 |
+
'id': 2,
|
58 |
+
'title': 'جدول الكميات - مشروع تطوير شبكة طرق',
|
59 |
+
'date': '2024-03-20',
|
60 |
+
'summary': 'يتضمن جدول الكميات لمشروع تطوير شبكة طرق تفاصيل الأعمال المطلوبة والكميات التقديرية. يشمل المشروع إنشاء طرق جديدة بطول 15 كم وتطوير طرق قائمة بطول 10 كم، بالإضافة إلى إنشاء 3 جسور و5 أنفاق.',
|
61 |
+
'key_points': [
|
62 |
+
'إجمالي أعمال الحفر: 250,000 م3',
|
63 |
+
'إجمالي أعمال الردم: 180,000 م3',
|
64 |
+
'إجمالي أعمال الخرسانة: 45,000 م3',
|
65 |
+
'إجمالي أعمال الأسفلت: 120,000 م2',
|
66 |
+
'إجمالي أعمال الإنارة: 500 عمود إنارة'
|
67 |
+
],
|
68 |
+
'entities': {
|
69 |
+
'الجهة المالكة': 'وزارة النقل',
|
70 |
+
'موقع المشروع': 'جدة',
|
71 |
+
'رقم المناقصة': 'T-2024-002',
|
72 |
+
'تاريخ الطرح': '2024-03-10',
|
73 |
+
'تاريخ الإقفال': '2024-04-20'
|
74 |
+
}
|
75 |
+
},
|
76 |
+
{
|
77 |
+
'id': 3,
|
78 |
+
'title': 'المواصفات الفنية - مشروع بناء مدرسة',
|
79 |
+
'date': '2024-03-25',
|
80 |
+
'summary': 'تتضمن المواصفات الفنية لمشروع بناء مدرسة تفاصيل المتطلبات الفنية للمشروع. تتكون المدرسة من 3 طوابق بمساحة إجمالية 3000 متر مربع، وتشمل 20 فصلاً دراسياً، ومختبرات علوم، وقاعة متعددة الأغراض، ومكتبة، وغرف إدارية.',
|
81 |
+
'key_points': [
|
82 |
+
'نوع الهيكل: خرساني مسلح',
|
83 |
+
'نظام التكييف: نظام مركزي',
|
84 |
+
'نظام الإنارة: LED موفر للطاقة',
|
85 |
+
'نظام مكافحة الحريق: نظام رش آلي',
|
86 |
+
'متطلبات خاصة: نظام طاقة شمسية لتوفير 30% من احتياجات الطاقة'
|
87 |
+
],
|
88 |
+
'entities': {
|
89 |
+
'الجهة المالكة': 'وزارة التعليم',
|
90 |
+
'موقع المشروع': 'الدمام',
|
91 |
+
'رقم المناقصة': 'T-2024-003',
|
92 |
+
'تاريخ الطرح': '2024-03-15',
|
93 |
+
'تاريخ الإقفال': '2024-04-25'
|
94 |
+
}
|
95 |
+
}
|
96 |
+
]
|
97 |
+
|
98 |
+
if 'ai_models' not in st.session_state:
|
99 |
+
st.session_state.ai_models = [
|
100 |
+
{
|
101 |
+
'id': 1,
|
102 |
+
'name': 'نموذج تحليل المستندات',
|
103 |
+
'description': 'نموذج ذكاء اصطناعي لتحليل مستندات المناقصات واستخراج المعلومات الرئيسية منها.',
|
104 |
+
'type': 'معالجة اللغة الطبيعية',
|
105 |
+
'accuracy': 92,
|
106 |
+
'last_updated': '2024-03-01'
|
107 |
+
},
|
108 |
+
{
|
109 |
+
'id': 2,
|
110 |
+
'name': 'نموذج تقدير التكاليف',
|
111 |
+
'description': 'نموذج ذكاء اصطناعي لتقدير تكاليف المشاريع بناءً على بيانات المشاريع السابقة.',
|
112 |
+
'type': 'تعلم آلي',
|
113 |
+
'accuracy': 85,
|
114 |
+
'last_updated': '2024-02-15'
|
115 |
+
},
|
116 |
+
{
|
117 |
+
'id': 3,
|
118 |
+
'name': 'نموذج تحليل المخاطر',
|
119 |
+
'description': 'نموذج ذكاء اصطناعي لتحليل المخاطر المحتملة للمشاريع وتقديم توصيات للتخفيف منها.',
|
120 |
+
'type': 'تعلم آلي',
|
121 |
+
'accuracy': 88,
|
122 |
+
'last_updated': '2024-02-20'
|
123 |
+
},
|
124 |
+
{
|
125 |
+
'id': 4,
|
126 |
+
'name': 'نموذج تحليل المنافسين',
|
127 |
+
'description': 'نموذج ذكاء اصطناعي لتحليل بيانات المنافسين وتقديم توصيات للتسعير التنافسي.',
|
128 |
+
'type': 'تعلم آلي',
|
129 |
+
'accuracy': 80,
|
130 |
+
'last_updated': '2024-03-10'
|
131 |
+
},
|
132 |
+
{
|
133 |
+
'id': 5,
|
134 |
+
'name': 'نموذج المساعد الذكي',
|
135 |
+
'description': 'نموذج ذكاء اصطناعي للإجابة على الاستفسارات وتقديم المساعدة في إدارة المناقصات.',
|
136 |
+
'type': 'معالجة اللغة الطبيعية',
|
137 |
+
'accuracy': 90,
|
138 |
+
'last_updated': '2024-03-15'
|
139 |
+
}
|
140 |
+
]
|
141 |
+
|
142 |
+
def render(self):
|
143 |
+
"""عرض واجهة وحدة الذكاء الاصطناعي"""
|
144 |
+
|
145 |
+
st.markdown("<h1 class='module-title'>وحدة الذكاء الاصطناعي</h1>", unsafe_allow_html=True)
|
146 |
+
|
147 |
+
tabs = st.tabs([
|
148 |
+
"المساعد الذكي",
|
149 |
+
"تحليل المستندات",
|
150 |
+
"تقدير التكاليف",
|
151 |
+
"تحليل المخاطر",
|
152 |
+
"نماذج الذكاء الاصطناعي"
|
153 |
+
])
|
154 |
+
|
155 |
+
with tabs[0]:
|
156 |
+
self._render_ai_assistant_tab()
|
157 |
+
|
158 |
+
with tabs[1]:
|
159 |
+
self._render_document_analysis_tab()
|
160 |
+
|
161 |
+
with tabs[2]:
|
162 |
+
self._render_cost_estimation_tab()
|
163 |
+
|
164 |
+
with tabs[3]:
|
165 |
+
self._render_risk_analysis_tab()
|
166 |
+
|
167 |
+
with tabs[4]:
|
168 |
+
self._render_ai_models_tab()
|
169 |
+
|
170 |
+
def _render_ai_assistant_tab(self):
|
171 |
+
"""عرض تبويب المساعد الذكي"""
|
172 |
+
|
173 |
+
st.markdown("### المساعد الذكي")
|
174 |
+
|
175 |
+
# عرض محادثة المساعد الذكي
|
176 |
+
chat_container = st.container()
|
177 |
+
|
178 |
+
with chat_container:
|
179 |
+
for message in st.session_state.chat_history:
|
180 |
+
if message['role'] == 'user':
|
181 |
+
st.markdown(f"<div style='background-color: #e6f7ff; padding: 10px; border-radius: 10px; margin-bottom: 10px; text-align: right;'><strong>أنت:</strong> {message['content']}</div>", unsafe_allow_html=True)
|
182 |
+
else:
|
183 |
+
st.markdown(f"<div style='background-color: #f0f0f0; padding: 10px; border-radius: 10px; margin-bottom: 10px;'><strong>المساعد:</strong> {message['content']}</div>", unsafe_allow_html=True)
|
184 |
+
|
185 |
+
# إدخال رسالة جديدة
|
186 |
+
with st.form(key="chat_form"):
|
187 |
+
user_input = st.text_area("اكتب رسالتك هنا:", key="user_input", height=100)
|
188 |
+
submit_button = st.form_submit_button("إرسال")
|
189 |
+
|
190 |
+
if submit_button and user_input:
|
191 |
+
# إضافة رسالة المستخدم إلى المحادثة
|
192 |
+
st.session_state.chat_history.append({
|
193 |
+
'role': 'user',
|
194 |
+
'content': user_input
|
195 |
+
})
|
196 |
+
|
197 |
+
# محاكاة استجابة المساعد الذكي
|
198 |
+
ai_responses = {
|
199 |
+
"تكلفة": "بناءً على تحليل بيانات المشاريع السابقة، أتوقع أن تكون تكلفة هذا المشروع في حدود 15-18 مليون ريال. يمكنني تقديم تحليل تفصيلي إذا وفرت لي المزيد من المعلومات عن نطاق المشروع والمواصفات المطلوبة.",
|
200 |
+
"مخاطر": "من أهم المخاطر المحتملة لهذا النوع من المشاريع: تأخر التوريدات، نقص العمالة الماهرة، التغييرات في نطاق العمل، والظروف الجوية غير المتوقعة. أنصح بوضع خطة إدارة مخاطر شاملة وتخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع.",
|
201 |
+
"منافس": "بناءً على تحليل المناقصات السابقة، يبدو أن المنافس الرئيسي يقدم أسعاراً أقل بنسبة 5-8% من متوسط السوق، لكنه يواجه تحديات في الالتزام بالجداول الزمنية. يمكنك التركيز على نقاط قوتك في الالتزام بالمواعيد وجودة التنفيذ في عرضك.",
|
202 |
+
"مستند": "يمكنني تحليل مستندات المناقصة لاستخراج المعلومات الرئيسية مثل نطاق العمل، الشروط والمواصفات، الجداول الزمنية، وشروط الدفع. يرجى تحميل المستندات في تبويب تحليل المستندات.",
|
203 |
+
"تسعير": "لتحسين استراتيجية التسعير، أنصح بتحليل هيكل التكاليف بدقة، ودراسة أسعار المنافسين، وتقييم القيمة المضافة التي تقدمها. يمكنك استخدام وحدة التسعير لإنشاء سيناريوهات تسعير مختلفة ومقارنتها.",
|
204 |
+
"موارد": "بناءً على نطاق المشروع، أتوقع أنك ستحتاج إلى فريق من 15-20 مهندساً وفنياً، بالإضافة إلى معدات إنشائية رئيسية. يمكنك استخدام وحدة الموارد لتخطيط احتياجات المشروع بشكل تفصيلي."
|
205 |
+
}
|
206 |
+
|
207 |
+
# تحديد الاستجابة المناسبة بناءً على كلمات مفتاحية في رسالة المستخدم
|
208 |
+
response = "أشكرك على رسالتك. يمكنني مساعدتك في إدارة المناقصات وتحليل المستندات وتقدير التكاليف وتحليل المخاطر. يرجى توضيح ما تحتاجه بالتحديد."
|
209 |
+
|
210 |
+
for keyword, resp in ai_responses.items():
|
211 |
+
if keyword in user_input:
|
212 |
+
response = resp
|
213 |
+
break
|
214 |
+
|
215 |
+
# إضافة استجابة المساعد الذكي إلى المحادثة
|
216 |
+
st.session_state.chat_history.append({
|
217 |
+
'role': 'assistant',
|
218 |
+
'content': response
|
219 |
+
})
|
220 |
+
|
221 |
+
# إعادة تحميل الصفحة لعرض المحادثة المحدثة
|
222 |
+
st.rerun()
|
223 |
+
|
224 |
+
# عرض اقتراحات للأسئلة
|
225 |
+
st.markdown("### اقتراحات للأسئلة")
|
226 |
+
|
227 |
+
suggestions = [
|
228 |
+
"كيف يمكنني تقدير تكلفة مشروع إنشاء مبنى إداري؟",
|
229 |
+
"ما هي المخاطر المحتملة لمشروع تطوير شبكة طرق؟",
|
230 |
+
"كيف يمكنني تحليل استراتيجية المنافس الرئيسي؟",
|
231 |
+
"كيف يمكنني تحليل مستندات المناقصة بسرعة؟",
|
232 |
+
"ما هي أفضل استراتيجية للتسعير التنافسي؟",
|
233 |
+
"كيف يمكنني تخطيط الموارد اللازمة للمشروع؟"
|
234 |
+
]
|
235 |
+
|
236 |
+
col1, col2 = st.columns(2)
|
237 |
+
|
238 |
+
with col1:
|
239 |
+
for i in range(0, len(suggestions), 2):
|
240 |
+
if st.button(suggestions[i], key=f"suggestion_{i}"):
|
241 |
+
# إضافة السؤال المقترح إلى المحادثة
|
242 |
+
st.session_state.chat_history.append({
|
243 |
+
'role': 'user',
|
244 |
+
'content': suggestions[i]
|
245 |
+
})
|
246 |
+
|
247 |
+
# تح��يد الاستجابة المناسبة
|
248 |
+
for keyword, resp in ai_responses.items():
|
249 |
+
if keyword in suggestions[i].lower():
|
250 |
+
response = resp
|
251 |
+
break
|
252 |
+
else:
|
253 |
+
response = "أشكرك على سؤالك. يمكنني مساعدتك في ذلك. يرجى تقديم المزيد من التفاصيل حول احتياجاتك المحددة."
|
254 |
+
|
255 |
+
# إضافة استجابة المساعد الذكي إلى المحادثة
|
256 |
+
st.session_state.chat_history.append({
|
257 |
+
'role': 'assistant',
|
258 |
+
'content': response
|
259 |
+
})
|
260 |
+
|
261 |
+
# إعادة تحميل الصفحة لعرض المحادثة المحدثة
|
262 |
+
st.rerun()
|
263 |
+
|
264 |
+
with col2:
|
265 |
+
for i in range(1, len(suggestions), 2):
|
266 |
+
if st.button(suggestions[i], key=f"suggestion_{i}"):
|
267 |
+
# إضافة السؤال المقترح إلى المحادثة
|
268 |
+
st.session_state.chat_history.append({
|
269 |
+
'role': 'user',
|
270 |
+
'content': suggestions[i]
|
271 |
+
})
|
272 |
+
|
273 |
+
# تحديد الاستجابة المناسبة
|
274 |
+
for keyword, resp in ai_responses.items():
|
275 |
+
if keyword in suggestions[i].lower():
|
276 |
+
response = resp
|
277 |
+
break
|
278 |
+
else:
|
279 |
+
response = "أشكرك على سؤالك. يمكنني مساعدتك في ذلك. يرجى تقديم المزيد من التفاصيل حول احتياجاتك المحددة."
|
280 |
+
|
281 |
+
# إضافة استجابة المساعد الذكي إلى المحادثة
|
282 |
+
st.session_state.chat_history.append({
|
283 |
+
'role': 'assistant',
|
284 |
+
'content': response
|
285 |
+
})
|
286 |
+
|
287 |
+
# إعادة تحميل الصفحة لعرض المحادثة المحدثة
|
288 |
+
st.rerun()
|
289 |
+
|
290 |
+
def _render_document_analysis_tab(self):
|
291 |
+
"""عرض تبويب تحليل المستندات"""
|
292 |
+
|
293 |
+
st.markdown("### تحليل المستندات باستخدام الذكاء الاصطناعي")
|
294 |
+
|
295 |
+
# تحميل المستندات
|
296 |
+
st.markdown("#### تحميل المستندات")
|
297 |
+
|
298 |
+
uploaded_file = st.file_uploader("قم بتحميل مستند المناقصة (PDF, DOCX)", type=["pdf", "docx"])
|
299 |
+
|
300 |
+
if uploaded_file is not None:
|
301 |
+
if st.button("تحليل المستند"):
|
302 |
+
# محاكاة تحليل المستند
|
303 |
+
with st.spinner("جاري تحليل المستند..."):
|
304 |
+
time.sleep(2) # محاكاة وقت المعالجة
|
305 |
+
st.success("تم تحليل المستند بنجاح!")
|
306 |
+
|
307 |
+
# إضافة ملخص المستند إلى قائمة الملخصات
|
308 |
+
new_id = max([item['id'] for item in st.session_state.document_summaries], default=0) + 1
|
309 |
+
|
310 |
+
st.session_state.document_summaries.append({
|
311 |
+
'id': new_id,
|
312 |
+
'title': uploaded_file.name,
|
313 |
+
'date': time.strftime("%Y-%m-%d"),
|
314 |
+
'summary': 'تم تحليل المستند واستخراج المعلومات الرئيسية منه. يتضمن المستند شروط ومواصفات المناقصة، ونطاق العمل، والجدول الزمني، وشروط الدفع.',
|
315 |
+
'key_points': [
|
316 |
+
'مدة التنفيذ: 12 شهراً',
|
317 |
+
'قيمة الضمان الابتدائي: 2% من قيمة العطاء',
|
318 |
+
'قيمة الضمان النهائي: 5% من قيمة العقد',
|
319 |
+
'غرامة التأخير: 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%',
|
320 |
+
'شروط الدفع: دفعات شهرية حسب نسبة الإنجاز'
|
321 |
+
],
|
322 |
+
'entities': {
|
323 |
+
'الجهة المالكة': 'وزارة الإسكان',
|
324 |
+
'موقع المشروع': 'الرياض',
|
325 |
+
'رقم المناقصة': 'T-2024-004',
|
326 |
+
'تاريخ الطرح': '2024-03-25',
|
327 |
+
'تاريخ الإقفال': '2024-05-01'
|
328 |
+
}
|
329 |
+
})
|
330 |
+
|
331 |
+
# عرض ملخصات المستندات
|
332 |
+
st.markdown("#### ملخصات المستندات")
|
333 |
+
|
334 |
+
for summary in st.session_state.document_summaries:
|
335 |
+
with st.expander(f"{summary['title']} - {summary['date']}"):
|
336 |
+
st.markdown(f"**ملخص المستند:** {summary['summary']}")
|
337 |
+
|
338 |
+
st.markdown("**النقاط الرئيسية:**")
|
339 |
+
for point in summary['key_points']:
|
340 |
+
st.markdown(f"- {point}")
|
341 |
+
|
342 |
+
st.markdown("**الكيانات المستخرجة:**")
|
343 |
+
for entity, value in summary['entities'].items():
|
344 |
+
st.markdown(f"- **{entity}:** {value}")
|
345 |
+
|
346 |
+
col1, col2, col3 = st.columns(3)
|
347 |
+
|
348 |
+
with col1:
|
349 |
+
if st.button("تصدير الملخص", key=f"export_summary_{summary['id']}"):
|
350 |
+
st.success("تم تصدير الملخص بنجاح!")
|
351 |
+
|
352 |
+
with col2:
|
353 |
+
if st.button("إرسال إلى وحدة التسعير", key=f"send_to_pricing_{summary['id']}"):
|
354 |
+
st.success("تم إرسال البيانات إلى وحدة التسعير بنجاح!")
|
355 |
+
|
356 |
+
with col3:
|
357 |
+
if st.button("إرسال إلى وحدة المخاطر", key=f"send_to_risk_{summary['id']}"):
|
358 |
+
st.success("تم إرسال البيانات إلى وحدة المخاطر بنجاح!")
|
359 |
+
|
360 |
+
# استخراج جدول الكميات
|
361 |
+
st.markdown("#### استخراج جدول الكميات")
|
362 |
+
|
363 |
+
boq_file = st.file_uploader("قم بتحميل جدول الكميات (PDF, XLSX)", type=["pdf", "xlsx"])
|
364 |
+
|
365 |
+
if boq_file is not None:
|
366 |
+
if st.button("استخراج جدول الكميات"):
|
367 |
+
# محاكاة استخراج جدول الكميات
|
368 |
+
with st.spinner("جاري استخراج جدول الكميات..."):
|
369 |
+
time.sleep(2) # محاكاة وقت المعالجة
|
370 |
+
st.success("تم استخراج جدول الكميات بنجاح!")
|
371 |
+
|
372 |
+
# عرض جدول الكميات المستخرج
|
373 |
+
boq_data = {
|
374 |
+
'الكود': ['A-001', 'A-002', 'A-003', 'B-001', 'B-002'],
|
375 |
+
'الوصف': [
|
376 |
+
'أعمال الحفر والردم',
|
377 |
+
'توريد وصب خرسانة عادية',
|
378 |
+
'توريد وصب خرسانة مسلحة للأساسات',
|
379 |
+
'توريد وتركيب حديد تسليح',
|
380 |
+
'توريد وبناء طابوق'
|
381 |
+
],
|
382 |
+
'الوحدة': ['م3', 'م3', 'م3', 'طن', 'م2'],
|
383 |
+
'الكمية': [2000, 300, 200, 20, 500],
|
384 |
+
'سعر الوحدة': [45, 350, 450, 3500, 120],
|
385 |
+
'الإجمالي': [90000, 105000, 90000, 70000, 60000]
|
386 |
+
}
|
387 |
+
|
388 |
+
boq_df = pd.DataFrame(boq_data)
|
389 |
+
st.dataframe(boq_df, use_container_width=True, hide_index=True)
|
390 |
+
|
391 |
+
if st.button("إرسال إلى وحدة التسعير", key="send_boq_to_pricing"):
|
392 |
+
st.success("تم إرسال جدول الكميات إلى وحدة التسعير بنجاح!")
|
393 |
+
|
394 |
+
# تحليل الشروط والمواصفات
|
395 |
+
st.markdown("#### تحليل الشروط والمواصفات")
|
396 |
+
|
397 |
+
specs_file = st.file_uploader("قم بتحميل الشروط والمواصفات (PDF, DOCX)", type=["pdf", "docx"])
|
398 |
+
|
399 |
+
if specs_file is not None:
|
400 |
+
if st.button("تحليل الشروط والمواصفات"):
|
401 |
+
# محاكاة تحليل الشروط والمواصفات
|
402 |
+
with st.spinner("جاري تحليل الشروط والمواصفات..."):
|
403 |
+
time.sleep(2) # محاكاة وقت المعالجة
|
404 |
+
st.success("تم تحليل الشروط والمواصفات بنجاح!")
|
405 |
+
|
406 |
+
# عرض نتائج التحليل
|
407 |
+
st.markdown("**الشروط الرئيسية:**")
|
408 |
+
st.markdown("- مدة التنفيذ: 12 شهراً")
|
409 |
+
st.markdown("- قيمة الضمان الابتدائي: 2% من قيمة العطاء")
|
410 |
+
st.markdown("- قيمة الضمان النهائي: 5% من قيمة العقد")
|
411 |
+
st.markdown("- غرامة التأخير: 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%")
|
412 |
+
st.markdown("- شروط الدفع: دفعات شهرية حسب نسبة الإنجاز")
|
413 |
+
|
414 |
+
st.markdown("**المواصفات الفنية الرئيسية:**")
|
415 |
+
st.markdown("- نوع الهيكل: خرساني مسلح")
|
416 |
+
st.markdown("- نظام التكييف: نظام مركزي")
|
417 |
+
st.markdown("- نظام الإنارة: LED موفر للطاقة")
|
418 |
+
st.markdown("- نظام مكافحة الحريق: نظام رش آلي")
|
419 |
+
st.markdown("- متطلبات خاصة: نظام طاقة شمسية لتوفير 30% من احتياجات الطاقة")
|
420 |
+
|
421 |
+
if st.button("إرسال إلى وحدة المخاطر", key="send_specs_to_risk"):
|
422 |
+
st.success("تم إرسال تحليل الشروط والمواصفات إلى وحدة المخاطر بنجاح!")
|
423 |
+
|
424 |
+
def _render_cost_estimation_tab(self):
|
425 |
+
"""عرض تبويب تقدير التكاليف"""
|
426 |
+
|
427 |
+
st.markdown("### تقدير التكاليف باستخدام الذكاء الاصطناعي")
|
428 |
+
|
429 |
+
# إدخال معلومات المشروع
|
430 |
+
st.markdown("#### معلومات المشروع")
|
431 |
+
|
432 |
+
col1, col2 = st.columns(2)
|
433 |
+
|
434 |
+
with col1:
|
435 |
+
project_type = st.selectbox(
|
436 |
+
"نوع المشروع",
|
437 |
+
["مبنى إداري", "مبنى سكني", "مدرسة", "مستشفى", "طرق", "جسور", "بنية تحتية", "أخرى"]
|
438 |
+
)
|
439 |
+
|
440 |
+
project_area = st.number_input("المساحة الإجمالية (م2)", min_value=0, value=5000)
|
441 |
+
|
442 |
+
project_location = st.selectbox(
|
443 |
+
"موقع المشروع",
|
444 |
+
["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك", "أخرى"]
|
445 |
+
)
|
446 |
+
|
447 |
+
with col2:
|
448 |
+
project_duration = st.number_input("مدة التنفيذ (شهر)", min_value=1, value=18)
|
449 |
+
|
450 |
+
project_quality = st.select_slider(
|
451 |
+
"مستوى الجودة",
|
452 |
+
options=["اقتصادي", "متوسط", "عالي", "ممتاز"]
|
453 |
+
)
|
454 |
+
|
455 |
+
project_complexity = st.select_slider(
|
456 |
+
"مستوى التعقيد",
|
457 |
+
options=["بسيط", "متوسط", "معقد", "معقد جداً"]
|
458 |
+
)
|
459 |
+
|
460 |
+
# تقدير التكاليف
|
461 |
+
if st.button("تقدير التكاليف"):
|
462 |
+
# محاكاة تقدير التكاليف
|
463 |
+
with st.spinner("جاري تقدير التكاليف..."):
|
464 |
+
time.sleep(2) # محاكاة وقت المعالجة
|
465 |
+
st.success("تم تقدير التكاليف بنجاح!")
|
466 |
+
|
467 |
+
# عرض نتائج التقدير
|
468 |
+
st.markdown("#### نتائج تقدير التكاليف")
|
469 |
+
|
470 |
+
# تحديد التكلفة التقديرية بناءً على نوع المشروع والمساحة
|
471 |
+
base_cost_per_sqm = {
|
472 |
+
"مبنى إداري": 3500,
|
473 |
+
"مبنى سكني": 3000,
|
474 |
+
"مدرسة": 3200,
|
475 |
+
"مستشفى": 5000,
|
476 |
+
"طرق": 1500,
|
477 |
+
"جسور": 8000,
|
478 |
+
"بنية تحتية": 2500,
|
479 |
+
"أخرى": 3000
|
480 |
+
}
|
481 |
+
|
482 |
+
# تعديل التكلفة بناءً على الموقع
|
483 |
+
location_factor = {
|
484 |
+
"الرياض": 1.0,
|
485 |
+
"جدة": 1.05,
|
486 |
+
"الدمام": 0.95,
|
487 |
+
"مكة": 1.1,
|
488 |
+
"المدينة": 1.0,
|
489 |
+
"أبها": 0.9,
|
490 |
+
"تبوك": 0.85,
|
491 |
+
"أخرى": 1.0
|
492 |
+
}
|
493 |
+
|
494 |
+
# تعديل التكلفة بناءً على مستوى الجودة
|
495 |
+
quality_factor = {
|
496 |
+
"اقتصادي": 0.8,
|
497 |
+
"متوسط": 1.0,
|
498 |
+
"عالي": 1.2,
|
499 |
+
"ممتاز": 1.5
|
500 |
+
}
|
501 |
+
|
502 |
+
# تعديل التكلفة بناءً على مستوى التعقيد
|
503 |
+
complexity_factor = {
|
504 |
+
"بسيط": 0.9,
|
505 |
+
"متوسط": 1.0,
|
506 |
+
"معقد": 1.2,
|
507 |
+
"معقد جداً": 1.4
|
508 |
+
}
|
509 |
+
|
510 |
+
# حساب التكلفة التقديرية
|
511 |
+
base_cost = base_cost_per_sqm[project_type] * project_area
|
512 |
+
adjusted_cost = base_cost * location_factor[project_location] * quality_factor[project_quality] * complexity_factor[project_complexity]
|
513 |
+
|
514 |
+
# عرض التكلفة التقديرية
|
515 |
+
col1, col2 = st.columns(2)
|
516 |
+
|
517 |
+
with col1:
|
518 |
+
st.metric("التكلفة التقديرية", f"{adjusted_cost:,.0f} ريال")
|
519 |
+
|
520 |
+
with col2:
|
521 |
+
st.metric("التكلفة لكل متر مربع", f"{adjusted_cost / project_area:,.0f} ريال/م2")
|
522 |
+
|
523 |
+
# عرض تفاصيل التكاليف
|
524 |
+
st.markdown("#### تفاصيل التكاليف")
|
525 |
+
|
526 |
+
# تقسيم التكاليف إلى فئات
|
527 |
+
cost_breakdown = {
|
528 |
+
"الأعمال الإنشائية": 0.35,
|
529 |
+
"الأعمال المعمارية": 0.25,
|
530 |
+
"الأعمال الكهربائية": 0.15,
|
531 |
+
"الأعمال الميكانيكية": 0.15,
|
532 |
+
"أعمال الموقع والتجهيزات": 0.10
|
533 |
+
}
|
534 |
+
|
535 |
+
cost_details = {
|
536 |
+
"الفئة": list(cost_breakdown.keys()),
|
537 |
+
"النسبة": [f"{v * 100:.0f}%" for v in cost_breakdown.values()],
|
538 |
+
"التكلفة": [adjusted_cost * v for v in cost_breakdown.values()]
|
539 |
+
}
|
540 |
+
|
541 |
+
cost_df = pd.DataFrame(cost_details)
|
542 |
+
st.dataframe(cost_df, use_container_width=True, hide_index=True)
|
543 |
+
|
544 |
+
# عرض رسم بياني للتكاليف
|
545 |
+
fig = px.pie(
|
546 |
+
cost_df,
|
547 |
+
values="التكلفة",
|
548 |
+
names="الفئة",
|
549 |
+
title="توزيع التكاليف حسب الفئة"
|
550 |
+
)
|
551 |
+
|
552 |
+
st.plotly_chart(fig, use_container_width=True)
|
553 |
+
|
554 |
+
# عرض تحليل التكاليف المباشرة وغير المباشرة
|
555 |
+
st.markdown("#### تحليل التكاليف المباشرة وغير المباشرة")
|
556 |
+
|
557 |
+
direct_cost = adjusted_cost * 0.85
|
558 |
+
indirect_cost = adjusted_cost * 0.15
|
559 |
+
|
560 |
+
direct_indirect_data = {
|
561 |
+
"نوع التكلفة": ["تكاليف مباشرة", "تكاليف غير مباشرة"],
|
562 |
+
"النسبة": ["85%", "15%"],
|
563 |
+
"التكلفة": [direct_cost, indirect_cost]
|
564 |
+
}
|
565 |
+
|
566 |
+
direct_indirect_df = pd.DataFrame(direct_indirect_data)
|
567 |
+
st.dataframe(direct_indirect_df, use_container_width=True, hide_index=True)
|
568 |
+
|
569 |
+
# عرض رسم بياني للتكاليف المباشرة وغير المباشرة
|
570 |
+
fig = px.bar(
|
571 |
+
direct_indirect_df,
|
572 |
+
x="نوع التكلفة",
|
573 |
+
y="التكلفة",
|
574 |
+
title="التكاليف المباشرة وغير المباشرة",
|
575 |
+
color="نوع التكلفة",
|
576 |
+
text_auto='.2s'
|
577 |
+
)
|
578 |
+
|
579 |
+
st.plotly_chart(fig, use_container_width=True)
|
580 |
+
|
581 |
+
# عرض توصيات لتحسين التكاليف
|
582 |
+
st.markdown("#### توصيات لتحسين التكاليف")
|
583 |
+
|
584 |
+
st.markdown("1. **تحسين تصميم المشروع:** يمكن تحسين التصميم لتقليل التكاليف مع الحفاظ على الجودة.")
|
585 |
+
st.markdown("2. **استخدام مواد بديلة:** يمكن استخدام مواد بديلة بتكلفة أقل مع الحفاظ على الجودة.")
|
586 |
+
st.markdown("3. **تحسين جدولة المشروع:** يمكن تحسين جدولة المشروع لتقليل مدة التنفيذ وبالتالي تقليل التكاليف غير المباشرة.")
|
587 |
+
st.markdown("4. **تحسين إدارة الموارد:** يمكن تحسين إدارة الموارد لتقليل الهدر وزيادة الإنتاجية.")
|
588 |
+
st.markdown("5. **التفاوض مع الموردين:** يمكن التفاوض مع الموردين للحصول على أسعار أفضل.")
|
589 |
+
|
590 |
+
# إرسال التقدير إلى وحدة التسعير
|
591 |
+
if st.button("إرسال إلى وحدة التسعير", key="send_estimate_to_pricing"):
|
592 |
+
st.success("تم إرسال تقدير التكاليف إلى وحدة التسعير بنجاح!")
|
593 |
+
|
594 |
+
# مقارنة التكاليف مع المشاريع السابقة
|
595 |
+
st.markdown("#### مقارنة التكاليف مع المشاريع السابقة")
|
596 |
+
|
597 |
+
# بيانات افتراضية للمشاريع السابقة
|
598 |
+
previous_projects_data = {
|
599 |
+
"المشروع": ["مبنى إداري - الرياض", "مبنى إداري - جدة", "مبنى إداري - الدمام", "مبنى سكني - الرياض", "مدرسة - جدة"],
|
600 |
+
"المساحة (م2)": [4500, 5200, 4800, 6000, 3500],
|
601 |
+
"التكلفة الإجمالية": [16200000, 19500000, 15800000, 18500000, 11200000],
|
602 |
+
"التكلفة لكل متر مربع": [3600, 3750, 3290, 3080, 3200]
|
603 |
+
}
|
604 |
+
|
605 |
+
previous_projects_df = pd.DataFrame(previous_projects_data)
|
606 |
+
st.dataframe(previous_projects_df, use_container_width=True, hide_index=True)
|
607 |
+
|
608 |
+
# عرض رسم بياني لمقارنة التكاليف
|
609 |
+
fig = px.bar(
|
610 |
+
previous_projects_df,
|
611 |
+
x="المشروع",
|
612 |
+
y="التكلفة لكل متر مربع",
|
613 |
+
title="مقارنة التكلفة لكل متر مربع للمشاريع السابقة",
|
614 |
+
color="المشروع",
|
615 |
+
text_auto='.0f'
|
616 |
+
)
|
617 |
+
|
618 |
+
st.plotly_chart(fig, use_container_width=True)
|
619 |
+
|
620 |
+
def _render_risk_analysis_tab(self):
|
621 |
+
"""عرض تبويب تحليل المخاطر"""
|
622 |
+
|
623 |
+
st.markdown("### تحليل المخاطر باستخدام الذكاء الاصطناعي")
|
624 |
+
|
625 |
+
# إدخال معلومات المشروع
|
626 |
+
st.markdown("#### معلومات المشروع")
|
627 |
+
|
628 |
+
col1, col2 = st.columns(2)
|
629 |
+
|
630 |
+
with col1:
|
631 |
+
project_type = st.selectbox(
|
632 |
+
"نوع المشروع",
|
633 |
+
["مبنى إداري", "مبنى سكني", "مدرسة", "مستشفى", "طرق", "جسور", "بنية تحتية", "أخرى"],
|
634 |
+
key="risk_project_type"
|
635 |
+
)
|
636 |
+
|
637 |
+
project_budget = st.number_input("ميزانية المشروع (ريال)", min_value=0, value=15000000)
|
638 |
+
|
639 |
+
project_location = st.selectbox(
|
640 |
+
"موقع المشروع",
|
641 |
+
["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك", "أخرى"],
|
642 |
+
key="risk_project_location"
|
643 |
+
)
|
644 |
+
|
645 |
+
with col2:
|
646 |
+
project_duration = st.number_input("مدة التنفيذ (شهر)", min_value=1, value=18, key="risk_project_duration")
|
647 |
+
|
648 |
+
project_complexity = st.select_slider(
|
649 |
+
"مستوى التعقيد",
|
650 |
+
options=["بسيط", "متوسط", "معقد", "معقد جداً"],
|
651 |
+
key="risk_project_complexity"
|
652 |
+
)
|
653 |
+
|
654 |
+
project_experience = st.select_slider(
|
655 |
+
"مستوى الخبرة في هذا النوع من المشاريع",
|
656 |
+
options=["منخفض", "متوسط", "عالي", "ممتاز"]
|
657 |
+
)
|
658 |
+
|
659 |
+
# تحليل المخاطر
|
660 |
+
if st.button("تحليل المخاطر"):
|
661 |
+
# محاكاة تحليل المخاطر
|
662 |
+
with st.spinner("جاري تحليل المخاطر..."):
|
663 |
+
time.sleep(2) # محاكاة وقت المعالجة
|
664 |
+
st.success("تم تحليل المخاطر بنجاح!")
|
665 |
+
|
666 |
+
# عرض نتائج التحليل
|
667 |
+
st.markdown("#### نتائج تحليل المخاطر")
|
668 |
+
|
669 |
+
# بيانات افتراضية للمخاطر
|
670 |
+
risks_data = {
|
671 |
+
"المخاطرة": [
|
672 |
+
"تأخر التوريدات",
|
673 |
+
"نقص العمالة الماهرة",
|
674 |
+
"التغييرات في نطاق العمل",
|
675 |
+
"الظروف الجوية غير المتوقعة",
|
676 |
+
"مشاكل في التصميم",
|
677 |
+
"تأخر الدفعات",
|
678 |
+
"مشاكل في الموقع",
|
679 |
+
"تغيير الأنظمة واللوائح",
|
680 |
+
"مشاكل في الجودة",
|
681 |
+
"مشاكل في التنسيق مع الجهات الحكومية"
|
682 |
+
],
|
683 |
+
"الاحتمالية": [
|
684 |
+
"متوسطة",
|
685 |
+
"عالية",
|
686 |
+
"متوسطة",
|
687 |
+
"منخفضة",
|
688 |
+
"منخفضة",
|
689 |
+
"متوسطة",
|
690 |
+
"منخفضة",
|
691 |
+
"منخفضة",
|
692 |
+
"متوسطة",
|
693 |
+
"عالية"
|
694 |
+
],
|
695 |
+
"التأثير": [
|
696 |
+
"عالي",
|
697 |
+
"عالي",
|
698 |
+
"عالي",
|
699 |
+
"متوسط",
|
700 |
+
"عالي",
|
701 |
+
"متوسط",
|
702 |
+
"متوسط",
|
703 |
+
"عالي",
|
704 |
+
"عالي",
|
705 |
+
"متوسط"
|
706 |
+
],
|
707 |
+
"درجة المخاطرة": [
|
708 |
+
"عالية",
|
709 |
+
"عالية",
|
710 |
+
"عالية",
|
711 |
+
"متوسطة",
|
712 |
+
"متوسطة",
|
713 |
+
"متوسطة",
|
714 |
+
"منخفضة",
|
715 |
+
"متوسطة",
|
716 |
+
"عالية",
|
717 |
+
"عالية"
|
718 |
+
]
|
719 |
+
}
|
720 |
+
|
721 |
+
risks_df = pd.DataFrame(risks_data)
|
722 |
+
st.dataframe(risks_df, use_container_width=True, hide_index=True)
|
723 |
+
|
724 |
+
# عرض مصفوفة المخاطر
|
725 |
+
st.markdown("#### مصفوفة المخاطر")
|
726 |
+
|
727 |
+
# تحويل الاحتمالية والتأثير إلى قيم عددية
|
728 |
+
probability_map = {"منخفضة": 1, "متوسطة": 2, "عالية": 3}
|
729 |
+
impact_map = {"منخفض": 1, "متوسط": 2, "عالي": 3}
|
730 |
+
|
731 |
+
risk_matrix_data = []
|
732 |
+
|
733 |
+
for i, risk in enumerate(risks_data["المخاطرة"]):
|
734 |
+
prob = probability_map[risks_data["الاحتمالية"][i]]
|
735 |
+
impact = impact_map[risks_data["التأثير"][i]]
|
736 |
+
risk_matrix_data.append({
|
737 |
+
"المخاطرة": risk,
|
738 |
+
"الاحتمالية": prob,
|
739 |
+
"التأثير": impact,
|
740 |
+
"درجة المخاطرة": prob * impact
|
741 |
+
})
|
742 |
+
|
743 |
+
# إنشاء مصفوفة المخاطر
|
744 |
+
risk_matrix = np.zeros((3, 3))
|
745 |
+
|
746 |
+
for risk in risk_matrix_data:
|
747 |
+
prob = risk["الاحتمالية"] - 1 # تعديل الفهرس ليبدأ من 0
|
748 |
+
impact = risk["التأثير"] - 1 # تعديل الفهرس ليبدأ من 0
|
749 |
+
risk_matrix[prob, impact] += 1
|
750 |
+
|
751 |
+
# عرض مصفوفة المخاطر كرسم بياني حراري
|
752 |
+
fig, ax = plt.subplots(figsize=(10, 8))
|
753 |
+
|
754 |
+
im = ax.imshow(risk_matrix, cmap="YlOrRd")
|
755 |
+
|
756 |
+
# إضافة النص إلى الخلايا
|
757 |
+
for i in range(3):
|
758 |
+
for j in range(3):
|
759 |
+
text = ax.text(j, i, int(risk_matrix[i, j]), ha="center", va="center", color="black")
|
760 |
+
|
761 |
+
# إضافة العناوين
|
762 |
+
ax.set_xticks(np.arange(3))
|
763 |
+
ax.set_yticks(np.arange(3))
|
764 |
+
ax.set_xticklabels(["منخفض", "متوسط", "عالي"])
|
765 |
+
ax.set_yticklabels(["منخفضة", "متوسطة", "عالية"])
|
766 |
+
|
767 |
+
# إضافة العناوين الرئيسية
|
768 |
+
ax.set_xlabel("التأثير")
|
769 |
+
ax.set_ylabel("الاحتمالية")
|
770 |
+
ax.set_title("مصفوفة المخاطر")
|
771 |
+
|
772 |
+
# إضافة شريط الألوان
|
773 |
+
cbar = ax.figure.colorbar(im, ax=ax)
|
774 |
+
cbar.ax.set_ylabel("عدد المخاطر", rotation=-90, va="bottom")
|
775 |
+
|
776 |
+
# عرض الرسم البياني
|
777 |
+
st.pyplot(fig)
|
778 |
+
|
779 |
+
# عرض توزيع المخاطر حسب الدرجة
|
780 |
+
st.markdown("#### توزيع المخاطر حسب الدرجة")
|
781 |
+
|
782 |
+
risk_degree_counts = {
|
783 |
+
"منخفضة": sum(1 for degree in risks_data["درجة المخاطرة"] if degree == "منخفضة"),
|
784 |
+
"متوسطة": sum(1 for degree in risks_data["درجة المخاطرة"] if degree == "متوسطة"),
|
785 |
+
"عالية": sum(1 for degree in risks_data["درجة المخاطرة"] if degree == "عالية")
|
786 |
+
}
|
787 |
+
|
788 |
+
risk_degree_df = pd.DataFrame({
|
789 |
+
"درجة المخاطرة": list(risk_degree_counts.keys()),
|
790 |
+
"العدد": list(risk_degree_counts.values())
|
791 |
+
})
|
792 |
+
|
793 |
+
fig = px.pie(
|
794 |
+
risk_degree_df,
|
795 |
+
values="العدد",
|
796 |
+
names="درجة المخاطرة",
|
797 |
+
title="توزيع المخاطر حسب الدرجة",
|
798 |
+
color="درجة المخاطرة",
|
799 |
+
color_discrete_map={"منخفضة": "green", "متوسطة": "orange", "عالية": "red"}
|
800 |
+
)
|
801 |
+
|
802 |
+
st.plotly_chart(fig, use_container_width=True)
|
803 |
+
|
804 |
+
# عرض خطة إدارة المخاطر
|
805 |
+
st.markdown("#### خطة إدارة المخاطر")
|
806 |
+
|
807 |
+
# بيانات افتراضية لخطة إدارة المخاطر
|
808 |
+
risk_management_data = {
|
809 |
+
"المخاطرة": [
|
810 |
+
"تأخر التوريدات",
|
811 |
+
"نقص العمالة الماهرة",
|
812 |
+
"التغييرات في نطاق العمل",
|
813 |
+
"مشاكل في الجودة",
|
814 |
+
"مشاكل في التنسيق مع الجهات الحكومية"
|
815 |
+
],
|
816 |
+
"استراتيجية المواجهة": [
|
817 |
+
"تخفيف",
|
818 |
+
"تخفيف",
|
819 |
+
"تجنب",
|
820 |
+
"تخفيف",
|
821 |
+
"نقل"
|
822 |
+
],
|
823 |
+
"الإجراءات": [
|
824 |
+
"التعاقد مع موردين متعددين، وضع جدول زمني للتوريدات مع هامش أمان، متابعة التوريدات بشكل دوري",
|
825 |
+
"التعاقد مع شركات توريد عمالة موثوقة، تدريب العمالة الحالية، وضع حوافز للعمالة الماهرة",
|
826 |
+
"توثيق نطاق العمل بشكل دقيق، وضع إجراءات للتغييرات في نطاق العمل، تحديد صلاحيات اعتماد التغييرات",
|
827 |
+
"وضع خطة لضبط الجودة، تعيين مسؤول للجودة، إجراء اختبارات دورية للجودة",
|
828 |
+
"التعاقد مع استشاري متخصص في التنسيق مع الجهات الحكومية، تحديد متطلبات الجهات الحكومية مسبقاً"
|
829 |
+
],
|
830 |
+
"المسؤول": [
|
831 |
+
"مدير المشتريات",
|
832 |
+
"مدير الموارد البشرية",
|
833 |
+
"مدير المشروع",
|
834 |
+
"مدير الجودة",
|
835 |
+
"مدير العلاقات الحكومية"
|
836 |
+
],
|
837 |
+
"الموعد النهائي": [
|
838 |
+
"قبل بدء المشروع بشهر",
|
839 |
+
"قبل بدء المشروع بشهرين",
|
840 |
+
"قبل بدء المشروع بأسبوعين",
|
841 |
+
"مستمر طوال فترة المشروع",
|
842 |
+
"قبل بدء المشروع بشهر"
|
843 |
+
]
|
844 |
+
}
|
845 |
+
|
846 |
+
risk_management_df = pd.DataFrame(risk_management_data)
|
847 |
+
st.dataframe(risk_management_df, use_container_width=True, hide_index=True)
|
848 |
+
|
849 |
+
# عرض توصيات لإدارة المخاطر
|
850 |
+
st.markdown("#### توصيات لإدارة المخاطر")
|
851 |
+
|
852 |
+
st.markdown("1. **تخصيص احتياطي للطوارئ:** يوصى بتخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع.")
|
853 |
+
st.markdown("2. **مراجعة خطة إدارة المخاطر بشكل دوري:** يجب مراجعة خطة إدارة المخاطر بشكل دوري وتحديثها حسب الحاجة.")
|
854 |
+
st.markdown("3. **تعيين مسؤول لإدارة المخاطر:** يوصى بتعيين مسؤول لإدارة المخاطر في المشروع.")
|
855 |
+
st.markdown("4. **توثيق الدروس المستفادة:** يجب توثيق الدروس المستفادة من إدارة المخاطر في المشاريع السابقة.")
|
856 |
+
st.markdown("5. **التواصل المستمر مع أصحاب المصلحة:** يجب التواصل المستمر مع أصحاب المصلحة لتحديد المخاطر المحتملة.")
|
857 |
+
|
858 |
+
# إرسال تحليل المخاطر إلى وحدة التسعير
|
859 |
+
if st.button("إرسال إلى وحدة التسعير", key="send_risk_to_pricing"):
|
860 |
+
st.success("تم إرسال تحليل المخاطر إلى وحدة التسعير بنجاح!")
|
861 |
+
|
862 |
+
def _render_ai_models_tab(self):
|
863 |
+
"""عرض تبويب نماذج الذكاء الاصطناعي"""
|
864 |
+
|
865 |
+
st.markdown("### نماذج الذكاء الاصطناعي")
|
866 |
+
|
867 |
+
# عرض نماذج الذكاء الاصطناعي
|
868 |
+
st.markdown("#### قائمة نماذج الذكاء الاصطناعي")
|
869 |
+
|
870 |
+
# تحويل قائمة النماذج إلى DataFrame
|
871 |
+
models_df = pd.DataFrame(st.session_state.ai_models)
|
872 |
+
|
873 |
+
# عرض النماذج كجدول
|
874 |
+
st.dataframe(
|
875 |
+
models_df,
|
876 |
+
column_config={
|
877 |
+
"id": st.column_config.NumberColumn("الرقم"),
|
878 |
+
"name": st.column_config.TextColumn("اسم النموذج"),
|
879 |
+
"description": st.column_config.TextColumn("الوصف"),
|
880 |
+
"type": st.column_config.TextColumn("النوع"),
|
881 |
+
"accuracy": st.column_config.ProgressColumn("الدقة (%)", min_value=0, max_value=100),
|
882 |
+
"last_updated": st.column_config.DateColumn("تاريخ التحديث")
|
883 |
+
},
|
884 |
+
use_container_width=True,
|
885 |
+
hide_index=True
|
886 |
+
)
|
887 |
+
|
888 |
+
# عرض تفاصيل النماذج
|
889 |
+
st.markdown("#### تفاصيل النماذج")
|
890 |
+
|
891 |
+
for model in st.session_state.ai_models:
|
892 |
+
with st.expander(f"{model['name']} - دقة {model['accuracy']}%"):
|
893 |
+
st.markdown(f"**الوصف:** {model['description']}")
|
894 |
+
st.markdown(f"**النوع:** {model['type']}")
|
895 |
+
st.markdown(f"**تاريخ التحديث:** {model['last_updated']}")
|
896 |
+
|
897 |
+
if model['name'] == "نموذج تحليل المستندات":
|
898 |
+
st.markdown("**القدرات:**")
|
899 |
+
st.markdown("- استخراج المعلومات الرئيسية من مستندات المناقصات")
|
900 |
+
st.markdown("- تحليل الشروط والمواصفات")
|
901 |
+
st.markdown("- استخراج جداول الكميات")
|
902 |
+
st.markdown("- تحديد الكيانات المهمة مثل الجهة المالكة، موقع المشروع، تواريخ المناقصة")
|
903 |
+
st.markdown("- تلخيص المستندات الطويلة")
|
904 |
+
|
905 |
+
elif model['name'] == "نموذج تقدير التكاليف":
|
906 |
+
st.markdown("**القدرات:**")
|
907 |
+
st.markdown("- تقدير تكاليف المشاريع بناءً على بيانات المشاريع السابقة")
|
908 |
+
st.markdown("- تحليل العوامل المؤثرة على التكاليف")
|
909 |
+
st.markdown("- تقديم توصيات لتحسين التكاليف")
|
910 |
+
st.markdown("- مقارنة التكاليف مع المشاريع المماثلة")
|
911 |
+
st.markdown("- تحليل التكاليف المباشرة وغير المباشرة")
|
912 |
+
|
913 |
+
elif model['name'] == "نموذج تحليل المخاطر":
|
914 |
+
st.markdown("**القدرات:**")
|
915 |
+
st.markdown("- تحديد المخاطر المحتملة للمشاريع")
|
916 |
+
st.markdown("- تقييم احتمالية وتأثير المخاطر")
|
917 |
+
st.markdown("- إنشاء مصفوفة المخاطر")
|
918 |
+
st.markdown("- تقديم توصيات لإدارة المخاطر")
|
919 |
+
st.markdown("- تحليل المخاطر بناءً على بيانات المشاريع السابقة")
|
920 |
+
|
921 |
+
elif model['name'] == "نموذج تحليل المنافسين":
|
922 |
+
st.markdown("**القدرات:**")
|
923 |
+
st.markdown("- تحليل بيانات المنافسين")
|
924 |
+
st.markdown("- تحديد نقاط القوة والضعف للمنافسين")
|
925 |
+
st.markdown("- تقديم توصيات للتسعير التنافسي")
|
926 |
+
st.markdown("- تحليل استراتيجيات المنافسين")
|
927 |
+
st.markdown("- تحليل حصص السوق")
|
928 |
+
|
929 |
+
elif model['name'] == "نموذج المساعد الذكي":
|
930 |
+
st.markdown("**القدرات:**")
|
931 |
+
st.markdown("- الإجابة على الاستفسارات المتعلقة بإدارة المناقصات")
|
932 |
+
st.markdown("- تقديم توصيات لتحسين إدارة المناقصات")
|
933 |
+
st.markdown("- مساعدة المستخدمين في استخدام النظام")
|
934 |
+
st.markdown("- تقديم معلومات عن المشاريع والمناقصات")
|
935 |
+
st.markdown("- تقديم إحصائيات وتحليلات عن المناقصات")
|
936 |
+
|
937 |
+
# عرض أداء النماذج
|
938 |
+
st.markdown("#### أداء النماذج")
|
939 |
+
|
940 |
+
# إنشاء رسم بياني لأداء النماذج
|
941 |
+
performance_df = pd.DataFrame({
|
942 |
+
"النموذج": [model['name'] for model in st.session_state.ai_models],
|
943 |
+
"الدقة (%)": [model['accuracy'] for model in st.session_state.ai_models]
|
944 |
+
})
|
945 |
+
|
946 |
+
fig = px.bar(
|
947 |
+
performance_df,
|
948 |
+
x="النموذج",
|
949 |
+
y="الدقة (%)",
|
950 |
+
title="أداء نماذج الذكاء الاصطناعي",
|
951 |
+
color="الدقة (%)",
|
952 |
+
text_auto='.0f'
|
953 |
+
)
|
954 |
+
|
955 |
+
fig.update_layout(yaxis_range=[0, 100])
|
956 |
+
|
957 |
+
st.plotly_chart(fig, use_container_width=True)
|
958 |
+
|
959 |
+
# تدريب النماذج
|
960 |
+
st.markdown("#### تدريب النماذج")
|
961 |
+
|
962 |
+
col1, col2 = st.columns(2)
|
963 |
+
|
964 |
+
with col1:
|
965 |
+
model_to_train = st.selectbox(
|
966 |
+
"اختر النموذج للتدريب",
|
967 |
+
[model['name'] for model in st.session_state.ai_models]
|
968 |
+
)
|
969 |
+
|
970 |
+
with col2:
|
971 |
+
training_data = st.file_uploader("قم بتحميل بيانات التدريب (CSV, XLSX)", type=["csv", "xlsx"])
|
972 |
+
|
973 |
+
if st.button("تدريب النموذج"):
|
974 |
+
# محاكاة تدريب النموذج
|
975 |
+
with st.spinner(f"جاري تدريب {model_to_train}..."):
|
976 |
+
time.sleep(3) # محاكاة وقت التدريب
|
977 |
+
st.success(f"تم تدريب {model_to_train} بنجاح!")
|
978 |
+
|
979 |
+
# تحديث دقة النموذج
|
980 |
+
for i, model in enumerate(st.session_state.ai_models):
|
981 |
+
if model['name'] == model_to_train:
|
982 |
+
# زيادة الدقة بنسبة عشوائية بين 1% و 3%
|
983 |
+
import random
|
984 |
+
accuracy_increase = random.uniform(1, 3)
|
985 |
+
new_accuracy = min(model['accuracy'] + accuracy_increase, 99)
|
986 |
+
st.session_state.ai_models[i]['accuracy'] = new_accuracy
|
987 |
+
st.session_state.ai_models[i]['last_updated'] = time.strftime("%Y-%m-%d")
|
988 |
+
|
989 |
+
st.metric(
|
990 |
+
"الدقة الجديدة",
|
991 |
+
f"{new_accuracy:.1f}%",
|
992 |
+
f"+{accuracy_increase:.1f}%"
|
993 |
+
)
|
994 |
+
|
995 |
+
break
|
996 |
+
|
997 |
+
# تقييم النماذج
|
998 |
+
st.markdown("#### تقييم النماذج")
|
999 |
+
|
1000 |
+
col1, col2 = st.columns(2)
|
1001 |
+
|
1002 |
+
with col1:
|
1003 |
+
model_to_evaluate = st.selectbox(
|
1004 |
+
"اختر النموذج للتقييم",
|
1005 |
+
[model['name'] for model in st.session_state.ai_models],
|
1006 |
+
key="model_to_evaluate"
|
1007 |
+
)
|
1008 |
+
|
1009 |
+
with col2:
|
1010 |
+
evaluation_data = st.file_uploader("قم بتحميل بيانات التقييم (CSV, XLSX)", type=["csv", "xlsx"], key="evaluation_data")
|
1011 |
+
|
1012 |
+
if st.button("تقييم النموذج"):
|
1013 |
+
# محاكاة تقييم النموذج
|
1014 |
+
with st.spinner(f"جاري تقييم {model_to_evaluate}..."):
|
1015 |
+
time.sleep(2) # محاكاة وقت التقييم
|
1016 |
+
st.success(f"تم تقييم {model_to_evaluate} بنجاح!")
|
1017 |
+
|
1018 |
+
# عرض نتائج التقييم
|
1019 |
+
for model in st.session_state.ai_models:
|
1020 |
+
if model['name'] == model_to_evaluate:
|
1021 |
+
accuracy = model['accuracy']
|
1022 |
+
break
|
1023 |
+
|
1024 |
+
evaluation_metrics = {
|
1025 |
+
"المقياس": ["الدقة", "الاستدعاء", "F1", "AUC-ROC"],
|
1026 |
+
"القيمة": [accuracy / 100, (accuracy - 5) / 100, (accuracy - 3) / 100, (accuracy - 2) / 100]
|
1027 |
+
}
|
1028 |
+
|
1029 |
+
evaluation_df = pd.DataFrame(evaluation_metrics)
|
1030 |
+
st.dataframe(evaluation_df, use_container_width=True, hide_index=True)
|
1031 |
+
|
1032 |
+
# عرض مصفوفة الارتباك
|
1033 |
+
st.markdown("##### مصفوفة الارتباك")
|
1034 |
+
|
1035 |
+
# إنشاء مصفوفة ارتباك افتراضية
|
1036 |
+
confusion_matrix = np.array([
|
1037 |
+
[85, 10, 5],
|
1038 |
+
[8, 80, 12],
|
1039 |
+
[7, 13, 80]
|
1040 |
+
])
|
1041 |
+
|
1042 |
+
fig, ax = plt.subplots(figsize=(10, 8))
|
1043 |
+
|
1044 |
+
im = ax.imshow(confusion_matrix, cmap="Blues")
|
1045 |
+
|
1046 |
+
# إضافة النص إلى الخلايا
|
1047 |
+
for i in range(3):
|
1048 |
+
for j in range(3):
|
1049 |
+
text = ax.text(j, i, confusion_matrix[i, j], ha="center", va="center", color="black")
|
1050 |
+
|
1051 |
+
# إضافة العناوين
|
1052 |
+
ax.set_xticks(np.arange(3))
|
1053 |
+
ax.set_yticks(np.arange(3))
|
1054 |
+
ax.set_xticklabels(["الفئة 1", "الفئة 2", "الفئة 3"])
|
1055 |
+
ax.set_yticklabels(["الفئة 1", "الفئة 2", "الفئة 3"])
|
1056 |
+
|
1057 |
+
# إضافة العناوين الرئيسية
|
1058 |
+
ax.set_xlabel("الفئة المتوقعة")
|
1059 |
+
ax.set_ylabel("الفئة الحقيقية")
|
1060 |
+
ax.set_title("مصفوفة الارتباك")
|
1061 |
+
|
1062 |
+
# إضافة شريط الألوان
|
1063 |
+
cbar = ax.figure.colorbar(im, ax=ax)
|
1064 |
+
cbar.ax.set_ylabel("عدد العينات", rotation=-90, va="bottom")
|
1065 |
+
|
1066 |
+
# عرض الرسم البياني
|
1067 |
+
st.pyplot(fig)
|
modules/ai_assistant/assistant.py
ADDED
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة المساعد الذكي لنظام إدارة المناقصات - Hybrid Face
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import logging
|
7 |
+
import threading
|
8 |
+
import datetime
|
9 |
+
import json
|
10 |
+
import re
|
11 |
+
from pathlib import Path
|
12 |
+
|
13 |
+
# تهيئة السجل
|
14 |
+
logging.basicConfig(
|
15 |
+
level=logging.INFO,
|
16 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
17 |
+
)
|
18 |
+
logger = logging.getLogger('ai_assistant')
|
19 |
+
|
20 |
+
class AIAssistant:
|
21 |
+
"""المساعد الذكي"""
|
22 |
+
|
23 |
+
def __init__(self, config=None, db=None):
|
24 |
+
"""تهيئة المساعد الذكي"""
|
25 |
+
self.config = config
|
26 |
+
self.db = db
|
27 |
+
self.processing_in_progress = False
|
28 |
+
self.current_query = None
|
29 |
+
self.processing_results = {}
|
30 |
+
self.conversation_history = []
|
31 |
+
|
32 |
+
# إعدادات المساعد الذكي
|
33 |
+
self.ai_model = config.AI_MODEL if config and hasattr(config, 'AI_MODEL') else "gpt-4"
|
34 |
+
self.ai_temperature = config.AI_TEMPERATURE if config and hasattr(config, 'AI_TEMPERATURE') else 0.7
|
35 |
+
self.ai_max_tokens = config.AI_MAX_TOKENS if config and hasattr(config, 'AI_MAX_TOKENS') else 2000
|
36 |
+
|
37 |
+
# إنشاء مجلد المساعد الذكي إذا لم يكن موجوداً
|
38 |
+
if config and hasattr(config, 'EXPORTS_PATH'):
|
39 |
+
self.exports_path = Path(config.EXPORTS_PATH)
|
40 |
+
else:
|
41 |
+
self.exports_path = Path('data/exports')
|
42 |
+
|
43 |
+
if not self.exports_path.exists():
|
44 |
+
self.exports_path.mkdir(parents=True, exist_ok=True)
|
45 |
+
|
46 |
+
def process_query(self, query, context=None, callback=None):
|
47 |
+
"""معالجة استعلام المستخدم"""
|
48 |
+
if self.processing_in_progress:
|
49 |
+
logger.warning("هناك عملية معالجة جارية بالفعل")
|
50 |
+
return False
|
51 |
+
|
52 |
+
self.processing_in_progress = True
|
53 |
+
self.current_query = query
|
54 |
+
self.processing_results = {
|
55 |
+
"query": query,
|
56 |
+
"context": context,
|
57 |
+
"processing_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
58 |
+
"status": "جاري المعالجة",
|
59 |
+
"response": "",
|
60 |
+
"suggestions": [],
|
61 |
+
"references": []
|
62 |
+
}
|
63 |
+
|
64 |
+
# إضافة الاستعلام إلى سجل المحادثة
|
65 |
+
self.conversation_history.append({
|
66 |
+
"role": "user",
|
67 |
+
"content": query,
|
68 |
+
"timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
69 |
+
})
|
70 |
+
|
71 |
+
# بدء المعالجة في خيط منفصل
|
72 |
+
thread = threading.Thread(
|
73 |
+
target=self._process_query_thread,
|
74 |
+
args=(query, context, callback)
|
75 |
+
)
|
76 |
+
thread.daemon = True
|
77 |
+
thread.start()
|
78 |
+
|
79 |
+
return True
|
80 |
+
|
81 |
+
def _process_query_thread(self, query, context, callback):
|
82 |
+
"""خيط معالجة الاستعلام"""
|
83 |
+
try:
|
84 |
+
# تحليل الاستعلام
|
85 |
+
query_type = self._analyze_query(query)
|
86 |
+
|
87 |
+
# معالجة الاستعلام بناءً على نوعه
|
88 |
+
if query_type == "document_analysis":
|
89 |
+
response = self._handle_document_analysis_query(query, context)
|
90 |
+
elif query_type == "pricing":
|
91 |
+
response = self._handle_pricing_query(query, context)
|
92 |
+
elif query_type == "risk_analysis":
|
93 |
+
response = self._handle_risk_analysis_query(query, context)
|
94 |
+
elif query_type == "project_management":
|
95 |
+
response = self._handle_project_management_query(query, context)
|
96 |
+
elif query_type == "reporting":
|
97 |
+
response = self._handle_reporting_query(query, context)
|
98 |
+
else:
|
99 |
+
response = self._handle_general_query(query, context)
|
100 |
+
|
101 |
+
# توليد اقتراحات
|
102 |
+
suggestions = self._generate_suggestions(query_type, query, response)
|
103 |
+
|
104 |
+
# تحديث نتائج المعالجة
|
105 |
+
self.processing_results["response"] = response
|
106 |
+
self.processing_results["query_type"] = query_type
|
107 |
+
self.processing_results["suggestions"] = suggestions
|
108 |
+
self.processing_results["status"] = "اكتملت المعالجة"
|
109 |
+
self.processing_results["processing_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
110 |
+
|
111 |
+
# إضافة الاستجابة إلى سجل المحادثة
|
112 |
+
self.conversation_history.append({
|
113 |
+
"role": "assistant",
|
114 |
+
"content": response,
|
115 |
+
"timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
116 |
+
})
|
117 |
+
|
118 |
+
logger.info(f"اكتملت معالجة الاستعلام: {query[:50]}...")
|
119 |
+
|
120 |
+
except Exception as e:
|
121 |
+
logger.error(f"خطأ في معالجة الاستعلام: {str(e)}")
|
122 |
+
self.processing_results["status"] = "فشلت ا��معالجة"
|
123 |
+
self.processing_results["error"] = str(e)
|
124 |
+
|
125 |
+
# إضافة رسالة الخطأ إلى سجل المحادثة
|
126 |
+
self.conversation_history.append({
|
127 |
+
"role": "system",
|
128 |
+
"content": f"حدث خطأ أثناء معالجة الاستعلام: {str(e)}",
|
129 |
+
"timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
130 |
+
})
|
131 |
+
|
132 |
+
finally:
|
133 |
+
self.processing_in_progress = False
|
134 |
+
|
135 |
+
# استدعاء دالة الاستجابة إذا تم توفيرها
|
136 |
+
if callback and callable(callback):
|
137 |
+
callback(self.processing_results)
|
138 |
+
|
139 |
+
def _analyze_query(self, query):
|
140 |
+
"""تحليل نوع الاستعلام"""
|
141 |
+
query = query.lower()
|
142 |
+
|
143 |
+
# تحديد نوع الاستعلام بناءً على الكلمات المفتاحية
|
144 |
+
if any(keyword in query for keyword in ["تحليل المستند", "تحليل وثيقة", "استخراج بيانات", "قراءة مستند"]):
|
145 |
+
return "document_analysis"
|
146 |
+
elif any(keyword in query for keyword in ["تسعير", "سعر", "تكلفة", "ميزانية", "تقدير"]):
|
147 |
+
return "pricing"
|
148 |
+
elif any(keyword in query for keyword in ["مخاطر", "تحليل المخاطر", "تقييم المخاطر"]):
|
149 |
+
return "risk_analysis"
|
150 |
+
elif any(keyword in query for keyword in ["مشروع", "إدارة المشروع", "جدول زمني", "خطة"]):
|
151 |
+
return "project_management"
|
152 |
+
elif any(keyword in query for keyword in ["تقرير", "إحصائيات", "تحليل البيانات", "رسم بياني"]):
|
153 |
+
return "reporting"
|
154 |
+
else:
|
155 |
+
return "general"
|
156 |
+
|
157 |
+
def _handle_document_analysis_query(self, query, context):
|
158 |
+
"""معالجة استعلام تحليل المستندات"""
|
159 |
+
# محاكاة استجابة المساعد الذكي لاستعلام تحليل المستندات
|
160 |
+
response = """
|
161 |
+
يمكنني مساعدتك في تحليل المستندات واستخراج المعلومات المهمة منها. لتحليل مستند، يرجى اتباع الخطوات التالية:
|
162 |
+
|
163 |
+
1. انتقل إلى وحدة "تحليل المستندات" من القائمة الجانبية.
|
164 |
+
2. انقر على زر "تحميل مستند" واختر المستند المراد تحليله.
|
165 |
+
3. حدد نوع المستند (مناقصة، عقد، مواصفات فنية، إلخ).
|
166 |
+
4. انقر على زر "تحليل" لبدء عملية التحليل.
|
167 |
+
|
168 |
+
سيقوم النظام باستخراج المعلومات التالية من المستند:
|
169 |
+
- البنود والكميات
|
170 |
+
- الكيانات (العميل، الموقع، المقاول، إلخ)
|
171 |
+
- التواريخ المهمة
|
172 |
+
- المبالغ والتكاليف
|
173 |
+
- المخاطر المحتملة
|
174 |
+
|
175 |
+
بعد اكتمال التحليل، يمكنك مراجعة النتائج وتعديلها إذا لزم الأمر، ثم استخدامها في وحدات النظام الأخرى مثل التسعير وتحليل المخاطر.
|
176 |
+
"""
|
177 |
+
|
178 |
+
# إضافة مراجع ذات صلة
|
179 |
+
self.processing_results["references"] = [
|
180 |
+
{"title": "دليل استخدام وحدة تحليل المستندات", "type": "manual"},
|
181 |
+
{"title": "أنواع المستندات المدعومة", "type": "documentation"},
|
182 |
+
{"title": "تقنيات استخراج البيانات من المستندات", "type": "article"}
|
183 |
+
]
|
184 |
+
|
185 |
+
return response
|
186 |
+
|
187 |
+
def _handle_pricing_query(self, query, context):
|
188 |
+
"""معالجة استعلام التسعير"""
|
189 |
+
# محاكاة استجابة المساعد الذكي لاستعلام التسعير
|
190 |
+
response = """
|
191 |
+
يمكنني مساعدتك في تسعير المشاريع وتقدير التكاليف. لإنشاء تسعير لمشروع، يرجى اتباع الخطوات التالية:
|
192 |
+
|
193 |
+
1. انتقل إلى وحدة "التسعير المتكامل" من القائمة الجانبية.
|
194 |
+
2. اختر المشروع المراد تسعيره أو أنشئ مشروعاً جديداً.
|
195 |
+
3. أدخل بنود المشروع والكميات التقديرية (يمكن استيرادها من نتائج تحليل المستندات).
|
196 |
+
4. حدد الموارد المطلوبة (مواد، معدات، عمالة).
|
197 |
+
5. اختر استراتيجية التسعير المناسبة:
|
198 |
+
- شاملة: تغطية كاملة للتكاليف والمخاطر مع هامش ربح مناسب.
|
199 |
+
- تنافسية: تخفيض الهوامش لتقديم سعر تنافسي.
|
200 |
+
- متوازنة: توازن بين الربحية والتنافسية.
|
201 |
+
6. انقر على زر "حساب التسعير" لإنشاء التسعير.
|
202 |
+
|
203 |
+
سيقوم النظام بحساب:
|
204 |
+
- التكاليف المباشرة (بنود المشروع)
|
205 |
+
- التكاليف غير المباشرة (نفقات عامة، إدارية، ربح)
|
206 |
+
- تكاليف المخاطر
|
207 |
+
- ضريبة القيمة المضافة
|
208 |
+
- السعر النهائي
|
209 |
+
|
210 |
+
يمكنك تعديل المعلمات وإعادة حساب التسعير، ثم تصدير النتائج إلى تقرير مفصل.
|
211 |
+
"""
|
212 |
+
|
213 |
+
# إضافة مراجع ذات صلة
|
214 |
+
self.processing_results["references"] = [
|
215 |
+
{"title": "دليل استخدام وحدة التسعير المتكامل", "type": "manual"},
|
216 |
+
{"title": "استراتيجيات التسعير", "type": "documentation"},
|
217 |
+
{"title": "حساب التكاليف غير المباشرة", "type": "article"}
|
218 |
+
]
|
219 |
+
|
220 |
+
return response
|
221 |
+
|
222 |
+
def _handle_risk_analysis_query(self, query, context):
|
223 |
+
"""معالجة استعلام تحليل المخاطر"""
|
224 |
+
# محاكاة استجابة المساعد الذكي لاستعلام تحليل المخاطر
|
225 |
+
response = """
|
226 |
+
يمكنني مساعدتك في تحليل وإدارة مخاطر المشروع. لإجراء تحليل للمخاطر، يرجى اتباع الخطوات التالية:
|
227 |
+
|
228 |
+
1. انتقل إلى وحدة "تحليل المخاطر" من القائمة الجانبية.
|
229 |
+
2. اختر المشروع المراد تحليل مخاطره.
|
230 |
+
3. اختر طريقة التحليل:
|
231 |
+
- شاملة: تحليل مفصل يغطي جميع جوانب المشروع.
|
232 |
+
- أساسية: تحليل سريع للمخاطر الرئيسية.
|
233 |
+
4. انقر على زر "تحليل المخاطر" لبدء التحليل.
|
234 |
+
|
235 |
+
سيقوم النظام بما يلي:
|
236 |
+
- تحديد المخاطر المحتملة بناءً على بيانات المشروع
|
237 |
+
- تصنيف المخاطر إلى فئات (فني، مالي، إداري، إلخ)
|
238 |
+
- إنشاء مصفوفة المخاطر (الاحتمالية × التأثير)
|
239 |
+
- تطوير استراتيجيات التخفيف لكل مخاطرة
|
240 |
+
- إنشاء ملخص للمخاطر وتوصيات
|
241 |
+
|
242 |
+
يمكنك مراجعة نتائج التحليل وتعديلها، ثم تصدير التقرير النهائي واستخدامه في خطة إدارة المشروع.
|
243 |
+
"""
|
244 |
+
|
245 |
+
# إضافة مراجع ذات صلة
|
246 |
+
self.processing_results["references"] = [
|
247 |
+
{"title": "دليل استخدام وحدة تحليل المخاطر", "type": "manual"},
|
248 |
+
{"title": "منهجيات تحليل المخاطر", "type": "documentation"},
|
249 |
+
{"title": "استراتيجيات التخفيف من المخاطر", "type": "article"}
|
250 |
+
]
|
251 |
+
|
252 |
+
return response
|
253 |
+
|
254 |
+
def _handle_project_management_query(self, query, context):
|
255 |
+
"""معالجة استعلام إدارة المشاريع"""
|
256 |
+
# محاكاة استجابة المساعد الذكي لاستعلام إدارة المشاريع
|
257 |
+
response = """
|
258 |
+
يمكنني مساعدتك في إدارة المشاريع وتتبع تقدمها. لإدارة مشروع، يرجى اتباع الخطوات التالية:
|
259 |
+
|
260 |
+
1. انتقل إلى وحدة "إدارة المشاريع" من القائمة الجانبية.
|
261 |
+
2. أنشئ مشروعاً جديداً أو اختر مشروعاً موجوداً.
|
262 |
+
3. أدخل معلومات المشروع الأساسية (الاسم، العميل، الوصف، التواريخ).
|
263 |
+
4. أضف بنود المشروع (يمكن استيرادها من نتائج تحليل المستندات).
|
264 |
+
5. أنشئ الجدول الزمني للمشروع وحدد المراحل والمهام.
|
265 |
+
6. عين الموارد للمهام وحدد التبعيات بينها.
|
266 |
+
|
267 |
+
يمكنك استخدام وحدة إدارة المشاريع لـ:
|
268 |
+
- تتبع تقدم المشروع ومقارنته بالخطة
|
269 |
+
- إدارة الموارد وتوزيعها
|
270 |
+
- متابعة المشكلات والمخاطر
|
271 |
+
- إدارة التغييرات في نطاق العمل
|
272 |
+
- إنشاء تقارير حالة المشروع
|
273 |
+
|
274 |
+
كما يمكنك دمج نتائج التسعير وتحليل المخاطر في خطة المشروع لإدارة شاملة.
|
275 |
+
"""
|
276 |
+
|
277 |
+
# إضافة مراجع ذات صلة
|
278 |
+
self.processing_results["references"] = [
|
279 |
+
{"title": "دليل استخدام وحدة إدارة المشاريع", "type": "manual"},
|
280 |
+
{"title": "أفضل ممارسات إدارة المشاريع", "type": "documentation"},
|
281 |
+
{"title": "إنشاء جداول زمنية فعالة", "type": "article"}
|
282 |
+
]
|
283 |
+
|
284 |
+
return response
|
285 |
+
|
286 |
+
def _handle_reporting_query(self, query, context):
|
287 |
+
"""معالجة استعلام التقارير"""
|
288 |
+
# محاكاة استجابة المساعد الذكي لاستعلام التقارير
|
289 |
+
response = """
|
290 |
+
يمكنني مساعدتك في إنشاء تقارير وتحليلات للمشاريع والمناقصات. لإنشاء تقرير، يرجى اتباع الخطوات التالية:
|
291 |
+
|
292 |
+
1. انتقل إلى وحدة "التقارير والتحليلات" من القائمة الجانبية.
|
293 |
+
2. اختر نوع التقرير:
|
294 |
+
- تقرير المشروع: معلومات شاملة عن مشروع محدد
|
295 |
+
- تقرير التسعير: تفاصيل تسعير مشروع أو مناقصة
|
296 |
+
- تقرير المخاطر: تحليل مخاطر المشروع واستراتيجيات التخفيف
|
297 |
+
- تقرير الأداء: مقارنة الأداء الفعلي بالمخطط
|
298 |
+
- تقرير مالي: تحليل مالي للمشاريع والمناقصات
|
299 |
+
3. حدد معلمات التقرير (المشروع، الفترة الزمنية، إلخ).
|
300 |
+
4. انقر على زر "إنشاء التقرير".
|
301 |
+
|
302 |
+
يمكنك تخصيص التقارير بإضافة أو إزالة أقسام، وتغيير طريقة عرض البيانات (جداول، رسوم بيانية، إلخ).
|
303 |
+
|
304 |
+
التقارير المنشأة يمكن:
|
305 |
+
- تصديرها بتنسيقات مختلفة (PDF، Excel، Word)
|
306 |
+
- مشاركتها مع أعضاء الفريق أو العملاء
|
307 |
+
- جدولتها للإنشاء التلقائي بشكل دوري
|
308 |
+
- حفظها كقوالب لاستخدامها في المستقبل
|
309 |
+
"""
|
310 |
+
|
311 |
+
# إضافة مراجع ذات صلة
|
312 |
+
self.processing_results["references"] = [
|
313 |
+
{"title": "دليل استخدام وحدة التقارير والتحليلات", "type": "manual"},
|
314 |
+
{"title": "أنواع التقارير المتاحة", "type": "documentation"},
|
315 |
+
{"title": "إنشاء رسوم بيانية فعالة", "type": "article"}
|
316 |
+
]
|
317 |
+
|
318 |
+
return response
|
319 |
+
|
320 |
+
def _handle_general_query(self, query, context):
|
321 |
+
"""معالجة استعلام عام"""
|
322 |
+
# محاكاة استجابة المساعد الذكي لاستعلام عام
|
323 |
+
response = """
|
324 |
+
مرحباً بك في المساعد الذكي لنظام إدارة المناقصات Hybrid Face. يمكنني مساعدتك في مجموعة متنوعة من المهام المتعلقة بإدارة المناقصات والمشاريع.
|
325 |
+
|
326 |
+
يمكنني مساعدتك في:
|
327 |
+
- تحليل مستندات المناقصات واستخراج المعلومات المهمة منها
|
328 |
+
- تسعير المشاريع وتقدير التكاليف
|
329 |
+
- تحليل وإدارة مخاطر المشاريع
|
330 |
+
- إدارة المشاريع وتتبع تقدمها
|
331 |
+
- إنشاء تقارير وتحليلات
|
332 |
+
|
333 |
+
للحصول على مساعدة محددة، يرجى طرح سؤال يتعلق بإحدى هذه المجالات. على سبيل المثال:
|
334 |
+
- "كيف يمكنني تحليل مستند مناقصة؟"
|
335 |
+
- "ساعدني في تسعير مشروع جديد"
|
336 |
+
- "كيف أقوم بتحليل مخاطر المشروع؟"
|
337 |
+
- "أريد إنشاء تقرير عن حالة المشروع"
|
338 |
+
|
339 |
+
يمكنك أيضاً استخدام الوحدات المختلفة في النظام مباشرة من القائمة الجانبية.
|
340 |
+
"""
|
341 |
+
|
342 |
+
# إضافة مراجع ذات صلة
|
343 |
+
self.processing_results["references"] = [
|
344 |
+
{"title": "دليل المستخدم الشامل", "type": "manual"},
|
345 |
+
{"title": "نظرة عامة على النظام", "type": "documentation"},
|
346 |
+
{"title": "الأسئلة الشائعة", "type": "faq"}
|
347 |
+
]
|
348 |
+
|
349 |
+
return response
|
350 |
+
|
351 |
+
def _generate_suggestions(self, query_type, query, response):
|
352 |
+
"""توليد اقتراحات للمستخدم بناءً على الاستعلام والاستجابة"""
|
353 |
+
suggestions = []
|
354 |
+
|
355 |
+
if query_type == "document_analysis":
|
356 |
+
suggestions = [
|
357 |
+
"كيف يمكنني استيراد نتائج تحليل المستندات إلى وحدة التسعير؟",
|
358 |
+
"ما هي أنواع المستندات المدعومة للتحليل؟",
|
359 |
+
"كيف يمكنني تحسين دقة تحليل المستندات؟"
|
360 |
+
]
|
361 |
+
elif query_type == "pricing":
|
362 |
+
suggestions = [
|
363 |
+
"ما هي استراتيجية التسعير المناسبة لمشروعي؟",
|
364 |
+
"كيف يمكنني حساب التكاليف غير المباشرة؟",
|
365 |
+
"كيف أضيف تكاليف المخاطر إلى التسعير؟"
|
366 |
+
]
|
367 |
+
elif query_type == "risk_analysis":
|
368 |
+
suggestions = [
|
369 |
+
"ما هي أفضل استراتيجيات التخفيف من المخاطر؟",
|
370 |
+
"كيف يمكنني تحديد المخاطر الحرجة في المشروع؟",
|
371 |
+
"كيف أدمج تحليل المخاطر في خطة المشروع؟"
|
372 |
+
]
|
373 |
+
elif query_type == "project_management":
|
374 |
+
suggestions = [
|
375 |
+
"كيف أنشئ جدولاً زمنياً فعالاً للمشروع؟",
|
376 |
+
"كيف أتتبع تقدم المشروع مقارنة بالخطة؟",
|
377 |
+
"كيف أدير التغييرات في نطاق المشروع؟"
|
378 |
+
]
|
379 |
+
elif query_type == "reporting":
|
380 |
+
suggestions = [
|
381 |
+
"ما هي أنواع التقارير المتاحة في النظام؟",
|
382 |
+
"كيف يمكنني تخصيص تقرير المشروع؟",
|
383 |
+
"كيف أقوم بجدولة إنشاء تقارير دورية؟"
|
384 |
+
]
|
385 |
+
else:
|
386 |
+
suggestions = [
|
387 |
+
"كيف يمكنني البدء باستخدام النظام؟",
|
388 |
+
"ما هي الوحدات المتاحة في النظام؟",
|
389 |
+
"كيف يمكنني إنشاء مشروع جديد؟"
|
390 |
+
]
|
391 |
+
|
392 |
+
return suggestions
|
393 |
+
|
394 |
+
def get_processing_status(self):
|
395 |
+
"""الحصول على حالة المعالجة الحالية"""
|
396 |
+
if not self.processing_in_progress:
|
397 |
+
if not self.processing_results:
|
398 |
+
return {"status": "لا توجد معالجة جارية"}
|
399 |
+
else:
|
400 |
+
return {"status": self.processing_results.get("status", "غير معروف")}
|
401 |
+
|
402 |
+
return {
|
403 |
+
"status": "جاري المعالجة",
|
404 |
+
"query": self.current_query,
|
405 |
+
"start_time": self.processing_results.get("processing_start_time")
|
406 |
+
}
|
407 |
+
|
408 |
+
def get_processing_results(self):
|
409 |
+
"""الحصول على نتائج المعالجة"""
|
410 |
+
return self.processing_results
|
411 |
+
|
412 |
+
def get_conversation_history(self, limit=10):
|
413 |
+
"""الحصول على سجل المحادثة"""
|
414 |
+
if limit and limit > 0:
|
415 |
+
return self.conversation_history[-limit:]
|
416 |
+
return self.conversation_history
|
417 |
+
|
418 |
+
def clear_conversation_history(self):
|
419 |
+
"""مسح سجل المحادثة"""
|
420 |
+
self.conversation_history = []
|
421 |
+
return True
|
422 |
+
|
423 |
+
def export_conversation_history(self, output_path=None):
|
424 |
+
"""تصدير سجل المحادثة إلى ملف JSON"""
|
425 |
+
if not self.conversation_history:
|
426 |
+
logger.warning("لا يوجد سجل محادثة للتصدير")
|
427 |
+
return None
|
428 |
+
|
429 |
+
if not output_path:
|
430 |
+
# إنشاء اسم ملف افتراضي
|
431 |
+
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
432 |
+
filename = f"conversation_history_{timestamp}.json"
|
433 |
+
output_path = os.path.join(self.exports_path, filename)
|
434 |
+
|
435 |
+
try:
|
436 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
437 |
+
json.dump(self.conversation_history, f, ensure_ascii=False, indent=4)
|
438 |
+
|
439 |
+
logger.info(f"تم تصدير سجل المحادثة إلى: {output_path}")
|
440 |
+
return output_path
|
441 |
+
|
442 |
+
except Exception as e:
|
443 |
+
logger.error(f"خطأ في تصدير سجل المحادثة: {str(e)}")
|
444 |
+
return None
|
modules/data_analysis/data_analysis_app.py
ADDED
@@ -0,0 +1,1022 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة تحليل البيانات - التطبيق الرئيسي
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import matplotlib.pyplot as plt
|
9 |
+
import plotly.express as px
|
10 |
+
import plotly.graph_objects as go
|
11 |
+
from datetime import datetime
|
12 |
+
import time
|
13 |
+
import io
|
14 |
+
import os
|
15 |
+
import json
|
16 |
+
import base64
|
17 |
+
from pathlib import Path
|
18 |
+
|
19 |
+
class DataAnalysisApp:
|
20 |
+
"""وحدة تحليل البيانات"""
|
21 |
+
|
22 |
+
def __init__(self):
|
23 |
+
"""تهيئة وحدة تحليل البيانات"""
|
24 |
+
|
25 |
+
# تهيئة حالة الجلسة
|
26 |
+
if 'uploaded_data' not in st.session_state:
|
27 |
+
st.session_state.uploaded_data = None
|
28 |
+
|
29 |
+
if 'data_sources' not in st.session_state:
|
30 |
+
st.session_state.data_sources = [
|
31 |
+
{
|
32 |
+
'id': 1,
|
33 |
+
'name': 'بيانات المناقصات السابقة',
|
34 |
+
'type': 'CSV',
|
35 |
+
'rows': 250,
|
36 |
+
'columns': 15,
|
37 |
+
'last_updated': '2024-03-01',
|
38 |
+
'description': 'بيانات المناقصات السابقة للشركة خلال الثلاث سنوات الماضية'
|
39 |
+
},
|
40 |
+
{
|
41 |
+
'id': 2,
|
42 |
+
'name': 'بيانات المنافسين',
|
43 |
+
'type': 'Excel',
|
44 |
+
'rows': 120,
|
45 |
+
'columns': 10,
|
46 |
+
'last_updated': '2024-02-15',
|
47 |
+
'description': 'بيانات المنافسين الرئيسيين في السوق وأسعارهم التنافسية'
|
48 |
+
},
|
49 |
+
{
|
50 |
+
'id': 3,
|
51 |
+
'name': 'بيانات أسعار المواد',
|
52 |
+
'type': 'CSV',
|
53 |
+
'rows': 500,
|
54 |
+
'columns': 8,
|
55 |
+
'last_updated': '2024-03-10',
|
56 |
+
'description': 'بيانات أسعار المواد الرئيسية المستخدمة في المشاريع'
|
57 |
+
},
|
58 |
+
{
|
59 |
+
'id': 4,
|
60 |
+
'name': 'بيانات الموردين',
|
61 |
+
'type': 'Excel',
|
62 |
+
'rows': 80,
|
63 |
+
'columns': 12,
|
64 |
+
'last_updated': '2024-02-20',
|
65 |
+
'description': 'بيانات الموردين الرئيسيين وأسعارهم وجودة منتجاتهم'
|
66 |
+
},
|
67 |
+
{
|
68 |
+
'id': 5,
|
69 |
+
'name': 'بيانات المشاريع المنجزة',
|
70 |
+
'type': 'CSV',
|
71 |
+
'rows': 150,
|
72 |
+
'columns': 20,
|
73 |
+
'last_updated': '2024-03-15',
|
74 |
+
'description': 'بيانات المشاريع المنجزة وتكاليفها الفعلية ومدة تنفيذها'
|
75 |
+
}
|
76 |
+
]
|
77 |
+
|
78 |
+
if 'sample_data' not in st.session_state:
|
79 |
+
# إنشاء بيانات افتراضية للمناقصات السابقة
|
80 |
+
np.random.seed(42)
|
81 |
+
|
82 |
+
# إنشاء بيانات المناقصات السابقة
|
83 |
+
n_tenders = 50
|
84 |
+
tender_ids = [f"T-{2021 + i//20}-{i%20 + 1:03d}" for i in range(n_tenders)]
|
85 |
+
tender_types = np.random.choice(["مبنى إداري", "مبنى سكني", "مدرسة", "مستشفى", "طرق", "جسور", "بنية تحتية"], n_tenders)
|
86 |
+
tender_locations = np.random.choice(["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك"], n_tenders)
|
87 |
+
tender_areas = np.random.randint(1000, 10000, n_tenders)
|
88 |
+
tender_durations = np.random.randint(6, 36, n_tenders)
|
89 |
+
tender_budgets = np.random.randint(1000000, 50000000, n_tenders)
|
90 |
+
tender_costs = np.array([budget * np.random.uniform(0.8, 1.1) for budget in tender_budgets])
|
91 |
+
tender_profits = tender_budgets - tender_costs
|
92 |
+
tender_profit_margins = tender_profits / tender_budgets * 100
|
93 |
+
tender_statuses = np.random.choice(["فائز", "خاسر", "قيد التنفيذ", "منجز"], n_tenders)
|
94 |
+
tender_dates = [f"202{1 + i//20}-{np.random.randint(1, 13):02d}-{np.random.randint(1, 29):02d}" for i in range(n_tenders)]
|
95 |
+
|
96 |
+
# إنشاء DataFrame للمناقصات السابقة
|
97 |
+
tenders_data = {
|
98 |
+
"رقم المناقصة": tender_ids,
|
99 |
+
"نوع المشروع": tender_types,
|
100 |
+
"الموقع": tender_locations,
|
101 |
+
"المساحة (م2)": tender_areas,
|
102 |
+
"المدة (شهر)": tender_durations,
|
103 |
+
"الميزانية (ريال)": tender_budgets,
|
104 |
+
"التكلفة (ريال)": tender_costs,
|
105 |
+
"الربح (ريال)": tender_profits,
|
106 |
+
"هامش الربح (%)": tender_profit_margins,
|
107 |
+
"الحالة": tender_statuses,
|
108 |
+
"تاريخ التقديم": tender_dates
|
109 |
+
}
|
110 |
+
|
111 |
+
st.session_state.sample_data = {
|
112 |
+
"tenders": pd.DataFrame(tenders_data)
|
113 |
+
}
|
114 |
+
|
115 |
+
# إنشاء بيانات أسعار المواد
|
116 |
+
n_materials = 30
|
117 |
+
material_ids = [f"M-{i+1:03d}" for i in range(n_materials)]
|
118 |
+
material_names = [
|
119 |
+
"خرسانة جاهزة", "حديد تسليح", "طابوق", "أسمنت", "رمل", "بحص", "خشب", "ألمنيوم", "زجاج", "دهان",
|
120 |
+
"سيراميك", "رخام", "جبس", "عازل مائي", "عازل حراري", "أنابيب PVC", "أسلاك كهربائية", "مفاتيح كهربائية",
|
121 |
+
"إنارة", "تكييف", "مصاعد", "أبواب خشبية", "أبواب حديدية", "نوافذ ألمنيوم", "نوافذ زجاجية",
|
122 |
+
"أرضيات خشبية", "أرضيات بلاط", "أرضيات رخام", "أرضيات سيراميك", "أرضيات بورسلين"
|
123 |
+
]
|
124 |
+
material_units = np.random.choice(["م3", "طن", "م2", "كجم", "لتر", "قطعة", "متر"], n_materials)
|
125 |
+
material_prices_2021 = np.random.randint(50, 5000, n_materials)
|
126 |
+
material_prices_2022 = np.array([price * np.random.uniform(1.0, 1.2) for price in material_prices_2021])
|
127 |
+
material_prices_2023 = np.array([price * np.random.uniform(1.0, 1.15) for price in material_prices_2022])
|
128 |
+
material_prices_2024 = np.array([price * np.random.uniform(0.95, 1.1) for price in material_prices_2023])
|
129 |
+
|
130 |
+
# إنشاء DataFrame لأسعار المواد
|
131 |
+
materials_data = {
|
132 |
+
"رمز المادة": material_ids,
|
133 |
+
"اسم المادة": material_names,
|
134 |
+
"الوحدة": material_units,
|
135 |
+
"سعر 2021 (ريال)": material_prices_2021,
|
136 |
+
"سعر 2022 (ريال)": material_prices_2022,
|
137 |
+
"سعر 2023 (ريال)": material_prices_2023,
|
138 |
+
"سعر 2024 (ريال)": material_prices_2024,
|
139 |
+
"نسبة التغير 2021-2024 (%)": (material_prices_2024 - material_prices_2021) / material_prices_2021 * 100
|
140 |
+
}
|
141 |
+
|
142 |
+
st.session_state.sample_data["materials"] = pd.DataFrame(materials_data)
|
143 |
+
|
144 |
+
# إنشاء بيانات المنافسين
|
145 |
+
n_competitors = 10
|
146 |
+
competitor_ids = [f"C-{i+1:02d}" for i in range(n_competitors)]
|
147 |
+
competitor_names = [
|
148 |
+
"شركة الإنشاءات المتطورة", "شركة البناء الحديث", "شركة التطوير العمراني", "شركة الإعمار الدولية",
|
149 |
+
"شركة البنية التحتية المتكاملة", "شركة المقاولات العامة", "شركة التشييد والبناء", "شركة الهندسة والإنشاءات",
|
150 |
+
"شركة المشاريع الكبرى", "شركة التطوير العقاري"
|
151 |
+
]
|
152 |
+
competitor_specialties = np.random.choice(["مباني", "طرق", "جسور", "بنية تحتية", "متعددة"], n_competitors)
|
153 |
+
competitor_sizes = np.random.choice(["صغيرة", "متوسطة", "كبيرة"], n_competitors)
|
154 |
+
competitor_market_shares = np.random.uniform(1, 15, n_competitors)
|
155 |
+
competitor_win_rates = np.random.uniform(10, 60, n_competitors)
|
156 |
+
competitor_avg_margins = np.random.uniform(5, 20, n_competitors)
|
157 |
+
|
158 |
+
# إنشاء DataFrame للمنافسين
|
159 |
+
competitors_data = {
|
160 |
+
"رمز المنافس": competitor_ids,
|
161 |
+
"اسم المنافس": competitor_names,
|
162 |
+
"التخصص": competitor_specialties,
|
163 |
+
"الحجم": competitor_sizes,
|
164 |
+
"حصة السوق (%)": competitor_market_shares,
|
165 |
+
"معدل الفوز (%)": competitor_win_rates,
|
166 |
+
"متوسط هامش الربح (%)": competitor_avg_margins
|
167 |
+
}
|
168 |
+
|
169 |
+
st.session_state.sample_data["competitors"] = pd.DataFrame(competitors_data)
|
170 |
+
|
171 |
+
def render(self):
|
172 |
+
"""عرض واجهة وحدة تحليل البيانات"""
|
173 |
+
|
174 |
+
st.markdown("<h1 class='module-title'>وحدة تحليل البيانات</h1>", unsafe_allow_html=True)
|
175 |
+
|
176 |
+
tabs = st.tabs([
|
177 |
+
"لوحة المعلومات",
|
178 |
+
"تحليل المناقصات",
|
179 |
+
"تحليل الأسعار",
|
180 |
+
"تحليل المنافسين",
|
181 |
+
"استيراد وتصدير البيانات"
|
182 |
+
])
|
183 |
+
|
184 |
+
with tabs[0]:
|
185 |
+
self._render_dashboard_tab()
|
186 |
+
|
187 |
+
with tabs[1]:
|
188 |
+
self._render_tenders_analysis_tab()
|
189 |
+
|
190 |
+
with tabs[2]:
|
191 |
+
self._render_price_analysis_tab()
|
192 |
+
|
193 |
+
with tabs[3]:
|
194 |
+
self._render_competitors_analysis_tab()
|
195 |
+
|
196 |
+
with tabs[4]:
|
197 |
+
self._render_import_export_tab()
|
198 |
+
|
199 |
+
def _render_dashboard_tab(self):
|
200 |
+
"""عرض تبويب لوحة المعلومات"""
|
201 |
+
|
202 |
+
st.markdown("### لوحة المعلومات")
|
203 |
+
|
204 |
+
# عرض مؤشرات الأداء الرئيسية
|
205 |
+
st.markdown("#### مؤشرات الأداء الرئيسية")
|
206 |
+
|
207 |
+
# استخراج البيانات اللازمة للمؤشرات
|
208 |
+
tenders_df = st.session_state.sample_data["tenders"]
|
209 |
+
|
210 |
+
# حساب المؤشرات
|
211 |
+
total_tenders = len(tenders_df)
|
212 |
+
won_tenders = len(tenders_df[tenders_df["الحالة"] == "فائز"])
|
213 |
+
win_rate = won_tenders / total_tenders * 100
|
214 |
+
avg_profit_margin = tenders_df["هامش الربح (%)"].mean()
|
215 |
+
total_profit = tenders_df["الربح (ريال)"].sum()
|
216 |
+
|
217 |
+
# عرض المؤشرات
|
218 |
+
col1, col2, col3, col4 = st.columns(4)
|
219 |
+
|
220 |
+
with col1:
|
221 |
+
st.metric("إجمالي المناقصات", f"{total_tenders}")
|
222 |
+
|
223 |
+
with col2:
|
224 |
+
st.metric("معدل الفوز", f"{win_rate:.1f}%")
|
225 |
+
|
226 |
+
with col3:
|
227 |
+
st.metric("متوسط هامش الربح", f"{avg_profit_margin:.1f}%")
|
228 |
+
|
229 |
+
with col4:
|
230 |
+
st.metric("إجمالي الربح", f"{total_profit:,.0f} ريال")
|
231 |
+
|
232 |
+
# عرض توزيع المناقصات حسب الحالة
|
233 |
+
st.markdown("#### توزيع المناقصات حسب الحالة")
|
234 |
+
|
235 |
+
status_counts = tenders_df["الحالة"].value_counts().reset_index()
|
236 |
+
status_counts.columns = ["الحالة", "العدد"]
|
237 |
+
|
238 |
+
fig = px.pie(
|
239 |
+
status_counts,
|
240 |
+
values="العدد",
|
241 |
+
names="الحالة",
|
242 |
+
title="توزيع المناقصات حسب الحالة",
|
243 |
+
color="الحالة",
|
244 |
+
color_discrete_map={
|
245 |
+
"فائز": "#2ecc71",
|
246 |
+
"خاسر": "#e74c3c",
|
247 |
+
"قيد التنفيذ": "#3498db",
|
248 |
+
"منجز": "#f39c12"
|
249 |
+
}
|
250 |
+
)
|
251 |
+
|
252 |
+
st.plotly_chart(fig, use_container_width=True)
|
253 |
+
|
254 |
+
# عرض توزيع المناقصات حسب نوع المشروع
|
255 |
+
st.markdown("#### توزيع المناقصات حسب نوع المشروع")
|
256 |
+
|
257 |
+
type_counts = tenders_df["نوع المشروع"].value_counts().reset_index()
|
258 |
+
type_counts.columns = ["نوع المشروع", "العدد"]
|
259 |
+
|
260 |
+
fig = px.bar(
|
261 |
+
type_counts,
|
262 |
+
x="نوع المشروع",
|
263 |
+
y="العدد",
|
264 |
+
title="توزيع المناقصات حسب نوع المشروع",
|
265 |
+
color="نوع المشروع",
|
266 |
+
text_auto=True
|
267 |
+
)
|
268 |
+
|
269 |
+
st.plotly_chart(fig, use_container_width=True)
|
270 |
+
|
271 |
+
# عرض تطور هامش الربح عبر الزمن
|
272 |
+
st.markdown("#### تطور هامش الربح عبر الزمن")
|
273 |
+
|
274 |
+
# إضافة عمود السنة
|
275 |
+
tenders_df["السنة"] = tenders_df["تاريخ التقديم"].str[:4]
|
276 |
+
|
277 |
+
# حساب متوسط هامش الربح لكل سنة
|
278 |
+
profit_margin_by_year = tenders_df.groupby("السنة")["هامش الربح (%)"].mean().reset_index()
|
279 |
+
|
280 |
+
fig = px.line(
|
281 |
+
profit_margin_by_year,
|
282 |
+
x="السنة",
|
283 |
+
y="هامش الربح (%)",
|
284 |
+
title="تطور متوسط هامش الربح عبر السنوات",
|
285 |
+
markers=True
|
286 |
+
)
|
287 |
+
|
288 |
+
st.plotly_chart(fig, use_container_width=True)
|
289 |
+
|
290 |
+
# عرض توزيع المناقصات حسب الموقع
|
291 |
+
st.markdown("#### توزيع المناقصات حسب الموقع")
|
292 |
+
|
293 |
+
location_counts = tenders_df["الموقع"].value_counts().reset_index()
|
294 |
+
location_counts.columns = ["الموقع", "العدد"]
|
295 |
+
|
296 |
+
fig = px.bar(
|
297 |
+
location_counts,
|
298 |
+
x="الموقع",
|
299 |
+
y="العدد",
|
300 |
+
title="توزيع المناقصات حسب الموقع",
|
301 |
+
color="الموقع",
|
302 |
+
text_auto=True
|
303 |
+
)
|
304 |
+
|
305 |
+
st.plotly_chart(fig, use_container_width=True)
|
306 |
+
|
307 |
+
# عرض العلاقة بين الميزانية والتكلفة
|
308 |
+
st.markdown("#### العلاقة بين الميزانية والتكلفة")
|
309 |
+
|
310 |
+
fig = px.scatter(
|
311 |
+
tenders_df,
|
312 |
+
x="الميزانية (ريال)",
|
313 |
+
y="التكلفة (ريال)",
|
314 |
+
color="الحالة",
|
315 |
+
size="المساحة (م2)",
|
316 |
+
hover_name="رقم المناقصة",
|
317 |
+
hover_data=["نوع المشروع", "الموقع", "هامش الربح (%)"],
|
318 |
+
title="العلاقة بين الميزانية والتكلفة",
|
319 |
+
color_discrete_map={
|
320 |
+
"فائز": "#2ecc71",
|
321 |
+
"خاسر": "#e74c3c",
|
322 |
+
"قيد التنفيذ": "#3498db",
|
323 |
+
"منجز": "#f39c12"
|
324 |
+
}
|
325 |
+
)
|
326 |
+
|
327 |
+
# إضافة خط الميزانية = التكلفة
|
328 |
+
max_value = max(tenders_df["الميزانية (ريال)"].max(), tenders_df["التكلفة (ريال)"].max())
|
329 |
+
fig.add_trace(
|
330 |
+
go.Scatter(
|
331 |
+
x=[0, max_value],
|
332 |
+
y=[0, max_value],
|
333 |
+
mode="lines",
|
334 |
+
line=dict(color="gray", dash="dash"),
|
335 |
+
name="الميزانية = التكلفة"
|
336 |
+
)
|
337 |
+
)
|
338 |
+
|
339 |
+
st.plotly_chart(fig, use_container_width=True)
|
340 |
+
|
341 |
+
def _render_tenders_analysis_tab(self):
|
342 |
+
"""عرض تبويب تحليل المناقصات"""
|
343 |
+
|
344 |
+
st.markdown("### تحليل المناقصات")
|
345 |
+
|
346 |
+
# استخراج البيانات
|
347 |
+
tenders_df = st.session_state.sample_data["tenders"]
|
348 |
+
|
349 |
+
# عرض خيارات التصفية
|
350 |
+
st.markdown("#### خيارات التصفية")
|
351 |
+
|
352 |
+
col1, col2, col3 = st.columns(3)
|
353 |
+
|
354 |
+
with col1:
|
355 |
+
selected_status = st.multiselect(
|
356 |
+
"الحالة",
|
357 |
+
options=tenders_df["الحالة"].unique(),
|
358 |
+
default=tenders_df["الحالة"].unique()
|
359 |
+
)
|
360 |
+
|
361 |
+
with col2:
|
362 |
+
selected_types = st.multiselect(
|
363 |
+
"نوع المشروع",
|
364 |
+
options=tenders_df["نوع المشروع"].unique(),
|
365 |
+
default=tenders_df["نوع المشروع"].unique()
|
366 |
+
)
|
367 |
+
|
368 |
+
with col3:
|
369 |
+
selected_locations = st.multiselect(
|
370 |
+
"الموقع",
|
371 |
+
options=tenders_df["الموقع"].unique(),
|
372 |
+
default=tenders_df["الموقع"].unique()
|
373 |
+
)
|
374 |
+
|
375 |
+
# تطبيق التصفية
|
376 |
+
filtered_df = tenders_df[
|
377 |
+
tenders_df["الحالة"].isin(selected_status) &
|
378 |
+
tenders_df["نوع المشروع"].isin(selected_types) &
|
379 |
+
tenders_df["الموقع"].isin(selected_locations)
|
380 |
+
]
|
381 |
+
|
382 |
+
# عرض البيانات المصفاة
|
383 |
+
st.markdown("#### البيانات المصفاة")
|
384 |
+
|
385 |
+
st.dataframe(filtered_df, use_container_width=True, hide_index=True)
|
386 |
+
|
387 |
+
# عرض إحصائيات البيانات المصفاة
|
388 |
+
st.markdown("#### إحصائيات البيانات المصفاة")
|
389 |
+
|
390 |
+
col1, col2, col3, col4 = st.columns(4)
|
391 |
+
|
392 |
+
with col1:
|
393 |
+
st.metric("عدد المناقصات", f"{len(filtered_df)}")
|
394 |
+
|
395 |
+
with col2:
|
396 |
+
won_count = len(filtered_df[filtered_df["الحالة"] == "فائز"])
|
397 |
+
win_rate = won_count / len(filtered_df) * 100 if len(filtered_df) > 0 else 0
|
398 |
+
st.metric("معدل الفوز", f"{win_rate:.1f}%")
|
399 |
+
|
400 |
+
with col3:
|
401 |
+
avg_profit_margin = filtered_df["هامش الربح (%)"].mean()
|
402 |
+
st.metric("متوسط هامش الربح", f"{avg_profit_margin:.1f}%")
|
403 |
+
|
404 |
+
with col4:
|
405 |
+
total_profit = filtered_df["الربح (ريال)"].sum()
|
406 |
+
st.metric("إجمالي الربح", f"{total_profit:,.0f} ريال")
|
407 |
+
|
408 |
+
# عرض تحليل هامش الربح حسب نوع المشروع
|
409 |
+
st.markdown("#### تحليل هامش الربح حسب نوع المشروع")
|
410 |
+
|
411 |
+
profit_margin_by_type = filtered_df.groupby("نوع المشروع")["هامش الربح (%)"].mean().reset_index()
|
412 |
+
|
413 |
+
fig = px.bar(
|
414 |
+
profit_margin_by_type,
|
415 |
+
x="نوع المشروع",
|
416 |
+
y="هامش الربح (%)",
|
417 |
+
title="متوسط هامش الربح حسب نوع المشروع",
|
418 |
+
color="نوع المشروع",
|
419 |
+
text_auto=".1f"
|
420 |
+
)
|
421 |
+
|
422 |
+
st.plotly_chart(fig, use_container_width=True)
|
423 |
+
|
424 |
+
# عرض تحليل هامش الربح حسب الموقع
|
425 |
+
st.markdown("#### تحليل هامش الربح حسب الموقع")
|
426 |
+
|
427 |
+
profit_margin_by_location = filtered_df.groupby("الموقع")["هامش الربح (%)"].mean().reset_index()
|
428 |
+
|
429 |
+
fig = px.bar(
|
430 |
+
profit_margin_by_location,
|
431 |
+
x="الموقع",
|
432 |
+
y="هامش الربح (%)",
|
433 |
+
title="متوسط هامش الربح حسب الموقع",
|
434 |
+
color="الموقع",
|
435 |
+
text_auto=".1f"
|
436 |
+
)
|
437 |
+
|
438 |
+
st.plotly_chart(fig, use_container_width=True)
|
439 |
+
|
440 |
+
# عرض تحليل معدل الفوز حسب نوع المشروع
|
441 |
+
st.markdown("#### تحليل معدل الفوز حسب نوع المشروع")
|
442 |
+
|
443 |
+
# حساب معدل الفوز لكل نوع مشروع
|
444 |
+
win_rate_by_type = []
|
445 |
+
|
446 |
+
for project_type in filtered_df["نوع المشروع"].unique():
|
447 |
+
type_df = filtered_df[filtered_df["نوع المشروع"] == project_type]
|
448 |
+
won_count = len(type_df[type_df["الحالة"] == "فائز"])
|
449 |
+
total_count = len(type_df)
|
450 |
+
win_rate = won_count / total_count * 100 if total_count > 0 else 0
|
451 |
+
win_rate_by_type.append({
|
452 |
+
"نوع المشروع": project_type,
|
453 |
+
"معدل الفوز (%)": win_rate,
|
454 |
+
"عدد المناقصات": total_count
|
455 |
+
})
|
456 |
+
|
457 |
+
win_rate_by_type_df = pd.DataFrame(win_rate_by_type)
|
458 |
+
|
459 |
+
fig = px.bar(
|
460 |
+
win_rate_by_type_df,
|
461 |
+
x="نوع المشروع",
|
462 |
+
y="معدل الفوز (%)",
|
463 |
+
title="معدل الفوز حسب نوع المشروع",
|
464 |
+
color="نوع المشروع",
|
465 |
+
text_auto=".1f",
|
466 |
+
hover_data=["عدد المناقصات"]
|
467 |
+
)
|
468 |
+
|
469 |
+
st.plotly_chart(fig, use_container_width=True)
|
470 |
+
|
471 |
+
# عرض تحليل العلاقة بين حجم المشروع وهامش الربح
|
472 |
+
st.markdown("#### العلاقة بين حجم المشروع وهامش الربح")
|
473 |
+
|
474 |
+
fig = px.scatter(
|
475 |
+
filtered_df,
|
476 |
+
x="الميزانية (ريال)",
|
477 |
+
y="هامش الربح (%)",
|
478 |
+
color="الحالة",
|
479 |
+
size="المساحة (م2)",
|
480 |
+
hover_name="رقم المناقصة",
|
481 |
+
hover_data=["نوع المشروع", "الموقع", "المدة (شهر)"],
|
482 |
+
title="العلاقة بين حجم المشروع وهامش الربح",
|
483 |
+
color_discrete_map={
|
484 |
+
"فائز": "#2ecc71",
|
485 |
+
"خاسر": "#e74c3c",
|
486 |
+
"قيد التنفيذ": "#3498db",
|
487 |
+
"منجز": "#f39c12"
|
488 |
+
}
|
489 |
+
)
|
490 |
+
|
491 |
+
# إضافة خط الاتجاه
|
492 |
+
fig.update_layout(
|
493 |
+
shapes=[
|
494 |
+
dict(
|
495 |
+
type="line",
|
496 |
+
xref="x",
|
497 |
+
yref="y",
|
498 |
+
x0=filtered_df["الميزانية (ريال)"].min(),
|
499 |
+
y0=filtered_df["هامش الربح (%)"].mean(),
|
500 |
+
x1=filtered_df["الميزانية (ريال)"].max(),
|
501 |
+
y1=filtered_df["هامش الربح (%)"].mean(),
|
502 |
+
line=dict(color="gray", dash="dash")
|
503 |
+
)
|
504 |
+
]
|
505 |
+
)
|
506 |
+
|
507 |
+
st.plotly_chart(fig, use_container_width=True)
|
508 |
+
|
509 |
+
# عرض تحليل العلاقة بين مدة المشروع وهامش الربح
|
510 |
+
st.markdown("#### العلاقة بين مدة المشروع وهامش الربح")
|
511 |
+
|
512 |
+
fig = px.scatter(
|
513 |
+
filtered_df,
|
514 |
+
x="المدة (شهر)",
|
515 |
+
y="هامش الربح (%)",
|
516 |
+
color="الحالة",
|
517 |
+
size="الميزانية (ريال)",
|
518 |
+
hover_name="رقم المناقصة",
|
519 |
+
hover_data=["نوع المشروع", "الموقع", "المساحة (م2)"],
|
520 |
+
title="العلاقة بين مدة المشروع وهامش الربح",
|
521 |
+
color_discrete_map={
|
522 |
+
"فائز": "#2ecc71",
|
523 |
+
"خاسر": "#e74c3c",
|
524 |
+
"قيد التنفيذ": "#3498db",
|
525 |
+
"منجز": "#f39c12"
|
526 |
+
}
|
527 |
+
)
|
528 |
+
|
529 |
+
st.plotly_chart(fig, use_container_width=True)
|
530 |
+
|
531 |
+
def _render_price_analysis_tab(self):
|
532 |
+
"""عرض تبويب تحليل الأسعار"""
|
533 |
+
|
534 |
+
st.markdown("### تحليل الأسعار")
|
535 |
+
|
536 |
+
# استخراج البيانات
|
537 |
+
materials_df = st.session_state.sample_data["materials"]
|
538 |
+
|
539 |
+
# عرض بيانات أسعار المواد
|
540 |
+
st.markdown("#### بيانات أسعار المواد")
|
541 |
+
|
542 |
+
st.dataframe(materials_df, use_container_width=True, hide_index=True)
|
543 |
+
|
544 |
+
# عرض تطور أسعار المواد عبر السنوات
|
545 |
+
st.markdown("#### تطور أسعار المواد عبر السنوات")
|
546 |
+
|
547 |
+
# اختيار المواد للعرض
|
548 |
+
selected_materials = st.multiselect(
|
549 |
+
"اختر المواد للعرض",
|
550 |
+
options=materials_df["اسم المادة"].unique(),
|
551 |
+
default=materials_df["اسم المادة"].unique()[:5]
|
552 |
+
)
|
553 |
+
|
554 |
+
if selected_materials:
|
555 |
+
# تحضير البيانات للرسم البياني
|
556 |
+
filtered_materials = materials_df[materials_df["اسم المادة"].isin(selected_materials)]
|
557 |
+
|
558 |
+
# تحويل البيانات من العرض العريض إلى العرض الطويل
|
559 |
+
melted_df = pd.melt(
|
560 |
+
filtered_materials,
|
561 |
+
id_vars=["رمز المادة", "اسم المادة", "الوحدة"],
|
562 |
+
value_vars=["سعر 2021 (ريال)", "سعر 2022 (ريال)", "سعر 2023 (ريال)", "سعر 2024 (ريال)"],
|
563 |
+
var_name="السنة",
|
564 |
+
value_name="السعر (ريال)"
|
565 |
+
)
|
566 |
+
|
567 |
+
# استخراج السنة من اسم العمود
|
568 |
+
melted_df["السنة"] = melted_df["السنة"].str.extract(r"سعر (\d{4})")
|
569 |
+
|
570 |
+
# رسم بياني لتطور الأسعار
|
571 |
+
fig = px.line(
|
572 |
+
melted_df,
|
573 |
+
x="السنة",
|
574 |
+
y="السعر (ريال)",
|
575 |
+
color="اسم المادة",
|
576 |
+
title="تطور أسعار المواد عبر السنوات",
|
577 |
+
markers=True,
|
578 |
+
hover_data=["الوحدة"]
|
579 |
+
)
|
580 |
+
|
581 |
+
st.plotly_chart(fig, use_container_width=True)
|
582 |
+
|
583 |
+
# عرض نسبة التغير في أسعار المواد
|
584 |
+
st.markdown("#### نسبة التغير في أسعار المواد (2021-2024)")
|
585 |
+
|
586 |
+
# ترتيب المواد حسب نسبة التغير
|
587 |
+
sorted_materials = materials_df.sort_values("نسبة التغير 2021-2024 (%)", ascending=False)
|
588 |
+
|
589 |
+
fig = px.bar(
|
590 |
+
sorted_materials,
|
591 |
+
x="اسم المادة",
|
592 |
+
y="نسبة التغير 2021-2024 (%)",
|
593 |
+
title="نسبة التغير في أسعار المواد (2021-2024)",
|
594 |
+
color="نسبة التغير 2021-2024 (%)",
|
595 |
+
color_continuous_scale="RdYlGn_r",
|
596 |
+
text_auto=".1f"
|
597 |
+
)
|
598 |
+
|
599 |
+
st.plotly_chart(fig, use_container_width=True)
|
600 |
+
|
601 |
+
# عرض توزيع أسعار المواد حسب الفئة
|
602 |
+
st.markdown("#### توزيع أسعار المواد حسب الفئة")
|
603 |
+
|
604 |
+
# تصنيف المواد إلى فئات
|
605 |
+
materials_df["فئة المادة"] = materials_df["اسم المادة"].apply(
|
606 |
+
lambda x: "مواد إنشائية" if x in ["خرسانة جاهزة", "حديد تسليح", "طابوق", "أسمنت", "رمل", "بحص", "خشب"]
|
607 |
+
else "مواد تشطيب" if x in ["ألمنيوم", "زجاج", "دهان", "سيراميك", "رخام", "جبس", "عازل مائي", "عازل حراري"]
|
608 |
+
else "مواد كهربائية" if x in ["أنابيب PVC", "أسلاك كهربائية", "مفاتيح كهربائية", "إنارة"]
|
609 |
+
else "مواد ميكانيكية" if x in ["تكييف", "مصاعد"]
|
610 |
+
else "أبواب ونوافذ" if x in ["أبواب خشبية", "أبواب حديدية", "نوافذ ألمنيوم", "نوافذ زجاجية"]
|
611 |
+
else "أرضيات" if x in ["أرضيات خشبية", "أرضيات بلاط", "أرضيات رخام", "أرضيات سيراميك", "أرضيات بورسلين"]
|
612 |
+
else "أخرى"
|
613 |
+
)
|
614 |
+
|
615 |
+
# حساب متوسط نسبة التغير لكل فئة
|
616 |
+
category_change = materials_df.groupby("فئة المادة")["نسبة التغير 2021-2024 (%)"].mean().reset_index()
|
617 |
+
|
618 |
+
fig = px.bar(
|
619 |
+
category_change,
|
620 |
+
x="فئة المادة",
|
621 |
+
y="نسبة التغير 2021-2024 (%)",
|
622 |
+
title="متوسط نسبة التغير في أسعار المواد حسب الفئة (2021-2024)",
|
623 |
+
color="فئة المادة",
|
624 |
+
text_auto=".1f"
|
625 |
+
)
|
626 |
+
|
627 |
+
st.plotly_chart(fig, use_container_width=True)
|
628 |
+
|
629 |
+
# عرض تحليل تأثير تغير الأسعار على تكاليف المشاريع
|
630 |
+
st.markdown("#### تحليل تأثير تغير الأسعار على تكاليف المشاريع")
|
631 |
+
|
632 |
+
# إنشاء بيانات افتراضية لتوزيع التكاليف
|
633 |
+
cost_distribution = {
|
634 |
+
"الفئة": [
|
635 |
+
"مواد إنشائية",
|
636 |
+
"مواد تشطيب",
|
637 |
+
"مواد كهربائية",
|
638 |
+
"مواد ميكانيكية",
|
639 |
+
"أبواب ونوافذ",
|
640 |
+
"أرضيات",
|
641 |
+
"عمالة",
|
642 |
+
"معدات",
|
643 |
+
"نفقات عامة"
|
644 |
+
],
|
645 |
+
"النسبة من التكلفة (%)": [30, 20, 10, 15, 5, 5, 10, 3, 2]
|
646 |
+
}
|
647 |
+
|
648 |
+
cost_distribution_df = pd.DataFrame(cost_distribution)
|
649 |
+
|
650 |
+
# حساب تأثير تغير الأسعار على التكاليف
|
651 |
+
impact_data = []
|
652 |
+
|
653 |
+
for index, row in cost_distribution_df.iterrows():
|
654 |
+
category = row["الفئة"]
|
655 |
+
cost_percentage = row["النسبة من التكلفة (%)"]
|
656 |
+
|
657 |
+
if category in category_change["فئة المادة"].values:
|
658 |
+
price_change = category_change[category_change["فئة المادة"] == category]["نسبة التغير 2021-2024 (%)"].values[0]
|
659 |
+
else:
|
660 |
+
# افتراض نسبة تغير للف��ات غير المدرجة
|
661 |
+
price_change = 10 if category == "عمالة" else 5
|
662 |
+
|
663 |
+
impact = cost_percentage * price_change / 100
|
664 |
+
|
665 |
+
impact_data.append({
|
666 |
+
"الفئة": category,
|
667 |
+
"النسبة من التكلفة (%)": cost_percentage,
|
668 |
+
"نسبة التغير في الأسعار (%)": price_change,
|
669 |
+
"التأثير على التكلفة الإجمالية (%)": impact
|
670 |
+
})
|
671 |
+
|
672 |
+
impact_df = pd.DataFrame(impact_data)
|
673 |
+
|
674 |
+
# حساب إجمالي التأثير على التكلفة
|
675 |
+
total_impact = impact_df["التأثير على التكلفة الإجمالية (%)"].sum()
|
676 |
+
|
677 |
+
st.metric("إجمالي التأثير على التكلفة", f"{total_impact:.1f}%")
|
678 |
+
|
679 |
+
# عرض جدول التأثير
|
680 |
+
st.dataframe(impact_df, use_container_width=True, hide_index=True)
|
681 |
+
|
682 |
+
# رسم بياني للتأثير على التكلفة
|
683 |
+
fig = px.bar(
|
684 |
+
impact_df,
|
685 |
+
x="الفئة",
|
686 |
+
y="التأثير على التكلفة الإجمالية (%)",
|
687 |
+
title="تأثير تغير الأسعار على التكلفة الإجمالية للمشاريع",
|
688 |
+
color="الفئة",
|
689 |
+
text_auto=".1f"
|
690 |
+
)
|
691 |
+
|
692 |
+
st.plotly_chart(fig, use_container_width=True)
|
693 |
+
|
694 |
+
# عرض توصيات لإدارة تغير الأسعار
|
695 |
+
st.markdown("#### توصيات لإدارة تغير الأسعار")
|
696 |
+
|
697 |
+
st.markdown("""
|
698 |
+
1. **التعاقد المسبق مع الموردين:** التعاقد المسبق مع الموردين لتثبيت الأسعار لفترة زمنية محددة.
|
699 |
+
2. **تنويع مصادر التوريد:** تنويع مصادر التوريد لتقليل مخاطر ارتفاع الأسعار من مصدر واحد.
|
700 |
+
3. **شراء المواد مقدماً:** شراء المواد الرئيسية مقدماً للمشاريع المستقبلية عندما تكون الأسعار منخفضة.
|
701 |
+
4. **استخدام مواد بديلة:** استخدام مواد بديلة ذات جودة مماثلة وأسعار أقل.
|
702 |
+
5. **تضمين بند تعديل الأسعار في العقود:** تضمين بند تعديل الأسعار في العقود لتغطية التغيرات الكبيرة في أسعار المواد.
|
703 |
+
6. **تحسين كفاءة استخدام المواد:** تحسين كفاءة استخدام المواد لتقليل الهدر وتقليل التكاليف.
|
704 |
+
7. **مراقبة اتجاهات الأسعار:** مراقبة اتجاهات الأسعار بشكل مستمر واتخاذ القرارات بناءً على التوقعات المستقبلية.
|
705 |
+
""")
|
706 |
+
|
707 |
+
def _render_competitors_analysis_tab(self):
|
708 |
+
"""عرض تبويب تحليل المنافسين"""
|
709 |
+
|
710 |
+
st.markdown("### تحليل المنافسين")
|
711 |
+
|
712 |
+
# استخراج البيانات
|
713 |
+
competitors_df = st.session_state.sample_data["competitors"]
|
714 |
+
|
715 |
+
# عرض بيانات المنافسين
|
716 |
+
st.markdown("#### بيانات المنافسين")
|
717 |
+
|
718 |
+
st.dataframe(competitors_df, use_container_width=True, hide_index=True)
|
719 |
+
|
720 |
+
# عرض حصص السوق للمنافسين
|
721 |
+
st.markdown("#### حصص السوق للمنافسين")
|
722 |
+
|
723 |
+
# ترتيب المنافسين حسب حصة السوق
|
724 |
+
sorted_competitors = competitors_df.sort_values("حصة السوق (%)", ascending=False)
|
725 |
+
|
726 |
+
fig = px.pie(
|
727 |
+
sorted_competitors,
|
728 |
+
values="حصة السوق (%)",
|
729 |
+
names="اسم المنافس",
|
730 |
+
title="حصص السوق للمنافسين",
|
731 |
+
hover_data=["التخصص", "الحجم"]
|
732 |
+
)
|
733 |
+
|
734 |
+
st.plotly_chart(fig, use_container_width=True)
|
735 |
+
|
736 |
+
# عرض معدلات الفوز للمنافسين
|
737 |
+
st.markdown("#### معدلات الفوز للمنافسين")
|
738 |
+
|
739 |
+
# ترتيب المنافسين حسب معدل الفوز
|
740 |
+
sorted_by_win_rate = competitors_df.sort_values("معدل الفوز (%)", ascending=False)
|
741 |
+
|
742 |
+
fig = px.bar(
|
743 |
+
sorted_by_win_rate,
|
744 |
+
x="اسم المنافس",
|
745 |
+
y="معدل الفوز (%)",
|
746 |
+
title="معدلات الفوز للمنافسين",
|
747 |
+
color="معدل الفوز (%)",
|
748 |
+
text_auto=".1f",
|
749 |
+
hover_data=["التخصص", "الحجم", "حصة السوق (%)"]
|
750 |
+
)
|
751 |
+
|
752 |
+
st.plotly_chart(fig, use_container_width=True)
|
753 |
+
|
754 |
+
# عرض متوسط هوامش الربح للمنافسين
|
755 |
+
st.markdown("#### متوسط هوامش الربح للمنافسين")
|
756 |
+
|
757 |
+
# ترتيب المنافسين حسب متو��ط هامش الربح
|
758 |
+
sorted_by_margin = competitors_df.sort_values("متوسط هامش الربح (%)", ascending=False)
|
759 |
+
|
760 |
+
fig = px.bar(
|
761 |
+
sorted_by_margin,
|
762 |
+
x="اسم المنافس",
|
763 |
+
y="متوسط هامش الربح (%)",
|
764 |
+
title="متوسط هوامش الربح للمنافسين",
|
765 |
+
color="متوسط هامش الربح (%)",
|
766 |
+
text_auto=".1f",
|
767 |
+
hover_data=["التخصص", "الحجم", "حصة السوق (%)"]
|
768 |
+
)
|
769 |
+
|
770 |
+
st.plotly_chart(fig, use_container_width=True)
|
771 |
+
|
772 |
+
# عرض تحليل المنافسين حسب التخصص
|
773 |
+
st.markdown("#### تحليل المنافسين حسب التخصص")
|
774 |
+
|
775 |
+
# حساب متوسط معدل الفوز وهامش الربح لكل تخصص
|
776 |
+
specialty_analysis = competitors_df.groupby("التخصص").agg({
|
777 |
+
"معدل الفوز (%)": "mean",
|
778 |
+
"متوسط هامش الربح (%)": "mean",
|
779 |
+
"حصة السوق (%)": "sum"
|
780 |
+
}).reset_index()
|
781 |
+
|
782 |
+
# عرض تحليل التخصصات
|
783 |
+
st.dataframe(specialty_analysis, use_container_width=True, hide_index=True)
|
784 |
+
|
785 |
+
# رسم بياني للعلاقة بين معدل الفوز وهامش الربح حسب التخصص
|
786 |
+
fig = px.scatter(
|
787 |
+
specialty_analysis,
|
788 |
+
x="معدل الفوز (%)",
|
789 |
+
y="متوسط هامش الربح (%)",
|
790 |
+
size="حصة السوق (%)",
|
791 |
+
color="التخصص",
|
792 |
+
hover_name="التخصص",
|
793 |
+
title="العلاقة بين معدل الفوز وهامش الربح حسب التخصص",
|
794 |
+
text="التخصص"
|
795 |
+
)
|
796 |
+
|
797 |
+
st.plotly_chart(fig, use_container_width=True)
|
798 |
+
|
799 |
+
# عرض تحليل المنافسين حسب الحجم
|
800 |
+
st.markdown("#### تحليل المنافسين حسب الحجم")
|
801 |
+
|
802 |
+
# حساب متوسط معدل الفوز وهامش الربح لكل حجم
|
803 |
+
size_analysis = competitors_df.groupby("الحجم").agg({
|
804 |
+
"معدل الفوز (%)": "mean",
|
805 |
+
"متوسط هامش الربح (%)": "mean",
|
806 |
+
"حصة السوق (%)": "sum"
|
807 |
+
}).reset_index()
|
808 |
+
|
809 |
+
# عرض تحليل الأحجام
|
810 |
+
st.dataframe(size_analysis, use_container_width=True, hide_index=True)
|
811 |
+
|
812 |
+
# رسم بياني للعلاقة بين معدل الفوز وهامش الربح حسب الحجم
|
813 |
+
fig = px.scatter(
|
814 |
+
size_analysis,
|
815 |
+
x="معدل الفوز (%)",
|
816 |
+
y="متوسط هامش الربح (%)",
|
817 |
+
size="حصة السوق (%)",
|
818 |
+
color="الحجم",
|
819 |
+
hover_name="الحجم",
|
820 |
+
title="العلاقة بين معدل الفوز وهامش الربح حسب الحجم",
|
821 |
+
text="الحجم"
|
822 |
+
)
|
823 |
+
|
824 |
+
st.plotly_chart(fig, use_container_width=True)
|
825 |
+
|
826 |
+
# عرض تحليل نقاط القوة والضعف للمنافسين
|
827 |
+
st.markdown("#### تحليل نقاط القوة والضعف للمنافسين الرئيسيين")
|
828 |
+
|
829 |
+
# اختيار المنافسين للتحليل
|
830 |
+
top_competitors = competitors_df.sort_values("حصة السوق (%)", ascending=False).head(3)
|
831 |
+
|
832 |
+
for index, competitor in top_competitors.iterrows():
|
833 |
+
with st.expander(f"{competitor['اسم المنافس']} - حصة السوق: {competitor['حصة السوق (%)]:.1f}%"):
|
834 |
+
st.markdown(f"**التخصص:** {competitor['التخصص']}")
|
835 |
+
st.markdown(f"**الحجم:** {competitor['الحجم']}")
|
836 |
+
st.markdown(f"**معدل الفوز:** {competitor['معدل الفوز (%)]:.1f}%")
|
837 |
+
st.markdown(f"**متوسط هامش الربح:** {competitor['متوسط هامش الربح (%)]:.1f}%")
|
838 |
+
|
839 |
+
st.markdown("**نقاط القوة:**")
|
840 |
+
if competitor["الحجم"] == "كبيرة":
|
841 |
+
st.markdown("- قدرة مالية كبيرة")
|
842 |
+
st.markdown("- خبرة واسعة في المشاريع الكبيرة")
|
843 |
+
st.markdown("- سمعة قوية في السوق")
|
844 |
+
st.markdown("- شبكة علاقات واسعة")
|
845 |
+
elif competitor["الحجم"] == "متوسطة":
|
846 |
+
st.markdown("- مرونة في التعامل مع المشاريع")
|
847 |
+
st.markdown("- تكاليف تشغيلية أقل")
|
848 |
+
st.markdown("- تخصص في مجالات محددة")
|
849 |
+
st.markdown("- سرعة في اتخاذ القرارات")
|
850 |
+
else:
|
851 |
+
st.markdown("- مرونة عالية")
|
852 |
+
st.markdown("- تكاليف تشغيلية منخفضة")
|
853 |
+
st.markdown("- خدمة عملاء متميزة")
|
854 |
+
st.markdown("- تخصص دقيق في مجال محدد")
|
855 |
+
|
856 |
+
st.markdown("**نقاط الضعف:**")
|
857 |
+
if competitor["الحجم"] == "كبيرة":
|
858 |
+
st.markdown("- بطء في اتخاذ القرارات")
|
859 |
+
st.markdown("- تكاليف تشغيلية عالية")
|
860 |
+
st.markdown("- أقل مرونة في التعامل مع التغييرات")
|
861 |
+
st.markdown("- تركيز على المشاريع الكبيرة فقط")
|
862 |
+
elif competitor["الحجم"] == "متوسطة":
|
863 |
+
st.markdown("- قدرة مالية محدودة مقارنة بالشركات الكبيرة")
|
864 |
+
st.markdown("- صعوبة في المنافسة على المشاريع الكبيرة")
|
865 |
+
st.markdown("- محدودية الموارد البشرية")
|
866 |
+
st.markdown("- صعوبة في الحصول على تمويل")
|
867 |
+
else:
|
868 |
+
st.markdown("- قدرة مالية محدودة جداً")
|
869 |
+
st.markdown("- صعوبة في المنافسة على المشاريع المتوسطة والكبيرة")
|
870 |
+
st.markdown("- محدودية الموارد البشرية والفنية")
|
871 |
+
st.markdown("- صعوبة في الحصول على تمويل")
|
872 |
+
|
873 |
+
# عرض توصيات للتعامل مع المنافسين
|
874 |
+
st.markdown("#### توصيات للتعامل مع المنافسين")
|
875 |
+
|
876 |
+
st.markdown("""
|
877 |
+
1. **التركيز على نقاط القوة:** التركيز على نقاط القوة الخاصة بالشركة والتي تميزها عن المنافسين.
|
878 |
+
2. **استهداف شرائح سوقية محددة:** استهداف شرائح سوقية محددة والتركيز على تلبية احتياجاتها بشكل أفضل من المنافسين.
|
879 |
+
3. **تطوير علاقات قوية مع العملاء:** تطوير علاقات قوية مع العملاء لضمان ولائهم وتكرار التعامل معهم.
|
880 |
+
4. **الابتكار في الخدمات والحلول:** تقديم حلول مبتكرة وخدمات متميزة تلبي احتياجات العملاء بشكل أفضل من المنافسين.
|
881 |
+
5. **تحسين الكفاءة التشغيلية:** تحسين الكفاءة التشغيلية لتقليل التكاليف وزيادة القدرة التنافسية.
|
882 |
+
6. **بناء تحالفات استراتيجية:** بناء تحالفات استراتيجية مع شركات أخرى لتعزيز القدرة التنافسية.
|
883 |
+
7. **مراقبة المنافسين باستمرار:** مراقبة المنافسين باستمرار وتحليل استراتيجياتهم وتحركاتهم في السوق.
|
884 |
+
""")
|
885 |
+
|
886 |
+
def _render_import_export_tab(self):
|
887 |
+
"""عرض تبويب استيراد وتصدير البيانات"""
|
888 |
+
|
889 |
+
st.markdown("### استيراد وتصدير البيانات")
|
890 |
+
|
891 |
+
# عرض مصادر البيانات الحالية
|
892 |
+
st.markdown("#### مصادر البيانات الحالية")
|
893 |
+
|
894 |
+
# تحويل قائمة مصادر البيانات إلى DataFrame
|
895 |
+
sources_df = pd.DataFrame(st.session_state.data_sources)
|
896 |
+
|
897 |
+
# عرض مصادر البيانات كجدول
|
898 |
+
st.dataframe(
|
899 |
+
sources_df,
|
900 |
+
column_config={
|
901 |
+
"id": st.column_config.NumberColumn("الرقم"),
|
902 |
+
"name": st.column_config.TextColumn("اسم المصدر"),
|
903 |
+
"type": st.column_config.TextColumn("النوع"),
|
904 |
+
"rows": st.column_config.NumberColumn("عدد الصفوف"),
|
905 |
+
"columns": st.column_config.NumberColumn("عدد الأعمدة"),
|
906 |
+
"last_updated": st.column_config.DateColumn("تاريخ التحديث"),
|
907 |
+
"description": st.column_config.TextColumn("الوصف")
|
908 |
+
},
|
909 |
+
use_container_width=True,
|
910 |
+
hide_index=True
|
911 |
+
)
|
912 |
+
|
913 |
+
# استيراد بيانات جديدة
|
914 |
+
st.markdown("#### استيراد بيانات جديدة")
|
915 |
+
|
916 |
+
col1, col2 = st.columns(2)
|
917 |
+
|
918 |
+
with col1:
|
919 |
+
data_type = st.selectbox(
|
920 |
+
"نوع البيانات",
|
921 |
+
["بيانات المناقصات", "بيانات المنافسين", "بيانات أسعار المواد", "بيانات الموردين", "بيانات المشاريع", "أخرى"]
|
922 |
+
)
|
923 |
+
|
924 |
+
with col2:
|
925 |
+
file_format = st.selectbox(
|
926 |
+
"صيغة الملف",
|
927 |
+
["CSV", "Excel", "JSON"]
|
928 |
+
)
|
929 |
+
|
930 |
+
uploaded_file = st.file_uploader(f"قم بتحميل ملف {file_format}", type=["csv", "xlsx", "json"])
|
931 |
+
|
932 |
+
if uploaded_file is not None:
|
933 |
+
if st.button("استيراد البيانات"):
|
934 |
+
# محاكاة استيراد البيانات
|
935 |
+
with st.spinner("جاري استيراد البيانات..."):
|
936 |
+
time.sleep(2) # محاكاة وقت المعالجة
|
937 |
+
|
938 |
+
# تحديث قائمة مصادر البيانات
|
939 |
+
new_id = max([source["id"] for source in st.session_state.data_sources]) + 1
|
940 |
+
|
941 |
+
st.session_state.data_sources.append({
|
942 |
+
"id": new_id,
|
943 |
+
"name": f"{data_type} - {uploaded_file.name}",
|
944 |
+
"type": file_format,
|
945 |
+
"rows": np.random.randint(50, 500),
|
946 |
+
"columns": np.random.randint(5, 20),
|
947 |
+
"last_updated": time.strftime("%Y-%m-%d"),
|
948 |
+
"description": f"بيانات تم استيرادها من ملف {uploaded_file.name}"
|
949 |
+
})
|
950 |
+
|
951 |
+
st.success("تم استيراد البيانات بنجاح!")
|
952 |
+
st.rerun()
|
953 |
+
|
954 |
+
# تصدير البيانات
|
955 |
+
st.markdown("#### تصدير البيانات")
|
956 |
+
|
957 |
+
col1, col2 = st.columns(2)
|
958 |
+
|
959 |
+
with col1:
|
960 |
+
export_data_type = st.selectbox(
|
961 |
+
"نوع البيانات للتصدير",
|
962 |
+
["بيانات المناقصات", "بيانات المنافسين", "بيانات أسعار المواد", "بيانات الموردين", "بيانات المشاريع", "تقرير تحليلي شامل"]
|
963 |
+
)
|
964 |
+
|
965 |
+
with col2:
|
966 |
+
export_format = st.selectbox(
|
967 |
+
"صيغة التصدير",
|
968 |
+
["CSV", "Excel", "JSON", "PDF"]
|
969 |
+
)
|
970 |
+
|
971 |
+
if st.button("تصدير البيانات"):
|
972 |
+
# محاكاة تصدير البيانات
|
973 |
+
with st.spinner("جاري تصدير البيانات..."):
|
974 |
+
time.sleep(2) # محاكاة وقت المعالجة
|
975 |
+
st.success(f"تم تصدير {export_data_type} بصيغة {export_format} بنجاح!")
|
976 |
+
|
977 |
+
# إنشاء رابط تنزيل وهمي
|
978 |
+
if export_data_type == "بيانات المناقصات":
|
979 |
+
df = st.session_state.sample_data["tenders"]
|
980 |
+
elif export_data_type == "بيانات المنافسين":
|
981 |
+
df = st.session_state.sample_data["competitors"]
|
982 |
+
elif export_data_type == "بيانات أسعار المواد":
|
983 |
+
df = st.session_state.sample_data["materials"]
|
984 |
+
else:
|
985 |
+
# إنشاء DataFrame وهمي للأنواع الأخرى
|
986 |
+
df = pd.DataFrame({
|
987 |
+
"البيان": ["بيان 1", "بيان 2", "بيان 3"],
|
988 |
+
"القيمة": [100, 200, 300]
|
989 |
+
})
|
990 |
+
|
991 |
+
# تحويل DataFrame إلى CSV
|
992 |
+
csv = df.to_csv(index=False)
|
993 |
+
b64 = base64.b64encode(csv.encode()).decode()
|
994 |
+
href = f'<a href="data:file/csv;base64,{b64}" download="{export_data_type}.csv">انقر هنا لتنزيل الملف</a>'
|
995 |
+
st.markdown(href, unsafe_allow_html=True)
|
996 |
+
|
997 |
+
# تكامل البيانات مع الوحدات الأخرى
|
998 |
+
st.markdown("#### تكامل البيانات مع الوحدات الأخرى")
|
999 |
+
|
1000 |
+
st.markdown("""
|
1001 |
+
يمكن تكامل البيانات مع الوحدات الأخرى في النظام من خلال:
|
1002 |
+
|
1003 |
+
1. **إرسال البيانات إلى وحدة التسعير:** إرسال بيانات أسعار المواد وبيانات المناقصات السابقة إلى وحدة التسعير لتحسين دقة التسعير.
|
1004 |
+
2. **إرسال البيانات إلى وحدة تحليل المخاطر:** إرسال بيانات المناقصات السابقة وبيانات المنافسين إلى وحدة تحليل المخاطر لتحسين تقييم المخاطر.
|
1005 |
+
3. **إرسال البيانات إلى وحدة الذكاء الاصطناعي:** إرسال البيانات إلى وحدة الذكاء الاصطناعي لتدريب النماذج وتحسين دقة التنبؤات.
|
1006 |
+
4. **إرسال البيانات إلى وحدة إدارة المشاريع:** إرسال بيانات المشاريع المنجزة إلى وحدة إدارة المشاريع لتحسين تخطيط وإدارة المشاريع المستقبلية.
|
1007 |
+
5. **إرسال البيانات إلى وحدة التقارير:** إرسال البيانات إلى وحدة التقارير لإنشاء تقارير تحليلية شاملة.
|
1008 |
+
""")
|
1009 |
+
|
1010 |
+
col1, col2, col3 = st.columns(3)
|
1011 |
+
|
1012 |
+
with col1:
|
1013 |
+
if st.button("إرسال إلى وحدة التسعير"):
|
1014 |
+
st.success("تم إرسال البيانات إلى وحدة التسعير بنجاح!")
|
1015 |
+
|
1016 |
+
with col2:
|
1017 |
+
if st.button("إرسال إلى وحدة تحليل المخاطر"):
|
1018 |
+
st.success("تم إرسال البيانات إلى وحدة تحليل المخاطر بنجاح!")
|
1019 |
+
|
1020 |
+
with col3:
|
1021 |
+
if st.button("إرسال إلى وحدة الذكاء الاصطناعي"):
|
1022 |
+
st.success("تم إرسال البيانات إلى وحدة الذكاء الاصطناعي بنجاح!")
|
modules/document_analysis/analyzer.py
ADDED
@@ -0,0 +1,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._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 |
+
logger.info(f"تحليل مستند PDF: {document_path}")
|
116 |
+
|
117 |
+
# في التطبيق الفعلي، سيتم استخدام مكتبة مثل PyPDF2 أو pdfplumber
|
118 |
+
# لاستخراج النص من ملف PDF وتحليله
|
119 |
+
|
120 |
+
# محاكاة استخراج البنود
|
121 |
+
self.analysis_results["items"] = [
|
122 |
+
{"id": 1, "name": "أعمال الحفر", "description": "حفر وإزالة التربة", "unit": "م³", "estimated_quantity": 1500},
|
123 |
+
{"id": 2, "name": "أعمال الخرسانة", "description": "صب خرسانة مسلحة", "unit": "م³", "estimated_quantity": 750},
|
124 |
+
{"id": 3, "name": "أعمال الأسفلت", "description": "تمهيد وفرش طبقة أسفلت", "unit": "م²", "estimated_quantity": 5000}
|
125 |
+
]
|
126 |
+
|
127 |
+
# محاكاة استخراج الكيانات
|
128 |
+
self.analysis_results["entities"] = [
|
129 |
+
{"type": "client", "name": "وزارة النقل", "mentions": 5},
|
130 |
+
{"type": "location", "name": "المنطقة الشرقية", "mentions": 3},
|
131 |
+
{"type": "contractor", "name": "شركة المقاولات المتحدة", "mentions": 2}
|
132 |
+
]
|
133 |
+
|
134 |
+
# محاكاة استخراج التواريخ
|
135 |
+
self.analysis_results["dates"] = [
|
136 |
+
{"type": "start_date", "date": "2025-05-01", "description": "تاريخ بدء المشروع"},
|
137 |
+
{"type": "end_date", "date": "2025-11-30", "description": "تاريخ انتهاء المشروع"},
|
138 |
+
{"type": "submission_date", "date": "2025-04-15", "description": "تاريخ تقديم العروض"}
|
139 |
+
]
|
140 |
+
|
141 |
+
# محاكاة استخراج المبالغ
|
142 |
+
self.analysis_results["amounts"] = [
|
143 |
+
{"type": "estimated_cost", "amount": 5000000, "currency": "SAR", "description": "التكلفة التقديرية للمشروع"},
|
144 |
+
{"type": "advance_payment", "amount": 500000, "currency": "SAR", "description": "الدفعة المقدمة (10%)"},
|
145 |
+
{"type": "performance_bond", "amount": 250000, "currency": "SAR", "description": "ضمان حسن التنفيذ (5%)"}
|
146 |
+
]
|
147 |
+
|
148 |
+
# محاكاة استخراج المخاطر
|
149 |
+
self.analysis_results["risks"] = [
|
150 |
+
{"type": "delay_risk", "description": "مخاطر التأخير في التنفيذ", "probability": "متوسط", "impact": "عالي"},
|
151 |
+
{"type": "cost_risk", "description": "مخاطر زيادة التكاليف", "probability": "عالي", "impact": "عالي"},
|
152 |
+
{"type": "quality_risk", "description": "مخاطر جودة التنفيذ", "probability": "منخفض", "impact": "متوسط"}
|
153 |
+
]
|
154 |
+
|
155 |
+
except Exception as e:
|
156 |
+
logger.error(f"خطأ في تحليل مستند PDF: {str(e)}")
|
157 |
+
raise
|
158 |
+
|
159 |
+
def _analyze_docx(self, document_path, document_type):
|
160 |
+
"""تحليل مستند Word"""
|
161 |
+
try:
|
162 |
+
# محاكاة تحليل مستند Word
|
163 |
+
logger.info(f"تحليل مستند Word: {document_path}")
|
164 |
+
|
165 |
+
# في التطبيق الفعلي، سيتم استخدام مكتبة مثل python-docx
|
166 |
+
# لاستخراج النص من ملف Word وتحليله
|
167 |
+
|
168 |
+
# محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
|
169 |
+
# (مشابه لتحليل PDF)
|
170 |
+
self.analysis_results["items"] = [
|
171 |
+
{"id": 1, "name": "توريد معدات", "description": "توريد معدات المشروع", "unit": "مجموعة", "estimated_quantity": 10},
|
172 |
+
{"id": 2, "name": "تركيب المعدات", "description": "تركيب وتشغيل المعدات", "unit": "مجموعة", "estimated_quantity": 10},
|
173 |
+
{"id": 3, "name": "التدريب", "description": "تدريب الموظفين على استخدام المعدات", "unit": "يوم", "estimated_quantity": 20}
|
174 |
+
]
|
175 |
+
|
176 |
+
# محاكاة استخراج الكيانات والتواريخ والمبالغ والمخاطر
|
177 |
+
# (مشابه لتحليل PDF)
|
178 |
+
|
179 |
+
except Exception as e:
|
180 |
+
logger.error(f"خطأ في تحليل مستند Word: {str(e)}")
|
181 |
+
raise
|
182 |
+
|
183 |
+
def _analyze_xlsx(self, document_path, document_type):
|
184 |
+
"""تحليل مستند Excel"""
|
185 |
+
try:
|
186 |
+
# محاكاة تحليل مستند Excel
|
187 |
+
logger.info(f"تحليل مستند Excel: {document_path}")
|
188 |
+
|
189 |
+
# في التطبيق الفعلي، سيتم استخدام مكتبة مثل pandas أو openpyxl
|
190 |
+
# لاستخراج البيانات من ملف Excel وتحليلها
|
191 |
+
|
192 |
+
# محاكاة استخراج البنود
|
193 |
+
self.analysis_results["items"] = [
|
194 |
+
{"id": 1, "name": "بند 1", "description": "وصف البند 1", "unit": "وحدة", "estimated_quantity": 100},
|
195 |
+
{"id": 2, "name": "بند 2", "description": "وصف البند 2", "unit": "وحدة", "estimated_quantity": 200},
|
196 |
+
{"id": 3, "name": "بند 3", "description": "وصف البند 3", "unit": "وحدة", "estimated_quantity": 300}
|
197 |
+
]
|
198 |
+
|
199 |
+
# محاكاة استخراج المبالغ
|
200 |
+
self.analysis_results["amounts"] = [
|
201 |
+
{"type": "item_cost", "amount": 10000, "currency": "SAR", "description": "تكلفة البند 1"},
|
202 |
+
{"type": "item_cost", "amount": 20000, "currency": "SAR", "description": "تكلفة البند 2"},
|
203 |
+
{"type": "item_cost", "amount": 30000, "currency": "SAR", "description": "تكلفة البند 3"}
|
204 |
+
]
|
205 |
+
|
206 |
+
except Exception as e:
|
207 |
+
logger.error(f"خطأ في تحليل مستند Excel: {str(e)}")
|
208 |
+
raise
|
209 |
+
|
210 |
+
def _analyze_txt(self, document_path, document_type):
|
211 |
+
"""تحليل مستند نصي"""
|
212 |
+
try:
|
213 |
+
# محاكاة تحليل مستند نصي
|
214 |
+
logger.info(f"تحليل مستند نصي: {document_path}")
|
215 |
+
|
216 |
+
# في التطبيق الفعلي، سيتم قراءة الملف النصي وتحليله
|
217 |
+
|
218 |
+
# محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
|
219 |
+
# (مشابه للتحليلات الأخرى)
|
220 |
+
|
221 |
+
except Exception as e:
|
222 |
+
logger.error(f"خطأ في تحليل مستند نصي: {str(e)}")
|
223 |
+
raise
|
224 |
+
|
225 |
+
def get_analysis_status(self):
|
226 |
+
"""الحصول على حالة التحليل الحالي"""
|
227 |
+
if not self.analysis_in_progress:
|
228 |
+
if not self.analysis_results:
|
229 |
+
return {"status": "لا يوجد تحليل جارٍ"}
|
230 |
+
else:
|
231 |
+
return {"status": self.analysis_results.get("status", "غير معروف")}
|
232 |
+
|
233 |
+
return {
|
234 |
+
"status": "جاري التحليل",
|
235 |
+
"document_path": self.current_document,
|
236 |
+
"start_time": self.analysis_results.get("analysis_start_time")
|
237 |
+
}
|
238 |
+
|
239 |
+
def get_analysis_results(self):
|
240 |
+
"""الحصول على نتائج التحليل"""
|
241 |
+
return self.analysis_results
|
242 |
+
|
243 |
+
def export_analysis_results(self, output_path=None):
|
244 |
+
"""تصدير نتائج التحليل إلى ملف JSON"""
|
245 |
+
if not self.analysis_results:
|
246 |
+
logger.warning("لا توجد نتائج تحليل للتصدير")
|
247 |
+
return None
|
248 |
+
|
249 |
+
if not output_path:
|
250 |
+
# إنشاء اسم ملف افتراضي
|
251 |
+
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
252 |
+
filename = f"analysis_results_{timestamp}.json"
|
253 |
+
output_path = os.path.join(self.documents_path, filename)
|
254 |
+
|
255 |
+
try:
|
256 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
257 |
+
json.dump(self.analysis_results, f, ensure_ascii=False, indent=4)
|
258 |
+
|
259 |
+
logger.info(f"تم تصدير نتائج التحليل إلى: {output_path}")
|
260 |
+
return output_path
|
261 |
+
|
262 |
+
except Exception as e:
|
263 |
+
logger.error(f"خطأ في تصدير نتائج التحليل: {str(e)}")
|
264 |
+
return None
|
265 |
+
|
266 |
+
def import_analysis_results(self, input_path):
|
267 |
+
"""استيراد نتائج التحليل من ملف JSON"""
|
268 |
+
if not os.path.exists(input_path):
|
269 |
+
logger.error(f"ملف نتائج التحليل غير موجود: {input_path}")
|
270 |
+
return False
|
271 |
+
|
272 |
+
try:
|
273 |
+
with open(input_path, 'r', encoding='utf-8') as f:
|
274 |
+
self.analysis_results = json.load(f)
|
275 |
+
|
276 |
+
logger.info(f"تم استيراد نتائج التحليل من: {input_path}")
|
277 |
+
return True
|
278 |
+
|
279 |
+
except Exception as e:
|
280 |
+
logger.error(f"خطأ في استيراد نتائج التحليل: {str(e)}")
|
281 |
+
return False
|
modules/document_analysis/document_app.py
ADDED
@@ -0,0 +1,887 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة تحليل المستندات - التطبيق الرئيسي
|
3 |
+
"""
|
4 |
+
|
5 |
+
# استيراد المكتبات القياسية
|
6 |
+
import os
|
7 |
+
import sys
|
8 |
+
import logging
|
9 |
+
import base64
|
10 |
+
import json
|
11 |
+
import time
|
12 |
+
from io import BytesIO
|
13 |
+
from pathlib import Path
|
14 |
+
from urllib.parse import urlparse
|
15 |
+
from tempfile import NamedTemporaryFile
|
16 |
+
|
17 |
+
# استيراد مكتبة Streamlit
|
18 |
+
import streamlit as st
|
19 |
+
|
20 |
+
# استيراد المكتبات الإضافية
|
21 |
+
import pandas as pd
|
22 |
+
import numpy as np
|
23 |
+
import matplotlib.pyplot as plt
|
24 |
+
import plotly.express as px
|
25 |
+
import plotly.graph_objects as go
|
26 |
+
import requests
|
27 |
+
from PIL import Image
|
28 |
+
|
29 |
+
# محاولة استيراد خدمات تحليل المستندات
|
30 |
+
try:
|
31 |
+
from .services.text_extractor import TextExtractor
|
32 |
+
from .services.item_extractor import ItemExtractor
|
33 |
+
from .services.document_parser import DocumentParser
|
34 |
+
except ImportError:
|
35 |
+
try:
|
36 |
+
from modules.document_analysis.services.text_extractor import TextExtractor
|
37 |
+
from modules.document_analysis.services.item_extractor import ItemExtractor
|
38 |
+
from modules.document_analysis.services.document_parser import DocumentParser
|
39 |
+
except ImportError:
|
40 |
+
# تعريف فئات وهمية في حالة عدم وجود الخدمات
|
41 |
+
class TextExtractor:
|
42 |
+
def __init__(self, config=None):
|
43 |
+
self.config = config or {}
|
44 |
+
|
45 |
+
def extract_from_pdf(self, file_path):
|
46 |
+
return "نص مستخرج مؤقت من PDF"
|
47 |
+
|
48 |
+
def extract_from_docx(self, file_path):
|
49 |
+
return "نص مستخرج مؤقت من DOCX"
|
50 |
+
|
51 |
+
def extract_from_image(self, file_path):
|
52 |
+
return "نص مستخرج مؤقت من صورة"
|
53 |
+
|
54 |
+
def extract(self, file_path):
|
55 |
+
_, ext = os.path.splitext(file_path)
|
56 |
+
ext = ext.lower()
|
57 |
+
|
58 |
+
if ext == '.pdf':
|
59 |
+
return self.extract_from_pdf(file_path)
|
60 |
+
elif ext in ('.doc', '.docx'):
|
61 |
+
return self.extract_from_docx(file_path)
|
62 |
+
elif ext in ('.jpg', '.jpeg', '.png'):
|
63 |
+
return self.extract_from_image(file_path)
|
64 |
+
else:
|
65 |
+
return "نوع ملف غير مدعوم"
|
66 |
+
|
67 |
+
class ItemExtractor:
|
68 |
+
def __init__(self, config=None):
|
69 |
+
self.config = config or {}
|
70 |
+
|
71 |
+
def extract_tables(self, document):
|
72 |
+
return [{"عنوان": "جدول مؤقت", "بيانات": []}]
|
73 |
+
|
74 |
+
def extract_items(self, document):
|
75 |
+
return [
|
76 |
+
{"رقم البند": "A1", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", "الوحدة": "م3", "الكمية": 250.0},
|
77 |
+
{"رقم البند": "A2", "وصف البند": "توريد وتركيب حديد التسليح للأساسات", "الوحدة": "طن", "الكمية": 25.0},
|
78 |
+
{"رقم البند": "A3", "وصف البند": "أعمال العزل المائي للأساسات", "الوحدة": "م2", "الكمية": 500.0}
|
79 |
+
]
|
80 |
+
|
81 |
+
class DocumentParser:
|
82 |
+
def __init__(self, config=None):
|
83 |
+
self.config = config or {}
|
84 |
+
|
85 |
+
def parse_document(self, file_path):
|
86 |
+
return {
|
87 |
+
"metadata": {
|
88 |
+
"title": "مستند مؤقت",
|
89 |
+
"author": "غير معروف",
|
90 |
+
"date": "2024-01-01",
|
91 |
+
"pages": 10
|
92 |
+
},
|
93 |
+
"content": "محتوى مؤقت للمستند",
|
94 |
+
"tables": [],
|
95 |
+
"items": []
|
96 |
+
}
|
97 |
+
|
98 |
+
def extract_metadata(self, file_path):
|
99 |
+
return {
|
100 |
+
"title": "مستند مؤقت",
|
101 |
+
"author": "غير معروف",
|
102 |
+
"date": "2024-01-01",
|
103 |
+
"pages": 10
|
104 |
+
}
|
105 |
+
|
106 |
+
|
107 |
+
class DocumentAnalysisApp:
|
108 |
+
"""وحدة تحليل المستندات"""
|
109 |
+
|
110 |
+
def __init__(self):
|
111 |
+
"""تهيئة وحدة تحليل المستندات"""
|
112 |
+
|
113 |
+
# تهيئة خدمات تحليل المستندات
|
114 |
+
self.text_extractor = TextExtractor()
|
115 |
+
self.item_extractor = ItemExtractor()
|
116 |
+
self.document_parser = DocumentParser()
|
117 |
+
|
118 |
+
# تهيئة حالة الجلسة
|
119 |
+
if 'analyzed_documents' not in st.session_state:
|
120 |
+
st.session_state.analyzed_documents = []
|
121 |
+
|
122 |
+
if 'extracted_items' not in st.session_state:
|
123 |
+
st.session_state.extracted_items = []
|
124 |
+
|
125 |
+
# إنشاء مجلد مؤقت للملفات
|
126 |
+
self.temp_dir = Path("temp_documents")
|
127 |
+
self.temp_dir.mkdir(exist_ok=True)
|
128 |
+
|
129 |
+
def render(self):
|
130 |
+
"""عرض واجهة وحدة تحليل المستندات"""
|
131 |
+
|
132 |
+
st.markdown("<h1 class='module-title'>وحدة تحليل المستندات</h1>", unsafe_allow_html=True)
|
133 |
+
|
134 |
+
tabs = st.tabs([
|
135 |
+
"تحليل المستندات",
|
136 |
+
"استخراج البنود والكميات",
|
137 |
+
"تحليل الصور والمخططات",
|
138 |
+
"مكتبة المستندات",
|
139 |
+
"الإعدادات"
|
140 |
+
])
|
141 |
+
|
142 |
+
with tabs[0]:
|
143 |
+
self._render_document_analysis_tab()
|
144 |
+
|
145 |
+
with tabs[1]:
|
146 |
+
self._render_item_extraction_tab()
|
147 |
+
|
148 |
+
with tabs[2]:
|
149 |
+
self._render_image_analysis_tab()
|
150 |
+
|
151 |
+
with tabs[3]:
|
152 |
+
self._render_document_library_tab()
|
153 |
+
|
154 |
+
with tabs[4]:
|
155 |
+
self._render_settings_tab()
|
156 |
+
|
157 |
+
def _render_document_analysis_tab(self):
|
158 |
+
"""عرض تبويب تحليل المستندات"""
|
159 |
+
|
160 |
+
st.markdown("### تحليل المستندات")
|
161 |
+
|
162 |
+
# رفع المستند
|
163 |
+
uploaded_file = st.file_uploader("رفع مستند للتحليل", type=["pdf", "docx", "txt", "jpg", "jpeg", "png"], key="document_upload")
|
164 |
+
|
165 |
+
if uploaded_file is not None:
|
166 |
+
# حفظ الملف مؤقتاً
|
167 |
+
file_path = self._save_uploaded_file(uploaded_file)
|
168 |
+
|
169 |
+
if file_path:
|
170 |
+
st.success(f"تم رفع الملف: {uploaded_file.name}")
|
171 |
+
|
172 |
+
# عرض معلومات الملف
|
173 |
+
file_info = self._get_file_info(file_path)
|
174 |
+
|
175 |
+
col1, col2, col3 = st.columns(3)
|
176 |
+
|
177 |
+
with col1:
|
178 |
+
st.metric("نوع الملف", file_info["type"])
|
179 |
+
|
180 |
+
with col2:
|
181 |
+
st.metric("حجم الملف", file_info["size"])
|
182 |
+
|
183 |
+
with col3:
|
184 |
+
if "pages" in file_info:
|
185 |
+
st.metric("عدد الصفحات", file_info["pages"])
|
186 |
+
|
187 |
+
# خيارات التحليل
|
188 |
+
analysis_options = st.multiselect(
|
189 |
+
"اختر خيارات التحليل",
|
190 |
+
[
|
191 |
+
"استخراج النص",
|
192 |
+
"استخراج الجداول",
|
193 |
+
"استخراج البنود والكميات",
|
194 |
+
"استخراج المعلومات الرئيسية",
|
195 |
+
"تحليل هيكل المستند"
|
196 |
+
],
|
197 |
+
default=["استخراج النص", "استخراج البنود والكميات"],
|
198 |
+
key="analysis_options"
|
199 |
+
)
|
200 |
+
|
201 |
+
# زر بدء التحليل
|
202 |
+
if st.button("بدء التحليل", key="start_analysis_button"):
|
203 |
+
with st.spinner("جاري تحليل المستند..."):
|
204 |
+
# محاكاة وقت التحليل
|
205 |
+
time.sleep(2)
|
206 |
+
|
207 |
+
# تنفيذ التحليل المطلوب
|
208 |
+
analysis_results = {}
|
209 |
+
|
210 |
+
if "استخراج النص" in analysis_options:
|
211 |
+
analysis_results["text"] = self.text_extractor.extract(file_path)
|
212 |
+
|
213 |
+
if "استخراج الجداول" in analysis_options:
|
214 |
+
# محاكاة استخراج الجداول
|
215 |
+
tables = self.item_extractor.extract_tables(file_path)
|
216 |
+
analysis_results["tables"] = tables
|
217 |
+
|
218 |
+
if "استخراج البنود والكميات" in analysis_options:
|
219 |
+
# محاكاة استخراج البنود
|
220 |
+
items = self.item_extractor.extract_items(file_path)
|
221 |
+
analysis_results["items"] = items
|
222 |
+
|
223 |
+
# حفظ البنود المستخرجة في حالة الجلسة
|
224 |
+
st.session_state.extracted_items = items
|
225 |
+
|
226 |
+
if "استخراج المعلومات الرئيسية" in analysis_options:
|
227 |
+
# محاكاة استخراج المعلومات الرئيسية
|
228 |
+
metadata = self.document_parser.extract_metadata(file_path)
|
229 |
+
analysis_results["metadata"] = metadata
|
230 |
+
|
231 |
+
if "تحليل هيكل المستند" in analysis_options:
|
232 |
+
# محاكاة تحليل هيكل المستند
|
233 |
+
structure = {
|
234 |
+
"sections": [
|
235 |
+
{"title": "مقدمة", "level": 1, "page": 1},
|
236 |
+
{"title": "نطاق العمل", "level": 1, "page": 2},
|
237 |
+
{"title": "المواصفات الفنية", "level": 1, "page": 3},
|
238 |
+
{"title": "جدول الكميات", "level": 1, "page": 5},
|
239 |
+
{"title": "الشروط الخاصة", "level": 1, "page": 7}
|
240 |
+
]
|
241 |
+
}
|
242 |
+
analysis_results["structure"] = structure
|
243 |
+
|
244 |
+
# حفظ نتائج التحليل في حالة الجلسة
|
245 |
+
st.session_state.analyzed_documents.append({
|
246 |
+
"file_name": uploaded_file.name,
|
247 |
+
"file_path": str(file_path),
|
248 |
+
"analysis_options": analysis_options,
|
249 |
+
"results": analysis_results,
|
250 |
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
|
251 |
+
})
|
252 |
+
|
253 |
+
st.success("تم الانتهاء من تحليل المستند!")
|
254 |
+
|
255 |
+
# عرض نتائج التحليل
|
256 |
+
self._display_analysis_results(analysis_results)
|
257 |
+
|
258 |
+
# عرض سجل التحليلات السابقة
|
259 |
+
if st.session_state.analyzed_documents:
|
260 |
+
st.markdown("### سجل التحليلات السابقة")
|
261 |
+
|
262 |
+
for i, doc in enumerate(reversed(st.session_state.analyzed_documents)):
|
263 |
+
with st.expander(f"{doc['file_name']} ({doc['timestamp']})"):
|
264 |
+
st.markdown(f"**خيارات التحليل:** {', '.join(doc['analysis_options'])}")
|
265 |
+
|
266 |
+
# عرض نتائج التحليل
|
267 |
+
self._display_analysis_results(doc['results'])
|
268 |
+
|
269 |
+
# أزرار العمليات
|
270 |
+
col1, col2 = st.columns(2)
|
271 |
+
|
272 |
+
with col1:
|
273 |
+
if st.button("إرسال إلى وحدة التسعير", key=f"send_to_pricing_{i}"):
|
274 |
+
st.success("تم إرسال البيانات إلى وحدة التسعير بنجاح!")
|
275 |
+
|
276 |
+
with col2:
|
277 |
+
if st.button("تصدير النتائج", key=f"export_results_{i}"):
|
278 |
+
st.success("تم تصدير النتائج بنجاح!")
|
279 |
+
|
280 |
+
def _render_item_extraction_tab(self):
|
281 |
+
"""عرض تبويب استخراج البنود والكميات"""
|
282 |
+
|
283 |
+
st.markdown("### استخراج البنود والكميات")
|
284 |
+
|
285 |
+
# التحقق من وجود بنود مستخرجة
|
286 |
+
if not st.session_state.extracted_items:
|
287 |
+
st.warning("لا توجد بنود مستخرجة. يرجى تحليل مستند أولاً.")
|
288 |
+
|
289 |
+
# عرض بيانات افتراضية للتوضيح
|
290 |
+
st.markdown("### مثال توضيحي")
|
291 |
+
|
292 |
+
# بيانات افتراضية
|
293 |
+
sample_items = [
|
294 |
+
{"رقم البند": "A1", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", "الوحدة": "م3", "الكمية": 250.0},
|
295 |
+
{"رقم البند": "A2", "وصف البند": "توريد وتركيب حديد التسليح للأساسات", "الوحدة": "طن", "الكمية": 25.0},
|
296 |
+
{"رقم البند": "A3", "وصف البند": "أعمال العزل المائي للأساسات", "الوحدة": "م2", "الكمية": 500.0},
|
297 |
+
{"رقم البند": "A4", "وصف البند": "أعمال الردم والدك للأساسات", "الوحدة": "م3", "الكمية": 300.0},
|
298 |
+
{"رقم البند": "A5", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", "الوحدة": "م3", "الكمية": 120.0}
|
299 |
+
]
|
300 |
+
|
301 |
+
# عرض البنود كجدول
|
302 |
+
items_df = pd.DataFrame(sample_items)
|
303 |
+
st.dataframe(items_df, use_container_width=True, hide_index=True)
|
304 |
+
|
305 |
+
# زر لاستخدام البيانات التوضيحية
|
306 |
+
if st.button("استخدام البيانات التوضيحية", key="use_sample_data_button"):
|
307 |
+
st.session_state.extracted_items = sample_items
|
308 |
+
st.success("تم استخدام البيانات التوضيحية!")
|
309 |
+
st.rerun()
|
310 |
+
else:
|
311 |
+
# عرض البنود المستخرجة
|
312 |
+
items_df = pd.DataFrame(st.session_state.extracted_items)
|
313 |
+
|
314 |
+
# إضافة عمود سعر الوحدة والإجمالي إذا لم يكن موجوداً
|
315 |
+
if "سعر الوحدة" not in items_df.columns:
|
316 |
+
items_df["سعر الوحدة"] = 0.0
|
317 |
+
|
318 |
+
if "الإجمالي" not in items_df.columns:
|
319 |
+
items_df["الإجمالي"] = 0.0
|
320 |
+
|
321 |
+
# عرض البنود كجدول قابل للتعديل
|
322 |
+
st.markdown("### البنود المستخرجة")
|
323 |
+
edited_df = st.data_editor(items_df, use_container_width=True, hide_index=True, key="items_editor")
|
324 |
+
|
325 |
+
# تحديث البنود المستخرجة بعد التعديل
|
326 |
+
st.session_state.extracted_items = edited_df.to_dict('records')
|
327 |
+
|
328 |
+
# أزرار العمليات
|
329 |
+
col1, col2, col3 = st.columns(3)
|
330 |
+
|
331 |
+
with col1:
|
332 |
+
if st.button("إرسال إلى وحدة التسعير", key="send_to_pricing_button"):
|
333 |
+
# محاكاة إرسال البيانات إلى وحدة التسعير
|
334 |
+
if 'current_pricing' not in st.session_state:
|
335 |
+
st.session_state.current_pricing = {
|
336 |
+
'name': "مناقصة جديدة",
|
337 |
+
'number': "T-" + time.strftime("%Y-%m-%d"),
|
338 |
+
'client': "",
|
339 |
+
'location': "",
|
340 |
+
'method': "التسعير القياسي",
|
341 |
+
'submission_date': None,
|
342 |
+
'items': edited_df,
|
343 |
+
'status': 'جديد',
|
344 |
+
'created_at': time.strftime("%Y-%m-%d %H:%M:%S")
|
345 |
+
}
|
346 |
+
else:
|
347 |
+
st.session_state.current_pricing['items'] = edited_df
|
348 |
+
|
349 |
+
st.success("تم إرسال البنود إلى وحدة التسعير بنجاح!")
|
350 |
+
|
351 |
+
with col2:
|
352 |
+
if st.button("تصدير إلى Excel", key="export_to_excel_button"):
|
353 |
+
st.success("تم تصدير البنود إلى Excel بنجاح!")
|
354 |
+
|
355 |
+
with col3:
|
356 |
+
if st.button("مسح البنود", key="clear_items_button"):
|
357 |
+
st.session_state.extracted_items = []
|
358 |
+
st.warning("تم مسح البنود!")
|
359 |
+
st.rerun()
|
360 |
+
|
361 |
+
# عرض إحصائيات البنود
|
362 |
+
st.markdown("### إحصائيات البنود")
|
363 |
+
|
364 |
+
col1, col2, col3 = st.columns(3)
|
365 |
+
|
366 |
+
with col1:
|
367 |
+
st.metric("عدد البنود", len(edited_df))
|
368 |
+
|
369 |
+
with col2:
|
370 |
+
units_count = edited_df['الوحدة'].value_counts()
|
371 |
+
most_common_unit = units_count.index[0] if not units_count.empty else "غير متوفر"
|
372 |
+
st.metric("الوحدة الأكثر استخداماً", most_common_unit)
|
373 |
+
|
374 |
+
with col3:
|
375 |
+
total_quantity = edited_df['الكمية'].sum()
|
376 |
+
st.metric("إجمالي الكميات", f"{total_quantity:,.2f}")
|
377 |
+
|
378 |
+
# رسم بياني لتوزيع البنود حسب الوحدة
|
379 |
+
st.markdown("### توزيع البنود حسب الوحدة")
|
380 |
+
|
381 |
+
units_df = pd.DataFrame(units_count).reset_index()
|
382 |
+
units_df.columns = ['الوحدة', 'العدد']
|
383 |
+
|
384 |
+
fig = px.pie(
|
385 |
+
units_df,
|
386 |
+
values='العدد',
|
387 |
+
names='الوحدة',
|
388 |
+
title='توزيع البنود حسب الوحدة',
|
389 |
+
hole=0.4
|
390 |
+
)
|
391 |
+
|
392 |
+
st.plotly_chart(fig, use_container_width=True)
|
393 |
+
|
394 |
+
def _render_image_analysis_tab(self):
|
395 |
+
"""عرض تبويب تحليل الصور والمخططات"""
|
396 |
+
|
397 |
+
st.markdown("### تحليل الصور والمخططات")
|
398 |
+
|
399 |
+
# رفع الصورة
|
400 |
+
uploaded_image = st.file_uploader("رفع صورة أو مخطط للتحليل", type=["jpg", "jpeg", "png", "tif", "tiff"], key="image_upload")
|
401 |
+
|
402 |
+
if uploaded_image is not None:
|
403 |
+
# عرض الصورة
|
404 |
+
image = Image.open(uploaded_image)
|
405 |
+
st.image(image, caption=uploaded_image.name, use_column_width=True)
|
406 |
+
|
407 |
+
# خيارات التحليل
|
408 |
+
analysis_type = st.selectbox(
|
409 |
+
"نوع التحليل",
|
410 |
+
[
|
411 |
+
"استخراج النص من الصورة",
|
412 |
+
"تحليل المخططات الهندسية",
|
413 |
+
"قياس المساحات والأبعاد",
|
414 |
+
"تحليل مخصص"
|
415 |
+
],
|
416 |
+
key="image_analysis_type"
|
417 |
+
)
|
418 |
+
|
419 |
+
# زر بدء التحليل
|
420 |
+
if st.button("بدء التحليل", key="start_image_analysis_button"):
|
421 |
+
with st.spinner("جاري تحليل الصورة..."):
|
422 |
+
# محاكاة وقت التحليل
|
423 |
+
time.sleep(2)
|
424 |
+
|
425 |
+
if analysis_type == "استخراج النص من الصورة":
|
426 |
+
# محاكاة استخراج النص
|
427 |
+
extracted_text = "نص مستخرج من الصورة (محاكاة):\n\n"
|
428 |
+
extracted_text += "مواصفات المشروع:\n"
|
429 |
+
extracted_text += "- مساحة الأرض: 1000 م2\n"
|
430 |
+
extracted_text += "- عدد الطوابق: 3\n"
|
431 |
+
extracted_text += "- ارتفاع المبنى: 12 م\n"
|
432 |
+
|
433 |
+
st.markdown("### النص المستخرج من الصورة")
|
434 |
+
st.text_area("النص المستخرج", extracted_text, height=200)
|
435 |
+
|
436 |
+
elif analysis_type == "تحليل المخططات الهندسية":
|
437 |
+
# محاكاة تحليل المخططات
|
438 |
+
st.markdown("### نتائج تحليل المخطط الهندسي")
|
439 |
+
|
440 |
+
# محاكاة رسم تخطيطي للمخطط
|
441 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
442 |
+
ax.imshow(image)
|
443 |
+
|
444 |
+
# إضافة تعليقات توضيحية
|
445 |
+
ax.annotate('غرفة المعيشة', xy=(100, 100), xytext=(150, 50),
|
446 |
+
arrowprops=dict(facecolor='red', shrink=0.05))
|
447 |
+
|
448 |
+
ax.annotate('المطبخ', xy=(300, 150), xytext=(350, 100),
|
449 |
+
arrowprops=dict(facecolor='blue', shrink=0.05))
|
450 |
+
|
451 |
+
ax.annotate('غرفة النوم', xy=(200, 300), xytext=(250, 350),
|
452 |
+
arrowprops=dict(facecolor='green', shrink=0.05))
|
453 |
+
|
454 |
+
st.pyplot(fig)
|
455 |
+
|
456 |
+
# عرض معلومات المخطط
|
457 |
+
st.markdown("### معلومات المخطط")
|
458 |
+
|
459 |
+
col1, col2, col3 = st.columns(3)
|
460 |
+
|
461 |
+
with col1:
|
462 |
+
st.metric("المساحة الإجمالية", "150 م2")
|
463 |
+
|
464 |
+
with col2:
|
465 |
+
st.metric("عدد الغرف", "3")
|
466 |
+
|
467 |
+
with col3:
|
468 |
+
st.metric("عدد الحمامات", "2")
|
469 |
+
|
470 |
+
elif analysis_type == "قياس المساحات والأبعاد":
|
471 |
+
# محاكاة قياس المساحات
|
472 |
+
st.markdown("### نتائج قياس المساحات والأبعاد")
|
473 |
+
|
474 |
+
# محاكاة رسم تخطيطي للمساحات
|
475 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
476 |
+
ax.imshow(image)
|
477 |
+
|
478 |
+
# إضافة قياسات
|
479 |
+
ax.plot([50, 250], [50, 50], 'r-', linewidth=2)
|
480 |
+
ax.text(150, 40, '10 م', color='red', fontsize=12, ha='center')
|
481 |
+
|
482 |
+
ax.plot([50, 50], [50, 250], 'b-', linewidth=2)
|
483 |
+
ax.text(40, 150, '8 م', color='blue', fontsize=12, va='center', rotation=90)
|
484 |
+
|
485 |
+
st.pyplot(fig)
|
486 |
+
|
487 |
+
# عرض جدول القياسات
|
488 |
+
measurements = pd.DataFrame({
|
489 |
+
'العنصر': ['الطول', 'العرض', 'المساحة', 'المحيط'],
|
490 |
+
'القيمة': ['10 م', '8 م', '80 م2', '36 م']
|
491 |
+
})
|
492 |
+
|
493 |
+
st.dataframe(measurements, use_container_width=True, hide_index=True)
|
494 |
+
|
495 |
+
else: # تحليل مخصص
|
496 |
+
st.markdown("### نتائج التحليل المخصص")
|
497 |
+
st.info("تم تحليل الصورة بنجاح. يمكنك تخصيص التحليل حسب احتياجاتك.")
|
498 |
+
|
499 |
+
# خيارات التصدير
|
500 |
+
col1, col2 = st.columns(2)
|
501 |
+
|
502 |
+
with col1:
|
503 |
+
if st.button("تصدير نتائج التحليل", key="export_image_analysis_button"):
|
504 |
+
st.success("تم تصدير نتائج التحليل بنجاح!")
|
505 |
+
|
506 |
+
with col2:
|
507 |
+
if st.button("إرسال إلى وحدة التسعير", key="send_image_to_pricing_button"):
|
508 |
+
st.success("تم إرسال نت��ئج التحليل إلى وحدة التسعير بنجاح!")
|
509 |
+
|
510 |
+
def _render_document_library_tab(self):
|
511 |
+
"""عرض تبويب مكتبة المستندات"""
|
512 |
+
|
513 |
+
st.markdown("### مكتبة المستندات")
|
514 |
+
|
515 |
+
# بيانات افتراضية للمستندات
|
516 |
+
if 'document_library' not in st.session_state:
|
517 |
+
st.session_state.document_library = [
|
518 |
+
{
|
519 |
+
"id": 1,
|
520 |
+
"name": "كراسة شروط مشروع توسعة مستشفى الملك فهد",
|
521 |
+
"type": "PDF",
|
522 |
+
"size": "5.2 MB",
|
523 |
+
"pages": 120,
|
524 |
+
"upload_date": "2024-01-15",
|
525 |
+
"category": "كراسات الشروط",
|
526 |
+
"tags": ["صحي", "مستشفى", "توسعة"]
|
527 |
+
},
|
528 |
+
{
|
529 |
+
"id": 2,
|
530 |
+
"name": "جدول كميات صيانة محطات المياه",
|
531 |
+
"type": "Excel",
|
532 |
+
"size": "1.8 MB",
|
533 |
+
"pages": None,
|
534 |
+
"upload_date": "2024-02-10",
|
535 |
+
"category": "جداول الكميات",
|
536 |
+
"tags": ["مياه", "صيانة", "محطات"]
|
537 |
+
},
|
538 |
+
{
|
539 |
+
"id": 3,
|
540 |
+
"name": "مخططات إنشاء مدرسة ثانوية",
|
541 |
+
"type": "PDF",
|
542 |
+
"size": "12.5 MB",
|
543 |
+
"pages": 45,
|
544 |
+
"upload_date": "2024-02-25",
|
545 |
+
"category": "مخططات",
|
546 |
+
"tags": ["تعليم", "مدرسة", "إنشاء"]
|
547 |
+
},
|
548 |
+
{
|
549 |
+
"id": 4,
|
550 |
+
"name": "عقد إنشاء طريق دائري",
|
551 |
+
"type": "Word",
|
552 |
+
"size": "0.9 MB",
|
553 |
+
"pages": 28,
|
554 |
+
"upload_date": "2024-03-05",
|
555 |
+
"category": "عقود",
|
556 |
+
"tags": ["طرق", "إنشاء", "دائري"]
|
557 |
+
},
|
558 |
+
{
|
559 |
+
"id": 5,
|
560 |
+
"name": "تقرير فني لمشروع تطوير شبكة مياه",
|
561 |
+
"type": "PDF",
|
562 |
+
"size": "3.7 MB",
|
563 |
+
"pages": 65,
|
564 |
+
"upload_date": "2024-03-15",
|
565 |
+
"category": "تقارير فنية",
|
566 |
+
"tags": ["مياه", "شبكة", "تطوير"]
|
567 |
+
}
|
568 |
+
]
|
569 |
+
|
570 |
+
# البحث في المكتبة
|
571 |
+
search_query = st.text_input("البحث في المكتبة", key="library_search")
|
572 |
+
|
573 |
+
col1, col2, col3 = st.columns(3)
|
574 |
+
|
575 |
+
with col1:
|
576 |
+
category_filter = st.selectbox(
|
577 |
+
"تصفية حسب الفئة",
|
578 |
+
["الكل", "كراسات الشروط", "جداول الكميات", "مخططات", "عقود", "تقارير فنية"],
|
579 |
+
key="category_filter"
|
580 |
+
)
|
581 |
+
|
582 |
+
with col2:
|
583 |
+
type_filter = st.selectbox(
|
584 |
+
"تصفية حسب النوع",
|
585 |
+
["الكل", "PDF", "Word", "Excel", "Image"],
|
586 |
+
key="type_filter"
|
587 |
+
)
|
588 |
+
|
589 |
+
with col3:
|
590 |
+
sort_by = st.selectbox(
|
591 |
+
"ترتيب حسب",
|
592 |
+
["تاريخ الرفع (الأحدث أولاً)", "تاريخ الرفع (الأقدم أولاً)", "الاسم (أ-ي)", "الاسم (ي-أ)", "الحجم (الأكبر أولاً)", "الحجم (الأصغر أولاً)"],
|
593 |
+
key="sort_by"
|
594 |
+
)
|
595 |
+
|
596 |
+
# تطبيق التصفية والبحث
|
597 |
+
filtered_documents = st.session_state.document_library.copy()
|
598 |
+
|
599 |
+
# تطبيق البحث
|
600 |
+
if search_query:
|
601 |
+
filtered_documents = [doc for doc in filtered_documents if search_query.lower() in doc["name"].lower() or
|
602 |
+
any(search_query.lower() in tag.lower() for tag in doc["tags"])]
|
603 |
+
|
604 |
+
# تطبيق تصفية الفئة
|
605 |
+
if category_filter != "الكل":
|
606 |
+
filtered_documents = [doc for doc in filtered_documents if doc["category"] == category_filter]
|
607 |
+
|
608 |
+
# تطبيق تصفية النوع
|
609 |
+
if type_filter != "الكل":
|
610 |
+
filtered_documents = [doc for doc in filtered_documents if doc["type"] == type_filter]
|
611 |
+
|
612 |
+
# تطبيق الترتيب
|
613 |
+
if sort_by == "تاريخ الرفع (الأحدث أولاً)":
|
614 |
+
filtered_documents.sort(key=lambda x: x["upload_date"], reverse=True)
|
615 |
+
elif sort_by == "تاريخ الرفع (الأقدم أولاً)":
|
616 |
+
filtered_documents.sort(key=lambda x: x["upload_date"])
|
617 |
+
elif sort_by == "الاسم (أ-ي)":
|
618 |
+
filtered_documents.sort(key=lambda x: x["name"])
|
619 |
+
elif sort_by == "الاسم (ي-أ)":
|
620 |
+
filtered_documents.sort(key=lambda x: x["name"], reverse=True)
|
621 |
+
elif sort_by == "الحجم (الأكبر أولاً)":
|
622 |
+
filtered_documents.sort(key=lambda x: float(x["size"].split()[0]), reverse=True)
|
623 |
+
elif sort_by == "الحجم (الأصغر أولاً)":
|
624 |
+
filtered_documents.sort(key=lambda x: float(x["size"].split()[0]))
|
625 |
+
|
626 |
+
# عرض المستندات
|
627 |
+
st.markdown(f"### المستندات ({len(filtered_documents)})")
|
628 |
+
|
629 |
+
if not filtered_documents:
|
630 |
+
st.info("لا توجد مستندات تطابق معايير البحث.")
|
631 |
+
else:
|
632 |
+
# عرض المستندات كبطاقات
|
633 |
+
for i, doc in enumerate(filtered_documents):
|
634 |
+
with st.container():
|
635 |
+
col1, col2, col3 = st.columns([3, 1, 1])
|
636 |
+
|
637 |
+
with col1:
|
638 |
+
st.markdown(f"**{doc['name']}**")
|
639 |
+
st.markdown(f"الفئة: {doc['category']} | النوع: {doc['type']} | الحجم: {doc['size']} | تاريخ الرفع: {doc['upload_date']}")
|
640 |
+
st.markdown(f"الوسوم: {', '.join(doc['tags'])}")
|
641 |
+
|
642 |
+
with col2:
|
643 |
+
if st.button("عرض", key=f"view_doc_{i}"):
|
644 |
+
st.session_state.selected_document = doc
|
645 |
+
st.success(f"جاري عرض المستند: {doc['name']}")
|
646 |
+
|
647 |
+
with col3:
|
648 |
+
if st.button("تحليل", key=f"analyze_doc_{i}"):
|
649 |
+
st.session_state.selected_document = doc
|
650 |
+
st.success(f"جاري تحليل المستند: {doc['name']}")
|
651 |
+
|
652 |
+
st.markdown("---")
|
653 |
+
|
654 |
+
# رفع مستند جديد
|
655 |
+
st.markdown("### رفع مستند جديد")
|
656 |
+
|
657 |
+
uploaded_file = st.file_uploader("اختر ملفاً للرفع", type=["pdf", "docx", "xlsx", "jpg", "jpeg", "png"], key="library_upload")
|
658 |
+
|
659 |
+
if uploaded_file is not None:
|
660 |
+
col1, col2 = st.columns(2)
|
661 |
+
|
662 |
+
with col1:
|
663 |
+
doc_category = st.selectbox(
|
664 |
+
"فئة المستند",
|
665 |
+
["كراسات الشروط", "جداول الكميات", "مخططات", "عقود", "تقارير فنية", "أخرى"],
|
666 |
+
key="doc_category"
|
667 |
+
)
|
668 |
+
|
669 |
+
with col2:
|
670 |
+
doc_tags = st.text_input("الوسوم (مفصولة بفواصل)", key="doc_tags")
|
671 |
+
|
672 |
+
if st.button("رفع المستند", key="upload_to_library_button"):
|
673 |
+
# محاكاة رفع المستند
|
674 |
+
new_doc = {
|
675 |
+
"id": len(st.session_state.document_library) + 1,
|
676 |
+
"name": uploaded_file.name,
|
677 |
+
"type": uploaded_file.name.split(".")[-1].upper(),
|
678 |
+
"size": f"{uploaded_file.size / (1024 * 1024):.1f} MB",
|
679 |
+
"pages": None,
|
680 |
+
"upload_date": time.strftime("%Y-%m-%d"),
|
681 |
+
"category": doc_category,
|
682 |
+
"tags": [tag.strip() for tag in doc_tags.split(",") if tag.strip()]
|
683 |
+
}
|
684 |
+
|
685 |
+
st.session_state.document_library.append(new_doc)
|
686 |
+
st.success(f"تم رفع المستند: {uploaded_file.name}")
|
687 |
+
st.rerun()
|
688 |
+
|
689 |
+
def _render_settings_tab(self):
|
690 |
+
"""عرض تبويب الإعدادات"""
|
691 |
+
|
692 |
+
st.markdown("### إعدادات تحليل المستندات")
|
693 |
+
|
694 |
+
# إعدادات استخراج النص
|
695 |
+
with st.expander("إعدادات استخراج النص", expanded=True):
|
696 |
+
st.markdown("#### إعدادات استخراج النص")
|
697 |
+
|
698 |
+
ocr_engine = st.selectbox(
|
699 |
+
"محرك التعرف الضوئي على النصوص",
|
700 |
+
["Tesseract OCR", "Google Cloud Vision", "Amazon Textract", "Microsoft Azure OCR"],
|
701 |
+
index=0,
|
702 |
+
key="ocr_engine"
|
703 |
+
)
|
704 |
+
|
705 |
+
language = st.selectbox(
|
706 |
+
"لغة المستندات",
|
707 |
+
["العربية", "الإنجليزية", "العربية والإنجليزية"],
|
708 |
+
index=0,
|
709 |
+
key="ocr_language"
|
710 |
+
)
|
711 |
+
|
712 |
+
dpi = st.slider(
|
713 |
+
"دقة المسح (DPI)",
|
714 |
+
min_value=100,
|
715 |
+
max_value=600,
|
716 |
+
value=300,
|
717 |
+
step=50,
|
718 |
+
key="ocr_dpi"
|
719 |
+
)
|
720 |
+
|
721 |
+
if st.button("حفظ إعدادات استخراج النص", key="save_ocr_settings"):
|
722 |
+
st.success("تم حفظ إعدادات استخراج النص بنجاح!")
|
723 |
+
|
724 |
+
# إعدادات استخراج البنود
|
725 |
+
with st.expander("إعدادات استخراج البنود", expanded=True):
|
726 |
+
st.markdown("#### إعدادات استخراج البنود")
|
727 |
+
|
728 |
+
extraction_method = st.selectbox(
|
729 |
+
"طريقة استخراج البنود",
|
730 |
+
["تحليل الجداول", "تحليل النص", "الذكاء الاصطناعي", "مزيج"],
|
731 |
+
index=3,
|
732 |
+
key="extraction_method"
|
733 |
+
)
|
734 |
+
|
735 |
+
auto_detect_units = st.checkbox(
|
736 |
+
"اكتشاف الوحدات تلقائياً",
|
737 |
+
value=True,
|
738 |
+
key="auto_detect_units"
|
739 |
+
)
|
740 |
+
|
741 |
+
normalize_quantities = st.checkbox(
|
742 |
+
"توحيد صيغة الكميات",
|
743 |
+
value=True,
|
744 |
+
key="normalize_quantities"
|
745 |
+
)
|
746 |
+
|
747 |
+
if st.button("حفظ إعدادات استخراج البنود", key="save_extraction_settings"):
|
748 |
+
st.success("تم حفظ إعدادات استخراج البنود بنجاح!")
|
749 |
+
|
750 |
+
# إعدادات تحليل الصور
|
751 |
+
with st.expander("إعدادات تحليل الصور", expanded=True):
|
752 |
+
st.markdown("#### إعدادات تحليل الصور")
|
753 |
+
|
754 |
+
image_analysis_engine = st.selectbox(
|
755 |
+
"محرك تحليل الصور",
|
756 |
+
["OpenCV", "Google Cloud Vision", "Amazon Rekognition", "Microsoft Azure Computer Vision"],
|
757 |
+
index=0,
|
758 |
+
key="image_analysis_engine"
|
759 |
+
)
|
760 |
+
|
761 |
+
image_resolution = st.slider(
|
762 |
+
"دقة تحليل الصور",
|
763 |
+
min_value=1,
|
764 |
+
max_value=10,
|
765 |
+
value=5,
|
766 |
+
key="image_resolution"
|
767 |
+
)
|
768 |
+
|
769 |
+
if st.button("حفظ إعدادات تحليل الصور", key="save_image_analysis_settings"):
|
770 |
+
st.success("تم حفظ إعدادات تحليل الصور بنجاح!")
|
771 |
+
|
772 |
+
# إعدادات متقدمة
|
773 |
+
with st.expander("إعدادات متقدمة", expanded=False):
|
774 |
+
st.markdown("#### إعدادات متقدمة")
|
775 |
+
|
776 |
+
temp_files_retention = st.slider(
|
777 |
+
"مدة الاحتفاظ بالملفات المؤقتة (أيام)",
|
778 |
+
min_value=1,
|
779 |
+
max_value=30,
|
780 |
+
value=7,
|
781 |
+
key="temp_files_retention"
|
782 |
+
)
|
783 |
+
|
784 |
+
max_file_size = st.slider(
|
785 |
+
"الحد الأقصى لحجم الملف (ميجابايت)",
|
786 |
+
min_value=5,
|
787 |
+
max_value=100,
|
788 |
+
value=50,
|
789 |
+
key="max_file_size"
|
790 |
+
)
|
791 |
+
|
792 |
+
parallel_processing = st.checkbox(
|
793 |
+
"تفعيل المعالجة المتوازية",
|
794 |
+
value=True,
|
795 |
+
key="parallel_processing"
|
796 |
+
)
|
797 |
+
|
798 |
+
if st.button("حفظ الإعدادات المتقدمة", key="save_advanced_settings"):
|
799 |
+
st.success("تم حفظ الإعدادات المتقدمة بنجاح!")
|
800 |
+
|
801 |
+
def _save_uploaded_file(self, uploaded_file):
|
802 |
+
"""حفظ الملف المرفوع في مجلد مؤقت"""
|
803 |
+
try:
|
804 |
+
file_path = self.temp_dir / uploaded_file.name
|
805 |
+
with open(file_path, "wb") as f:
|
806 |
+
f.write(uploaded_file.getbuffer())
|
807 |
+
return file_path
|
808 |
+
except Exception as e:
|
809 |
+
st.error(f"حدث خطأ أثناء حفظ الملف: {str(e)}")
|
810 |
+
return None
|
811 |
+
|
812 |
+
def _get_file_info(self, file_path):
|
813 |
+
"""الحصول على معلومات الملف"""
|
814 |
+
file_info = {
|
815 |
+
"type": file_path.suffix[1:].upper(),
|
816 |
+
"size": f"{file_path.stat().st_size / (1024 * 1024):.2f} MB"
|
817 |
+
}
|
818 |
+
|
819 |
+
# محاولة الحصول على عدد الصفحات للملفات المدعومة
|
820 |
+
if file_path.suffix.lower() == ".pdf":
|
821 |
+
# محاكاة عدد الصفحات
|
822 |
+
file_info["pages"] = 10
|
823 |
+
|
824 |
+
return file_info
|
825 |
+
|
826 |
+
def _display_analysis_results(self, results):
|
827 |
+
"""عرض نتائج التحليل"""
|
828 |
+
|
829 |
+
if not results:
|
830 |
+
st.info("لا توجد نتائج للعرض.")
|
831 |
+
return
|
832 |
+
|
833 |
+
# عرض النص المستخرج
|
834 |
+
if "text" in results:
|
835 |
+
with st.expander("النص المستخرج", expanded=False):
|
836 |
+
st.text_area("النص", results["text"], height=200)
|
837 |
+
|
838 |
+
# عرض الجداول المستخرجة
|
839 |
+
if "tables" in results and results["tables"]:
|
840 |
+
with st.expander("الجداول المستخرجة", expanded=True):
|
841 |
+
for i, table in enumerate(results["tables"]):
|
842 |
+
st.markdown(f"**جدول {i+1}: {table.get('عنوان', 'بدون عنوان')}**")
|
843 |
+
|
844 |
+
if "بيانات" in table and table["بيانات"]:
|
845 |
+
# محاولة عرض البيانات كجدول
|
846 |
+
try:
|
847 |
+
df = pd.DataFrame(table["بيانات"])
|
848 |
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
849 |
+
except Exception:
|
850 |
+
st.text(str(table["بيانات"]))
|
851 |
+
else:
|
852 |
+
st.info("لا توجد بيانات في هذا الجدول.")
|
853 |
+
|
854 |
+
# عرض البنود المستخرجة
|
855 |
+
if "items" in results and results["items"]:
|
856 |
+
with st.expander("البنود المستخرجة", expanded=True):
|
857 |
+
items_df = pd.DataFrame(results["items"])
|
858 |
+
st.dataframe(items_df, use_container_width=True, hide_index=True)
|
859 |
+
|
860 |
+
# زر لإرسال البنود إلى وحدة التسعير
|
861 |
+
if st.button("إرسال البنود إلى وحدة التسعير", key="send_extracted_items_button"):
|
862 |
+
st.session_state.extracted_items = results["items"]
|
863 |
+
st.success("تم إرسال البنود المستخرجة إلى وحدة التسعير!")
|
864 |
+
|
865 |
+
# عرض المعلومات الرئيسية
|
866 |
+
if "metadata" in results:
|
867 |
+
with st.expander("المعلومات الرئيسية", expanded=True):
|
868 |
+
metadata = results["metadata"]
|
869 |
+
|
870 |
+
col1, col2 = st.columns(2)
|
871 |
+
|
872 |
+
with col1:
|
873 |
+
st.markdown(f"**عنوان المستند:** {metadata.get('title', 'غير متوفر')}")
|
874 |
+
st.markdown(f"**المؤلف:** {metadata.get('author', 'غير متوفر')}")
|
875 |
+
|
876 |
+
with col2:
|
877 |
+
st.markdown(f"**التاريخ:** {metadata.get('date', 'غير متوفر')}")
|
878 |
+
st.markdown(f"**عدد الصفحات:** {metadata.get('pages', 'غير متوفر')}")
|
879 |
+
|
880 |
+
# عرض هيكل المستند
|
881 |
+
if "structure" in results and "sections" in results["structure"]:
|
882 |
+
with st.expander("هيكل المستند", expanded=False):
|
883 |
+
sections = results["structure"]["sections"]
|
884 |
+
|
885 |
+
for section in sections:
|
886 |
+
indent = " " * (section["level"] * 4)
|
887 |
+
st.markdown(f"{indent}• **{section['title']}** (صفحة {section['page']})", unsafe_allow_html=True)
|
modules/document_comparison/document_comparison_app.py
ADDED
@@ -0,0 +1,1003 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة مقارنة المستندات - نظام تحليل المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import os
|
9 |
+
import sys
|
10 |
+
from pathlib import Path
|
11 |
+
import difflib
|
12 |
+
import re
|
13 |
+
import datetime
|
14 |
+
|
15 |
+
# إضافة مسار المشروع للنظام
|
16 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
17 |
+
|
18 |
+
# استيراد محسن واجهة المستخدم
|
19 |
+
from styling.enhanced_ui import UIEnhancer
|
20 |
+
|
21 |
+
class DocumentComparisonApp:
|
22 |
+
"""تطبيق مقارنة المستندات"""
|
23 |
+
|
24 |
+
def __init__(self):
|
25 |
+
"""تهيئة تطبيق مقارنة المستندات"""
|
26 |
+
self.ui = UIEnhancer(page_title="مقارنة المستندات - نظام تحليل المناقصات", page_icon="📄")
|
27 |
+
self.ui.apply_theme_colors()
|
28 |
+
|
29 |
+
# بيانات المستندات (نموذجية)
|
30 |
+
self.documents_data = [
|
31 |
+
{
|
32 |
+
"id": "DOC001",
|
33 |
+
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
34 |
+
"type": "كراسة شروط",
|
35 |
+
"version": "1.0",
|
36 |
+
"date": "2025-01-15",
|
37 |
+
"size": 2.4,
|
38 |
+
"pages": 45,
|
39 |
+
"related_entity": "T-2025-001",
|
40 |
+
"path": "/documents/T-2025-001/specs_v1.pdf"
|
41 |
+
},
|
42 |
+
{
|
43 |
+
"id": "DOC002",
|
44 |
+
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
45 |
+
"type": "كراسة شروط",
|
46 |
+
"version": "1.1",
|
47 |
+
"date": "2025-02-10",
|
48 |
+
"size": 2.6,
|
49 |
+
"pages": 48,
|
50 |
+
"related_entity": "T-2025-001",
|
51 |
+
"path": "/documents/T-2025-001/specs_v1.1.pdf"
|
52 |
+
},
|
53 |
+
{
|
54 |
+
"id": "DOC003",
|
55 |
+
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
56 |
+
"type": "كراسة شروط",
|
57 |
+
"version": "2.0",
|
58 |
+
"date": "2025-03-05",
|
59 |
+
"size": 2.8,
|
60 |
+
"pages": 52,
|
61 |
+
"related_entity": "T-2025-001",
|
62 |
+
"path": "/documents/T-2025-001/specs_v2.0.pdf"
|
63 |
+
},
|
64 |
+
{
|
65 |
+
"id": "DOC004",
|
66 |
+
"name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
|
67 |
+
"type": "جدول كميات",
|
68 |
+
"version": "1.0",
|
69 |
+
"date": "2025-01-15",
|
70 |
+
"size": 1.2,
|
71 |
+
"pages": 20,
|
72 |
+
"related_entity": "T-2025-001",
|
73 |
+
"path": "/documents/T-2025-001/boq_v1.0.xlsx"
|
74 |
+
},
|
75 |
+
{
|
76 |
+
"id": "DOC005",
|
77 |
+
"name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
|
78 |
+
"type": "جدول كميات",
|
79 |
+
"version": "1.1",
|
80 |
+
"date": "2025-02-20",
|
81 |
+
"size": 1.3,
|
82 |
+
"pages": 22,
|
83 |
+
"related_entity": "T-2025-001",
|
84 |
+
"path": "/documents/T-2025-001/boq_v1.1.xlsx"
|
85 |
+
},
|
86 |
+
{
|
87 |
+
"id": "DOC006",
|
88 |
+
"name": "المخططات - مناقصة إنشاء مبنى إداري",
|
89 |
+
"type": "مخططات",
|
90 |
+
"version": "1.0",
|
91 |
+
"date": "2025-01-15",
|
92 |
+
"size": 15.6,
|
93 |
+
"pages": 30,
|
94 |
+
"related_entity": "T-2025-001",
|
95 |
+
"path": "/documents/T-2025-001/drawings_v1.0.pdf"
|
96 |
+
},
|
97 |
+
{
|
98 |
+
"id": "DOC007",
|
99 |
+
"name": "المخططات - مناقصة إنشاء مبنى إداري",
|
100 |
+
"type": "مخططات",
|
101 |
+
"version": "2.0",
|
102 |
+
"date": "2025-03-10",
|
103 |
+
"size": 18.2,
|
104 |
+
"pages": 35,
|
105 |
+
"related_entity": "T-2025-001",
|
106 |
+
"path": "/documents/T-2025-001/drawings_v2.0.pdf"
|
107 |
+
},
|
108 |
+
{
|
109 |
+
"id": "DOC008",
|
110 |
+
"name": "كراسة الشروط - مناقصة صيانة طرق",
|
111 |
+
"type": "كراسة شروط",
|
112 |
+
"version": "1.0",
|
113 |
+
"date": "2025-02-05",
|
114 |
+
"size": 1.8,
|
115 |
+
"pages": 32,
|
116 |
+
"related_entity": "T-2025-002",
|
117 |
+
"path": "/documents/T-2025-002/specs_v1.0.pdf"
|
118 |
+
},
|
119 |
+
{
|
120 |
+
"id": "DOC009",
|
121 |
+
"name": "كراسة الشروط - مناقصة صيانة طرق",
|
122 |
+
"type": "كراسة شروط",
|
123 |
+
"version": "1.1",
|
124 |
+
"date": "2025-03-15",
|
125 |
+
"size": 1.9,
|
126 |
+
"pages": 34,
|
127 |
+
"related_entity": "T-2025-002",
|
128 |
+
"path": "/documents/T-2025-002/specs_v1.1.pdf"
|
129 |
+
},
|
130 |
+
{
|
131 |
+
"id": "DOC010",
|
132 |
+
"name": "جدول الكميات - مناقصة صيانة طرق",
|
133 |
+
"type": "جدول كميات",
|
134 |
+
"version": "1.0",
|
135 |
+
"date": "2025-02-05",
|
136 |
+
"size": 0.9,
|
137 |
+
"pages": 15,
|
138 |
+
"related_entity": "T-2025-002",
|
139 |
+
"path": "/documents/T-2025-002/boq_v1.0.xlsx"
|
140 |
+
}
|
141 |
+
]
|
142 |
+
|
143 |
+
# بيانات نموذجية لمحتوى المستندات (للعرض فقط)
|
144 |
+
self.sample_document_content = {
|
145 |
+
"DOC001": """
|
146 |
+
# كراسة الشروط والمواصفات
|
147 |
+
## مناقصة إنشاء مبنى إداري
|
148 |
+
|
149 |
+
### 1. مقدمة
|
150 |
+
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
|
151 |
+
|
152 |
+
### 2. نطاق العمل
|
153 |
+
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع، ويشمل ذلك:
|
154 |
+
- أعمال الهيكل الإنشائي
|
155 |
+
- أعمال التشطيبات الداخلية والخارجية
|
156 |
+
- أعمال الكهرباء والميكانيكا
|
157 |
+
- أعمال تنسيق الموقع
|
158 |
+
|
159 |
+
### 3. المواصفات الفنية
|
160 |
+
#### 3.1 أعمال الخرسانة
|
161 |
+
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 30 نيوتن/مم²
|
162 |
+
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
163 |
+
|
164 |
+
#### 3.2 أعمال التشطيبات
|
165 |
+
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
166 |
+
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
167 |
+
|
168 |
+
### 4. الشروط العامة
|
169 |
+
- مدة التنفيذ: 18 شهراً من تاريخ استلام الموقع
|
170 |
+
- غرامة التأخير: 0.1% من قيمة العقد عن كل يوم تأخير
|
171 |
+
- ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
|
172 |
+
""",
|
173 |
+
|
174 |
+
"DOC002": """
|
175 |
+
# كراسة الشروط والمواصفات
|
176 |
+
## مناقصة إنشاء مبنى إداري
|
177 |
+
|
178 |
+
### 1. مقدمة
|
179 |
+
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
|
180 |
+
|
181 |
+
### 2. نطاق العمل
|
182 |
+
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5500 متر مربع، ويشمل ذلك:
|
183 |
+
- أعمال الهيكل الإنشائي
|
184 |
+
- أعمال التشطيبات الداخلية والخارجية
|
185 |
+
- أعمال الكهرباء والميكانيكا
|
186 |
+
- أعمال تنسيق الموقع
|
187 |
+
- أعمال أنظمة الأمن والسلامة
|
188 |
+
|
189 |
+
### 3. المواصفات الفنية
|
190 |
+
#### 3.1 أعمال الخرسانة
|
191 |
+
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 35 نيوتن/مم²
|
192 |
+
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
193 |
+
|
194 |
+
#### 3.2 أعمال التشطيبات
|
195 |
+
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
196 |
+
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
197 |
+
- يجب استخدام زجاج عاكس للحرارة للواجهات
|
198 |
+
|
199 |
+
### 4. الشروط العامة
|
200 |
+
- مدة التنفيذ: 16 شهراً من تاريخ استلام الموقع
|
201 |
+
- غرامة التأخير: 0.15% من قيمة العقد عن كل يوم تأخير
|
202 |
+
- ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
|
203 |
+
""",
|
204 |
+
|
205 |
+
"DOC003": """
|
206 |
+
# كراسة الشروط والمواصفات
|
207 |
+
## مناقصة إنشاء مبنى إداري
|
208 |
+
|
209 |
+
### 1. مقدمة
|
210 |
+
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض وفقاً للمواصفات المعتمدة من الهيئة السعودية للمواصفات والمقاييس.
|
211 |
+
|
212 |
+
### 2. نطاق العمل
|
213 |
+
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
|
214 |
+
- أعمال الهيكل الإنشائي
|
215 |
+
- أعمال التشطيبات الداخلية والخارجية
|
216 |
+
- أعمال الكهرباء والميكانيكا
|
217 |
+
- أعمال تنسيق الموقع
|
218 |
+
- أعمال أنظمة الأمن والسلامة
|
219 |
+
- أعمال أنظمة المباني الذكية
|
220 |
+
|
221 |
+
### 3. المواصفات الفنية
|
222 |
+
#### 3.1 أعمال الخرسانة
|
223 |
+
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
|
224 |
+
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
225 |
+
- يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
|
226 |
+
|
227 |
+
#### 3.2 أعمال التشطيبات
|
228 |
+
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
229 |
+
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
230 |
+
- يجب استخدام زجاج عاكس للحرارة للواجهات
|
231 |
+
- يجب استخدام مواد صديقة للبيئة
|
232 |
+
|
233 |
+
### 4. الشروط العامة
|
234 |
+
- مدة التنفيذ: 15 شهراً من تاريخ استلام الموقع
|
235 |
+
- غرامة التأخير: 0.2% من قيمة العقد عن كل يوم تأخير
|
236 |
+
- ضمان الأعمال: 15 سنوات للهيكل الإنشائي، 7 سنوات للأعمال الميكانيكية والكهربائية
|
237 |
+
|
238 |
+
### 5. متطلبات الاستدامة
|
239 |
+
- يجب أن يحقق المبنى متطلبات الاستدامة وفقاً لمعايير LEED
|
240 |
+
- يجب توفير أنظمة لترشيد استهلاك الطاقة والمياه
|
241 |
+
"""
|
242 |
+
}
|
243 |
+
|
244 |
+
def run(self):
|
245 |
+
"""تشغيل تطبيق مقارنة المستندات"""
|
246 |
+
# إنشاء قائمة العناصر
|
247 |
+
menu_items = [
|
248 |
+
{"name": "لوحة المعلومات", "icon": "house"},
|
249 |
+
{"name": "المناقصات والعقود", "icon": "file-text"},
|
250 |
+
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
251 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
252 |
+
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
253 |
+
{"name": "الموارد والتكاليف", "icon": "people"},
|
254 |
+
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
255 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
256 |
+
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
257 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
258 |
+
{"name": "الإشعارات", "icon": "bell"},
|
259 |
+
{"name": "مقارنة المستندات", "icon": "files"},
|
260 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
261 |
+
{"name": "التقارير", "icon": "bar-chart"},
|
262 |
+
{"name": "الإعدادات", "icon": "gear"}
|
263 |
+
]
|
264 |
+
|
265 |
+
# إنشاء الشريط الجانبي
|
266 |
+
selected = self.ui.create_sidebar(menu_items)
|
267 |
+
|
268 |
+
# إنشاء ترويسة الصفحة
|
269 |
+
self.ui.create_header("مقارنة المستندات", "أدوات متقدمة لمقارنة وتحليل المستندات")
|
270 |
+
|
271 |
+
# إنشاء علامات تبويب للوظائف المختلفة
|
272 |
+
tabs = st.tabs(["مقارنة الإصدارات", "مقارنة المستندات", "تحليل التغييرات", "سجل التغييرات"])
|
273 |
+
|
274 |
+
# علامة تبويب مقارنة الإصدارات
|
275 |
+
with tabs[0]:
|
276 |
+
self.compare_versions()
|
277 |
+
|
278 |
+
# علامة تبويب مقارنة المستندات
|
279 |
+
with tabs[1]:
|
280 |
+
self.compare_documents()
|
281 |
+
|
282 |
+
# علامة تبويب تحليل التغييرات
|
283 |
+
with tabs[2]:
|
284 |
+
self.analyze_changes()
|
285 |
+
|
286 |
+
# علامة تبويب سجل التغييرات
|
287 |
+
with tabs[3]:
|
288 |
+
self.show_change_history()
|
289 |
+
|
290 |
+
def compare_versions(self):
|
291 |
+
"""مقارنة إصدارات المستندات"""
|
292 |
+
st.markdown("### مقارنة إصدارات المستندات")
|
293 |
+
|
294 |
+
# اختيار المناقصة
|
295 |
+
tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
296 |
+
selected_tender = st.selectbox(
|
297 |
+
"اختر المناقصة",
|
298 |
+
options=tender_options
|
299 |
+
)
|
300 |
+
|
301 |
+
# فلترة المستندات حسب المناقصة المختارة
|
302 |
+
filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
|
303 |
+
|
304 |
+
# اختيار نوع المستند
|
305 |
+
doc_types = list(set([doc["type"] for doc in filtered_docs]))
|
306 |
+
selected_type = st.selectbox(
|
307 |
+
"اختر نوع المستند",
|
308 |
+
options=doc_types
|
309 |
+
)
|
310 |
+
|
311 |
+
# فلترة المستندات حسب النوع المختار
|
312 |
+
type_filtered_docs = [doc for doc in filtered_docs if doc["type"] == selected_type]
|
313 |
+
|
314 |
+
# ترتيب المستندات حسب الإصدار
|
315 |
+
type_filtered_docs = sorted(type_filtered_docs, key=lambda x: x["version"])
|
316 |
+
|
317 |
+
if len(type_filtered_docs) < 2:
|
318 |
+
st.warning("يجب توفر إصدارين على الأقل للمقارنة")
|
319 |
+
else:
|
320 |
+
# اختيار الإصدارات للمقارنة
|
321 |
+
col1, col2 = st.columns(2)
|
322 |
+
|
323 |
+
with col1:
|
324 |
+
version_options = [f"{doc['name']} (الإصدار {doc['version']})" for doc in type_filtered_docs]
|
325 |
+
selected_version1_index = st.selectbox(
|
326 |
+
"الإصدار الأول",
|
327 |
+
options=range(len(version_options)),
|
328 |
+
format_func=lambda x: version_options[x]
|
329 |
+
)
|
330 |
+
selected_doc1 = type_filtered_docs[selected_version1_index]
|
331 |
+
|
332 |
+
with col2:
|
333 |
+
remaining_indices = [i for i in range(len(type_filtered_docs)) if i != selected_version1_index]
|
334 |
+
selected_version2_index = st.selectbox(
|
335 |
+
"الإصدار الثاني",
|
336 |
+
options=remaining_indices,
|
337 |
+
format_func=lambda x: version_options[x]
|
338 |
+
)
|
339 |
+
selected_doc2 = type_filtered_docs[selected_version2_index]
|
340 |
+
|
341 |
+
# زر بدء المقارنة
|
342 |
+
if st.button("بدء المقارنة", use_container_width=True):
|
343 |
+
# عرض معلومات المستندات المختارة
|
344 |
+
st.markdown("### معلومات المستندات المختارة")
|
345 |
+
|
346 |
+
col1, col2 = st.columns(2)
|
347 |
+
|
348 |
+
with col1:
|
349 |
+
st.markdown(f"**الإصدار الأول:** {selected_doc1['version']}")
|
350 |
+
st.markdown(f"**التاريخ:** {selected_doc1['date']}")
|
351 |
+
st.markdown(f"**عدد الصفحات:** {selected_doc1['pages']}")
|
352 |
+
st.markdown(f"**الحجم:** {selected_doc1['size']} ميجابايت")
|
353 |
+
|
354 |
+
with col2:
|
355 |
+
st.markdown(f"**الإصدار الثاني:** {selected_doc2['version']}")
|
356 |
+
st.markdown(f"**التاريخ:** {selected_doc2['date']}")
|
357 |
+
st.markdown(f"**عدد الصفحات:** {selected_doc2['pages']}")
|
358 |
+
st.markdown(f"**الحجم:** {selected_doc2['size']} ميجابايت")
|
359 |
+
|
360 |
+
# الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
|
361 |
+
doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
|
362 |
+
doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
|
363 |
+
|
364 |
+
# إجراء المقارنة
|
365 |
+
self.display_comparison(doc1_content, doc2_content)
|
366 |
+
|
367 |
+
def display_comparison(self, text1, text2):
|
368 |
+
"""عرض نتائج المقارنة بين نصين"""
|
369 |
+
st.markdown("### نتائج المقارنة")
|
370 |
+
|
371 |
+
# تقسيم النصوص إلى أسطر
|
372 |
+
lines1 = text1.splitlines()
|
373 |
+
lines2 = text2.splitlines()
|
374 |
+
|
375 |
+
# إجراء المقارنة باستخدام difflib
|
376 |
+
d = difflib.Differ()
|
377 |
+
diff = list(d.compare(lines1, lines2))
|
378 |
+
|
379 |
+
# عرض ملخص التغييرات
|
380 |
+
added = len([line for line in diff if line.startswith('+ ')])
|
381 |
+
removed = len([line for line in diff if line.startswith('- ')])
|
382 |
+
changed = len([line for line in diff if line.startswith('? ')])
|
383 |
+
|
384 |
+
col1, col2, col3 = st.columns(3)
|
385 |
+
|
386 |
+
with col1:
|
387 |
+
self.ui.create_metric_card(
|
388 |
+
"الإضافات",
|
389 |
+
str(added),
|
390 |
+
None,
|
391 |
+
self.ui.COLORS['success']
|
392 |
+
)
|
393 |
+
|
394 |
+
with col2:
|
395 |
+
self.ui.create_metric_card(
|
396 |
+
"الحذف",
|
397 |
+
str(removed),
|
398 |
+
None,
|
399 |
+
self.ui.COLORS['danger']
|
400 |
+
)
|
401 |
+
|
402 |
+
with col3:
|
403 |
+
self.ui.create_metric_card(
|
404 |
+
"التغييرات",
|
405 |
+
str(changed // 2), # تقسيم على 2 لأن كل تغيير يظهر مرتين
|
406 |
+
None,
|
407 |
+
self.ui.COLORS['warning']
|
408 |
+
)
|
409 |
+
|
410 |
+
# عرض التغييرات بالتفصيل
|
411 |
+
st.markdown("### التغييرات بالتفصيل")
|
412 |
+
|
413 |
+
# إنشاء عرض HTML للتغييرات
|
414 |
+
html_diff = []
|
415 |
+
for line in diff:
|
416 |
+
if line.startswith('+ '):
|
417 |
+
html_diff.append(f'<div style="background-color: #e6ffe6; padding: 2px 5px; margin: 2px 0; border-left: 3px solid green;">{line[2:]}</div>')
|
418 |
+
elif line.startswith('- '):
|
419 |
+
html_diff.append(f'<div style="background-color: #ffe6e6; padding: 2px 5px; margin: 2px 0; border-left: 3px solid red;">{line[2:]}</div>')
|
420 |
+
elif line.startswith('? '):
|
421 |
+
# تجاهل أسطر التفاصيل
|
422 |
+
continue
|
423 |
+
else:
|
424 |
+
html_diff.append(f'<div style="padding: 2px 5px; margin: 2px 0;">{line[2:]}</div>')
|
425 |
+
|
426 |
+
# عرض التغييرات
|
427 |
+
st.markdown(''.join(html_diff), unsafe_allow_html=True)
|
428 |
+
|
429 |
+
# خيارات إضافية
|
430 |
+
st.markdown("### خيارات إضافية")
|
431 |
+
|
432 |
+
col1, col2, col3 = st.columns(3)
|
433 |
+
|
434 |
+
with col1:
|
435 |
+
if st.button("تصدير التغييرات", use_container_width=True):
|
436 |
+
st.success("تم تصدير التغييرات بنجاح")
|
437 |
+
|
438 |
+
with col2:
|
439 |
+
if st.button("إنشاء تقرير", use_container_width=True):
|
440 |
+
st.success("تم إنشاء التقرير بنجاح")
|
441 |
+
|
442 |
+
with col3:
|
443 |
+
if st.button("حفظ المقارنة", use_container_width=True):
|
444 |
+
st.success("تم حفظ المقارنة بنجاح")
|
445 |
+
|
446 |
+
def compare_documents(self):
|
447 |
+
"""مقارنة مستندات مختلفة"""
|
448 |
+
st.markdown("### مقارنة مستندات مختلفة")
|
449 |
+
|
450 |
+
# اختيار المستند الأول
|
451 |
+
col1, col2 = st.columns(2)
|
452 |
+
|
453 |
+
with col1:
|
454 |
+
tender1_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
455 |
+
selected_tender1 = st.selectbox(
|
456 |
+
"اختر المناقصة الأولى",
|
457 |
+
options=tender1_options,
|
458 |
+
key="tender1"
|
459 |
+
)
|
460 |
+
|
461 |
+
# فلترة المستندات حسب المناقصة المختارة
|
462 |
+
filtered_docs1 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender1]
|
463 |
+
|
464 |
+
# اختيار المستند
|
465 |
+
doc_options1 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs1]
|
466 |
+
selected_doc1_index = st.selectbox(
|
467 |
+
"اختر المستند الأول",
|
468 |
+
options=range(len(doc_options1)),
|
469 |
+
format_func=lambda x: doc_options1[x],
|
470 |
+
key="doc1"
|
471 |
+
)
|
472 |
+
selected_doc1 = filtered_docs1[selected_doc1_index]
|
473 |
+
|
474 |
+
with col2:
|
475 |
+
tender2_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
476 |
+
selected_tender2 = st.selectbox(
|
477 |
+
"اختر المناقصة الثانية",
|
478 |
+
options=tender2_options,
|
479 |
+
key="tender2"
|
480 |
+
)
|
481 |
+
|
482 |
+
# فلترة المستندات حسب المناقصة المختارة
|
483 |
+
filtered_docs2 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender2]
|
484 |
+
|
485 |
+
# اختيار المستند
|
486 |
+
doc_options2 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs2]
|
487 |
+
selected_doc2_index = st.selectbox(
|
488 |
+
"اختر المستند الثاني",
|
489 |
+
options=range(len(doc_options2)),
|
490 |
+
format_func=lambda x: doc_options2[x],
|
491 |
+
key="doc2"
|
492 |
+
)
|
493 |
+
selected_doc2 = filtered_docs2[selected_doc2_index]
|
494 |
+
|
495 |
+
# خيارات المقارنة
|
496 |
+
st.markdown("### خيارات المقارنة")
|
497 |
+
|
498 |
+
col1, col2, col3 = st.columns(3)
|
499 |
+
|
500 |
+
with col1:
|
501 |
+
comparison_type = st.radio(
|
502 |
+
"نوع المقارنة",
|
503 |
+
options=["مقارنة كاملة", "مقارنة الأقسام المتطابقة فقط", "مقارنة الاختلافات فقط"]
|
504 |
+
)
|
505 |
+
|
506 |
+
with col2:
|
507 |
+
ignore_options = st.multiselect(
|
508 |
+
"تجاهل",
|
509 |
+
options=["المسافات", "علامات الترقيم", "حالة الأحرف", "الأرقام"],
|
510 |
+
default=["المسافات"]
|
511 |
+
)
|
512 |
+
|
513 |
+
with col3:
|
514 |
+
similarity_threshold = st.slider(
|
515 |
+
"عتبة التشابه",
|
516 |
+
min_value=0.0,
|
517 |
+
max_value=1.0,
|
518 |
+
value=0.7,
|
519 |
+
step=0.05
|
520 |
+
)
|
521 |
+
|
522 |
+
# زر بدء المقارنة
|
523 |
+
if st.button("بدء المقارنة بين المستندات", use_container_width=True):
|
524 |
+
# عرض معلومات المستندات المختارة
|
525 |
+
st.markdown("### معلومات المستندات المختارة")
|
526 |
+
|
527 |
+
col1, col2 = st.columns(2)
|
528 |
+
|
529 |
+
with col1:
|
530 |
+
st.markdown(f"**المستند الأول:** {selected_doc1['name']}")
|
531 |
+
st.markdown(f"**الإصدار:** {selected_doc1['version']}")
|
532 |
+
st.markdown(f"**التاريخ:** {selected_doc1['date']}")
|
533 |
+
st.markdown(f"**المناقصة:** {selected_doc1['related_entity']}")
|
534 |
+
|
535 |
+
with col2:
|
536 |
+
st.markdown(f"**المستند الثاني:** {selected_doc2['name']}")
|
537 |
+
st.markdown(f"**الإصدار:** {selected_doc2['version']}")
|
538 |
+
st.markdown(f"**التاريخ:** {selected_doc2['date']}")
|
539 |
+
st.markdown(f"**المناقصة:** {selected_doc2['related_entity']}")
|
540 |
+
|
541 |
+
# الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
|
542 |
+
doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
|
543 |
+
doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
|
544 |
+
|
545 |
+
# إجراء المقارنة
|
546 |
+
self.display_document_comparison(doc1_content, doc2_content, comparison_type, ignore_options, similarity_threshold)
|
547 |
+
|
548 |
+
def display_document_comparison(self, text1, text2, comparison_type, ignore_options, similarity_threshold):
|
549 |
+
"""عرض نتائج المقارنة بين مستندين"""
|
550 |
+
st.markdown("### نتائج المقارنة بين المستندين")
|
551 |
+
|
552 |
+
# تقسيم النصوص إلى أقسام (في هذا المثال، نستخدم العناوين كفواصل للأقسام)
|
553 |
+
sections1 = self.split_into_sections(text1)
|
554 |
+
sections2 = self.split_into_sections(text2)
|
555 |
+
|
556 |
+
# حساب نسبة التشابه الإجمالية
|
557 |
+
similarity = difflib.SequenceMatcher(None, text1, text2).ratio()
|
558 |
+
|
559 |
+
# عرض نسبة التشابه
|
560 |
+
st.markdown(f"**نسبة التشابه الإجمالية:** {similarity:.2%}")
|
561 |
+
|
562 |
+
# عرض مقارنة الأقسام
|
563 |
+
st.markdown("### مقارنة الأقسام")
|
564 |
+
|
565 |
+
# إنشاء جدول لمقارنة الأقسام
|
566 |
+
section_comparisons = []
|
567 |
+
|
568 |
+
for section1_title, section1_content in sections1.items():
|
569 |
+
best_match = None
|
570 |
+
best_similarity = 0
|
571 |
+
|
572 |
+
for section2_title, section2_content in sections2.items():
|
573 |
+
# حساب نسبة التشابه بين عناوين الأقسام
|
574 |
+
title_similarity = difflib.SequenceMatcher(None, section1_title, section2_title).ratio()
|
575 |
+
|
576 |
+
# حساب نسبة التشابه بين محتوى الأقسام
|
577 |
+
content_similarity = difflib.SequenceMatcher(None, section1_content, section2_content).ratio()
|
578 |
+
|
579 |
+
# حساب متوسط نسبة التشابه
|
580 |
+
avg_similarity = (title_similarity + content_similarity) / 2
|
581 |
+
|
582 |
+
if avg_similarity > best_similarity:
|
583 |
+
best_similarity = avg_similarity
|
584 |
+
best_match = {
|
585 |
+
"title": section2_title,
|
586 |
+
"content": section2_content,
|
587 |
+
"similarity": avg_similarity
|
588 |
+
}
|
589 |
+
|
590 |
+
# إضافة المقارنة إلى القائمة
|
591 |
+
if best_match and best_similarity >= similarity_threshold:
|
592 |
+
section_comparisons.append({
|
593 |
+
"section1_title": section1_title,
|
594 |
+
"section2_title": best_match["title"],
|
595 |
+
"similarity": best_similarity
|
596 |
+
})
|
597 |
+
else:
|
598 |
+
section_comparisons.append({
|
599 |
+
"section1_title": section1_title,
|
600 |
+
"section2_title": "غير موجود",
|
601 |
+
"similarity": 0
|
602 |
+
})
|
603 |
+
|
604 |
+
# إضافة الأقسام الموجودة في المستند الثاني فقط
|
605 |
+
for section2_title, section2_content in sections2.items():
|
606 |
+
if not any(comp["section2_title"] == section2_title for comp in section_comparisons):
|
607 |
+
section_comparisons.append({
|
608 |
+
"section1_title": "غير موجود",
|
609 |
+
"section2_title": section2_title,
|
610 |
+
"similarity": 0
|
611 |
+
})
|
612 |
+
|
613 |
+
# عرض جدول المقارنة
|
614 |
+
section_df = pd.DataFrame(section_comparisons)
|
615 |
+
section_df = section_df.rename(columns={
|
616 |
+
"section1_title": "القسم في المستند الأول",
|
617 |
+
"section2_title": "القسم في المستند الثاني",
|
618 |
+
"similarity": "نسبة التشابه"
|
619 |
+
})
|
620 |
+
|
621 |
+
# تنسيق نسبة التشابه
|
622 |
+
section_df["نسبة التشابه"] = section_df["نسبة التشابه"].apply(lambda x: f"{x:.2%}")
|
623 |
+
|
624 |
+
st.dataframe(
|
625 |
+
section_df,
|
626 |
+
use_container_width=True,
|
627 |
+
hide_index=True
|
628 |
+
)
|
629 |
+
|
630 |
+
# عرض تفاصيل المقارنة
|
631 |
+
st.markdown("### تفاصيل المقارنة")
|
632 |
+
|
633 |
+
# اختيار قسم للمقارنة التفصيلية
|
634 |
+
selected_section = st.selectbox(
|
635 |
+
"اختر قسماً للمقارنة التفصيلية",
|
636 |
+
options=[comp["section1_title"] for comp in section_comparisons if comp["section1_title"] != "غير موجود"]
|
637 |
+
)
|
638 |
+
|
639 |
+
# العثور على القسم المقابل في المستند الثاني
|
640 |
+
matching_comparison = next((comp for comp in section_comparisons if comp["section1_title"] == selected_section), None)
|
641 |
+
|
642 |
+
if matching_comparison and matching_comparison["section2_title"] != "غير موجود":
|
643 |
+
# الحصول على محتوى القسمين
|
644 |
+
section1_content = sections1[selected_section]
|
645 |
+
section2_content = sections2[matching_comparison["section2_title"]]
|
646 |
+
|
647 |
+
# عرض المقارنة التفصيلية
|
648 |
+
self.display_comparison(section1_content, section2_content)
|
649 |
+
else:
|
650 |
+
st.warning("القسم المحدد غير موجود في المستند الثاني")
|
651 |
+
|
652 |
+
def split_into_sections(self, text):
|
653 |
+
"""تقسيم النص إلى أقسام باستخدام العناوين"""
|
654 |
+
sections = {}
|
655 |
+
current_section = None
|
656 |
+
current_content = []
|
657 |
+
|
658 |
+
for line in text.splitlines():
|
659 |
+
# البحث عن العناوين (الأسطر التي تبدأ بـ #)
|
660 |
+
if line.strip().startswith('#'):
|
661 |
+
# حفظ القسم السابق إذا وجد
|
662 |
+
if current_section:
|
663 |
+
sections[current_section] = '\n'.join(current_content)
|
664 |
+
|
665 |
+
# بدء قسم جديد
|
666 |
+
current_section = line.strip()
|
667 |
+
current_content = []
|
668 |
+
elif current_section:
|
669 |
+
# إضافة السطر إلى محتوى القسم الحالي
|
670 |
+
current_content.append(line)
|
671 |
+
|
672 |
+
# حفظ القسم الأخير
|
673 |
+
if current_section:
|
674 |
+
sections[current_section] = '\n'.join(current_content)
|
675 |
+
|
676 |
+
return sections
|
677 |
+
|
678 |
+
def analyze_changes(self):
|
679 |
+
"""تحليل التغييرات في المستندات"""
|
680 |
+
st.markdown("### تحليل التغييرات في المستندات")
|
681 |
+
|
682 |
+
# اختيار المناقصة
|
683 |
+
tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
684 |
+
selected_tender = st.selectbox(
|
685 |
+
"اختر المناقصة",
|
686 |
+
options=tender_options,
|
687 |
+
key="analyze_tender"
|
688 |
+
)
|
689 |
+
|
690 |
+
# فلترة المستندات حسب المناقصة المختارة
|
691 |
+
filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
|
692 |
+
|
693 |
+
# تجميع المستندات حسب النوع
|
694 |
+
doc_types = {}
|
695 |
+
for doc in filtered_docs:
|
696 |
+
if doc["type"] not in doc_types:
|
697 |
+
doc_types[doc["type"]] = []
|
698 |
+
doc_types[doc["type"]].append(doc)
|
699 |
+
|
700 |
+
# عرض تحليل التغييرات لكل نوع مستند
|
701 |
+
for doc_type, docs in doc_types.items():
|
702 |
+
if len(docs) > 1:
|
703 |
+
with st.expander(f"تحليل التغييرات في {doc_type}"):
|
704 |
+
# ترتيب المستندات حسب الإصدار
|
705 |
+
sorted_docs = sorted(docs, key=lambda x: x["version"])
|
706 |
+
|
707 |
+
# عرض معلومات الإصدارات
|
708 |
+
st.markdown(f"**عدد الإصدارات:** {len(sorted_docs)}")
|
709 |
+
st.markdown(f"**أول إصدار:** {sorted_docs[0]['version']} ({sorted_docs[0]['date']})")
|
710 |
+
st.markdown(f"**آخر إصدار:** {sorted_docs[-1]['version']} ({sorted_docs[-1]['date']})")
|
711 |
+
|
712 |
+
# حساب التغييرات بين الإصدارات
|
713 |
+
changes = []
|
714 |
+
for i in range(1, len(sorted_docs)):
|
715 |
+
prev_doc = sorted_docs[i-1]
|
716 |
+
curr_doc = sorted_docs[i]
|
717 |
+
|
718 |
+
# حساب التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
|
719 |
+
page_diff = curr_doc["pages"] - prev_doc["pages"]
|
720 |
+
size_diff = curr_doc["size"] - prev_doc["size"]
|
721 |
+
|
722 |
+
changes.append({
|
723 |
+
"from_version": prev_doc["version"],
|
724 |
+
"to_version": curr_doc["version"],
|
725 |
+
"date": curr_doc["date"],
|
726 |
+
"page_diff": page_diff,
|
727 |
+
"size_diff": size_diff
|
728 |
+
})
|
729 |
+
|
730 |
+
# عرض جدول التغييرات
|
731 |
+
changes_df = pd.DataFrame(changes)
|
732 |
+
changes_df = changes_df.rename(columns={
|
733 |
+
"from_version": "من الإصدار",
|
734 |
+
"to_version": "إلى الإصدار",
|
735 |
+
"date": "تاريخ التغيير",
|
736 |
+
"page_diff": "التغيير في عدد الصفحات",
|
737 |
+
"size_diff": "التغيير في الحجم (ميجابايت)"
|
738 |
+
})
|
739 |
+
|
740 |
+
st.dataframe(
|
741 |
+
changes_df,
|
742 |
+
use_container_width=True,
|
743 |
+
hide_index=True
|
744 |
+
)
|
745 |
+
|
746 |
+
# عرض رسم بياني للتغييرات
|
747 |
+
st.markdown("#### تطور حجم المستند عبر الإصدارات")
|
748 |
+
|
749 |
+
versions = [doc["version"] for doc in sorted_docs]
|
750 |
+
sizes = [doc["size"] for doc in sorted_docs]
|
751 |
+
|
752 |
+
chart_data = pd.DataFrame({
|
753 |
+
"الإصدار": versions,
|
754 |
+
"الحجم (ميجابايت)": sizes
|
755 |
+
})
|
756 |
+
|
757 |
+
st.line_chart(chart_data.set_index("الإصدار"))
|
758 |
+
|
759 |
+
# عرض رسم بياني لعدد الصفحات
|
760 |
+
st.markdown("#### تطور عدد الصفحات عبر الإصدارات")
|
761 |
+
|
762 |
+
pages = [doc["pages"] for doc in sorted_docs]
|
763 |
+
|
764 |
+
chart_data = pd.DataFrame({
|
765 |
+
"الإصدار": versions,
|
766 |
+
"عدد الصفحات": pages
|
767 |
+
})
|
768 |
+
|
769 |
+
st.line_chart(chart_data.set_index("الإصدار"))
|
770 |
+
|
771 |
+
# تحليل التغييرات الشاملة
|
772 |
+
st.markdown("### تحليل التغييرات الشاملة")
|
773 |
+
|
774 |
+
# حساب إجمالي التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
|
775 |
+
total_docs = len(filtered_docs)
|
776 |
+
total_versions = sum(len(docs) for docs in doc_types.values())
|
777 |
+
avg_versions = total_versions / len(doc_types) if doc_types else 0
|
778 |
+
|
779 |
+
col1, col2, col3 = st.columns(3)
|
780 |
+
|
781 |
+
with col1:
|
782 |
+
self.ui.create_metric_card(
|
783 |
+
"إجمالي المستندات",
|
784 |
+
str(total_docs),
|
785 |
+
None,
|
786 |
+
self.ui.COLORS['primary']
|
787 |
+
)
|
788 |
+
|
789 |
+
with col2:
|
790 |
+
self.ui.create_metric_card(
|
791 |
+
"إجمالي الإصدارات",
|
792 |
+
str(total_versions),
|
793 |
+
None,
|
794 |
+
self.ui.COLORS['secondary']
|
795 |
+
)
|
796 |
+
|
797 |
+
with col3:
|
798 |
+
self.ui.create_metric_card(
|
799 |
+
"متوسط الإصدارات لكل نوع",
|
800 |
+
f"{avg_versions:.1f}",
|
801 |
+
None,
|
802 |
+
self.ui.COLORS['accent']
|
803 |
+
)
|
804 |
+
|
805 |
+
# عرض توزيع التغييرات حسب النوع
|
806 |
+
st.markdown("#### توزيع الإصدارات حسب نوع المستند")
|
807 |
+
|
808 |
+
type_counts = {doc_type: len(docs) for doc_type, docs in doc_types.items()}
|
809 |
+
|
810 |
+
chart_data = pd.DataFrame({
|
811 |
+
"نوع المستند": list(type_counts.keys()),
|
812 |
+
"عدد الإصدارات": list(type_counts.values())
|
813 |
+
})
|
814 |
+
|
815 |
+
st.bar_chart(chart_data.set_index("نوع المستند"))
|
816 |
+
|
817 |
+
def show_change_history(self):
|
818 |
+
"""عرض سجل التغييرات"""
|
819 |
+
st.markdown("### سجل التغييرات")
|
820 |
+
|
821 |
+
# إنشاء بيانات نموذجية لسجل التغييرات
|
822 |
+
change_history = [
|
823 |
+
{
|
824 |
+
"id": "CH001",
|
825 |
+
"document_id": "DOC001",
|
826 |
+
"document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
827 |
+
"from_version": "1.0",
|
828 |
+
"to_version": "1.1",
|
829 |
+
"change_date": "2025-02-10",
|
830 |
+
"change_type": "تحديث",
|
831 |
+
"changed_by": "أحمد محمد",
|
832 |
+
"description": "تحديث المواصفات الفنية وشروط التنفيذ",
|
833 |
+
"sections_changed": ["��طاق العمل", "المواصفات الفنية", "الشروط العامة"]
|
834 |
+
},
|
835 |
+
{
|
836 |
+
"id": "CH002",
|
837 |
+
"document_id": "DOC002",
|
838 |
+
"document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
839 |
+
"from_version": "1.1",
|
840 |
+
"to_version": "2.0",
|
841 |
+
"change_date": "2025-03-05",
|
842 |
+
"change_type": "تحديث رئيسي",
|
843 |
+
"changed_by": "سارة عبدالله",
|
844 |
+
"description": "إضافة متطلبات الاستدامة وتحديث المواصفات الفنية",
|
845 |
+
"sections_changed": ["المواصفات الفنية", "الشروط العامة", "متطلبات الاستدامة"]
|
846 |
+
},
|
847 |
+
{
|
848 |
+
"id": "CH003",
|
849 |
+
"document_id": "DOC004",
|
850 |
+
"document_name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
|
851 |
+
"from_version": "1.0",
|
852 |
+
"to_version": "1.1",
|
853 |
+
"change_date": "2025-02-20",
|
854 |
+
"change_type": "تحديث",
|
855 |
+
"changed_by": "خالد عمر",
|
856 |
+
"description": "تحديث الكميات وإضافة بنود جديدة",
|
857 |
+
"sections_changed": ["أعمال الهيكل الإنشائي", "أعمال التشطيبات", "أعمال الكهرباء"]
|
858 |
+
},
|
859 |
+
{
|
860 |
+
"id": "CH004",
|
861 |
+
"document_id": "DOC006",
|
862 |
+
"document_name": "المخططات - مناقصة إنشاء مبنى إداري",
|
863 |
+
"from_version": "1.0",
|
864 |
+
"to_version": "2.0",
|
865 |
+
"change_date": "2025-03-10",
|
866 |
+
"change_type": "تحديث رئيسي",
|
867 |
+
"changed_by": "محمد علي",
|
868 |
+
"description": "تحديث المخططات المعمارية والإنشائية",
|
869 |
+
"sections_changed": ["المخططات المعمارية", "المخططات الإنشائية", "مخططات الكهرباء"]
|
870 |
+
},
|
871 |
+
{
|
872 |
+
"id": "CH005",
|
873 |
+
"document_id": "DOC008",
|
874 |
+
"document_name": "كراسة الشروط - مناقصة صيانة طرق",
|
875 |
+
"from_version": "1.0",
|
876 |
+
"to_version": "1.1",
|
877 |
+
"change_date": "2025-03-15",
|
878 |
+
"change_type": "تحديث",
|
879 |
+
"changed_by": "فاطمة أحمد",
|
880 |
+
"description": "تحديث المواصفات الفنية وشروط التنفيذ",
|
881 |
+
"sections_changed": ["نطاق العمل", "المواصفات الفنية", "الشروط العامة"]
|
882 |
+
}
|
883 |
+
]
|
884 |
+
|
885 |
+
# إنشاء فلاتر للسجل
|
886 |
+
col1, col2, col3 = st.columns(3)
|
887 |
+
|
888 |
+
with col1:
|
889 |
+
document_filter = st.selectbox(
|
890 |
+
"المستند",
|
891 |
+
options=["الكل"] + list(set([ch["document_name"] for ch in change_history]))
|
892 |
+
)
|
893 |
+
|
894 |
+
with col2:
|
895 |
+
change_type_filter = st.selectbox(
|
896 |
+
"نوع التغيير",
|
897 |
+
options=["الكل"] + list(set([ch["change_type"] for ch in change_history]))
|
898 |
+
)
|
899 |
+
|
900 |
+
with col3:
|
901 |
+
date_range = st.date_input(
|
902 |
+
"نطاق التاريخ",
|
903 |
+
value=(
|
904 |
+
datetime.datetime.strptime("2025-01-01", "%Y-%m-%d").date(),
|
905 |
+
datetime.datetime.strptime("2025-12-31", "%Y-%m-%d").date()
|
906 |
+
)
|
907 |
+
)
|
908 |
+
|
909 |
+
# تطبيق الفلاتر
|
910 |
+
filtered_history = change_history
|
911 |
+
|
912 |
+
if document_filter != "الكل":
|
913 |
+
filtered_history = [ch for ch in filtered_history if ch["document_name"] == document_filter]
|
914 |
+
|
915 |
+
if change_type_filter != "الكل":
|
916 |
+
filtered_history = [ch for ch in filtered_history if ch["change_type"] == change_type_filter]
|
917 |
+
|
918 |
+
if len(date_range) == 2:
|
919 |
+
start_date, end_date = date_range
|
920 |
+
filtered_history = [
|
921 |
+
ch for ch in filtered_history
|
922 |
+
if start_date <= datetime.datetime.strptime(ch["change_date"], "%Y-%m-%d").date() <= end_date
|
923 |
+
]
|
924 |
+
|
925 |
+
# عرض سجل التغييرات
|
926 |
+
if not filtered_history:
|
927 |
+
st.info("لا توجد تغييرات تطابق الفلاتر المحددة")
|
928 |
+
else:
|
929 |
+
# تحويل البيانات إلى DataFrame
|
930 |
+
history_df = pd.DataFrame(filtered_history)
|
931 |
+
|
932 |
+
# إعادة ترتيب الأعمدة وتغيير أسمائها
|
933 |
+
display_df = history_df[[
|
934 |
+
"id", "document_name", "from_version", "to_version", "change_date", "change_type", "changed_by", "description"
|
935 |
+
]].rename(columns={
|
936 |
+
"id": "الرقم",
|
937 |
+
"document_name": "اسم المستند",
|
938 |
+
"from_version": "من الإصدار",
|
939 |
+
"to_version": "إلى الإصدار",
|
940 |
+
"change_date": "تاريخ التغيير",
|
941 |
+
"change_type": "نوع التغيير",
|
942 |
+
"changed_by": "بواسطة",
|
943 |
+
"description": "الوصف"
|
944 |
+
})
|
945 |
+
|
946 |
+
# عرض الجدول
|
947 |
+
st.dataframe(
|
948 |
+
display_df,
|
949 |
+
use_container_width=True,
|
950 |
+
hide_index=True
|
951 |
+
)
|
952 |
+
|
953 |
+
# عرض تفاصيل التغيير المحدد
|
954 |
+
st.markdown("### تفاصيل التغيير")
|
955 |
+
|
956 |
+
selected_change_id = st.selectbox(
|
957 |
+
"اختر تغييراً لعرض التفاصيل",
|
958 |
+
options=[ch["id"] for ch in filtered_history],
|
959 |
+
format_func=lambda x: next((f"{ch['id']} - {ch['document_name']} ({ch['from_version']} إلى {ch['to_version']})" for ch in filtered_history if ch["id"] == x), "")
|
960 |
+
)
|
961 |
+
|
962 |
+
# العثور على التغيير المحدد
|
963 |
+
selected_change = next((ch for ch in filtered_history if ch["id"] == selected_change_id), None)
|
964 |
+
|
965 |
+
if selected_change:
|
966 |
+
col1, col2 = st.columns(2)
|
967 |
+
|
968 |
+
with col1:
|
969 |
+
st.markdown(f"**المستند:** {selected_change['document_name']}")
|
970 |
+
st.markdown(f"**من الإصدار:** {selected_change['from_version']}")
|
971 |
+
st.markdown(f"**إلى الإصدار:** {selected_change['to_version']}")
|
972 |
+
st.markdown(f"**تاريخ التغيير:** {selected_change['change_date']}")
|
973 |
+
|
974 |
+
with col2:
|
975 |
+
st.markdown(f"**نوع التغيير:** {selected_change['change_type']}")
|
976 |
+
st.markdown(f"**بواسطة:** {selected_change['changed_by']}")
|
977 |
+
st.markdown(f"**الوصف:** {selected_change['description']}")
|
978 |
+
|
979 |
+
# عرض الأقسام التي تم تغييرها
|
980 |
+
st.markdown("#### الأقسام التي تم تغييرها")
|
981 |
+
|
982 |
+
for section in selected_change["sections_changed"]:
|
983 |
+
st.markdown(f"- {section}")
|
984 |
+
|
985 |
+
# أزرار الإجراءات
|
986 |
+
col1, col2, col3 = st.columns(3)
|
987 |
+
|
988 |
+
with col1:
|
989 |
+
if st.button("عرض التغييرات بالتفصيل", use_container_width=True):
|
990 |
+
st.success("تم فتح التغييرات بالتفصيل")
|
991 |
+
|
992 |
+
with col2:
|
993 |
+
if st.button("إنشاء تقرير", use_container_width=True):
|
994 |
+
st.success("تم إنشاء التقرير بنجاح")
|
995 |
+
|
996 |
+
with col3:
|
997 |
+
if st.button("تصدير التغييرات", use_container_width=True):
|
998 |
+
st.success("تم تصدير التغييرات بنجاح")
|
999 |
+
|
1000 |
+
# تشغيل التطبيق
|
1001 |
+
if __name__ == "__main__":
|
1002 |
+
doc_comparison_app = DocumentComparisonApp()
|
1003 |
+
doc_comparison_app.run()
|
modules/maps/maps_app.py
ADDED
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة الخرائط والمواقع - نظام تحليل المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import folium
|
9 |
+
from streamlit_folium import folium_static
|
10 |
+
import json
|
11 |
+
import os
|
12 |
+
import sys
|
13 |
+
from pathlib import Path
|
14 |
+
|
15 |
+
# إضافة مسار المشروع للنظام
|
16 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
17 |
+
|
18 |
+
# استيراد محسن واجهة المستخدم
|
19 |
+
from styling.enhanced_ui import UIEnhancer
|
20 |
+
|
21 |
+
class MapsApp:
|
22 |
+
"""تطبيق الخرائط والمواقع"""
|
23 |
+
|
24 |
+
def __init__(self):
|
25 |
+
"""تهيئة تطبيق الخرائط والمواقع"""
|
26 |
+
self.ui = UIEnhancer(page_title="الخرائط والمواقع - نظام تحليل المناقصات", page_icon="🗺️")
|
27 |
+
self.ui.apply_theme_colors()
|
28 |
+
|
29 |
+
# بيانات المشاريع (نموذجية)
|
30 |
+
self.projects_data = [
|
31 |
+
{
|
32 |
+
"id": "P001",
|
33 |
+
"name": "إنشاء مبنى إداري - الرياض",
|
34 |
+
"location": "الرياض",
|
35 |
+
"coordinates": [24.7136, 46.6753],
|
36 |
+
"status": "جاري التنفيذ",
|
37 |
+
"budget": 15000000,
|
38 |
+
"completion": 45,
|
39 |
+
"client": "وزارة الإسكان",
|
40 |
+
"start_date": "2024-10-15",
|
41 |
+
"end_date": "2025-12-30"
|
42 |
+
},
|
43 |
+
{
|
44 |
+
"id": "P002",
|
45 |
+
"name": "تطوير طريق الملك فهد - جدة",
|
46 |
+
"location": "جدة",
|
47 |
+
"coordinates": [21.5433, 39.1728],
|
48 |
+
"status": "قيد الدراسة",
|
49 |
+
"budget": 8500000,
|
50 |
+
"completion": 0,
|
51 |
+
"client": "أمانة جدة",
|
52 |
+
"start_date": "2025-05-01",
|
53 |
+
"end_date": "2026-02-28"
|
54 |
+
},
|
55 |
+
{
|
56 |
+
"id": "P003",
|
57 |
+
"name": "إنشاء مجمع سكني - الدمام",
|
58 |
+
"location": "الدمام",
|
59 |
+
"coordinates": [26.4207, 50.0888],
|
60 |
+
"status": "مكتمل",
|
61 |
+
"budget": 22000000,
|
62 |
+
"completion": 100,
|
63 |
+
"client": "شركة الإسكان للتطوير",
|
64 |
+
"start_date": "2023-08-10",
|
65 |
+
"end_date": "2025-01-15"
|
66 |
+
},
|
67 |
+
{
|
68 |
+
"id": "P004",
|
69 |
+
"name": "بناء مدرسة - أبها",
|
70 |
+
"location": "أبها",
|
71 |
+
"coordinates": [18.2164, 42.5053],
|
72 |
+
"status": "جاري التنفيذ",
|
73 |
+
"budget": 5200000,
|
74 |
+
"completion": 75,
|
75 |
+
"client": "وزارة التعليم",
|
76 |
+
"start_date": "2024-06-20",
|
77 |
+
"end_date": "2025-07-30"
|
78 |
+
},
|
79 |
+
{
|
80 |
+
"id": "P005",
|
81 |
+
"name": "تطوير شبكة مياه - المدينة المنورة",
|
82 |
+
"location": "المدينة المنورة",
|
83 |
+
"coordinates": [24.5247, 39.5692],
|
84 |
+
"status": "جاري التنفيذ",
|
85 |
+
"budget": 12800000,
|
86 |
+
"completion": 30,
|
87 |
+
"client": "شركة المياه الوطنية",
|
88 |
+
"start_date": "2024-11-05",
|
89 |
+
"end_date": "2026-03-15"
|
90 |
+
}
|
91 |
+
]
|
92 |
+
|
93 |
+
def run(self):
|
94 |
+
"""تشغيل تطبيق الخرائط والمواقع"""
|
95 |
+
# إنشاء قائمة العناصر
|
96 |
+
menu_items = [
|
97 |
+
{"name": "لوحة المعلومات", "icon": "house"},
|
98 |
+
{"name": "المناقصات والعقود", "icon": "file-text"},
|
99 |
+
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
100 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
101 |
+
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
102 |
+
{"name": "الموارد والتكاليف", "icon": "people"},
|
103 |
+
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
104 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
105 |
+
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
106 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
107 |
+
{"name": "الإشعارات", "icon": "bell"},
|
108 |
+
{"name": "مقارنة المستندات", "icon": "files"},
|
109 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
110 |
+
{"name": "التقارير", "icon": "bar-chart"},
|
111 |
+
{"name": "الإعدادات", "icon": "gear"}
|
112 |
+
]
|
113 |
+
|
114 |
+
# إنشاء الشريط الجانبي
|
115 |
+
selected = self.ui.create_sidebar(menu_items)
|
116 |
+
|
117 |
+
# إنشاء ترويسة الصفحة
|
118 |
+
self.ui.create_header("الخرائط والمواقع", "عرض وإدارة مواقع المشاريع")
|
119 |
+
|
120 |
+
# إنشاء علامات تبويب للوظائ�� المختلفة
|
121 |
+
tabs = st.tabs(["خريطة المشاريع", "تفاصيل المواقع", "إضافة موقع جديد", "تحليل المناطق"])
|
122 |
+
|
123 |
+
# علامة تبويب خريطة المشاريع
|
124 |
+
with tabs[0]:
|
125 |
+
self.show_projects_map()
|
126 |
+
|
127 |
+
# علامة تبويب تفاصيل المواقع
|
128 |
+
with tabs[1]:
|
129 |
+
self.show_location_details()
|
130 |
+
|
131 |
+
# علامة تبويب إضافة موقع جديد
|
132 |
+
with tabs[2]:
|
133 |
+
self.add_new_location()
|
134 |
+
|
135 |
+
# علامة تبويب تحليل المناطق
|
136 |
+
with tabs[3]:
|
137 |
+
self.analyze_regions()
|
138 |
+
|
139 |
+
def show_projects_map(self):
|
140 |
+
"""عرض خريطة المشاريع"""
|
141 |
+
# إنشاء فلاتر للخريطة
|
142 |
+
col1, col2, col3 = st.columns(3)
|
143 |
+
|
144 |
+
with col1:
|
145 |
+
status_filter = st.multiselect(
|
146 |
+
"حالة المشروع",
|
147 |
+
options=["الكل", "جاري التنفيذ", "قيد الدراسة", "مكتمل"],
|
148 |
+
default=["الكل"]
|
149 |
+
)
|
150 |
+
|
151 |
+
with col2:
|
152 |
+
location_filter = st.multiselect(
|
153 |
+
"الموقع",
|
154 |
+
options=["الكل"] + list(set([p["location"] for p in self.projects_data])),
|
155 |
+
default=["الكل"]
|
156 |
+
)
|
157 |
+
|
158 |
+
with col3:
|
159 |
+
budget_range = st.slider(
|
160 |
+
"نطاق الميزانية (مليون ريال)",
|
161 |
+
0.0, 25.0, (0.0, 25.0),
|
162 |
+
step=0.5
|
163 |
+
)
|
164 |
+
|
165 |
+
# تطبيق الفلاتر
|
166 |
+
filtered_projects = self.projects_data
|
167 |
+
|
168 |
+
if "الكل" not in status_filter and status_filter:
|
169 |
+
filtered_projects = [p for p in filtered_projects if p["status"] in status_filter]
|
170 |
+
|
171 |
+
if "الكل" not in location_filter and location_filter:
|
172 |
+
filtered_projects = [p for p in filtered_projects if p["location"] in location_filter]
|
173 |
+
|
174 |
+
filtered_projects = [p for p in filtered_projects if budget_range[0] * 1000000 <= p["budget"] <= budget_range[1] * 1000000]
|
175 |
+
|
176 |
+
# إنشاء الخريطة
|
177 |
+
st.markdown("### خريطة المشاريع")
|
178 |
+
|
179 |
+
# تحديد مركز الخريطة (وسط المملكة العربية السعودية تقريباً)
|
180 |
+
center = [24.0, 45.0]
|
181 |
+
|
182 |
+
# إنشاء خريطة folium
|
183 |
+
m = folium.Map(location=center, zoom_start=5, tiles="OpenStreetMap")
|
184 |
+
|
185 |
+
# إضافة المشاريع إلى الخريطة
|
186 |
+
for project in filtered_projects:
|
187 |
+
# تحديد لون العلامة بناءً على حالة المشروع
|
188 |
+
if project["status"] == "جاري التنفيذ":
|
189 |
+
color = "blue"
|
190 |
+
elif project["status"] == "قيد الدراسة":
|
191 |
+
color = "orange"
|
192 |
+
elif project["status"] == "مكتمل":
|
193 |
+
color = "green"
|
194 |
+
else:
|
195 |
+
color = "gray"
|
196 |
+
|
197 |
+
# إنشاء نص النافذة المنبثقة
|
198 |
+
popup_text = f"""
|
199 |
+
<div dir="rtl" style="text-align: right; width: 200px;">
|
200 |
+
<h4>{project['name']}</h4>
|
201 |
+
<p><strong>الحالة:</strong> {project['status']}</p>
|
202 |
+
<p><strong>الميزانية:</strong> {project['budget']:,} ريال</p>
|
203 |
+
<p><strong>نسبة الإنجاز:</strong> {project['completion']}%</p>
|
204 |
+
<p><strong>العميل:</strong> {project['client']}</p>
|
205 |
+
<p><strong>تاريخ البدء:</strong> {project['start_date']}</p>
|
206 |
+
<p><strong>تاريخ الانتهاء:</strong> {project['end_date']}</p>
|
207 |
+
<a href="#" onclick="alert('تم فتح تفاصيل المشروع');">عرض التفاصيل</a>
|
208 |
+
</div>
|
209 |
+
"""
|
210 |
+
|
211 |
+
# إضافة علامة للمشروع
|
212 |
+
folium.Marker(
|
213 |
+
location=project["coordinates"],
|
214 |
+
popup=folium.Popup(popup_text, max_width=300),
|
215 |
+
tooltip=project["name"],
|
216 |
+
icon=folium.Icon(color=color, icon="info-sign")
|
217 |
+
).add_to(m)
|
218 |
+
|
219 |
+
# عرض الخريطة
|
220 |
+
folium_static(m, width=1000, height=500)
|
221 |
+
|
222 |
+
# عرض إحصائيات المشاريع
|
223 |
+
st.markdown("### إحصائيات المشاريع")
|
224 |
+
|
225 |
+
col1, col2, col3, col4 = st.columns(4)
|
226 |
+
|
227 |
+
with col1:
|
228 |
+
self.ui.create_metric_card(
|
229 |
+
"إجمالي المشاريع",
|
230 |
+
str(len(filtered_projects)),
|
231 |
+
None,
|
232 |
+
self.ui.COLORS['primary']
|
233 |
+
)
|
234 |
+
|
235 |
+
with col2:
|
236 |
+
projects_in_progress = len([p for p in filtered_projects if p["status"] == "جاري التنفيذ"])
|
237 |
+
self.ui.create_metric_card(
|
238 |
+
"مشاريع جاري��",
|
239 |
+
str(projects_in_progress),
|
240 |
+
None,
|
241 |
+
self.ui.COLORS['secondary']
|
242 |
+
)
|
243 |
+
|
244 |
+
with col3:
|
245 |
+
total_budget = sum([p["budget"] for p in filtered_projects])
|
246 |
+
self.ui.create_metric_card(
|
247 |
+
"إجمالي الميزانية",
|
248 |
+
f"{total_budget/1000000:.1f} مليون ريال",
|
249 |
+
None,
|
250 |
+
self.ui.COLORS['accent']
|
251 |
+
)
|
252 |
+
|
253 |
+
with col4:
|
254 |
+
avg_completion = np.mean([p["completion"] for p in filtered_projects])
|
255 |
+
self.ui.create_metric_card(
|
256 |
+
"متوسط نسبة الإنجاز",
|
257 |
+
f"{avg_completion:.1f}%",
|
258 |
+
None,
|
259 |
+
self.ui.COLORS['success']
|
260 |
+
)
|
261 |
+
|
262 |
+
def show_location_details(self):
|
263 |
+
"""عرض تفاصيل المواقع"""
|
264 |
+
st.markdown("### تفاصيل مواقع المشاريع")
|
265 |
+
|
266 |
+
# إنشاء جدول بيانات المشاريع
|
267 |
+
projects_df = pd.DataFrame(self.projects_data)
|
268 |
+
projects_df = projects_df.rename(columns={
|
269 |
+
"id": "رقم المشروع",
|
270 |
+
"name": "اسم المشروع",
|
271 |
+
"location": "الموقع",
|
272 |
+
"status": "الحالة",
|
273 |
+
"budget": "الميزانية (ريال)",
|
274 |
+
"completion": "نسبة الإنجاز (%)",
|
275 |
+
"client": "العميل",
|
276 |
+
"start_date": "تاريخ البدء",
|
277 |
+
"end_date": "تاريخ الانتهاء"
|
278 |
+
})
|
279 |
+
|
280 |
+
# حذف عمود الإحداثيات من العرض
|
281 |
+
projects_df = projects_df.drop(columns=["coordinates"])
|
282 |
+
|
283 |
+
# عرض الجدول
|
284 |
+
st.dataframe(
|
285 |
+
projects_df,
|
286 |
+
use_container_width=True,
|
287 |
+
hide_index=True
|
288 |
+
)
|
289 |
+
|
290 |
+
# إضافة خيار تصدير البيانات
|
291 |
+
col1, col2 = st.columns([1, 5])
|
292 |
+
with col1:
|
293 |
+
self.ui.create_button("تصدير البيانات", "primary")
|
294 |
+
|
295 |
+
# عرض تفاصيل مشروع محدد
|
296 |
+
st.markdown("### تفاصيل مشروع محدد")
|
297 |
+
|
298 |
+
selected_project = st.selectbox(
|
299 |
+
"اختر مشروعاً لعرض التفاصيل",
|
300 |
+
options=[p["name"] for p in self.projects_data]
|
301 |
+
)
|
302 |
+
|
303 |
+
# العثور على المشروع المحدد
|
304 |
+
project = next((p for p in self.projects_data if p["name"] == selected_project), None)
|
305 |
+
|
306 |
+
if project:
|
307 |
+
col1, col2 = st.columns([2, 1])
|
308 |
+
|
309 |
+
with col1:
|
310 |
+
# عرض تفاصيل المشروع
|
311 |
+
st.markdown(f"#### {project['name']}")
|
312 |
+
st.markdown(f"**الموقع:** {project['location']}")
|
313 |
+
st.markdown(f"**الحالة:** {project['status']}")
|
314 |
+
st.markdown(f"**الميزانية:** {project['budget']:,} ريال")
|
315 |
+
st.markdown(f"**نسبة الإنجاز:** {project['completion']}%")
|
316 |
+
st.markdown(f"**العميل:** {project['client']}")
|
317 |
+
st.markdown(f"**تاريخ البدء:** {project['start_date']}")
|
318 |
+
st.markdown(f"**تاريخ الانتهاء:** {project['end_date']}")
|
319 |
+
|
320 |
+
# أزرار الإجراءات
|
321 |
+
col1, col2, col3 = st.columns(3)
|
322 |
+
with col1:
|
323 |
+
self.ui.create_button("تعديل البيانات", "primary")
|
324 |
+
with col2:
|
325 |
+
self.ui.create_button("عرض المستندات", "secondary")
|
326 |
+
with col3:
|
327 |
+
self.ui.create_button("تقرير الموقع", "accent")
|
328 |
+
|
329 |
+
with col2:
|
330 |
+
# عرض خريطة مصغرة للمشروع
|
331 |
+
m = folium.Map(location=project["coordinates"], zoom_start=12)
|
332 |
+
folium.Marker(
|
333 |
+
location=project["coordinates"],
|
334 |
+
tooltip=project["name"],
|
335 |
+
icon=folium.Icon(color="red", icon="info-sign")
|
336 |
+
).add_to(m)
|
337 |
+
folium_static(m, width=300, height=300)
|
338 |
+
|
339 |
+
def add_new_location(self):
|
340 |
+
"""إضافة موقع جديد"""
|
341 |
+
st.markdown("### إضافة موقع مشروع جديد")
|
342 |
+
|
343 |
+
# نموذج إضافة موقع جديد
|
344 |
+
with st.form("new_location_form"):
|
345 |
+
col1, col2 = st.columns(2)
|
346 |
+
|
347 |
+
with col1:
|
348 |
+
project_id = st.text_input("رقم المشروع", value="P00" + str(len(self.projects_data) + 1))
|
349 |
+
project_name = st.text_input("اسم المشروع")
|
350 |
+
location = st.text_input("الموقع")
|
351 |
+
status = st.selectbox(
|
352 |
+
"الحالة",
|
353 |
+
options=["جاري التنفيذ", "قيد الدراسة", "مكتمل"]
|
354 |
+
)
|
355 |
+
budget = st.number_input("الميزانية (ريال)", min_value=0, step=100000)
|
356 |
+
|
357 |
+
with col2:
|
358 |
+
completion = st.slider("نسبة الإنجاز (%)", 0, 100, 0)
|
359 |
+
client = st.text_input("العميل")
|
360 |
+
start_date = st.date_input("تاريخ البدء")
|
361 |
+
end_date = st.date_input("تاريخ الانتهاء")
|
362 |
+
|
363 |
+
st.markdown("### تحديد الموقع على الخريطة")
|
364 |
+
st.markdown("انقر على الخريطة لتحديد موقع المشروع أو أدخل الإحداثيات يدوياً")
|
365 |
+
|
366 |
+
col1, col2 = st.columns(2)
|
367 |
+
|
368 |
+
with col1:
|
369 |
+
latitude = st.number_input("خط العرض", value=24.0, format="%.4f")
|
370 |
+
|
371 |
+
with col2:
|
372 |
+
longitude = st.number_input("خط الطول", value=45.0, format="%.4f")
|
373 |
+
|
374 |
+
# عرض الخريطة لتحديد الموقع
|
375 |
+
m = folium.Map(location=[latitude, longitude], zoom_start=5)
|
376 |
+
folium.Marker(
|
377 |
+
location=[latitude, longitude],
|
378 |
+
tooltip="موقع المشروع الجديد",
|
379 |
+
icon=folium.Icon(color="red", icon="info-sign")
|
380 |
+
).add_to(m)
|
381 |
+
folium_static(m, width=700, height=300)
|
382 |
+
|
383 |
+
# زر الإرسال
|
384 |
+
submit_button = st.form_submit_button("إضافة المشروع")
|
385 |
+
|
386 |
+
if submit_button:
|
387 |
+
# إضافة المشروع الجديد (في تطبيق حقيقي، سيتم حفظ البيانات في قاعدة البيانات)
|
388 |
+
st.success("تم إضافة المشروع بنجاح!")
|
389 |
+
|
390 |
+
# إعادة تعيين النموذج
|
391 |
+
st.experimental_rerun()
|
392 |
+
|
393 |
+
def analyze_regions(self):
|
394 |
+
"""تحليل المناطق"""
|
395 |
+
st.markdown("### تحليل المناطق")
|
396 |
+
|
397 |
+
# إنشاء بيانات المناطق (نموذجية)
|
398 |
+
regions_data = {
|
399 |
+
"المنطقة": ["الرياض", "مكة المكرمة", "المدينة المنورة", "القصيم", "المنطقة الشرقية", "عسير", "تبوك", "حائل", "الحدود الشمالية", "جازان", "نجران", "الباحة", "الجوف"],
|
400 |
+
"عدد المشاريع": [15, 12, 8, 5, 18, 7, 4, 3, 2, 6, 3, 2, 3],
|
401 |
+
"إجمالي الميزانية (مليون ريال)": [120, 95, 45, 30, 150, 40, 25, 18, 12, 35, 20, 15, 22],
|
402 |
+
"متوسط مدة المشروع (شهر)": [18, 16, 14, 12, 20, 15, 12, 10, 9, 14, 12, 10, 11]
|
403 |
+
}
|
404 |
+
|
405 |
+
regions_df = pd.DataFrame(regions_data)
|
406 |
+
|
407 |
+
# عرض خريطة حرارية للمناطق
|
408 |
+
st.markdown("#### توزيع المشاريع حسب المناطق")
|
409 |
+
|
410 |
+
# في تطبيق حقيقي، يمكن استخدام خريطة حرارية حقيقية للمملكة
|
411 |
+
st.image("https://via.placeholder.com/800x400?text=خريطة+حرارية+للمشاريع+حسب+المناطق", use_column_width=True)
|
412 |
+
|
413 |
+
# عرض إحصائيات المناطق
|
414 |
+
st.markdown("#### إحصائيات المناطق")
|
415 |
+
|
416 |
+
# عرض الجدول
|
417 |
+
st.dataframe(
|
418 |
+
regions_df,
|
419 |
+
use_container_width=True,
|
420 |
+
hide_index=True
|
421 |
+
)
|
422 |
+
|
423 |
+
# عرض رسوم بيانية للمقارنة
|
424 |
+
st.markdown("#### مقارنة المناطق")
|
425 |
+
|
426 |
+
chart_type = st.radio(
|
427 |
+
"نوع الرسم البياني",
|
428 |
+
options=["عدد المشاريع", "إجمالي الميزانية", "متوسط مدة المشروع"],
|
429 |
+
horizontal=True
|
430 |
+
)
|
431 |
+
|
432 |
+
if chart_type == "عدد المشاريع":
|
433 |
+
chart_data = regions_df[["المنطقة", "عدد المشاريع"]].sort_values(by="عدد المشاريع", ascending=False)
|
434 |
+
st.bar_chart(chart_data.set_index("المنطقة"))
|
435 |
+
elif chart_type == "إجمالي الميزانية":
|
436 |
+
chart_data = regions_df[["المنطقة", "إجمالي الميزانية (مليون ريال)"]].sort_values(by="إجمالي الميزانية (مليون ريال)", ascending=False)
|
437 |
+
st.bar_chart(chart_data.set_index("المنطقة"))
|
438 |
+
else:
|
439 |
+
chart_data = regions_df[["المنطقة", "متوسط مدة المشروع (شهر)"]].sort_values(by="متوسط مدة المشروع (شهر)", ascending=False)
|
440 |
+
st.bar_chart(chart_data.set_index("المنطقة"))
|
441 |
+
|
442 |
+
# تحليل الكثافة
|
443 |
+
st.markdown("#### تحليل كثافة المشاريع")
|
444 |
+
st.markdown("""
|
445 |
+
يوضح هذا التحليل توزيع المشاريع حسب المناطق الجغرافية، مما يساعد في:
|
446 |
+
- تحديد المناطق ذات النشاط العالي
|
447 |
+
- تحديد فرص النمو في المن��طق الأقل نشاطاً
|
448 |
+
- تخطيط الموارد بناءً على التوزيع الجغرافي
|
449 |
+
""")
|
450 |
+
|
451 |
+
# في تطبيق حقيقي، يمكن إضافة تحليلات أكثر تفصيلاً
|
452 |
+
|
453 |
+
# تشغيل التطبيق
|
454 |
+
if __name__ == "__main__":
|
455 |
+
maps_app = MapsApp()
|
456 |
+
maps_app.run()
|
modules/notifications/notifications_app.py
ADDED
@@ -0,0 +1,672 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة الإشعارات الذكية - نظام تحليل المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import datetime
|
8 |
+
import json
|
9 |
+
import os
|
10 |
+
import sys
|
11 |
+
from pathlib import Path
|
12 |
+
|
13 |
+
# إضافة مسار المشروع للنظام
|
14 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
15 |
+
|
16 |
+
# استيراد محسن واجهة المستخدم
|
17 |
+
from styling.enhanced_ui import UIEnhancer
|
18 |
+
|
19 |
+
class NotificationsApp:
|
20 |
+
"""تطبيق الإشعارات الذكية"""
|
21 |
+
|
22 |
+
def __init__(self):
|
23 |
+
"""تهيئة تطبيق الإشعارات الذكية"""
|
24 |
+
self.ui = UIEnhancer(page_title="الإشعارات الذكية - نظام تحليل المناقصات", page_icon="🔔")
|
25 |
+
self.ui.apply_theme_colors()
|
26 |
+
|
27 |
+
# بيانات الإشعارات (نموذجية)
|
28 |
+
self.notifications_data = [
|
29 |
+
{
|
30 |
+
"id": "N001",
|
31 |
+
"title": "موعد تسليم مناقصة",
|
32 |
+
"message": "موعد تسليم مناقصة T-2025-001 (إنشاء مبنى إداري) بعد 5 أيام",
|
33 |
+
"type": "deadline",
|
34 |
+
"priority": "high",
|
35 |
+
"related_entity": "T-2025-001",
|
36 |
+
"created_at": "2025-03-25T10:30:00",
|
37 |
+
"is_read": False
|
38 |
+
},
|
39 |
+
{
|
40 |
+
"id": "N002",
|
41 |
+
"title": "ترسية مناقصة",
|
42 |
+
"message": "تم ترسية مناقصة T-2025-003 (توريد معدات) بنجاح",
|
43 |
+
"type": "award",
|
44 |
+
"priority": "medium",
|
45 |
+
"related_entity": "T-2025-003",
|
46 |
+
"created_at": "2025-03-28T14:15:00",
|
47 |
+
"is_read": True
|
48 |
+
},
|
49 |
+
{
|
50 |
+
"id": "N003",
|
51 |
+
"title": "تحديث مستندات",
|
52 |
+
"message": "تم تحديث مستندات مناقصة T-2025-002 (صيانة طرق)",
|
53 |
+
"type": "document",
|
54 |
+
"priority": "medium",
|
55 |
+
"related_entity": "T-2025-002",
|
56 |
+
"created_at": "2025-03-29T09:45:00",
|
57 |
+
"is_read": False
|
58 |
+
},
|
59 |
+
{
|
60 |
+
"id": "N004",
|
61 |
+
"title": "تغيير في المواصفات",
|
62 |
+
"message": "تم تغيير المواصفات الفنية لمناقصة T-2025-001 (إنشاء مبنى إداري)",
|
63 |
+
"type": "change",
|
64 |
+
"priority": "high",
|
65 |
+
"related_entity": "T-2025-001",
|
66 |
+
"created_at": "2025-03-27T11:20:00",
|
67 |
+
"is_read": False
|
68 |
+
},
|
69 |
+
{
|
70 |
+
"id": "N005",
|
71 |
+
"title": "تأخير في المشروع",
|
72 |
+
"message": "تأخير في تنفيذ مشروع P002 (تطوير طريق الملك فهد - جدة)",
|
73 |
+
"type": "delay",
|
74 |
+
"priority": "high",
|
75 |
+
"related_entity": "P002",
|
76 |
+
"created_at": "2025-03-26T16:10:00",
|
77 |
+
"is_read": True
|
78 |
+
},
|
79 |
+
{
|
80 |
+
"id": "N006",
|
81 |
+
"title": "اكتمال مرحلة",
|
82 |
+
"message": "اكتمال مرحلة الأساسات في مشروع P001 (إنشاء مبنى إداري - الرياض)",
|
83 |
+
"type": "milestone",
|
84 |
+
"priority": "low",
|
85 |
+
"related_entity": "P001",
|
86 |
+
"created_at": "2025-03-24T13:30:00",
|
87 |
+
"is_read": True
|
88 |
+
},
|
89 |
+
{
|
90 |
+
"id": "N007",
|
91 |
+
"title": "طلب معلومات إضافية",
|
92 |
+
"message": "طلب معلومات إضافية لمناقصة T-2025-004 (تجهيز مختبرات)",
|
93 |
+
"type": "request",
|
94 |
+
"priority": "medium",
|
95 |
+
"related_entity": "T-2025-004",
|
96 |
+
"created_at": "2025-03-30T08:15:00",
|
97 |
+
"is_read": False
|
98 |
+
},
|
99 |
+
{
|
100 |
+
"id": "N008",
|
101 |
+
"title": "تحديث أسعار المواد",
|
102 |
+
"message": "تم تحديث أسعار مواد البناء في قاعدة البيانات",
|
103 |
+
"type": "update",
|
104 |
+
"priority": "low",
|
105 |
+
"related_entity": "DB-MATERIALS",
|
106 |
+
"created_at": "2025-03-29T15:40:00",
|
107 |
+
"is_read": False
|
108 |
+
},
|
109 |
+
{
|
110 |
+
"id": "N009",
|
111 |
+
"title": "اجتماع فريق العمل",
|
112 |
+
"message": "اجتماع فريق العمل لمناقشة مناقصة T-2025-001 غداً الساعة 10:00 صباحاً",
|
113 |
+
"type": "meeting",
|
114 |
+
"priority": "medium",
|
115 |
+
"related_entity": "T-2025-001",
|
116 |
+
"created_at": "2025-03-28T16:20:00",
|
117 |
+
"is_read": True
|
118 |
+
},
|
119 |
+
{
|
120 |
+
"id": "N010",
|
121 |
+
"title": "تغيير في الميزانية",
|
122 |
+
"message": "تم تغيير الميزانية المخصصة لمشروع P004 (بناء مدرسة - أبها)",
|
123 |
+
"type": "budget",
|
124 |
+
"priority": "high",
|
125 |
+
"related_entity": "P004",
|
126 |
+
"created_at": "2025-03-25T14:50:00",
|
127 |
+
"is_read": False
|
128 |
+
}
|
129 |
+
]
|
130 |
+
|
131 |
+
# إعدادات الإشعارات (نموذجية)
|
132 |
+
self.notification_settings = {
|
133 |
+
"deadline": True,
|
134 |
+
"award": True,
|
135 |
+
"document": True,
|
136 |
+
"change": True,
|
137 |
+
"delay": True,
|
138 |
+
"milestone": True,
|
139 |
+
"request": True,
|
140 |
+
"update": True,
|
141 |
+
"meeting": True,
|
142 |
+
"budget": True,
|
143 |
+
"email_notifications": True,
|
144 |
+
"sms_notifications": False,
|
145 |
+
"push_notifications": True,
|
146 |
+
"notification_frequency": "realtime"
|
147 |
+
}
|
148 |
+
|
149 |
+
def run(self):
|
150 |
+
"""تشغيل تطبيق الإشعارات الذكية"""
|
151 |
+
# إنشاء قائمة العناصر
|
152 |
+
menu_items = [
|
153 |
+
{"name": "لوحة المعلومات", "icon": "house"},
|
154 |
+
{"name": "المناقصات والعقود", "icon": "file-text"},
|
155 |
+
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
156 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
157 |
+
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
158 |
+
{"name": "الموارد والتكاليف", "icon": "people"},
|
159 |
+
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
160 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
161 |
+
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
162 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
163 |
+
{"name": "الإشعارات", "icon": "bell"},
|
164 |
+
{"name": "مقارنة المستندات", "icon": "files"},
|
165 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
166 |
+
{"name": "التقارير", "icon": "bar-chart"},
|
167 |
+
{"name": "الإعدادات", "icon": "gear"}
|
168 |
+
]
|
169 |
+
|
170 |
+
# إنشاء الشريط الجانبي
|
171 |
+
selected = self.ui.create_sidebar(menu_items)
|
172 |
+
|
173 |
+
# إنشاء ترويسة الصفحة
|
174 |
+
self.ui.create_header("الإشعارات الذكية", "إدارة ومتابعة الإشعارات والتنبيهات")
|
175 |
+
|
176 |
+
# إنشاء علامات تبويب للوظائف المختلفة
|
177 |
+
tabs = st.tabs(["الإشعارات الحالية", "إعدادات الإشعارات", "إنشاء إشعار", "سجل الإشعارات"])
|
178 |
+
|
179 |
+
# علامة تبويب الإشعارات الحالية
|
180 |
+
with tabs[0]:
|
181 |
+
self.show_current_notifications()
|
182 |
+
|
183 |
+
# علامة تبويب إعدادات الإشعارات
|
184 |
+
with tabs[1]:
|
185 |
+
self.show_notification_settings()
|
186 |
+
|
187 |
+
# علامة تبويب إنشاء إشعار
|
188 |
+
with tabs[2]:
|
189 |
+
self.create_notification()
|
190 |
+
|
191 |
+
# علامة تبويب سجل الإشعارات
|
192 |
+
with tabs[3]:
|
193 |
+
self.show_notification_history()
|
194 |
+
|
195 |
+
def show_current_notifications(self):
|
196 |
+
"""عرض الإشعارات الحالية"""
|
197 |
+
st.markdown("### الإشعارات الحالية")
|
198 |
+
|
199 |
+
# إنشاء فلاتر للإشعارات
|
200 |
+
col1, col2, col3 = st.columns(3)
|
201 |
+
|
202 |
+
with col1:
|
203 |
+
type_filter = st.multiselect(
|
204 |
+
"نوع الإشعار",
|
205 |
+
options=["الكل", "موعد نهائي", "ترسية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"],
|
206 |
+
default=["الكل"]
|
207 |
+
)
|
208 |
+
|
209 |
+
with col2:
|
210 |
+
priority_filter = st.multiselect(
|
211 |
+
"الأولوية",
|
212 |
+
options=["الكل", "عالية", "متوسطة", "منخفضة"],
|
213 |
+
default=["الكل"]
|
214 |
+
)
|
215 |
+
|
216 |
+
with col3:
|
217 |
+
read_filter = st.radio(
|
218 |
+
"الحالة",
|
219 |
+
options=["الكل", "غير مقروءة", "مقروءة"],
|
220 |
+
horizontal=True
|
221 |
+
)
|
222 |
+
|
223 |
+
# تطبيق الفلاتر
|
224 |
+
filtered_notifications = self.notifications_data
|
225 |
+
|
226 |
+
# تحويل أنواع الإشعارات من الإنجليزية إلى العربية للفلترة
|
227 |
+
type_mapping = {
|
228 |
+
"موعد نهائي": "deadline",
|
229 |
+
"ترسية": "award",
|
230 |
+
"مستند": "document",
|
231 |
+
"تغيير": "change",
|
232 |
+
"تأخير": "delay",
|
233 |
+
"مرحلة": "milestone",
|
234 |
+
"طلب": "request",
|
235 |
+
"تحديث": "update",
|
236 |
+
"اجتماع": "meeting",
|
237 |
+
"ميزانية": "budget"
|
238 |
+
}
|
239 |
+
|
240 |
+
# تحويل الأولويات من العربية إلى الإنجليزية للفلترة
|
241 |
+
priority_mapping = {
|
242 |
+
"عالية": "high",
|
243 |
+
"متوسطة": "medium",
|
244 |
+
"منخفضة": "low"
|
245 |
+
}
|
246 |
+
|
247 |
+
if "الكل" not in type_filter and type_filter:
|
248 |
+
filtered_types = [type_mapping[t] for t in type_filter if t in type_mapping]
|
249 |
+
filtered_notifications = [n for n in filtered_notifications if n["type"] in filtered_types]
|
250 |
+
|
251 |
+
if "الكل" not in priority_filter and priority_filter:
|
252 |
+
filtered_priorities = [priority_mapping[p] for p in priority_filter if p in priority_mapping]
|
253 |
+
filtered_notifications = [n for n in filtered_notifications if n["priority"] in filtered_priorities]
|
254 |
+
|
255 |
+
if read_filter == "غير مقروءة":
|
256 |
+
filtered_notifications = [n for n in filtered_notifications if not n["is_read"]]
|
257 |
+
elif read_filter == "مقروءة":
|
258 |
+
filtered_notifications = [n for n in filtered_notifications if n["is_read"]]
|
259 |
+
|
260 |
+
# عرض عدد الإشعارات غير المقروءة
|
261 |
+
unread_count = len([n for n in filtered_notifications if not n["is_read"]])
|
262 |
+
|
263 |
+
st.markdown(f"**عدد الإشعارات غير المقروءة:** {unread_count}")
|
264 |
+
|
265 |
+
# زر تحديث وتعليم الكل كمقروء
|
266 |
+
col1, col2 = st.columns([1, 1])
|
267 |
+
with col1:
|
268 |
+
if st.button("تحديث الإشعارات", use_container_width=True):
|
269 |
+
st.success("تم تحديث الإشعارات بنجاح")
|
270 |
+
|
271 |
+
with col2:
|
272 |
+
if st.button("تعليم الكل كمقروء", use_container_width=True):
|
273 |
+
st.success("تم تعليم جميع الإشعارات كمقروءة")
|
274 |
+
|
275 |
+
# عرض الإشعارات
|
276 |
+
if not filtered_notifications:
|
277 |
+
st.info("لا توجد إشعارات تطابق الفلاتر المحددة")
|
278 |
+
else:
|
279 |
+
for notification in filtered_notifications:
|
280 |
+
self.display_notification(notification)
|
281 |
+
|
282 |
+
def display_notification(self, notification):
|
283 |
+
"""عرض إشعار واحد"""
|
284 |
+
# تحديد لون الإشعار بناءً على الأولوية
|
285 |
+
if notification["priority"] == "high":
|
286 |
+
color = self.ui.COLORS['danger']
|
287 |
+
priority_text = "عالية"
|
288 |
+
elif notification["priority"] == "medium":
|
289 |
+
color = self.ui.COLORS['warning']
|
290 |
+
priority_text = "متوسطة"
|
291 |
+
else:
|
292 |
+
color = self.ui.COLORS['secondary']
|
293 |
+
priority_text = "منخفضة"
|
294 |
+
|
295 |
+
# تحويل نوع الإشعار إلى العربية
|
296 |
+
type_mapping = {
|
297 |
+
"deadline": "موعد نهائي",
|
298 |
+
"award": "ترسية",
|
299 |
+
"document": "مستند",
|
300 |
+
"change": "تغيير",
|
301 |
+
"delay": "تأخير",
|
302 |
+
"milestone": "مرحلة",
|
303 |
+
"request": "طلب",
|
304 |
+
"update": "تحديث",
|
305 |
+
"meeting": "اجتماع",
|
306 |
+
"budget": "ميزانية"
|
307 |
+
}
|
308 |
+
|
309 |
+
notification_type = type_mapping.get(notification["type"], notification["type"])
|
310 |
+
|
311 |
+
# تحويل التاريخ إلى تنسيق مناسب
|
312 |
+
created_at = datetime.datetime.fromisoformat(notification["created_at"])
|
313 |
+
formatted_date = created_at.strftime("%Y-%m-%d %H:%M")
|
314 |
+
|
315 |
+
# تحديد أيقونة الإشعار
|
316 |
+
icon_mapping = {
|
317 |
+
"deadline": "⏰",
|
318 |
+
"award": "🏆",
|
319 |
+
"document": "📄",
|
320 |
+
"change": "🔄",
|
321 |
+
"delay": "⚠️",
|
322 |
+
"milestone": "🏁",
|
323 |
+
"request": "❓",
|
324 |
+
"update": "🔄",
|
325 |
+
"meeting": "👥",
|
326 |
+
"budget": "💰"
|
327 |
+
}
|
328 |
+
|
329 |
+
icon = icon_mapping.get(notification["type"], "📌")
|
330 |
+
|
331 |
+
# إنشاء بطاقة الإشعار
|
332 |
+
st.markdown(
|
333 |
+
f"""
|
334 |
+
<div style="border-left: 5px solid {color}; padding: 10px; margin-bottom: 10px; background-color: {'#f8f9fa' if st.session_state.theme == 'light' else '#2b2b2b'}; border-radius: 5px; {'opacity: 0.7;' if notification['is_read'] else ''}">
|
335 |
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
336 |
+
<div>
|
337 |
+
<h4 style="margin: 0;">{icon} {notification['title']}</h4>
|
338 |
+
<p style="margin: 5px 0;">{notification['message']}</p>
|
339 |
+
<div style="display: flex; gap: 10px; font-size: 0.8em; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">
|
340 |
+
<span>النوع: {notification_type}</span>
|
341 |
+
<span>الأولوية: {priority_text}</span>
|
342 |
+
<span>التاريخ: {formatted_date}</span>
|
343 |
+
</div>
|
344 |
+
</div>
|
345 |
+
<div>
|
346 |
+
<button style="background: none; border: none; cursor: pointer; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">✓</button>
|
347 |
+
<button style="background: none; border: none; cursor: pointer; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">🗑️</button>
|
348 |
+
</div>
|
349 |
+
</div>
|
350 |
+
</div>
|
351 |
+
""",
|
352 |
+
unsafe_allow_html=True
|
353 |
+
)
|
354 |
+
|
355 |
+
def show_notification_settings(self):
|
356 |
+
"""عرض إعدادات الإشعارات"""
|
357 |
+
st.markdown("### إعدادات الإشعارات")
|
358 |
+
|
359 |
+
# إنشاء نموذج الإعدادات
|
360 |
+
with st.form("notification_settings_form"):
|
361 |
+
st.markdown("#### أنواع الإشعارات")
|
362 |
+
|
363 |
+
col1, col2 = st.columns(2)
|
364 |
+
|
365 |
+
with col1:
|
366 |
+
deadline = st.checkbox("المواعيد النهائية", value=self.notification_settings["deadline"])
|
367 |
+
award = st.checkbox("ترسية المناقصات", value=self.notification_settings["award"])
|
368 |
+
document = st.checkbox("تحديثات المستندات", value=self.notification_settings["document"])
|
369 |
+
change = st.checkbox("التغييرات في المواصفات", value=self.notification_settings["change"])
|
370 |
+
delay = st.checkbox("التأخيرات في المشاريع", value=self.notification_settings["delay"])
|
371 |
+
|
372 |
+
with col2:
|
373 |
+
milestone = st.checkbox("اكتمال المراحل", value=self.notification_settings["milestone"])
|
374 |
+
request = st.checkbox("طلبات المعلومات", value=self.notification_settings["request"])
|
375 |
+
update = st.checkbox("تحديثات النظام", value=self.notification_settings["update"])
|
376 |
+
meeting = st.checkbox("الاجتماعات", value=self.notification_settings["meeting"])
|
377 |
+
budget = st.checkbox("تغييرات الميزانية", value=self.notification_settings["budget"])
|
378 |
+
|
379 |
+
st.markdown("#### طرق الإشعار")
|
380 |
+
|
381 |
+
col1, col2, col3 = st.columns(3)
|
382 |
+
|
383 |
+
with col1:
|
384 |
+
email_notifications = st.checkbox("البريد الإلكتروني", value=self.notification_settings["email_notifications"])
|
385 |
+
|
386 |
+
with col2:
|
387 |
+
sms_notifications = st.checkbox("الرسائل النصية", value=self.notification_settings["sms_notifications"])
|
388 |
+
|
389 |
+
with col3:
|
390 |
+
push_notifications = st.checkbox("إشعارات الويب", value=self.notification_settings["push_notifications"])
|
391 |
+
|
392 |
+
st.markdown("#### تكرار الإشعارات")
|
393 |
+
|
394 |
+
notification_frequency = st.radio(
|
395 |
+
"تكرار الإشعارات",
|
396 |
+
options=["في الوقت الحقيقي", "مرة واحدة يومياً", "مرة واحدة أسبوعياً"],
|
397 |
+
index=0 if self.notification_settings["notification_frequency"] == "realtime" else 1 if self.notification_settings["notification_frequency"] == "daily" else 2,
|
398 |
+
horizontal=True
|
399 |
+
)
|
400 |
+
|
401 |
+
# زر حفظ الإعدادات
|
402 |
+
submit_button = st.form_submit_button("حفظ الإعدادات")
|
403 |
+
|
404 |
+
if submit_button:
|
405 |
+
# تحديث الإعدادات (في تطبيق حقيقي، سيتم حفظ الإعدادات في قاعدة البيانات)
|
406 |
+
self.notification_settings.update({
|
407 |
+
"deadline": deadline,
|
408 |
+
"award": award,
|
409 |
+
"document": document,
|
410 |
+
"change": change,
|
411 |
+
"delay": delay,
|
412 |
+
"milestone": milestone,
|
413 |
+
"request": request,
|
414 |
+
"update": update,
|
415 |
+
"meeting": meeting,
|
416 |
+
"budget": budget,
|
417 |
+
"email_notifications": email_notifications,
|
418 |
+
"sms_notifications": sms_notifications,
|
419 |
+
"push_notifications": push_notifications,
|
420 |
+
"notification_frequency": "realtime" if notification_frequency == "في الوقت الحقيقي" else "daily" if notification_frequency == "مرة واحدة يومياً" else "weekly"
|
421 |
+
})
|
422 |
+
|
423 |
+
st.success("تم حفظ الإعدادات بنجاح")
|
424 |
+
|
425 |
+
# إعدادات متقدمة
|
426 |
+
st.markdown("### إعدادات متقدمة")
|
427 |
+
|
428 |
+
with st.expander("إعدادات متقدمة"):
|
429 |
+
st.markdown("#### جدولة الإشعارات")
|
430 |
+
|
431 |
+
col1, col2 = st.columns(2)
|
432 |
+
|
433 |
+
with col1:
|
434 |
+
st.time_input("وقت الإشعارات اليومية", datetime.time(9, 0))
|
435 |
+
|
436 |
+
with col2:
|
437 |
+
st.selectbox(
|
438 |
+
"يوم الإشعارات الأسبوعية",
|
439 |
+
options=["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"],
|
440 |
+
index=0
|
441 |
+
)
|
442 |
+
|
443 |
+
st.markdown("#### فلترة الإشعارات")
|
444 |
+
|
445 |
+
min_priority = st.select_slider(
|
446 |
+
"الحد الأدنى للأولوية",
|
447 |
+
options=["منخفضة", "متوسطة", "عالية"],
|
448 |
+
value="منخفضة"
|
449 |
+
)
|
450 |
+
|
451 |
+
st.markdown("#### حفظ الإشعارات")
|
452 |
+
|
453 |
+
retention_period = st.slider(
|
454 |
+
"فترة الاحتفاظ بالإشعارات (بالأيام)",
|
455 |
+
min_value=7,
|
456 |
+
max_value=365,
|
457 |
+
value=90,
|
458 |
+
step=1
|
459 |
+
)
|
460 |
+
|
461 |
+
if st.button("حفظ الإعدادات المتقدمة"):
|
462 |
+
st.success("تم حفظ الإعدادات المتقدمة بنجاح")
|
463 |
+
|
464 |
+
def create_notification(self):
|
465 |
+
"""إنشاء إشعار جديد"""
|
466 |
+
st.markdown("### إنشاء إشعار جديد")
|
467 |
+
|
468 |
+
# إنشاء نموذج إشعار جديد
|
469 |
+
with st.form("new_notification_form"):
|
470 |
+
title = st.text_input("عنوان الإشعار")
|
471 |
+
message = st.text_area("نص الإشعار")
|
472 |
+
|
473 |
+
col1, col2 = st.columns(2)
|
474 |
+
|
475 |
+
with col1:
|
476 |
+
notification_type = st.selectbox(
|
477 |
+
"نوع الإشعار",
|
478 |
+
options=["موعد نهائي", "ترسية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"]
|
479 |
+
)
|
480 |
+
|
481 |
+
# تحويل نوع الإشعار إلى الإنجليزية
|
482 |
+
type_mapping = {
|
483 |
+
"موعد نهائي": "deadline",
|
484 |
+
"ترسية": "award",
|
485 |
+
"مستند": "document",
|
486 |
+
"تغيير": "change",
|
487 |
+
"تأخير": "delay",
|
488 |
+
"مرحلة": "milestone",
|
489 |
+
"طلب": "request",
|
490 |
+
"تحديث": "update",
|
491 |
+
"اجتماع": "meeting",
|
492 |
+
"ميزانية": "budget"
|
493 |
+
}
|
494 |
+
|
495 |
+
notification_type_en = type_mapping.get(notification_type, "update")
|
496 |
+
|
497 |
+
with col2:
|
498 |
+
priority = st.selectbox(
|
499 |
+
"الأولوية",
|
500 |
+
options=["عالية", "متوسطة", "منخفضة"]
|
501 |
+
)
|
502 |
+
|
503 |
+
# تحويل الأولوية إلى الإنجليزية
|
504 |
+
priority_mapping = {
|
505 |
+
"عالية": "high",
|
506 |
+
"متوسطة": "medium",
|
507 |
+
"منخفضة": "low"
|
508 |
+
}
|
509 |
+
|
510 |
+
priority_en = priority_mapping.get(priority, "medium")
|
511 |
+
|
512 |
+
related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة أو المشروع)")
|
513 |
+
|
514 |
+
col1, col2 = st.columns(2)
|
515 |
+
|
516 |
+
with col1:
|
517 |
+
send_email = st.checkbox("إرسال بريد إلكتروني")
|
518 |
+
|
519 |
+
with col2:
|
520 |
+
send_push = st.checkbox("إرسال إشعار ويب")
|
521 |
+
|
522 |
+
# زر إنشاء الإشعار
|
523 |
+
submit_button = st.form_submit_button("إنشاء الإشعار")
|
524 |
+
|
525 |
+
if submit_button:
|
526 |
+
if not title or not message:
|
527 |
+
st.error("يرجى ملء جميع الحقول المطلوبة")
|
528 |
+
else:
|
529 |
+
# إنشاء الإشعار الجديد (في تطبيق حقيقي، سيتم حفظ الإشعار في قاعدة البيانات)
|
530 |
+
new_notification = {
|
531 |
+
"id": f"N{len(self.notifications_data) + 1:03d}",
|
532 |
+
"title": title,
|
533 |
+
"message": message,
|
534 |
+
"type": notification_type_en,
|
535 |
+
"priority": priority_en,
|
536 |
+
"related_entity": related_entity,
|
537 |
+
"created_at": datetime.datetime.now().isoformat(),
|
538 |
+
"is_read": False
|
539 |
+
}
|
540 |
+
|
541 |
+
# إضافة الإشعار إلى القائمة (في تطبيق حقيقي، سيتم إضافته إلى قاعدة البيانات)
|
542 |
+
self.notifications_data.append(new_notification)
|
543 |
+
|
544 |
+
st.success("تم إنشاء الإشعار بنجاح")
|
545 |
+
|
546 |
+
# إظهار تفاصيل الإرسال
|
547 |
+
if send_email:
|
548 |
+
st.info("تم إرسال الإشعار عبر البريد الإلكتروني")
|
549 |
+
|
550 |
+
if send_push:
|
551 |
+
st.info("تم إرسال إشعار الويب")
|
552 |
+
|
553 |
+
def show_notification_history(self):
|
554 |
+
"""عرض سجل الإشعارات"""
|
555 |
+
st.markdown("### سجل الإشعارات")
|
556 |
+
|
557 |
+
# إنشاء فلاتر للسجل
|
558 |
+
col1, col2 = st.columns(2)
|
559 |
+
|
560 |
+
with col1:
|
561 |
+
date_range = st.date_input(
|
562 |
+
"نطاق التاريخ",
|
563 |
+
value=(
|
564 |
+
datetime.datetime.now() - datetime.timedelta(days=30),
|
565 |
+
datetime.datetime.now()
|
566 |
+
)
|
567 |
+
)
|
568 |
+
|
569 |
+
with col2:
|
570 |
+
entity_filter = st.text_input("البحث حسب الكيان المرتبط")
|
571 |
+
|
572 |
+
# تحويل البيانات إلى DataFrame
|
573 |
+
notifications_df = pd.DataFrame(self.notifications_data)
|
574 |
+
|
575 |
+
# تحويل حقل created_at إلى datetime
|
576 |
+
notifications_df["created_at"] = pd.to_datetime(notifications_df["created_at"])
|
577 |
+
|
578 |
+
# تطبيق فلتر التاريخ
|
579 |
+
if len(date_range) == 2:
|
580 |
+
start_date, end_date = date_range
|
581 |
+
start_date = pd.to_datetime(start_date)
|
582 |
+
end_date = pd.to_datetime(end_date) + datetime.timedelta(days=1) # لتضمين اليوم الأخير
|
583 |
+
notifications_df = notifications_df[(notifications_df["created_at"] >= start_date) & (notifications_df["created_at"] <= end_date)]
|
584 |
+
|
585 |
+
# تطبيق فلتر الكيان المرتبط
|
586 |
+
if entity_filter:
|
587 |
+
notifications_df = notifications_df[notifications_df["related_entity"].str.contains(entity_filter, case=False)]
|
588 |
+
|
589 |
+
# تحويل أنواع الإشعارات من الإنجليزية إلى العربية للعرض
|
590 |
+
type_mapping = {
|
591 |
+
"deadline": "موعد نهائي",
|
592 |
+
"award": "ترسية",
|
593 |
+
"document": "مستند",
|
594 |
+
"change": "تغيير",
|
595 |
+
"delay": "تأخير",
|
596 |
+
"milestone": "مرحلة",
|
597 |
+
"request": "طلب",
|
598 |
+
"update": "تحديث",
|
599 |
+
"meeting": "اجتماع",
|
600 |
+
"budget": "ميزانية"
|
601 |
+
}
|
602 |
+
|
603 |
+
notifications_df["type_ar"] = notifications_df["type"].map(type_mapping)
|
604 |
+
|
605 |
+
# تحويل الأولويات من الإنجليزية إلى العربية للعرض
|
606 |
+
priority_mapping = {
|
607 |
+
"high": "عالية",
|
608 |
+
"medium": "متوسطة",
|
609 |
+
"low": "منخفضة"
|
610 |
+
}
|
611 |
+
|
612 |
+
notifications_df["priority_ar"] = notifications_df["priority"].map(priority_mapping)
|
613 |
+
|
614 |
+
# تحويل حالة القراءة إلى نص
|
615 |
+
notifications_df["is_read_text"] = notifications_df["is_read"].map({True: "مقروءة", False: "غير مقروءة"})
|
616 |
+
|
617 |
+
# تنسيق التاريخ
|
618 |
+
notifications_df["created_at_formatted"] = notifications_df["created_at"].dt.strftime("%Y-%m-%d %H:%M")
|
619 |
+
|
620 |
+
# إعادة ترتيب الأعمدة وتغيير أسمائها
|
621 |
+
display_df = notifications_df[[
|
622 |
+
"id", "title", "type_ar", "priority_ar", "related_entity", "created_at_formatted", "is_read_text"
|
623 |
+
]].rename(columns={
|
624 |
+
"id": "الرقم",
|
625 |
+
"title": "العنوان",
|
626 |
+
"type_ar": "النوع",
|
627 |
+
"priority_ar": "الأولوية",
|
628 |
+
"related_entity": "الكيان المرتبط",
|
629 |
+
"created_at_formatted": "تاريخ الإنشاء",
|
630 |
+
"is_read_text": "الحالة"
|
631 |
+
})
|
632 |
+
|
633 |
+
# عرض الجدول
|
634 |
+
st.dataframe(
|
635 |
+
display_df,
|
636 |
+
use_container_width=True,
|
637 |
+
hide_index=True
|
638 |
+
)
|
639 |
+
|
640 |
+
# إضافة خيارات التصدير
|
641 |
+
col1, col2 = st.columns([1, 5])
|
642 |
+
with col1:
|
643 |
+
if st.button("تصدير البيانات", use_container_width=True):
|
644 |
+
st.success("تم تصدير البيانات بنجاح")
|
645 |
+
|
646 |
+
# عرض إحصائيات
|
647 |
+
st.markdown("### إحصائيات الإشعارات")
|
648 |
+
|
649 |
+
col1, col2, col3 = st.columns(3)
|
650 |
+
|
651 |
+
with col1:
|
652 |
+
# إحصائيات حسب النوع
|
653 |
+
type_counts = notifications_df["type_ar"].value_counts()
|
654 |
+
st.markdown("#### الإشعارات حسب النوع")
|
655 |
+
st.bar_chart(type_counts)
|
656 |
+
|
657 |
+
with col2:
|
658 |
+
# إحصائيات حسب الأولوية
|
659 |
+
priority_counts = notifications_df["priority_ar"].value_counts()
|
660 |
+
st.markdown("#### الإشعارات حسب الأولوية")
|
661 |
+
st.bar_chart(priority_counts)
|
662 |
+
|
663 |
+
with col3:
|
664 |
+
# إحصائيات حسب الحالة
|
665 |
+
read_counts = notifications_df["is_read_text"].value_counts()
|
666 |
+
st.markdown("#### الإشعارات حسب الحالة")
|
667 |
+
st.bar_chart(read_counts)
|
668 |
+
|
669 |
+
# تشغيل التطبيق
|
670 |
+
if __name__ == "__main__":
|
671 |
+
notifications_app = NotificationsApp()
|
672 |
+
notifications_app.run()
|
modules/pricing/price_analyzer.py
ADDED
@@ -0,0 +1,1695 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
محلل الأسعار لنظام إدارة المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import matplotlib.pyplot as plt
|
9 |
+
import seaborn as sns
|
10 |
+
from datetime import datetime, timedelta
|
11 |
+
from scipy import stats
|
12 |
+
import logging
|
13 |
+
|
14 |
+
logger = logging.getLogger('tender_system.pricing.analyzer')
|
15 |
+
|
16 |
+
class PriceAnalyzer:
|
17 |
+
"""فئة تحليل الأسعار"""
|
18 |
+
|
19 |
+
def __init__(self, db_connector):
|
20 |
+
"""تهيئة محلل الأسعار"""
|
21 |
+
self.db = db_connector
|
22 |
+
self.charts_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "data", "charts")
|
23 |
+
|
24 |
+
# إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
|
25 |
+
os.makedirs(self.charts_dir, exist_ok=True)
|
26 |
+
|
27 |
+
def get_price_history(self, item_id, start_date=None, end_date=None):
|
28 |
+
"""الحصول على تاريخ الأسعار لبند معين
|
29 |
+
|
30 |
+
المعلمات:
|
31 |
+
item_id (int): معرف البند
|
32 |
+
start_date (str, optional): تاريخ البداية بتنسيق 'YYYY-MM-DD'
|
33 |
+
end_date (str, optional): تاريخ النهاية بتنسيق 'YYYY-MM-DD'
|
34 |
+
|
35 |
+
العائد:
|
36 |
+
pandas.DataFrame: إطار بيانات يحتوي على تاريخ الأسعار
|
37 |
+
"""
|
38 |
+
try:
|
39 |
+
query = """
|
40 |
+
SELECT
|
41 |
+
pih.id,
|
42 |
+
pih.price,
|
43 |
+
pih.price_date,
|
44 |
+
pih.price_source,
|
45 |
+
pih.notes,
|
46 |
+
pib.code,
|
47 |
+
pib.name,
|
48 |
+
mu.name as unit_name,
|
49 |
+
mu.symbol as unit_symbol
|
50 |
+
FROM
|
51 |
+
pricing_items_history pih
|
52 |
+
JOIN
|
53 |
+
pricing_items_base pib ON pih.base_item_id = pib.id
|
54 |
+
LEFT JOIN
|
55 |
+
measurement_units mu ON pib.unit_id = mu.id
|
56 |
+
WHERE
|
57 |
+
pih.base_item_id = ?
|
58 |
+
"""
|
59 |
+
|
60 |
+
params = [item_id]
|
61 |
+
|
62 |
+
if start_date:
|
63 |
+
query += " AND pih.price_date >= ?"
|
64 |
+
params.append(start_date)
|
65 |
+
|
66 |
+
if end_date:
|
67 |
+
query += " AND pih.price_date <= ?"
|
68 |
+
params.append(end_date)
|
69 |
+
|
70 |
+
query += " ORDER BY pih.price_date ASC"
|
71 |
+
|
72 |
+
results = self.db.fetch_all(query, params)
|
73 |
+
|
74 |
+
if not results:
|
75 |
+
logger.warning(f"لا توجد بيانات تاريخية للسعر للبند رقم {item_id}")
|
76 |
+
return pd.DataFrame()
|
77 |
+
|
78 |
+
# تحويل النتائج إلى إطار بيانات
|
79 |
+
df = pd.DataFrame(results, columns=[
|
80 |
+
'id', 'price', 'price_date', 'price_source', 'notes',
|
81 |
+
'code', 'name', 'unit_name', 'unit_symbol'
|
82 |
+
])
|
83 |
+
|
84 |
+
# تحويل تاريخ السعر إلى نوع datetime
|
85 |
+
df['price_date'] = pd.to_datetime(df['price_date'])
|
86 |
+
|
87 |
+
return df
|
88 |
+
|
89 |
+
except Exception as e:
|
90 |
+
logger.error(f"خطأ في الحصول على تاريخ الأسعار: {str(e)}")
|
91 |
+
return pd.DataFrame()
|
92 |
+
|
93 |
+
def analyze_price_trends(self, item_id, start_date=None, end_date=None):
|
94 |
+
"""تحليل اتجاهات الأسعار
|
95 |
+
|
96 |
+
المعلمات:
|
97 |
+
item_id (int): معرف البند
|
98 |
+
start_date (str, optional): تاريخ البداية بتنسيق 'YYYY-MM-DD'
|
99 |
+
end_date (str, optional): تاريخ النهاية بتنسيق 'YYYY-MM-DD'
|
100 |
+
|
101 |
+
العائد:
|
102 |
+
dict: قاموس يحتوي على نتائج تحليل اتجاهات الأسعار
|
103 |
+
"""
|
104 |
+
try:
|
105 |
+
# الحصول على تاريخ الأسعار
|
106 |
+
df = self.get_price_history(item_id, start_date, end_date)
|
107 |
+
|
108 |
+
if df.empty:
|
109 |
+
return {
|
110 |
+
'status': 'error',
|
111 |
+
'message': 'لا توجد بيانات كافية لتحليل اتجاهات الأسعار'
|
112 |
+
}
|
113 |
+
|
114 |
+
# حساب الإحصاءات الأساسية
|
115 |
+
stats_data = {
|
116 |
+
'min_price': df['price'].min(),
|
117 |
+
'max_price': df['price'].max(),
|
118 |
+
'avg_price': df['price'].mean(),
|
119 |
+
'median_price': df['price'].median(),
|
120 |
+
'std_dev': df['price'].std(),
|
121 |
+
'price_range': df['price'].max() - df['price'].min(),
|
122 |
+
'count': len(df),
|
123 |
+
'start_date': df['price_date'].min().strftime('%Y-%m-%d'),
|
124 |
+
'end_date': df['price_date'].max().strftime('%Y-%m-%d'),
|
125 |
+
'duration_days': (df['price_date'].max() - df['price_date'].min()).days,
|
126 |
+
'item_name': df['name'].iloc[0],
|
127 |
+
'item_code': df['code'].iloc[0],
|
128 |
+
'unit': df['unit_symbol'].iloc[0] if not pd.isna(df['unit_symbol'].iloc[0]) else ''
|
129 |
+
}
|
130 |
+
|
131 |
+
# حساب التغير المطلق والنسبي
|
132 |
+
if len(df) >= 2:
|
133 |
+
first_price = df['price'].iloc[0]
|
134 |
+
last_price = df['price'].iloc[-1]
|
135 |
+
|
136 |
+
stats_data['absolute_change'] = last_price - first_price
|
137 |
+
stats_data['percentage_change'] = ((last_price - first_price) / first_price) * 100
|
138 |
+
|
139 |
+
# حساب معدل التغير السنوي
|
140 |
+
years = stats_data['duration_days'] / 365.0
|
141 |
+
if years > 0:
|
142 |
+
stats_data['annual_change_rate'] = (((last_price / first_price) ** (1 / years)) - 1) * 100
|
143 |
+
else:
|
144 |
+
stats_data['annual_change_rate'] = 0
|
145 |
+
else:
|
146 |
+
stats_data['absolute_change'] = 0
|
147 |
+
stats_data['percentage_change'] = 0
|
148 |
+
stats_data['annual_change_rate'] = 0
|
149 |
+
|
150 |
+
# تحليل الاتجاه باستخدام الانحدار الخطي
|
151 |
+
if len(df) >= 3:
|
152 |
+
# إنشاء متغير مستقل (الأيام منذ أول تاريخ)
|
153 |
+
df['days'] = (df['price_date'] - df['price_date'].min()).dt.days
|
154 |
+
|
155 |
+
# حساب الانحدار الخطي
|
156 |
+
slope, intercept, r_value, p_value, std_err = stats.linregress(df['days'], df['price'])
|
157 |
+
|
158 |
+
stats_data['trend_slope'] = slope
|
159 |
+
stats_data['trend_intercept'] = intercept
|
160 |
+
stats_data['trend_r_squared'] = r_value ** 2
|
161 |
+
stats_data['trend_p_value'] = p_value
|
162 |
+
stats_data['trend_std_err'] = std_err
|
163 |
+
|
164 |
+
# تحديد اتجاه السعر
|
165 |
+
if p_value < 0.05: # إذا كان الاتجاه ذو دلالة إحصائية
|
166 |
+
if slope > 0:
|
167 |
+
stats_data['trend_direction'] = 'upward'
|
168 |
+
stats_data['trend_description'] = 'اتجاه تصاعدي'
|
169 |
+
elif slope < 0:
|
170 |
+
stats_data['trend_direction'] = 'downward'
|
171 |
+
stats_data['trend_description'] = 'اتجاه تنازلي'
|
172 |
+
else:
|
173 |
+
stats_data['trend_direction'] = 'stable'
|
174 |
+
stats_data['trend_description'] = 'مستقر'
|
175 |
+
else:
|
176 |
+
stats_data['trend_direction'] = 'no_significant_trend'
|
177 |
+
stats_data['trend_description'] = 'لا يوجد اتجاه واضح'
|
178 |
+
|
179 |
+
# حساب التقلب (معامل الاختلاف)
|
180 |
+
stats_data['volatility'] = (df['price'].std() / df['price'].mean()) * 100
|
181 |
+
|
182 |
+
# تصنيف التقلب
|
183 |
+
if stats_data['volatility'] < 5:
|
184 |
+
stats_data['volatility_level'] = 'low'
|
185 |
+
stats_data['volatility_description'] = 'منخفض'
|
186 |
+
elif stats_data['volatility'] < 15:
|
187 |
+
stats_data['volatility_level'] = 'medium'
|
188 |
+
stats_data['volatility_description'] = 'متوسط'
|
189 |
+
else:
|
190 |
+
stats_data['volatility_level'] = 'high'
|
191 |
+
stats_data['volatility_description'] = 'مرتفع'
|
192 |
+
else:
|
193 |
+
stats_data['trend_direction'] = 'insufficient_data'
|
194 |
+
stats_data['trend_description'] = 'بيانات غير كافية'
|
195 |
+
stats_data['volatility'] = 0
|
196 |
+
stats_data['volatility_level'] = 'unknown'
|
197 |
+
stats_data['volatility_description'] = 'غير معروف'
|
198 |
+
|
199 |
+
# إنشاء رسم بياني للاتجاه
|
200 |
+
chart_path = self._create_trend_chart(df, stats_data, item_id)
|
201 |
+
stats_data['chart_path'] = chart_path
|
202 |
+
|
203 |
+
return {
|
204 |
+
'status': 'success',
|
205 |
+
'data': stats_data
|
206 |
+
}
|
207 |
+
|
208 |
+
except Exception as e:
|
209 |
+
logger.error(f"خطأ في تحليل اتجاهات الأسعار: {str(e)}")
|
210 |
+
return {
|
211 |
+
'status': 'error',
|
212 |
+
'message': f'حدث خطأ أثناء تحليل اتجاهات الأسعار: {str(e)}'
|
213 |
+
}
|
214 |
+
|
215 |
+
def _create_trend_chart(self, df, stats_data, item_id):
|
216 |
+
"""إنشاء رسم بياني للاتجاه
|
217 |
+
|
218 |
+
المعلمات:
|
219 |
+
df (pandas.DataFrame): إطار البيانات
|
220 |
+
stats_data (dict): بيانات الإحصاءات
|
221 |
+
item_id (int): معرف البند
|
222 |
+
|
223 |
+
العائد:
|
224 |
+
str: مسار ملف الرسم البياني
|
225 |
+
"""
|
226 |
+
try:
|
227 |
+
# إنشاء رسم بياني جديد
|
228 |
+
plt.figure(figsize=(10, 6))
|
229 |
+
|
230 |
+
# رسم نقاط البيانات
|
231 |
+
plt.scatter(df['price_date'], df['price'], color='blue', alpha=0.6, label='أسعار فعلية')
|
232 |
+
|
233 |
+
# رسم خط الاتجاه إذا كان هناك بيانات كافية
|
234 |
+
if len(df) >= 3 and 'trend_slope' in stats_data:
|
235 |
+
# إنشاء خط الاتجاه
|
236 |
+
x_trend = pd.date_range(start=df['price_date'].min(), end=df['price_date'].max(), periods=100)
|
237 |
+
days_trend = [(date - df['price_date'].min()).days for date in x_trend]
|
238 |
+
y_trend = stats_data['trend_slope'] * np.array(days_trend) + stats_data['trend_intercept']
|
239 |
+
|
240 |
+
# رسم خط الاتجاه
|
241 |
+
plt.plot(x_trend, y_trend, color='red', linestyle='--', label='خط الاتجاه')
|
242 |
+
|
243 |
+
# رسم خط متوسط السعر
|
244 |
+
plt.axhline(y=stats_data['avg_price'], color='green', linestyle='-', alpha=0.5, label='متوسط السعر')
|
245 |
+
|
246 |
+
# إضافة عنوان ومحاور
|
247 |
+
plt.title(f"تحليل اتجاه السعر - {stats_data['item_name']} ({stats_data['item_code']})")
|
248 |
+
plt.xlabel('التاريخ')
|
249 |
+
plt.ylabel(f"السعر ({stats_data['unit']})")
|
250 |
+
|
251 |
+
# إضافة شبكة
|
252 |
+
plt.grid(True, linestyle='--', alpha=0.7)
|
253 |
+
|
254 |
+
# إضافة وسيلة إيضاح
|
255 |
+
plt.legend()
|
256 |
+
|
257 |
+
# تنسيق التاريخ على المحور السيني
|
258 |
+
plt.gcf().autofmt_xdate()
|
259 |
+
|
260 |
+
# إضافة معلومات إحصائية
|
261 |
+
info_text = (
|
262 |
+
f"التغير: {stats_data['percentage_change']:.2f}%\n"
|
263 |
+
f"التقلب: {stats_data['volatility']:.2f}%\n"
|
264 |
+
)
|
265 |
+
|
266 |
+
if 'trend_r_squared' in stats_data:
|
267 |
+
info_text += f"R²: {stats_data['trend_r_squared']:.3f}"
|
268 |
+
|
269 |
+
plt.annotate(info_text, xy=(0.02, 0.95), xycoords='axes fraction',
|
270 |
+
bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="gray", alpha=0.8))
|
271 |
+
|
272 |
+
# حفظ الرسم البياني
|
273 |
+
chart_filename = f"price_trend_{item_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
|
274 |
+
chart_path = os.path.join(self.charts_dir, chart_filename)
|
275 |
+
plt.savefig(chart_path, dpi=100, bbox_inches='tight')
|
276 |
+
plt.close()
|
277 |
+
|
278 |
+
return chart_path
|
279 |
+
|
280 |
+
except Exception as e:
|
281 |
+
logger.error(f"خطأ في إنشاء رسم بياني للاتجاه: {str(e)}")
|
282 |
+
return None
|
283 |
+
|
284 |
+
def compare_prices(self, items, date=None):
|
285 |
+
"""مقارنة الأسعار بين عدة بنود
|
286 |
+
|
287 |
+
المعلمات:
|
288 |
+
items (list): قائمة بمعرفات البنود
|
289 |
+
date (str, optional): تاريخ المقارنة بتنسيق 'YYYY-MM-DD'
|
290 |
+
|
291 |
+
العائد:
|
292 |
+
dict: قاموس يحتوي على نتائج مقارنة الأسعار
|
293 |
+
"""
|
294 |
+
try:
|
295 |
+
if not items:
|
296 |
+
return {
|
297 |
+
'status': 'error',
|
298 |
+
'message': 'لم يتم تحديد أي بنود للمقارنة'
|
299 |
+
}
|
300 |
+
|
301 |
+
comparison_data = []
|
302 |
+
|
303 |
+
for item_id in items:
|
304 |
+
# الحصول على معلومات البند الأساسية
|
305 |
+
item_query = """
|
306 |
+
SELECT
|
307 |
+
id, code, name, description,
|
308 |
+
(SELECT name FROM measurement_units WHERE id = unit_id) as unit_name,
|
309 |
+
(SELECT symbol FROM measurement_units WHERE id = unit_id) as unit_symbol,
|
310 |
+
base_price, last_updated_date
|
311 |
+
FROM
|
312 |
+
pricing_items_base
|
313 |
+
WHERE
|
314 |
+
id = ?
|
315 |
+
"""
|
316 |
+
|
317 |
+
item_result = self.db.fetch_one(item_query, [item_id])
|
318 |
+
|
319 |
+
if not item_result:
|
320 |
+
logger.warning(f"البند رقم {item_id} غير موجود")
|
321 |
+
continue
|
322 |
+
|
323 |
+
item_data = {
|
324 |
+
'id': item_result[0],
|
325 |
+
'code': item_result[1],
|
326 |
+
'name': item_result[2],
|
327 |
+
'description': item_result[3],
|
328 |
+
'unit_name': item_result[4],
|
329 |
+
'unit_symbol': item_result[5],
|
330 |
+
'base_price': item_result[6],
|
331 |
+
'last_updated_date': item_result[7]
|
332 |
+
}
|
333 |
+
|
334 |
+
# إذا تم تحديد تاريخ، نبحث عن السعر في ذلك التاريخ
|
335 |
+
if date:
|
336 |
+
price_query = """
|
337 |
+
SELECT price, price_date, price_source
|
338 |
+
FROM pricing_items_history
|
339 |
+
WHERE base_item_id = ?
|
340 |
+
AND price_date <= ?
|
341 |
+
ORDER BY price_date DESC
|
342 |
+
LIMIT 1
|
343 |
+
"""
|
344 |
+
|
345 |
+
price_result = self.db.fetch_one(price_query, [item_id, date])
|
346 |
+
|
347 |
+
if price_result:
|
348 |
+
item_data['price'] = price_result[0]
|
349 |
+
item_data['price_date'] = price_result[1]
|
350 |
+
item_data['price_source'] = price_result[2]
|
351 |
+
else:
|
352 |
+
# إذا لم يتم العثور على سعر في التاريخ المحدد، نستخدم السعر الأساسي
|
353 |
+
item_data['price'] = item_data['base_price']
|
354 |
+
item_data['price_date'] = item_data['last_updated_date']
|
355 |
+
item_data['price_source'] = 'base_price'
|
356 |
+
else:
|
357 |
+
# إذا لم يتم تحديد تاريخ، نستخدم أحدث سعر
|
358 |
+
price_query = """
|
359 |
+
SELECT price, price_date, price_source
|
360 |
+
FROM pricing_items_history
|
361 |
+
WHERE base_item_id = ?
|
362 |
+
ORDER BY price_date DESC
|
363 |
+
LIMIT 1
|
364 |
+
"""
|
365 |
+
|
366 |
+
price_result = self.db.fetch_one(price_query, [item_id])
|
367 |
+
|
368 |
+
if price_result:
|
369 |
+
item_data['price'] = price_result[0]
|
370 |
+
item_data['price_date'] = price_result[1]
|
371 |
+
item_data['price_source'] = price_result[2]
|
372 |
+
else:
|
373 |
+
# إذا لم يتم العثور على سعر، نستخدم السعر الأساسي
|
374 |
+
item_data['price'] = item_data['base_price']
|
375 |
+
item_data['price_date'] = item_data['last_updated_date']
|
376 |
+
item_data['price_source'] = 'base_price'
|
377 |
+
|
378 |
+
comparison_data.append(item_data)
|
379 |
+
|
380 |
+
if not comparison_data:
|
381 |
+
return {
|
382 |
+
'status': 'error',
|
383 |
+
'message': 'لم يتم العثور على أي بنود للمقارنة'
|
384 |
+
}
|
385 |
+
|
386 |
+
# إنشاء رسم بياني للمقارنة
|
387 |
+
chart_path = self._create_comparison_chart(comparison_data, date)
|
388 |
+
|
389 |
+
return {
|
390 |
+
'status': 'success',
|
391 |
+
'data': {
|
392 |
+
'items': comparison_data,
|
393 |
+
'comparison_date': date if date else 'latest',
|
394 |
+
'chart_path': chart_path
|
395 |
+
}
|
396 |
+
}
|
397 |
+
|
398 |
+
except Exception as e:
|
399 |
+
logger.error(f"خطأ في مقارنة الأسعار: {str(e)}")
|
400 |
+
return {
|
401 |
+
'status': 'error',
|
402 |
+
'message': f'حدث خطأ أثناء مقارنة الأسعار: {str(e)}'
|
403 |
+
}
|
404 |
+
|
405 |
+
def _create_comparison_chart(self, comparison_data, date=None):
|
406 |
+
"""إنشاء رسم بياني للمقارنة
|
407 |
+
|
408 |
+
المعلمات:
|
409 |
+
comparison_data (list): بيانات المقارنة
|
410 |
+
date (str, optional): تاريخ المقارنة
|
411 |
+
|
412 |
+
العائد:
|
413 |
+
str: مسار ملف الرسم البياني
|
414 |
+
"""
|
415 |
+
try:
|
416 |
+
# إنشاء رسم بياني جديد
|
417 |
+
plt.figure(figsize=(12, 6))
|
418 |
+
|
419 |
+
# إعداد البيانات للرسم
|
420 |
+
names = [f"{item['code']} - {item['name']}" for item in comparison_data]
|
421 |
+
prices = [item['price'] for item in comparison_data]
|
422 |
+
|
423 |
+
# رسم الأعمدة
|
424 |
+
bars = plt.bar(names, prices, color='skyblue', edgecolor='navy')
|
425 |
+
|
426 |
+
# إضافة القيم فوق الأعمدة
|
427 |
+
for bar in bars:
|
428 |
+
height = bar.get_height()
|
429 |
+
plt.text(bar.get_x() + bar.get_width()/2., height + 0.1,
|
430 |
+
f'{height:.2f}', ha='center', va='bottom')
|
431 |
+
|
432 |
+
# إضافة عنوان ومحاور
|
433 |
+
title = "مقارنة الأسعار"
|
434 |
+
if date:
|
435 |
+
title += f" (بتاريخ {date})"
|
436 |
+
|
437 |
+
plt.title(title)
|
438 |
+
plt.xlabel('البنود')
|
439 |
+
plt.ylabel('السعر')
|
440 |
+
|
441 |
+
# تدوير تسميات المحور السيني لتجنب التداخل
|
442 |
+
plt.xticks(rotation=45, ha='right')
|
443 |
+
|
444 |
+
# إضافة شبكة
|
445 |
+
plt.grid(True, linestyle='--', alpha=0.7, axis='y')
|
446 |
+
|
447 |
+
# ضبط التخطيط
|
448 |
+
plt.tight_layout()
|
449 |
+
|
450 |
+
# حفظ الرسم البياني
|
451 |
+
chart_filename = f"price_comparison_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
|
452 |
+
chart_path = os.path.join(self.charts_dir, chart_filename)
|
453 |
+
plt.savefig(chart_path, dpi=100, bbox_inches='tight')
|
454 |
+
plt.close()
|
455 |
+
|
456 |
+
return chart_path
|
457 |
+
|
458 |
+
except Exception as e:
|
459 |
+
logger.error(f"خطأ في إنشاء رسم بياني للمقارنة: {str(e)}")
|
460 |
+
return None
|
461 |
+
|
462 |
+
def calculate_price_volatility(self, item_id, period='1y'):
|
463 |
+
"""حساب تقلب الأسعار
|
464 |
+
|
465 |
+
المعلمات:
|
466 |
+
item_id (int): معرف البند
|
467 |
+
period (str): الفترة الزمنية ('1m', '3m', '6m', '1y', '2y', '5y', 'all')
|
468 |
+
|
469 |
+
العائد:
|
470 |
+
dict: قاموس يحتوي على نتائج حساب تقلب الأسعار
|
471 |
+
"""
|
472 |
+
try:
|
473 |
+
# تحديد تاريخ البداية بناءً على الفترة
|
474 |
+
end_date = datetime.now().strftime('%Y-%m-%d')
|
475 |
+
|
476 |
+
if period == '1m':
|
477 |
+
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
478 |
+
elif period == '3m':
|
479 |
+
start_date = (datetime.now() - timedelta(days=90)).strftime('%Y-%m-%d')
|
480 |
+
elif period == '6m':
|
481 |
+
start_date = (datetime.now() - timedelta(days=180)).strftime('%Y-%m-%d')
|
482 |
+
elif period == '1y':
|
483 |
+
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
|
484 |
+
elif period == '2y':
|
485 |
+
start_date = (datetime.now() - timedelta(days=730)).strftime('%Y-%m-%d')
|
486 |
+
elif period == '5y':
|
487 |
+
start_date = (datetime.now() - timedelta(days=1825)).strftime('%Y-%m-%d')
|
488 |
+
else: # 'all'
|
489 |
+
start_date = None
|
490 |
+
|
491 |
+
# الحصول على تاريخ الأسعار
|
492 |
+
df = self.get_price_history(item_id, start_date, end_date)
|
493 |
+
|
494 |
+
if df.empty or len(df) < 2:
|
495 |
+
return {
|
496 |
+
'status': 'error',
|
497 |
+
'message': 'لا توجد بيانات كافية لحساب تقلب الأسعار'
|
498 |
+
}
|
499 |
+
|
500 |
+
# حساب التقلب (معامل الاختلاف)
|
501 |
+
mean_price = df['price'].mean()
|
502 |
+
std_dev = df['price'].std()
|
503 |
+
volatility = (std_dev / mean_price) * 100
|
504 |
+
|
505 |
+
# حساب التغيرات النسبية
|
506 |
+
df['price_shift'] = df['price'].shift(1)
|
507 |
+
df = df.dropna()
|
508 |
+
|
509 |
+
if not df.empty:
|
510 |
+
df['price_change_pct'] = ((df['price'] - df['price_shift']) / df['price_shift']) * 100
|
511 |
+
|
512 |
+
# حساب إحصاءات التغيرات
|
513 |
+
max_increase = df['price_change_pct'].max()
|
514 |
+
max_decrease = df['price_change_pct'].min()
|
515 |
+
avg_change = df['price_change_pct'].mean()
|
516 |
+
median_change = df['price_change_pct'].median()
|
517 |
+
|
518 |
+
# حساب عدد التغيرات الإيجابية والسلبية
|
519 |
+
positive_changes = (df['price_change_pct'] > 0).sum()
|
520 |
+
negative_changes = (df['price_change_pct'] < 0).sum()
|
521 |
+
no_changes = (df['price_change_pct'] == 0).sum()
|
522 |
+
|
523 |
+
# تصنيف التقلب
|
524 |
+
if volatility < 5:
|
525 |
+
volatility_level = 'low'
|
526 |
+
volatility_description = 'منخفض'
|
527 |
+
elif volatility < 15:
|
528 |
+
volatility_level = 'medium'
|
529 |
+
volatility_description = 'متوسط'
|
530 |
+
else:
|
531 |
+
volatility_level = 'high'
|
532 |
+
volatility_description = 'مرتفع'
|
533 |
+
|
534 |
+
# إنشاء رسم بياني للتقلب
|
535 |
+
chart_path = self._create_volatility_chart(df, item_id, period)
|
536 |
+
|
537 |
+
return {
|
538 |
+
'status': 'success',
|
539 |
+
'data': {
|
540 |
+
'item_id': item_id,
|
541 |
+
'item_name': df['name'].iloc[0],
|
542 |
+
'item_code': df['code'].iloc[0],
|
543 |
+
'period': period,
|
544 |
+
'start_date': df['price_date'].min().strftime('%Y-%m-%d'),
|
545 |
+
'end_date': df['price_date'].max().strftime('%Y-%m-%d'),
|
546 |
+
'data_points': len(df),
|
547 |
+
'mean_price': mean_price,
|
548 |
+
'std_dev': std_dev,
|
549 |
+
'volatility': volatility,
|
550 |
+
'volatility_level': volatility_level,
|
551 |
+
'volatility_description': volatility_description,
|
552 |
+
'max_increase': max_increase,
|
553 |
+
'max_decrease': max_decrease,
|
554 |
+
'avg_change': avg_change,
|
555 |
+
'median_change': median_change,
|
556 |
+
'positive_changes': positive_changes,
|
557 |
+
'negative_changes': negative_changes,
|
558 |
+
'no_changes': no_changes,
|
559 |
+
'chart_path': chart_path
|
560 |
+
}
|
561 |
+
}
|
562 |
+
else:
|
563 |
+
return {
|
564 |
+
'status': 'error',
|
565 |
+
'message': 'لا توجد بيانات كافية لحساب تقلب الأسعار بعد معالجة البيانات'
|
566 |
+
}
|
567 |
+
|
568 |
+
except Exception as e:
|
569 |
+
logger.error(f"خطأ في حساب تقلب الأسعار: {str(e)}")
|
570 |
+
return {
|
571 |
+
'status': 'error',
|
572 |
+
'message': f'حدث خطأ أثناء حساب تقلب الأسعار: {str(e)}'
|
573 |
+
}
|
574 |
+
|
575 |
+
def _create_volatility_chart(self, df, item_id, period):
|
576 |
+
"""إنشاء رسم بياني للتقلب
|
577 |
+
|
578 |
+
المعلمات:
|
579 |
+
df (pandas.DataFrame): إطار البيانات
|
580 |
+
item_id (int): معرف البند
|
581 |
+
period (str): الفترة الزمنية
|
582 |
+
|
583 |
+
العائد:
|
584 |
+
str: مسار ملف الرسم البياني
|
585 |
+
"""
|
586 |
+
try:
|
587 |
+
# إنشاء رسم بياني بمحورين
|
588 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), gridspec_kw={'height_ratios': [2, 1]})
|
589 |
+
|
590 |
+
# الرسم البياني العلوي: سعر البند عبر الزمن
|
591 |
+
ax1.plot(df['price_date'], df['price'], 'b-', linewidth=2)
|
592 |
+
ax1.set_title(f"سعر البند عبر الزمن - {df['name'].iloc[0]} ({df['code'].iloc[0]})")
|
593 |
+
ax1.set_xlabel('التاريخ')
|
594 |
+
ax1.set_ylabel('السعر')
|
595 |
+
ax1.grid(True, linestyle='--', alpha=0.7)
|
596 |
+
|
597 |
+
# إضافة نطاق الانحراف المعياري
|
598 |
+
mean_price = df['price'].mean()
|
599 |
+
std_dev = df['price'].std()
|
600 |
+
|
601 |
+
ax1.axhline(y=mean_price, color='g', linestyle='-', alpha=0.8, label='متوسط السعر')
|
602 |
+
ax1.axhline(y=mean_price + std_dev, color='r', linestyle='--', alpha=0.5, label='انحراف معياري +1')
|
603 |
+
ax1.axhline(y=mean_price - std_dev, color='r', linestyle='--', alpha=0.5, label='انحراف معياري -1')
|
604 |
+
|
605 |
+
ax1.fill_between(df['price_date'], mean_price - std_dev, mean_price + std_dev, color='gray', alpha=0.2)
|
606 |
+
ax1.legend()
|
607 |
+
|
608 |
+
# الرسم البياني السفلي: التغيرات النسبية
|
609 |
+
ax2.bar(df['price_date'], df['price_change_pct'], color='skyblue', edgecolor='navy', alpha=0.7)
|
610 |
+
ax2.set_title('التغيرات النسبية في السعر (%)')
|
611 |
+
ax2.set_xlabel('التاريخ')
|
612 |
+
ax2.set_ylabel('التغير النسبي (%)')
|
613 |
+
ax2.grid(True, linestyle='--', alpha=0.7)
|
614 |
+
|
615 |
+
# إضافة خط الصفر
|
616 |
+
ax2.axhline(y=0, color='k', linestyle='-', alpha=0.3)
|
617 |
+
|
618 |
+
# تنسيق التاريخ على المحور السيني
|
619 |
+
fig.autofmt_xdate()
|
620 |
+
|
621 |
+
# ضبط التخطيط
|
622 |
+
plt.tight_layout()
|
623 |
+
|
624 |
+
# حفظ الرسم البياني
|
625 |
+
chart_filename = f"price_volatility_{item_id}_{period}_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
|
626 |
+
chart_path = os.path.join(self.charts_dir, chart_filename)
|
627 |
+
plt.savefig(chart_path, dpi=100, bbox_inches='tight')
|
628 |
+
plt.close()
|
629 |
+
|
630 |
+
return chart_path
|
631 |
+
|
632 |
+
except Exception as e:
|
633 |
+
logger.error(f"خطأ في إنشاء رسم بياني للتقلب: {str(e)}")
|
634 |
+
return None
|
635 |
+
|
636 |
+
def perform_sensitivity_analysis(self, project_id, variable_items, ranges):
|
637 |
+
"""إجراء تحليل الحساسية
|
638 |
+
|
639 |
+
المعلمات:
|
640 |
+
project_id (int): معرف المشروع
|
641 |
+
variable_items (list): قائمة بمعرفات البنود المتغيرة
|
642 |
+
ranges (dict): نطاقات التغيير لكل بند
|
643 |
+
|
644 |
+
العائد:
|
645 |
+
dict: قاموس يحتوي على نتائج تحليل الحساسية
|
646 |
+
"""
|
647 |
+
try:
|
648 |
+
# الحصول على بنود المشروع
|
649 |
+
query = """
|
650 |
+
SELECT
|
651 |
+
id, item_number, description, quantity, unit_price, total_price
|
652 |
+
FROM
|
653 |
+
project_pricing_items
|
654 |
+
WHERE
|
655 |
+
project_id = ?
|
656 |
+
"""
|
657 |
+
|
658 |
+
results = self.db.fetch_all(query, [project_id])
|
659 |
+
|
660 |
+
if not results:
|
661 |
+
return {
|
662 |
+
'status': 'error',
|
663 |
+
'message': 'لا توجد بنود للمشروع المحدد'
|
664 |
+
}
|
665 |
+
|
666 |
+
# تحويل النتائج إلى إطار بيانات
|
667 |
+
project_items = pd.DataFrame(results, columns=[
|
668 |
+
'id', 'item_number', 'description', 'quantity', 'unit_price', 'total_price'
|
669 |
+
])
|
670 |
+
|
671 |
+
# حساب إجمالي المشروع الأصلي
|
672 |
+
original_total = project_items['total_price'].sum()
|
673 |
+
|
674 |
+
# تحضير بيانات تحليل الحساسية
|
675 |
+
sensitivity_data = []
|
676 |
+
|
677 |
+
for item_id in variable_items:
|
678 |
+
if item_id not in project_items['id'].values:
|
679 |
+
logger.warning(f"البند رقم {item_id} غير موجود في المشروع")
|
680 |
+
continue
|
681 |
+
|
682 |
+
# الحصول على معلومات البند
|
683 |
+
item_info = project_items[project_items['id'] == item_id].iloc[0]
|
684 |
+
|
685 |
+
# الحصول على نطاق التغيير للبند
|
686 |
+
if str(item_id) in ranges:
|
687 |
+
item_range = ranges[str(item_id)]
|
688 |
+
else:
|
689 |
+
# استخدام نطاق افتراضي إذا لم يتم تحديد نطاق
|
690 |
+
item_range = {'min': -20, 'max': 20, 'step': 10}
|
691 |
+
|
692 |
+
# إنشاء قائمة بنسب التغيير
|
693 |
+
change_percentages = list(range(
|
694 |
+
item_range['min'],
|
695 |
+
item_range['max'] + item_range['step'],
|
696 |
+
item_range['step']
|
697 |
+
))
|
698 |
+
|
699 |
+
item_sensitivity = {
|
700 |
+
'item_id': item_id,
|
701 |
+
'item_number': item_info['item_number'],
|
702 |
+
'description': item_info['description'],
|
703 |
+
'original_price': item_info['unit_price'],
|
704 |
+
'original_total': item_info['total_price'],
|
705 |
+
'changes': []
|
706 |
+
}
|
707 |
+
|
708 |
+
# حساب تأثير كل نسبة تغيير
|
709 |
+
for percentage in change_percentages:
|
710 |
+
# حساب السعر الجديد
|
711 |
+
new_price = item_info['unit_price'] * (1 + percentage / 100)
|
712 |
+
new_total = new_price * item_info['quantity']
|
713 |
+
|
714 |
+
# حساب إجمالي المشروع الجديد
|
715 |
+
project_total = original_total - item_info['total_price'] + new_total
|
716 |
+
|
717 |
+
# حساب التغير في إجمالي المشروع
|
718 |
+
project_change = ((project_total - original_total) / original_total) * 100
|
719 |
+
|
720 |
+
item_sensitivity['changes'].append({
|
721 |
+
'percentage': percentage,
|
722 |
+
'new_price': new_price,
|
723 |
+
'new_total': new_total,
|
724 |
+
'project_total': project_total,
|
725 |
+
'project_change': project_change
|
726 |
+
})
|
727 |
+
|
728 |
+
sensitivity_data.append(item_sensitivity)
|
729 |
+
|
730 |
+
if not sensitivity_data:
|
731 |
+
return {
|
732 |
+
'status': 'error',
|
733 |
+
'message': 'لا توجد بنود صالحة لتحليل الحساسية'
|
734 |
+
}
|
735 |
+
|
736 |
+
# إنشاء رسم بياني لتحليل الحساسية
|
737 |
+
chart_path = self._create_sensitivity_chart(sensitivity_data, original_total, project_id)
|
738 |
+
|
739 |
+
return {
|
740 |
+
'status': 'success',
|
741 |
+
'data': {
|
742 |
+
'project_id': project_id,
|
743 |
+
'original_total': original_total,
|
744 |
+
'sensitivity_data': sensitivity_data,
|
745 |
+
'chart_path': chart_path
|
746 |
+
}
|
747 |
+
}
|
748 |
+
|
749 |
+
except Exception as e:
|
750 |
+
logger.error(f"خطأ في إجراء تحليل الحساسية: {str(e)}")
|
751 |
+
return {
|
752 |
+
'status': 'error',
|
753 |
+
'message': f'حدث خطأ أثناء إجراء تحليل الحساسية: {str(e)}'
|
754 |
+
}
|
755 |
+
|
756 |
+
def _create_sensitivity_chart(self, sensitivity_data, original_total, project_id):
|
757 |
+
"""إنشاء رسم بياني لتحليل الحساسية
|
758 |
+
|
759 |
+
المعلمات:
|
760 |
+
sensitivity_data (list): بيانات تحليل الحساسية
|
761 |
+
original_total (float): إجمالي المشروع الأصلي
|
762 |
+
project_id (int): معرف المشروع
|
763 |
+
|
764 |
+
العائد:
|
765 |
+
str: مسار ملف الرسم البياني
|
766 |
+
"""
|
767 |
+
try:
|
768 |
+
# إنشاء رسم بياني جديد
|
769 |
+
plt.figure(figsize=(12, 8))
|
770 |
+
|
771 |
+
# رسم خطوط الحساسية لكل بند
|
772 |
+
for item in sensitivity_data:
|
773 |
+
percentages = [change['percentage'] for change in item['changes']]
|
774 |
+
project_changes = [change['project_change'] for change in item['changes']]
|
775 |
+
|
776 |
+
plt.plot(percentages, project_changes, marker='o', linewidth=2,
|
777 |
+
label=f"{item['item_number']} - {item['description'][:30]}...")
|
778 |
+
|
779 |
+
# إضافة عنوان ومحاور
|
780 |
+
plt.title(f"تحليل الحساسية للمشروع رقم {project_id}")
|
781 |
+
plt.xlabel('نسبة التغيير في سعر البند (%)')
|
782 |
+
plt.ylabel('نسبة التغيير في إجمالي المشروع (%)')
|
783 |
+
|
784 |
+
# إضافة خط الصفر
|
785 |
+
plt.axhline(y=0, color='k', linestyle='-', alpha=0.3)
|
786 |
+
plt.axvline(x=0, color='k', linestyle='-', alpha=0.3)
|
787 |
+
|
788 |
+
# إضافة شبكة
|
789 |
+
plt.grid(True, linestyle='--', alpha=0.7)
|
790 |
+
|
791 |
+
# إضافة وسيلة إيضاح
|
792 |
+
plt.legend(loc='best')
|
793 |
+
|
794 |
+
# إضافة معلومات إضافية
|
795 |
+
info_text = f"إجمالي المشروع الأصلي: {original_total:,.2f}"
|
796 |
+
plt.annotate(info_text, xy=(0.02, 0.02), xycoords='axes fraction',
|
797 |
+
bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="gray", alpha=0.8))
|
798 |
+
|
799 |
+
# ضبط التخطيط
|
800 |
+
plt.tight_layout()
|
801 |
+
|
802 |
+
# حفظ الرسم البياني
|
803 |
+
chart_filename = f"sensitivity_analysis_{project_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
|
804 |
+
chart_path = os.path.join(self.charts_dir, chart_filename)
|
805 |
+
plt.savefig(chart_path, dpi=100, bbox_inches='tight')
|
806 |
+
plt.close()
|
807 |
+
|
808 |
+
return chart_path
|
809 |
+
|
810 |
+
except Exception as e:
|
811 |
+
logger.error(f"خطأ في إنشاء رسم بياني لتحليل الحساسية: {str(e)}")
|
812 |
+
return None
|
813 |
+
|
814 |
+
def analyze_price_correlations(self, items):
|
815 |
+
"""تحليل ارتباطات الأسعار بين عدة بنود
|
816 |
+
|
817 |
+
المعلمات:
|
818 |
+
items (list): قائمة بمعرفات البنود
|
819 |
+
|
820 |
+
العائد:
|
821 |
+
dict: قاموس يحتوي على نتائج تحليل الارتباطات
|
822 |
+
"""
|
823 |
+
try:
|
824 |
+
if not items or len(items) < 2:
|
825 |
+
return {
|
826 |
+
'status': 'error',
|
827 |
+
'message': 'يجب تحديد بندين على الأقل لتحليل الارتباطات'
|
828 |
+
}
|
829 |
+
|
830 |
+
# جمع بيانات الأسعار لجميع البنود
|
831 |
+
all_prices = {}
|
832 |
+
item_names = {}
|
833 |
+
|
834 |
+
for item_id in items:
|
835 |
+
# الحصول على تاريخ الأسعار
|
836 |
+
df = self.get_price_history(item_id)
|
837 |
+
|
838 |
+
if df.empty:
|
839 |
+
logger.warning(f"لا توجد بيانات تاريخية للسعر للبند رقم {item_id}")
|
840 |
+
continue
|
841 |
+
|
842 |
+
# تخزين بيانات الأسعار
|
843 |
+
all_prices[item_id] = df[['price_date', 'price']].copy()
|
844 |
+
item_names[item_id] = f"{df['code'].iloc[0]} - {df['name'].iloc[0]}"
|
845 |
+
|
846 |
+
if len(all_prices) < 2:
|
847 |
+
return {
|
848 |
+
'status': 'error',
|
849 |
+
'message': 'لا توجد بيانات كافية لتحليل الارتباطات'
|
850 |
+
}
|
851 |
+
|
852 |
+
# إنشاء إطار بيانات موحد بتواريخ مشتركة
|
853 |
+
# أولاً، نجمع جميع التواريخ الفريدة
|
854 |
+
all_dates = set()
|
855 |
+
for item_id, df in all_prices.items():
|
856 |
+
all_dates.update(df['price_date'].dt.strftime('%Y-%m-%d').tolist())
|
857 |
+
|
858 |
+
# إنشاء إطار بيانات جديد بجميع التواريخ
|
859 |
+
unified_df = pd.DataFrame({'price_date': sorted(list(all_dates))})
|
860 |
+
unified_df['price_date'] = pd.to_datetime(unified_df['price_date'])
|
861 |
+
|
862 |
+
# إضافة أسعار كل بند
|
863 |
+
for item_id, df in all_prices.items():
|
864 |
+
# تحويل إطار البيانات إلى سلسلة زمنية مفهرسة بالتاريخ
|
865 |
+
price_series = df.set_index('price_date')['price']
|
866 |
+
|
867 |
+
# إعادة فهرسة السلسلة الزمنية لتتوافق مع التواريخ الموحدة
|
868 |
+
unified_df[f'price_{item_id}'] = unified_df['price_date'].map(
|
869 |
+
lambda x: price_series.get(x, None)
|
870 |
+
)
|
871 |
+
|
872 |
+
# ملء القيم المفقودة باستخدام الاستيفاء الخطي
|
873 |
+
price_columns = [col for col in unified_df.columns if col.startswith('price_')]
|
874 |
+
unified_df[price_columns] = unified_df[price_columns].interpolate(method='linear')
|
875 |
+
|
876 |
+
# حذف الصفوف التي لا تزال تحتوي على قيم مفقودة
|
877 |
+
unified_df = unified_df.dropna()
|
878 |
+
|
879 |
+
if len(unified_df) < 3:
|
880 |
+
return {
|
881 |
+
'status': 'error',
|
882 |
+
'message': 'لا توجد بيانات كافية بعد معالجة التواريخ المشتركة'
|
883 |
+
}
|
884 |
+
|
885 |
+
# حساب مصفوفة الارتباط
|
886 |
+
correlation_matrix = unified_df[price_columns].corr()
|
887 |
+
|
888 |
+
# تحويل مصفوفة الارتباط إلى تنسيق أكثر قابلية للقراءة
|
889 |
+
correlation_data = []
|
890 |
+
|
891 |
+
for i, item1_id in enumerate(items):
|
892 |
+
if f'price_{item1_id}' not in correlation_matrix.columns:
|
893 |
+
continue
|
894 |
+
|
895 |
+
for j, item2_id in enumerate(items):
|
896 |
+
if f'price_{item2_id}' not in correlation_matrix.columns or i >= j:
|
897 |
+
continue
|
898 |
+
|
899 |
+
correlation = correlation_matrix.loc[f'price_{item1_id}', f'price_{item2_id}']
|
900 |
+
|
901 |
+
# تحديد قوة واتجاه الارتباط
|
902 |
+
if abs(correlation) < 0.3:
|
903 |
+
strength = 'weak'
|
904 |
+
strength_description = 'ضعيف'
|
905 |
+
elif abs(correlation) < 0.7:
|
906 |
+
strength = 'moderate'
|
907 |
+
strength_description = 'متوسط'
|
908 |
+
else:
|
909 |
+
strength = 'strong'
|
910 |
+
strength_description = 'قوي'
|
911 |
+
|
912 |
+
if correlation > 0:
|
913 |
+
direction = 'positive'
|
914 |
+
direction_description = 'طردي'
|
915 |
+
else:
|
916 |
+
direction = 'negative'
|
917 |
+
direction_description = 'عكسي'
|
918 |
+
|
919 |
+
correlation_data.append({
|
920 |
+
'item1_id': item1_id,
|
921 |
+
'item1_name': item_names.get(item1_id, f'البند {item1_id}'),
|
922 |
+
'item2_id': item2_id,
|
923 |
+
'item2_name': item_names.get(item2_id, f'البند {item2_id}'),
|
924 |
+
'correlation': correlation,
|
925 |
+
'strength': strength,
|
926 |
+
'strength_description': strength_description,
|
927 |
+
'direction': direction,
|
928 |
+
'direction_description': direction_description
|
929 |
+
})
|
930 |
+
|
931 |
+
if not correlation_data:
|
932 |
+
return {
|
933 |
+
'status': 'error',
|
934 |
+
'message': 'لم يتم العثور على ارتباطات بين البنود المحددة'
|
935 |
+
}
|
936 |
+
|
937 |
+
# إنشاء رسم بياني للارتباطات
|
938 |
+
chart_path = self._create_correlation_chart(correlation_matrix, item_names)
|
939 |
+
|
940 |
+
# إنشاء رسم بياني لتطور الأسعار
|
941 |
+
trends_chart_path = self._create_price_trends_chart(unified_df, price_columns, item_names)
|
942 |
+
|
943 |
+
return {
|
944 |
+
'status': 'success',
|
945 |
+
'data': {
|
946 |
+
'correlation_data': correlation_data,
|
947 |
+
'chart_path': chart_path,
|
948 |
+
'trends_chart_path': trends_chart_path
|
949 |
+
}
|
950 |
+
}
|
951 |
+
|
952 |
+
except Exception as e:
|
953 |
+
logger.error(f"خطأ في تحليل ارتباطات الأسعار: {str(e)}")
|
954 |
+
return {
|
955 |
+
'status': 'error',
|
956 |
+
'message': f'حدث خطأ أثناء تحليل ارتباطات الأسعار: {str(e)}'
|
957 |
+
}
|
958 |
+
|
959 |
+
def _create_correlation_chart(self, correlation_matrix, item_names):
|
960 |
+
"""إنشاء رسم بياني لمصفوفة الارتباط
|
961 |
+
|
962 |
+
المعلمات:
|
963 |
+
correlation_matrix (pandas.DataFrame): مصفوفة الارتباط
|
964 |
+
item_names (dict): قاموس بأسماء البنود
|
965 |
+
|
966 |
+
العائد:
|
967 |
+
str: مسار ملف الرسم البياني
|
968 |
+
"""
|
969 |
+
try:
|
970 |
+
# إنشاء رسم بياني جديد
|
971 |
+
plt.figure(figsize=(10, 8))
|
972 |
+
|
973 |
+
# إنشاء خريطة حرارية للارتباطات
|
974 |
+
mask = np.triu(np.ones_like(correlation_matrix, dtype=bool))
|
975 |
+
cmap = sns.diverging_palette(230, 20, as_cmap=True)
|
976 |
+
|
977 |
+
# تعديل تسميات المحاور
|
978 |
+
labels = [item_names.get(int(col.split('_')[1]), col) for col in correlation_matrix.columns]
|
979 |
+
|
980 |
+
# رسم الخريطة الحرارية
|
981 |
+
sns.heatmap(correlation_matrix, mask=mask, cmap=cmap, vmax=1, vmin=-1, center=0,
|
982 |
+
square=True, linewidths=.5, cbar_kws={"shrink": .5}, annot=True,
|
983 |
+
xticklabels=labels, yticklabels=labels)
|
984 |
+
|
985 |
+
# إضافة عنوان
|
986 |
+
plt.title('مصفوفة ارتباط الأسعار بين البنود')
|
987 |
+
|
988 |
+
# ضبط التخطيط
|
989 |
+
plt.tight_layout()
|
990 |
+
|
991 |
+
# حفظ الرسم البياني
|
992 |
+
chart_filename = f"price_correlation_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
|
993 |
+
chart_path = os.path.join(self.charts_dir, chart_filename)
|
994 |
+
plt.savefig(chart_path, dpi=100, bbox_inches='tight')
|
995 |
+
plt.close()
|
996 |
+
|
997 |
+
return chart_path
|
998 |
+
|
999 |
+
except Exception as e:
|
1000 |
+
logger.error(f"خطأ في إنشاء رسم بياني لمصفوفة الارتباط: {str(e)}")
|
1001 |
+
return None
|
1002 |
+
|
1003 |
+
def _create_price_trends_chart(self, unified_df, price_columns, item_names):
|
1004 |
+
"""إنشاء رسم بياني لتطور الأسعار
|
1005 |
+
|
1006 |
+
المعلمات:
|
1007 |
+
unified_df (pandas.DataFrame): إطار البيانات الموحد
|
1008 |
+
price_columns (list): أسماء أعمدة الأسعار
|
1009 |
+
item_names (dict): قاموس بأسماء البنود
|
1010 |
+
|
1011 |
+
العائد:
|
1012 |
+
str: مسار ملف الرسم البياني
|
1013 |
+
"""
|
1014 |
+
try:
|
1015 |
+
# إنشاء رسم بياني جديد
|
1016 |
+
plt.figure(figsize=(12, 6))
|
1017 |
+
|
1018 |
+
# رسم تطور الأسعار لكل بند
|
1019 |
+
for col in price_columns:
|
1020 |
+
item_id = int(col.split('_')[1])
|
1021 |
+
item_name = item_names.get(item_id, f'البند {item_id}')
|
1022 |
+
|
1023 |
+
# تطبيع الأسعار للمقارنة (القيمة الأولى = 100)
|
1024 |
+
first_price = unified_df[col].iloc[0]
|
1025 |
+
normalized_prices = (unified_df[col] / first_price) * 100
|
1026 |
+
|
1027 |
+
plt.plot(unified_df['price_date'], normalized_prices, linewidth=2, label=item_name)
|
1028 |
+
|
1029 |
+
# إضافة عنوان ومحاور
|
1030 |
+
plt.title('تطور الأسعار النسبية للبنود (القيمة الأولى = 100)')
|
1031 |
+
plt.xlabel('التاريخ')
|
1032 |
+
plt.ylabel('السعر النسبي')
|
1033 |
+
|
1034 |
+
# إضافة شبكة
|
1035 |
+
plt.grid(True, linestyle='--', alpha=0.7)
|
1036 |
+
|
1037 |
+
# إضافة وسيلة إيضاح
|
1038 |
+
plt.legend(loc='best')
|
1039 |
+
|
1040 |
+
# تنسيق التاريخ على المحور السيني
|
1041 |
+
plt.gcf().autofmt_xdate()
|
1042 |
+
|
1043 |
+
# ضبط التخطيط
|
1044 |
+
plt.tight_layout()
|
1045 |
+
|
1046 |
+
# حفظ الرسم البياني
|
1047 |
+
chart_filename = f"price_trends_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
|
1048 |
+
chart_path = os.path.join(self.charts_dir, chart_filename)
|
1049 |
+
plt.savefig(chart_path, dpi=100, bbox_inches='tight')
|
1050 |
+
plt.close()
|
1051 |
+
|
1052 |
+
return chart_path
|
1053 |
+
|
1054 |
+
except Exception as e:
|
1055 |
+
logger.error(f"خطأ في إنشاء رسم بياني لتطور الأسعار: {str(e)}")
|
1056 |
+
return None
|
1057 |
+
|
1058 |
+
def compare_with_market_prices(self, items):
|
1059 |
+
"""مقارنة أسعار البنود مع أسعار السوق
|
1060 |
+
|
1061 |
+
المعلمات:
|
1062 |
+
items (list): قائمة بمعرفات البنود
|
1063 |
+
|
1064 |
+
العائد:
|
1065 |
+
dict: قاموس يحتوي على نتائج المقارنة
|
1066 |
+
"""
|
1067 |
+
try:
|
1068 |
+
if not items:
|
1069 |
+
return {
|
1070 |
+
'status': 'error',
|
1071 |
+
'message': 'لم يتم تحديد أي بنود للمقارنة'
|
1072 |
+
}
|
1073 |
+
|
1074 |
+
comparison_data = []
|
1075 |
+
|
1076 |
+
for item_id in items:
|
1077 |
+
# الحصول على معلومات البند الأساسية
|
1078 |
+
item_query = """
|
1079 |
+
SELECT
|
1080 |
+
id, code, name, description,
|
1081 |
+
(SELECT name FROM measurement_units WHERE id = unit_id) as unit_name,
|
1082 |
+
(SELECT symbol FROM measurement_units WHERE id = unit_id) as unit_symbol,
|
1083 |
+
base_price, last_updated_date
|
1084 |
+
FROM
|
1085 |
+
pricing_items_base
|
1086 |
+
WHERE
|
1087 |
+
id = ?
|
1088 |
+
"""
|
1089 |
+
|
1090 |
+
item_result = self.db.fetch_one(item_query, [item_id])
|
1091 |
+
|
1092 |
+
if not item_result:
|
1093 |
+
logger.warning(f"البند رقم {item_id} غير موجود")
|
1094 |
+
continue
|
1095 |
+
|
1096 |
+
item_data = {
|
1097 |
+
'id': item_result[0],
|
1098 |
+
'code': item_result[1],
|
1099 |
+
'name': item_result[2],
|
1100 |
+
'description': item_result[3],
|
1101 |
+
'unit_name': item_result[4],
|
1102 |
+
'unit_symbol': item_result[5],
|
1103 |
+
'base_price': item_result[6],
|
1104 |
+
'last_updated_date': item_result[7]
|
1105 |
+
}
|
1106 |
+
|
1107 |
+
# الحصول على أحدث سعر للبند
|
1108 |
+
price_query = """
|
1109 |
+
SELECT price, price_date, price_source
|
1110 |
+
FROM pricing_items_history
|
1111 |
+
WHERE base_item_id = ?
|
1112 |
+
ORDER BY price_date DESC
|
1113 |
+
LIMIT 1
|
1114 |
+
"""
|
1115 |
+
|
1116 |
+
price_result = self.db.fetch_one(price_query, [item_id])
|
1117 |
+
|
1118 |
+
if price_result:
|
1119 |
+
item_data['current_price'] = price_result[0]
|
1120 |
+
item_data['price_date'] = price_result[1]
|
1121 |
+
item_data['price_source'] = price_result[2]
|
1122 |
+
else:
|
1123 |
+
# إذا لم يتم العثور على سعر، نستخدم السعر الأساسي
|
1124 |
+
item_data['current_price'] = item_data['base_price']
|
1125 |
+
item_data['price_date'] = item_data['last_updated_date']
|
1126 |
+
item_data['price_source'] = 'base_price'
|
1127 |
+
|
1128 |
+
# الحصول على متوسط سعر السوق (من مصادر مختلفة)
|
1129 |
+
market_query = """
|
1130 |
+
SELECT AVG(price) as avg_price
|
1131 |
+
FROM pricing_items_history
|
1132 |
+
WHERE base_item_id = ? AND price_source != 'internal'
|
1133 |
+
AND price_date >= date('now', '-6 months')
|
1134 |
+
"""
|
1135 |
+
|
1136 |
+
market_result = self.db.fetch_one(market_query, [item_id])
|
1137 |
+
|
1138 |
+
if market_result and market_result[0]:
|
1139 |
+
item_data['market_price'] = market_result[0]
|
1140 |
+
|
1141 |
+
# حساب الفرق بين السعر الحالي وسعر السوق
|
1142 |
+
item_data['price_difference'] = item_data['current_price'] - item_data['market_price']
|
1143 |
+
item_data['price_difference_percentage'] = (item_data['price_difference'] / item_data['market_price']) * 100
|
1144 |
+
|
1145 |
+
# تحديد حالة السعر
|
1146 |
+
if abs(item_data['price_difference_percentage']) < 5:
|
1147 |
+
item_data['price_status'] = 'competitive'
|
1148 |
+
item_data['price_status_description'] = 'تنافسي'
|
1149 |
+
elif item_data['price_difference_percentage'] < 0:
|
1150 |
+
item_data['price_status'] = 'below_market'
|
1151 |
+
item_data['price_status_description'] = 'أقل من السوق'
|
1152 |
+
else:
|
1153 |
+
item_data['price_status'] = 'above_market'
|
1154 |
+
item_data['price_status_description'] = 'أعلى من السوق'
|
1155 |
+
else:
|
1156 |
+
# إذا لم يتم العثور على سعر سوق، نستخدم متوسط الأسعار الداخلية
|
1157 |
+
internal_query = """
|
1158 |
+
SELECT AVG(price) as avg_price
|
1159 |
+
FROM pricing_items_history
|
1160 |
+
WHERE base_item_id = ?
|
1161 |
+
AND price_date >= date('now', '-6 months')
|
1162 |
+
"""
|
1163 |
+
|
1164 |
+
internal_result = self.db.fetch_one(internal_query, [item_id])
|
1165 |
+
|
1166 |
+
if internal_result and internal_result[0]:
|
1167 |
+
item_data['market_price'] = internal_result[0]
|
1168 |
+
item_data['price_difference'] = item_data['current_price'] - item_data['market_price']
|
1169 |
+
item_data['price_difference_percentage'] = (item_data['price_difference'] / item_data['market_price']) * 100
|
1170 |
+
|
1171 |
+
# تحديد حالة السعر
|
1172 |
+
if abs(item_data['price_difference_percentage']) < 5:
|
1173 |
+
item_data['price_status'] = 'competitive'
|
1174 |
+
item_data['price_status_description'] = 'تنافسي'
|
1175 |
+
elif item_data['price_difference_percentage'] < 0:
|
1176 |
+
item_data['price_status'] = 'below_average'
|
1177 |
+
item_data['price_status_description'] = 'أقل من المتوسط'
|
1178 |
+
else:
|
1179 |
+
item_data['price_status'] = 'above_average'
|
1180 |
+
item_data['price_status_description'] = 'أعلى من المتوسط'
|
1181 |
+
else:
|
1182 |
+
item_data['market_price'] = None
|
1183 |
+
item_data['price_difference'] = None
|
1184 |
+
item_data['price_difference_percentage'] = None
|
1185 |
+
item_data['price_status'] = 'unknown'
|
1186 |
+
item_data['price_status_description'] = 'غير معروف'
|
1187 |
+
|
1188 |
+
comparison_data.append(item_data)
|
1189 |
+
|
1190 |
+
if not comparison_data:
|
1191 |
+
return {
|
1192 |
+
'status': 'error',
|
1193 |
+
'message': 'لم يتم العثور على أي بنود للمقارنة'
|
1194 |
+
}
|
1195 |
+
|
1196 |
+
# إنشاء رسم بياني للمقارنة
|
1197 |
+
chart_path = self._create_market_comparison_chart(comparison_data)
|
1198 |
+
|
1199 |
+
return {
|
1200 |
+
'status': 'success',
|
1201 |
+
'data': {
|
1202 |
+
'items': comparison_data,
|
1203 |
+
'chart_path': chart_path
|
1204 |
+
}
|
1205 |
+
}
|
1206 |
+
|
1207 |
+
except Exception as e:
|
1208 |
+
logger.error(f"خطأ في مقارنة الأسعار مع أسعار السوق: {str(e)}")
|
1209 |
+
return {
|
1210 |
+
'status': 'error',
|
1211 |
+
'message': f'حدث خطأ أثناء مقارنة الأسعار مع أسعار السوق: {str(e)}'
|
1212 |
+
}
|
1213 |
+
|
1214 |
+
def _create_market_comparison_chart(self, comparison_data):
|
1215 |
+
"""إنشاء رسم بياني لمقارنة الأسعار مع أسعار السوق
|
1216 |
+
|
1217 |
+
المعلمات:
|
1218 |
+
comparison_data (list): بيانات المقارنة
|
1219 |
+
|
1220 |
+
العائد:
|
1221 |
+
str: مسار ملف الرسم البياني
|
1222 |
+
"""
|
1223 |
+
try:
|
1224 |
+
# تصفية البنود التي لها أسعار سوق
|
1225 |
+
valid_items = [item for item in comparison_data if item.get('market_price') is not None]
|
1226 |
+
|
1227 |
+
if not valid_items:
|
1228 |
+
return None
|
1229 |
+
|
1230 |
+
# إنشاء رسم بياني جديد
|
1231 |
+
plt.figure(figsize=(12, 6))
|
1232 |
+
|
1233 |
+
# إعداد البيانات للرسم
|
1234 |
+
names = [f"{item['code']} - {item['name'][:20]}..." for item in valid_items]
|
1235 |
+
current_prices = [item['current_price'] for item in valid_items]
|
1236 |
+
market_prices = [item['market_price'] for item in valid_items]
|
1237 |
+
|
1238 |
+
# إنشاء مواقع الأعمدة
|
1239 |
+
x = np.arange(len(names))
|
1240 |
+
width = 0.35
|
1241 |
+
|
1242 |
+
# رسم الأعمدة
|
1243 |
+
plt.bar(x - width/2, current_prices, width, label='السعر الحالي', color='skyblue')
|
1244 |
+
plt.bar(x + width/2, market_prices, width, label='سعر السوق', color='lightgreen')
|
1245 |
+
|
1246 |
+
# إضافة تسميات وعنوان
|
1247 |
+
plt.xlabel('البنود')
|
1248 |
+
plt.ylabel('السعر')
|
1249 |
+
plt.title('مقارنة الأسعار الحالية مع أسعار السوق')
|
1250 |
+
plt.xticks(x, names, rotation=45, ha='right')
|
1251 |
+
plt.legend()
|
1252 |
+
|
1253 |
+
# إضافة شبكة
|
1254 |
+
plt.grid(True, linestyle='--', alpha=0.7, axis='y')
|
1255 |
+
|
1256 |
+
# إضافة قيم الفروق النسبية
|
1257 |
+
for i, item in enumerate(valid_items):
|
1258 |
+
if 'price_difference_percentage' in item and item['price_difference_percentage'] is not None:
|
1259 |
+
percentage = item['price_difference_percentage']
|
1260 |
+
color = 'green' if percentage < 0 else 'red' if percentage > 0 else 'black'
|
1261 |
+
plt.annotate(f"{percentage:.1f}%",
|
1262 |
+
xy=(x[i], max(current_prices[i], market_prices[i]) * 1.05),
|
1263 |
+
ha='center', va='bottom', color=color,
|
1264 |
+
bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="gray", alpha=0.8))
|
1265 |
+
|
1266 |
+
# ضبط التخطيط
|
1267 |
+
plt.tight_layout()
|
1268 |
+
|
1269 |
+
# حفظ الرسم البياني
|
1270 |
+
chart_filename = f"market_comparison_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
|
1271 |
+
chart_path = os.path.join(self.charts_dir, chart_filename)
|
1272 |
+
plt.savefig(chart_path, dpi=100, bbox_inches='tight')
|
1273 |
+
plt.close()
|
1274 |
+
|
1275 |
+
return chart_path
|
1276 |
+
|
1277 |
+
except Exception as e:
|
1278 |
+
logger.error(f"خطأ في إنشاء رسم بياني لمقارنة الأسعار مع أسعار السوق: {str(e)}")
|
1279 |
+
return None
|
1280 |
+
|
1281 |
+
def analyze_cost_drivers(self, project_id):
|
1282 |
+
"""تحليل محركات التكلفة للمشروع
|
1283 |
+
|
1284 |
+
المعلمات:
|
1285 |
+
project_id (int): معرف المشروع
|
1286 |
+
|
1287 |
+
العائد:
|
1288 |
+
dict: قاموس يحتوي على نتائج تحليل محركات التكلفة
|
1289 |
+
"""
|
1290 |
+
try:
|
1291 |
+
# الحصول على بنود المشروع
|
1292 |
+
query = """
|
1293 |
+
SELECT
|
1294 |
+
id, item_number, description, quantity, unit_price, total_price,
|
1295 |
+
(SELECT name FROM pricing_categories WHERE id =
|
1296 |
+
(SELECT category_id FROM pricing_items_base WHERE id = base_item_id)
|
1297 |
+
) as category_name
|
1298 |
+
FROM
|
1299 |
+
project_pricing_items
|
1300 |
+
WHERE
|
1301 |
+
project_id = ?
|
1302 |
+
"""
|
1303 |
+
|
1304 |
+
results = self.db.fetch_all(query, [project_id])
|
1305 |
+
|
1306 |
+
if not results:
|
1307 |
+
return {
|
1308 |
+
'status': 'error',
|
1309 |
+
'message': 'لا توجد بنود للمشروع المحدد'
|
1310 |
+
}
|
1311 |
+
|
1312 |
+
# تحويل النتائج إلى إطار بيانات
|
1313 |
+
df = pd.DataFrame(results, columns=[
|
1314 |
+
'id', 'item_number', 'description', 'quantity', 'unit_price',
|
1315 |
+
'total_price', 'category_name'
|
1316 |
+
])
|
1317 |
+
|
1318 |
+
# معالجة القيم المفقودة في عمود الفئة
|
1319 |
+
df['category_name'] = df['category_name'].fillna('أخرى')
|
1320 |
+
|
1321 |
+
# حساب إجمالي المشروع
|
1322 |
+
project_total = df['total_price'].sum()
|
1323 |
+
|
1324 |
+
# تحليل البنود حسب الفئة
|
1325 |
+
category_analysis = df.groupby('category_name').agg({
|
1326 |
+
'total_price': 'sum'
|
1327 |
+
}).reset_index()
|
1328 |
+
|
1329 |
+
# إضافة النسبة المئوية
|
1330 |
+
category_analysis['percentage'] = (category_analysis['total_price'] / project_total) * 100
|
1331 |
+
|
1332 |
+
# ترتيب الفئات حسب التكلفة
|
1333 |
+
category_analysis = category_analysis.sort_values('total_price', ascending=False)
|
1334 |
+
|
1335 |
+
# تحليل البنود الأعلى تكلفة
|
1336 |
+
top_items = df.sort_values('total_price', ascending=False).head(10)
|
1337 |
+
top_items['percentage'] = (top_items['total_price'] / project_total) * 100
|
1338 |
+
|
1339 |
+
# حساب تركيز التكلفة (نسبة باريتو)
|
1340 |
+
df_sorted = df.sort_values('total_price', ascending=False)
|
1341 |
+
df_sorted['cumulative_cost'] = df_sorted['total_price'].cumsum()
|
1342 |
+
df_sorted['cumulative_percentage'] = (df_sorted['cumulative_cost'] / project_total) * 100
|
1343 |
+
|
1344 |
+
# تحديد عدد البنود التي تشكل 80% من التكلفة
|
1345 |
+
items_80_percent = len(df_sorted[df_sorted['cumulative_percentage'] <= 80])
|
1346 |
+
if items_80_percent == 0:
|
1347 |
+
items_80_percent = 1
|
1348 |
+
|
1349 |
+
pareto_ratio = items_80_percent / len(df)
|
1350 |
+
|
1351 |
+
# إنشاء رسوم بيانية
|
1352 |
+
category_chart_path = self._create_category_chart(category_analysis)
|
1353 |
+
top_items_chart_path = self._create_top_items_chart(top_items)
|
1354 |
+
pareto_chart_path = self._create_pareto_chart(df_sorted)
|
1355 |
+
|
1356 |
+
return {
|
1357 |
+
'status': 'success',
|
1358 |
+
'data': {
|
1359 |
+
'project_id': project_id,
|
1360 |
+
'project_total': project_total,
|
1361 |
+
'category_analysis': category_analysis.to_dict('records'),
|
1362 |
+
'top_items': top_items.to_dict('records'),
|
1363 |
+
'pareto_ratio': pareto_ratio,
|
1364 |
+
'items_80_percent': items_80_percent,
|
1365 |
+
'total_items': len(df),
|
1366 |
+
'category_chart_path': category_chart_path,
|
1367 |
+
'top_items_chart_path': top_items_chart_path,
|
1368 |
+
'pareto_chart_path': pareto_chart_path
|
1369 |
+
}
|
1370 |
+
}
|
1371 |
+
|
1372 |
+
except Exception as e:
|
1373 |
+
logger.error(f"خطأ في تحليل محركات التكلفة: {str(e)}")
|
1374 |
+
return {
|
1375 |
+
'status': 'error',
|
1376 |
+
'message': f'حدث خطأ أثناء تحليل محركات التكلفة: {str(e)}'
|
1377 |
+
}
|
1378 |
+
|
1379 |
+
def _create_category_chart(self, category_analysis):
|
1380 |
+
"""إنشاء رسم بياني للتكاليف حسب الفئة
|
1381 |
+
|
1382 |
+
المعلمات:
|
1383 |
+
category_analysis (pandas.DataFrame): تحليل الفئات
|
1384 |
+
|
1385 |
+
العائد:
|
1386 |
+
str: مسار ملف الرسم البياني
|
1387 |
+
"""
|
1388 |
+
try:
|
1389 |
+
# إنشاء رسم بياني جديد
|
1390 |
+
plt.figure(figsize=(10, 6))
|
1391 |
+
|
1392 |
+
# رسم مخطط دائري
|
1393 |
+
plt.pie(
|
1394 |
+
category_analysis['total_price'],
|
1395 |
+
labels=category_analysis['category_name'],
|
1396 |
+
autopct='%1.1f%%',
|
1397 |
+
startangle=90,
|
1398 |
+
shadow=False,
|
1399 |
+
wedgeprops={'edgecolor': 'white', 'linewidth': 1}
|
1400 |
+
)
|
1401 |
+
|
1402 |
+
# إضافة عنوان
|
1403 |
+
plt.title('توزيع التكاليف حسب الفئة')
|
1404 |
+
|
1405 |
+
# جعل الرسم البياني دائريًا
|
1406 |
+
plt.axis('equal')
|
1407 |
+
|
1408 |
+
# ضبط التخطيط
|
1409 |
+
plt.tight_layout()
|
1410 |
+
|
1411 |
+
# حفظ الرسم البياني
|
1412 |
+
chart_filename = f"cost_category_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
|
1413 |
+
chart_path = os.path.join(self.charts_dir, chart_filename)
|
1414 |
+
plt.savefig(chart_path, dpi=100, bbox_inches='tight')
|
1415 |
+
plt.close()
|
1416 |
+
|
1417 |
+
return chart_path
|
1418 |
+
|
1419 |
+
except Exception as e:
|
1420 |
+
logger.error(f"خطأ في إنشاء رسم بياني للتكاليف حسب الفئة: {str(e)}")
|
1421 |
+
return None
|
1422 |
+
|
1423 |
+
def _create_top_items_chart(self, top_items):
|
1424 |
+
"""إنشاء رسم بياني للبنود الأعلى تكلفة
|
1425 |
+
|
1426 |
+
المعلمات:
|
1427 |
+
top_items (pandas.DataFrame): البنود الأعلى تكلفة
|
1428 |
+
|
1429 |
+
العائد:
|
1430 |
+
str: مسار ملف الرسم البياني
|
1431 |
+
"""
|
1432 |
+
try:
|
1433 |
+
# إنشاء رسم بياني جديد
|
1434 |
+
plt.figure(figsize=(12, 6))
|
1435 |
+
|
1436 |
+
# إعداد البيانات للرسم
|
1437 |
+
items = [f"{row['item_number']} - {row['description'][:20]}..." for _, row in top_items.iterrows()]
|
1438 |
+
costs = top_items['total_price'].tolist()
|
1439 |
+
|
1440 |
+
# رسم الأعمدة
|
1441 |
+
bars = plt.barh(items, costs, color='skyblue', edgecolor='navy')
|
1442 |
+
|
1443 |
+
# إضافة القيم على الأعمدة
|
1444 |
+
for i, bar in enumerate(bars):
|
1445 |
+
width = bar.get_width()
|
1446 |
+
plt.text(width * 1.01, bar.get_y() + bar.get_height()/2,
|
1447 |
+
f'{width:,.0f} ({top_items["percentage"].iloc[i]:.1f}%)',
|
1448 |
+
va='center')
|
1449 |
+
|
1450 |
+
# إضافة عنوان ومحاور
|
1451 |
+
plt.title('البنود الأعلى تكلفة')
|
1452 |
+
plt.xlabel('التكلفة')
|
1453 |
+
plt.ylabel('البنود')
|
1454 |
+
|
1455 |
+
# إضافة شبكة
|
1456 |
+
plt.grid(True, linestyle='--', alpha=0.7, axis='x')
|
1457 |
+
|
1458 |
+
# ضبط التخطيط
|
1459 |
+
plt.tight_layout()
|
1460 |
+
|
1461 |
+
# حفظ الرسم البياني
|
1462 |
+
chart_filename = f"top_cost_items_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
|
1463 |
+
chart_path = os.path.join(self.charts_dir, chart_filename)
|
1464 |
+
plt.savefig(chart_path, dpi=100, bbox_inches='tight')
|
1465 |
+
plt.close()
|
1466 |
+
|
1467 |
+
return chart_path
|
1468 |
+
|
1469 |
+
except Exception as e:
|
1470 |
+
logger.error(f"خطأ في إنشاء رسم بياني للبنود الأعلى تكلفة: {str(e)}")
|
1471 |
+
return None
|
1472 |
+
|
1473 |
+
def _create_pareto_chart(self, df_sorted):
|
1474 |
+
"""إنشاء رسم بياني لتحليل باريتو
|
1475 |
+
|
1476 |
+
المعلمات:
|
1477 |
+
df_sorted (pandas.DataFrame): إطار البيانات المرتب
|
1478 |
+
|
1479 |
+
العائد:
|
1480 |
+
str: مسار ملف الرسم البياني
|
1481 |
+
"""
|
1482 |
+
try:
|
1483 |
+
# إنشاء رسم بياني جديد
|
1484 |
+
fig, ax1 = plt.subplots(figsize=(12, 6))
|
1485 |
+
|
1486 |
+
# إعداد البيانات للرسم
|
1487 |
+
x = range(1, len(df_sorted) + 1)
|
1488 |
+
y1 = df_sorted['total_price'].tolist()
|
1489 |
+
y2 = df_sorted['cumulative_percentage'].tolist()
|
1490 |
+
|
1491 |
+
# رسم الأعمدة (التكلفة)
|
1492 |
+
ax1.bar(x, y1, color='skyblue', alpha=0.7)
|
1493 |
+
ax1.set_xlabel('عدد البنود')
|
1494 |
+
ax1.set_ylabel('التكلفة', color='navy')
|
1495 |
+
ax1.tick_params(axis='y', labelcolor='navy')
|
1496 |
+
|
1497 |
+
# إنشاء محور ثانوي
|
1498 |
+
ax2 = ax1.twinx()
|
1499 |
+
|
1500 |
+
# رسم الخط (النسبة التراكمية)
|
1501 |
+
ax2.plot(x, y2, 'r-', linewidth=2, marker='o', markersize=4)
|
1502 |
+
ax2.set_ylabel('النسبة التراكمية (%)', color='red')
|
1503 |
+
ax2.tick_params(axis='y', labelcolor='red')
|
1504 |
+
|
1505 |
+
# إضافة خط 80%
|
1506 |
+
ax2.axhline(y=80, color='green', linestyle='--', alpha=0.7)
|
1507 |
+
|
1508 |
+
# إضافة عنوان
|
1509 |
+
plt.title('تحليل باريتو للتكاليف')
|
1510 |
+
|
1511 |
+
# إضافة شبكة
|
1512 |
+
ax1.grid(True, linestyle='--', alpha=0.7)
|
1513 |
+
|
1514 |
+
# ضبط التخطيط
|
1515 |
+
fig.tight_layout()
|
1516 |
+
|
1517 |
+
# حفظ الرسم البياني
|
1518 |
+
chart_filename = f"pareto_analysis_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
|
1519 |
+
chart_path = os.path.join(self.charts_dir, chart_filename)
|
1520 |
+
plt.savefig(chart_path, dpi=100, bbox_inches='tight')
|
1521 |
+
plt.close()
|
1522 |
+
|
1523 |
+
return chart_path
|
1524 |
+
|
1525 |
+
except Exception as e:
|
1526 |
+
logger.error(f"خطأ في إنشاء رسم بياني لتحليل باريتو: {str(e)}")
|
1527 |
+
return None
|
1528 |
+
|
1529 |
+
def generate_price_analysis_charts(self, analysis_type, params):
|
1530 |
+
"""إنشاء رسوم بيانية لتحليل الأسعار
|
1531 |
+
|
1532 |
+
المعلمات:
|
1533 |
+
analysis_type (str): نوع التحليل
|
1534 |
+
params (dict): معلمات التحليل
|
1535 |
+
|
1536 |
+
العائد:
|
1537 |
+
dict: قاموس يحتوي على مسارات الرسوم البيانية
|
1538 |
+
"""
|
1539 |
+
try:
|
1540 |
+
if analysis_type == 'trend':
|
1541 |
+
# تحليل اتجاه السعر
|
1542 |
+
if 'item_id' not in params:
|
1543 |
+
return {
|
1544 |
+
'status': 'error',
|
1545 |
+
'message': 'لم يتم تحديد معرف البند'
|
1546 |
+
}
|
1547 |
+
|
1548 |
+
result = self.analyze_price_trends(
|
1549 |
+
params['item_id'],
|
1550 |
+
params.get('start_date'),
|
1551 |
+
params.get('end_date')
|
1552 |
+
)
|
1553 |
+
|
1554 |
+
if result['status'] == 'success':
|
1555 |
+
return {
|
1556 |
+
'status': 'success',
|
1557 |
+
'charts': [result['data']['chart_path']]
|
1558 |
+
}
|
1559 |
+
else:
|
1560 |
+
return result
|
1561 |
+
|
1562 |
+
elif analysis_type == 'comparison':
|
1563 |
+
# مقارنة الأسعار
|
1564 |
+
if 'items' not in params:
|
1565 |
+
return {
|
1566 |
+
'status': 'error',
|
1567 |
+
'message': 'لم يتم تحديد البنود للمقارنة'
|
1568 |
+
}
|
1569 |
+
|
1570 |
+
result = self.compare_prices(
|
1571 |
+
params['items'],
|
1572 |
+
params.get('date')
|
1573 |
+
)
|
1574 |
+
|
1575 |
+
if result['status'] == 'success':
|
1576 |
+
return {
|
1577 |
+
'status': 'success',
|
1578 |
+
'charts': [result['data']['chart_path']]
|
1579 |
+
}
|
1580 |
+
else:
|
1581 |
+
return result
|
1582 |
+
|
1583 |
+
elif analysis_type == 'volatility':
|
1584 |
+
# تحليل تقلب الأسعار
|
1585 |
+
if 'item_id' not in params:
|
1586 |
+
return {
|
1587 |
+
'status': 'error',
|
1588 |
+
'message': 'لم يتم تحديد معرف البند'
|
1589 |
+
}
|
1590 |
+
|
1591 |
+
result = self.calculate_price_volatility(
|
1592 |
+
params['item_id'],
|
1593 |
+
params.get('period', '1y')
|
1594 |
+
)
|
1595 |
+
|
1596 |
+
if result['status'] == 'success':
|
1597 |
+
return {
|
1598 |
+
'status': 'success',
|
1599 |
+
'charts': [result['data']['chart_path']]
|
1600 |
+
}
|
1601 |
+
else:
|
1602 |
+
return result
|
1603 |
+
|
1604 |
+
elif analysis_type == 'sensitivity':
|
1605 |
+
# تحليل الحساسية
|
1606 |
+
if 'project_id' not in params or 'variable_items' not in params:
|
1607 |
+
return {
|
1608 |
+
'status': 'error',
|
1609 |
+
'message': 'لم يتم تحديد معرف المشروع أو البنود المتغيرة'
|
1610 |
+
}
|
1611 |
+
|
1612 |
+
result = self.perform_sensitivity_analysis(
|
1613 |
+
params['project_id'],
|
1614 |
+
params['variable_items'],
|
1615 |
+
params.get('ranges', {})
|
1616 |
+
)
|
1617 |
+
|
1618 |
+
if result['status'] == 'success':
|
1619 |
+
return {
|
1620 |
+
'status': 'success',
|
1621 |
+
'charts': [result['data']['chart_path']]
|
1622 |
+
}
|
1623 |
+
else:
|
1624 |
+
return result
|
1625 |
+
|
1626 |
+
elif analysis_type == 'correlation':
|
1627 |
+
# تحليل الارتباطات
|
1628 |
+
if 'items' not in params:
|
1629 |
+
return {
|
1630 |
+
'status': 'error',
|
1631 |
+
'message': 'لم يتم تحديد البنود للتحليل'
|
1632 |
+
}
|
1633 |
+
|
1634 |
+
result = self.analyze_price_correlations(params['items'])
|
1635 |
+
|
1636 |
+
if result['status'] == 'success':
|
1637 |
+
return {
|
1638 |
+
'status': 'success',
|
1639 |
+
'charts': [result['data']['chart_path'], result['data']['trends_chart_path']]
|
1640 |
+
}
|
1641 |
+
else:
|
1642 |
+
return result
|
1643 |
+
|
1644 |
+
elif analysis_type == 'market_comparison':
|
1645 |
+
# مقارنة مع أسعار السوق
|
1646 |
+
if 'items' not in params:
|
1647 |
+
return {
|
1648 |
+
'status': 'error',
|
1649 |
+
'message': 'لم يتم تحديد البنود للمقارنة'
|
1650 |
+
}
|
1651 |
+
|
1652 |
+
result = self.compare_with_market_prices(params['items'])
|
1653 |
+
|
1654 |
+
if result['status'] == 'success':
|
1655 |
+
return {
|
1656 |
+
'status': 'success',
|
1657 |
+
'charts': [result['data']['chart_path']]
|
1658 |
+
}
|
1659 |
+
else:
|
1660 |
+
return result
|
1661 |
+
|
1662 |
+
elif analysis_type == 'cost_drivers':
|
1663 |
+
# تحليل محركات التكلفة
|
1664 |
+
if 'project_id' not in params:
|
1665 |
+
return {
|
1666 |
+
'status': 'error',
|
1667 |
+
'message': 'لم يتم تحديد معرف المشروع'
|
1668 |
+
}
|
1669 |
+
|
1670 |
+
result = self.analyze_cost_drivers(params['project_id'])
|
1671 |
+
|
1672 |
+
if result['status'] == 'success':
|
1673 |
+
return {
|
1674 |
+
'status': 'success',
|
1675 |
+
'charts': [
|
1676 |
+
result['data']['category_chart_path'],
|
1677 |
+
result['data']['top_items_chart_path'],
|
1678 |
+
result['data']['pareto_chart_path']
|
1679 |
+
]
|
1680 |
+
}
|
1681 |
+
else:
|
1682 |
+
return result
|
1683 |
+
|
1684 |
+
else:
|
1685 |
+
return {
|
1686 |
+
'status': 'error',
|
1687 |
+
'message': f'نوع التحليل غير معروف: {analysis_type}'
|
1688 |
+
}
|
1689 |
+
|
1690 |
+
except Exception as e:
|
1691 |
+
logger.error(f"خطأ في إنشاء رسوم بيانية لتحليل الأسعار: {str(e)}")
|
1692 |
+
return {
|
1693 |
+
'status': 'error',
|
1694 |
+
'message': f'حدث خطأ أثناء إنشاء رسوم بيانية لتحليل الأسعار: {str(e)}'
|
1695 |
+
}
|
modules/pricing/pricing_app.py
ADDED
@@ -0,0 +1,1760 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة التسعير - التطبيق الرئيسي
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import matplotlib.pyplot as plt
|
9 |
+
import plotly.express as px
|
10 |
+
import plotly.graph_objects as go
|
11 |
+
from datetime import datetime
|
12 |
+
import time
|
13 |
+
import io
|
14 |
+
import os
|
15 |
+
import json
|
16 |
+
import base64
|
17 |
+
from pathlib import Path
|
18 |
+
|
19 |
+
class PricingApp:
|
20 |
+
"""وحدة التسعير"""
|
21 |
+
|
22 |
+
def __init__(self):
|
23 |
+
"""تهيئة وحدة التسعير"""
|
24 |
+
|
25 |
+
# تهيئة حالة الجلسة
|
26 |
+
if 'bill_of_quantities' not in st.session_state:
|
27 |
+
st.session_state.bill_of_quantities = [
|
28 |
+
{
|
29 |
+
'id': 1,
|
30 |
+
'code': 'A-001',
|
31 |
+
'description': 'أعمال الحفر والردم',
|
32 |
+
'unit': 'م3',
|
33 |
+
'quantity': 1500,
|
34 |
+
'unit_price': 45,
|
35 |
+
'total_price': 67500,
|
36 |
+
'category': 'أعمال ترابية'
|
37 |
+
},
|
38 |
+
{
|
39 |
+
'id': 2,
|
40 |
+
'code': 'A-002',
|
41 |
+
'description': 'توريد وصب خرسانة عادية',
|
42 |
+
'unit': 'م3',
|
43 |
+
'quantity': 250,
|
44 |
+
'unit_price': 350,
|
45 |
+
'total_price': 87500,
|
46 |
+
'category': 'أعمال خرسانية'
|
47 |
+
},
|
48 |
+
{
|
49 |
+
'id': 3,
|
50 |
+
'code': 'A-003',
|
51 |
+
'description': 'توريد وصب خرسانة مسلحة للأساسات',
|
52 |
+
'unit': 'م3',
|
53 |
+
'quantity': 180,
|
54 |
+
'unit_price': 450,
|
55 |
+
'total_price': 81000,
|
56 |
+
'category': 'أعمال خرسانية'
|
57 |
+
},
|
58 |
+
{
|
59 |
+
'id': 4,
|
60 |
+
'code': 'B-001',
|
61 |
+
'description': 'توريد وتركيب حديد تسليح',
|
62 |
+
'unit': 'طن',
|
63 |
+
'quantity': 15,
|
64 |
+
'unit_price': 3500,
|
65 |
+
'total_price': 52500,
|
66 |
+
'category': 'أعمال حديد'
|
67 |
+
},
|
68 |
+
{
|
69 |
+
'id': 5,
|
70 |
+
'code': 'C-001',
|
71 |
+
'description': 'توريد وبناء طابوق',
|
72 |
+
'unit': 'م2',
|
73 |
+
'quantity': 450,
|
74 |
+
'unit_price': 120,
|
75 |
+
'total_price': 54000,
|
76 |
+
'category': 'أعمال بناء'
|
77 |
+
}
|
78 |
+
]
|
79 |
+
|
80 |
+
if 'cost_analysis' not in st.session_state:
|
81 |
+
st.session_state.cost_analysis = [
|
82 |
+
{
|
83 |
+
'id': 1,
|
84 |
+
'category': 'تكاليف مباشرة',
|
85 |
+
'subcategory': 'مواد',
|
86 |
+
'description': 'خرسانة',
|
87 |
+
'amount': 168500,
|
88 |
+
'percentage': 25.2
|
89 |
+
},
|
90 |
+
{
|
91 |
+
'id': 2,
|
92 |
+
'category': 'تكاليف مباشرة',
|
93 |
+
'subcategory': 'مواد',
|
94 |
+
'description': 'حديد تسليح',
|
95 |
+
'amount': 52500,
|
96 |
+
'percentage': 7.8
|
97 |
+
},
|
98 |
+
{
|
99 |
+
'id': 3,
|
100 |
+
'category': 'تكاليف مباشرة',
|
101 |
+
'subcategory': 'مواد',
|
102 |
+
'description': 'طابوق',
|
103 |
+
'amount': 54000,
|
104 |
+
'percentage': 8.1
|
105 |
+
},
|
106 |
+
{
|
107 |
+
'id': 4,
|
108 |
+
'category': 'تكاليف مباشرة',
|
109 |
+
'subcategory': 'عمالة',
|
110 |
+
'description': 'عمالة تنفيذ',
|
111 |
+
'amount': 120000,
|
112 |
+
'percentage': 17.9
|
113 |
+
},
|
114 |
+
{
|
115 |
+
'id': 5,
|
116 |
+
'category': 'تكاليف مباشرة',
|
117 |
+
'subcategory': 'معدات',
|
118 |
+
'description': 'معدات إنشائية',
|
119 |
+
'amount': 85000,
|
120 |
+
'percentage': 12.7
|
121 |
+
},
|
122 |
+
{
|
123 |
+
'id': 6,
|
124 |
+
'category': 'تكاليف غير مباشرة',
|
125 |
+
'subcategory': 'إدارة',
|
126 |
+
'description': 'إدارة المشروع',
|
127 |
+
'amount': 45000,
|
128 |
+
'percentage': 6.7
|
129 |
+
},
|
130 |
+
{
|
131 |
+
'id': 7,
|
132 |
+
'category': 'تكاليف غير مباشرة',
|
133 |
+
'subcategory': 'إدارة',
|
134 |
+
'description': 'إشراف هندسي',
|
135 |
+
'amount': 35000,
|
136 |
+
'percentage': 5.2
|
137 |
+
},
|
138 |
+
{
|
139 |
+
'id': 8,
|
140 |
+
'category': 'تكاليف غير مباشرة',
|
141 |
+
'subcategory': 'عامة',
|
142 |
+
'description': 'تأمينات وضمانات',
|
143 |
+
'amount': 25000,
|
144 |
+
'percentage': 3.7
|
145 |
+
},
|
146 |
+
{
|
147 |
+
'id': 9,
|
148 |
+
'category': 'تكاليف غير مباشرة',
|
149 |
+
'subcategory': 'عامة',
|
150 |
+
'description': 'مصاريف إدارية',
|
151 |
+
'amount': 30000,
|
152 |
+
'percentage': 4.5
|
153 |
+
},
|
154 |
+
{
|
155 |
+
'id': 10,
|
156 |
+
'category': 'أرباح',
|
157 |
+
'subcategory': 'أرباح',
|
158 |
+
'description': 'هامش الربح',
|
159 |
+
'amount': 55000,
|
160 |
+
'percentage': 8.2
|
161 |
+
}
|
162 |
+
]
|
163 |
+
|
164 |
+
if 'price_scenarios' not in st.session_state:
|
165 |
+
st.session_state.price_scenarios = [
|
166 |
+
{
|
167 |
+
'id': 1,
|
168 |
+
'name': 'السيناريو الأساسي',
|
169 |
+
'description': 'التسعير الأساسي مع هامش ربح 8%',
|
170 |
+
'total_cost': 615000,
|
171 |
+
'profit_margin': 8.2,
|
172 |
+
'total_price': 670000,
|
173 |
+
'is_active': True
|
174 |
+
},
|
175 |
+
{
|
176 |
+
'id': 2,
|
177 |
+
'name': 'سيناريو تنافسي',
|
178 |
+
'description': 'تخفيض هامش الربح للمنافسة',
|
179 |
+
'total_cost': 615000,
|
180 |
+
'profit_margin': 5.0,
|
181 |
+
'total_price': 650000,
|
182 |
+
'is_active': False
|
183 |
+
},
|
184 |
+
{
|
185 |
+
'id': 3,
|
186 |
+
'name': 'سيناريو مرتفع',
|
187 |
+
'description': 'زيادة هامش الربح للمشاريع ذات المخاطر العالية',
|
188 |
+
'total_cost': 615000,
|
189 |
+
'profit_margin': 12.0,
|
190 |
+
'total_price': 700000,
|
191 |
+
'is_active': False
|
192 |
+
}
|
193 |
+
]
|
194 |
+
|
195 |
+
def render(self):
|
196 |
+
"""عرض واجهة وحدة التسعير"""
|
197 |
+
|
198 |
+
st.markdown("<h1 class='module-title'>وحدة التسعير</h1>", unsafe_allow_html=True)
|
199 |
+
|
200 |
+
tabs = st.tabs([
|
201 |
+
"لوحة التحكم",
|
202 |
+
"جدول الكميات",
|
203 |
+
"تحليل التكاليف",
|
204 |
+
"سيناريوهات التسعير",
|
205 |
+
"المقارنة التنافسية",
|
206 |
+
"التقارير"
|
207 |
+
])
|
208 |
+
|
209 |
+
with tabs[0]:
|
210 |
+
self._render_dashboard_tab()
|
211 |
+
|
212 |
+
with tabs[1]:
|
213 |
+
self._render_bill_of_quantities_tab()
|
214 |
+
|
215 |
+
with tabs[2]:
|
216 |
+
self._render_cost_analysis_tab()
|
217 |
+
|
218 |
+
with tabs[3]:
|
219 |
+
self._render_pricing_scenarios_tab()
|
220 |
+
|
221 |
+
with tabs[4]:
|
222 |
+
self._render_competitive_analysis_tab()
|
223 |
+
|
224 |
+
with tabs[5]:
|
225 |
+
self._render_reports_tab()
|
226 |
+
|
227 |
+
def _render_dashboard_tab(self):
|
228 |
+
"""عرض تبويب لوحة التحكم"""
|
229 |
+
|
230 |
+
st.markdown("### لوحة تحكم التسعير")
|
231 |
+
|
232 |
+
# عرض ملخص التسعير
|
233 |
+
col1, col2, col3, col4 = st.columns(4)
|
234 |
+
|
235 |
+
# حساب إجمالي التكاليف
|
236 |
+
total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
|
237 |
+
total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
|
238 |
+
total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
|
239 |
+
total_cost = total_direct_cost + total_indirect_cost
|
240 |
+
total_price = total_cost + total_profit
|
241 |
+
|
242 |
+
with col1:
|
243 |
+
st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.0f} ريال")
|
244 |
+
|
245 |
+
with col2:
|
246 |
+
st.metric("إجمالي التكاليف غير المباشرة", f"{total_indirect_cost:,.0f} ريال")
|
247 |
+
|
248 |
+
with col3:
|
249 |
+
st.metric("إجمالي التكاليف", f"{total_cost:,.0f} ريال")
|
250 |
+
|
251 |
+
with col4:
|
252 |
+
st.metric("السعر الإجمالي", f"{total_price:,.0f} ريال")
|
253 |
+
|
254 |
+
# عرض توزيع التكاليف
|
255 |
+
st.markdown("### توزيع التكاليف")
|
256 |
+
|
257 |
+
# تجميع البيانات حسب الفئة
|
258 |
+
cost_categories = {}
|
259 |
+
|
260 |
+
for item in st.session_state.cost_analysis:
|
261 |
+
category = item['category']
|
262 |
+
if category in cost_categories:
|
263 |
+
cost_categories[category] += item['amount']
|
264 |
+
else:
|
265 |
+
cost_categories[category] = item['amount']
|
266 |
+
|
267 |
+
# إنشاء DataFrame للرسم البياني
|
268 |
+
cost_df = pd.DataFrame({
|
269 |
+
'الفئة': list(cost_categories.keys()),
|
270 |
+
'المبلغ': list(cost_categories.values())
|
271 |
+
})
|
272 |
+
|
273 |
+
# إنشاء رسم بياني دائري
|
274 |
+
fig = px.pie(
|
275 |
+
cost_df,
|
276 |
+
values='المبلغ',
|
277 |
+
names='الفئة',
|
278 |
+
title='توزيع التكاليف حسب الفئة',
|
279 |
+
color_discrete_sequence=px.colors.qualitative.Set3
|
280 |
+
)
|
281 |
+
|
282 |
+
st.plotly_chart(fig, use_container_width=True)
|
283 |
+
|
284 |
+
# عرض توزيع التكاليف المباشرة
|
285 |
+
st.markdown("### توزيع التكاليف المباشرة")
|
286 |
+
|
287 |
+
# تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
|
288 |
+
direct_cost_subcategories = {}
|
289 |
+
|
290 |
+
for item in st.session_state.cost_analysis:
|
291 |
+
if item['category'] == 'تكاليف مباشرة':
|
292 |
+
subcategory = item['subcategory']
|
293 |
+
if subcategory in direct_cost_subcategories:
|
294 |
+
direct_cost_subcategories[subcategory] += item['amount']
|
295 |
+
else:
|
296 |
+
direct_cost_subcategories[subcategory] = item['amount']
|
297 |
+
|
298 |
+
# إنشاء DataFrame للرسم البياني
|
299 |
+
direct_cost_df = pd.DataFrame({
|
300 |
+
'الفئة الفرعية': list(direct_cost_subcategories.keys()),
|
301 |
+
'المبلغ': list(direct_cost_subcategories.values())
|
302 |
+
})
|
303 |
+
|
304 |
+
# إنشاء رسم بياني شريطي
|
305 |
+
fig = px.bar(
|
306 |
+
direct_cost_df,
|
307 |
+
x='الفئة الفرعية',
|
308 |
+
y='المبلغ',
|
309 |
+
title='توزيع التكاليف المباشرة',
|
310 |
+
color='الفئة الفرعية',
|
311 |
+
text_auto='.2s'
|
312 |
+
)
|
313 |
+
|
314 |
+
st.plotly_chart(fig, use_container_width=True)
|
315 |
+
|
316 |
+
# عرض مقارنة سيناريوهات التسعير
|
317 |
+
st.markdown("### مقارنة سيناريوهات التسعير")
|
318 |
+
|
319 |
+
# إنشاء DataFrame للرسم البياني
|
320 |
+
scenarios_df = pd.DataFrame({
|
321 |
+
'السيناريو': [item['name'] for item in st.session_state.price_scenarios],
|
322 |
+
'التكلفة الإجمالية': [item['total_cost'] for item in st.session_state.price_scenarios],
|
323 |
+
'هامش الربح (%)': [item['profit_margin'] for item in st.session_state.price_scenarios],
|
324 |
+
'السعر الإجمالي': [item['total_price'] for item in st.session_state.price_scenarios]
|
325 |
+
})
|
326 |
+
|
327 |
+
# إنشاء رسم بياني شريطي مزدوج
|
328 |
+
fig = go.Figure()
|
329 |
+
|
330 |
+
# إضافة شريط للتكلفة الإجمالية
|
331 |
+
fig.add_trace(go.Bar(
|
332 |
+
x=scenarios_df['السيناريو'],
|
333 |
+
y=scenarios_df['التكلفة الإجمالية'],
|
334 |
+
name='التكلفة الإجمالية',
|
335 |
+
marker_color='indianred'
|
336 |
+
))
|
337 |
+
|
338 |
+
# إضافة شريط للسعر الإجمالي
|
339 |
+
fig.add_trace(go.Bar(
|
340 |
+
x=scenarios_df['السيناريو'],
|
341 |
+
y=scenarios_df['السعر الإجمالي'],
|
342 |
+
name='السعر الإجمالي',
|
343 |
+
marker_color='lightsalmon'
|
344 |
+
))
|
345 |
+
|
346 |
+
# إضافة خط لهامش الربح
|
347 |
+
fig.add_trace(go.Scatter(
|
348 |
+
x=scenarios_df['السيناريو'],
|
349 |
+
y=scenarios_df['هامش الربح (%)'] * 10000, # تكبير القيم لتظهر على الرسم البياني
|
350 |
+
name='هامش الربح (%)',
|
351 |
+
yaxis='y2',
|
352 |
+
line=dict(color='royalblue', width=4)
|
353 |
+
))
|
354 |
+
|
355 |
+
# تعديل تخطيط الرسم البياني
|
356 |
+
fig.update_layout(
|
357 |
+
title='مقارنة سيناريوهات التسعير',
|
358 |
+
xaxis_title='السيناريو',
|
359 |
+
yaxis_title='المبلغ (ريال)',
|
360 |
+
yaxis2=dict(
|
361 |
+
title='هامش الربح (%)',
|
362 |
+
titlefont=dict(color='royalblue'),
|
363 |
+
tickfont=dict(color='royalblue'),
|
364 |
+
overlaying='y',
|
365 |
+
side='right',
|
366 |
+
range=[0, 20]
|
367 |
+
),
|
368 |
+
barmode='group',
|
369 |
+
legend=dict(
|
370 |
+
x=0,
|
371 |
+
y=1.2,
|
372 |
+
orientation='h'
|
373 |
+
)
|
374 |
+
)
|
375 |
+
|
376 |
+
# تعديل النص على الأشرطة
|
377 |
+
fig.update_traces(
|
378 |
+
texttemplate='%{y:,.0f}',
|
379 |
+
textposition='outside'
|
380 |
+
)
|
381 |
+
|
382 |
+
st.plotly_chart(fig, use_container_width=True)
|
383 |
+
|
384 |
+
# عرض مؤشرات الأداء الرئيسية
|
385 |
+
st.markdown("### مؤشرات الأداء الرئيسية")
|
386 |
+
|
387 |
+
col1, col2, col3 = st.columns(3)
|
388 |
+
|
389 |
+
with col1:
|
390 |
+
# حساب نسبة التكاليف المباشرة من إجمالي التكاليف
|
391 |
+
direct_cost_percentage = (total_direct_cost / total_cost) * 100
|
392 |
+
st.metric("نسبة التكاليف المباشرة", f"{direct_cost_percentage:.1f}%")
|
393 |
+
|
394 |
+
with col2:
|
395 |
+
# حساب نسبة التكاليف غير المباشرة من إجمالي التكاليف
|
396 |
+
indirect_cost_percentage = (total_indirect_cost / total_cost) * 100
|
397 |
+
st.metric("نسبة التكاليف غير المباشرة", f"{indirect_cost_percentage:.1f}%")
|
398 |
+
|
399 |
+
with col3:
|
400 |
+
# حساب نسبة هامش الربح من السعر الإجمالي
|
401 |
+
profit_margin = (total_profit / total_price) * 100
|
402 |
+
st.metric("هامش الربح", f"{profit_margin:.1f}%")
|
403 |
+
|
404 |
+
def _render_bill_of_quantities_tab(self):
|
405 |
+
"""عرض تبويب جدول الكميات"""
|
406 |
+
|
407 |
+
st.markdown("### جدول الكميات")
|
408 |
+
|
409 |
+
# عرض جدول الكميات الحالي
|
410 |
+
st.markdown("#### قائمة البنود")
|
411 |
+
|
412 |
+
# تحويل قائمة البنود إلى DataFrame
|
413 |
+
boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
|
414 |
+
|
415 |
+
# عرض البنود كجدول قابل للتعديل
|
416 |
+
edited_df = st.data_editor(
|
417 |
+
boq_df,
|
418 |
+
column_config={
|
419 |
+
"id": st.column_config.NumberColumn("الرقم", disabled=True),
|
420 |
+
"code": st.column_config.TextColumn("الكود"),
|
421 |
+
"description": st.column_config.TextColumn("الوصف"),
|
422 |
+
"unit": st.column_config.SelectboxColumn(
|
423 |
+
"الوحدة",
|
424 |
+
options=["م3", "م2", "طن", "كجم", "عدد", "لتر", "متر"]
|
425 |
+
),
|
426 |
+
"quantity": st.column_config.NumberColumn("الكمية", min_value=0),
|
427 |
+
"unit_price": st.column_config.NumberColumn("سعر الوحدة (ريال)", min_value=0, format="%.2f"),
|
428 |
+
"total_price": st.column_config.NumberColumn("السعر الإجمالي (ريال)", min_value=0, format="%.2f", disabled=True),
|
429 |
+
"category": st.column_config.SelectboxColumn(
|
430 |
+
"الفئة",
|
431 |
+
options=["أعمال ترابية", "أعمال خرسانية", "أعمال حديد", "أعمال بناء", "أعمال تشطيب", "أعمال كهربائية", "أعمال ميكانيكية", "أعمال صحية", "أخرى"]
|
432 |
+
)
|
433 |
+
},
|
434 |
+
use_container_width=True,
|
435 |
+
hide_index=True,
|
436 |
+
num_rows="dynamic"
|
437 |
+
)
|
438 |
+
|
439 |
+
# تحديث السعر الإجمالي لكل بند
|
440 |
+
for i, row in edited_df.iterrows():
|
441 |
+
edited_df.at[i, 'total_price'] = row['quantity'] * row['unit_price']
|
442 |
+
|
443 |
+
# تحديث قائمة البنود
|
444 |
+
if not edited_df.equals(boq_df):
|
445 |
+
st.session_state.bill_of_quantities = edited_df.to_dict('records')
|
446 |
+
st.success("تم تحديث جدول الكميات بنجاح!")
|
447 |
+
|
448 |
+
# عرض إجمالي جدول الكميات
|
449 |
+
total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities)
|
450 |
+
st.metric("إجمالي جدول الكميات", f"{total_boq:,.2f} ريال")
|
451 |
+
|
452 |
+
# إضافة بند جديد
|
453 |
+
st.markdown("#### إضافة بند جديد")
|
454 |
+
|
455 |
+
with st.form(key="add_boq_item_form"):
|
456 |
+
col1, col2 = st.columns(2)
|
457 |
+
|
458 |
+
with col1:
|
459 |
+
new_code = st.text_input("الكود", key="new_boq_code")
|
460 |
+
new_description = st.text_area("الوصف", key="new_boq_description")
|
461 |
+
new_category = st.selectbox(
|
462 |
+
"الفئة",
|
463 |
+
["أعمال ترابية", "أعمال خرسانية", "أعمال حديد", "أعمال بناء", "أعمال تشطيب", "أعمال كهربائية", "أعمال ميكانيكية", "أعمال صحية", "أخرى"],
|
464 |
+
key="new_boq_category"
|
465 |
+
)
|
466 |
+
|
467 |
+
with col2:
|
468 |
+
new_unit = st.selectbox(
|
469 |
+
"الوحدة",
|
470 |
+
["م3", "م2", "طن", "كجم", "عدد", "لتر", "متر"],
|
471 |
+
key="new_boq_unit"
|
472 |
+
)
|
473 |
+
new_quantity = st.number_input("الكمية", min_value=0.0, key="new_boq_quantity")
|
474 |
+
new_unit_price = st.number_input("سعر الوحدة (ريال)", min_value=0.0, key="new_boq_unit_price")
|
475 |
+
|
476 |
+
submit_button = st.form_submit_button("إضافة بند")
|
477 |
+
|
478 |
+
if submit_button:
|
479 |
+
if new_code and new_description:
|
480 |
+
# إنشاء معرف جديد
|
481 |
+
new_id = max([item['id'] for item in st.session_state.bill_of_quantities], default=0) + 1
|
482 |
+
|
483 |
+
# حساب السعر الإجمالي
|
484 |
+
new_total_price = new_quantity * new_unit_price
|
485 |
+
|
486 |
+
# إضافة البند الجديد
|
487 |
+
st.session_state.bill_of_quantities.append({
|
488 |
+
'id': new_id,
|
489 |
+
'code': new_code,
|
490 |
+
'description': new_description,
|
491 |
+
'unit': new_unit,
|
492 |
+
'quantity': new_quantity,
|
493 |
+
'unit_price': new_unit_price,
|
494 |
+
'total_price': new_total_price,
|
495 |
+
'category': new_category
|
496 |
+
})
|
497 |
+
|
498 |
+
st.success(f"تمت إضافة البند '{new_code}' بنجاح!")
|
499 |
+
st.rerun()
|
500 |
+
else:
|
501 |
+
st.error("يرجى إدخال الكود والوصف.")
|
502 |
+
|
503 |
+
# تحليل جدول الكميات
|
504 |
+
st.markdown("#### تحليل جدول الكميات")
|
505 |
+
|
506 |
+
col1, col2 = st.columns(2)
|
507 |
+
|
508 |
+
with col1:
|
509 |
+
# توزيع البنود حسب الفئة
|
510 |
+
category_totals = {}
|
511 |
+
|
512 |
+
for item in st.session_state.bill_of_quantities:
|
513 |
+
category = item['category']
|
514 |
+
if category in category_totals:
|
515 |
+
category_totals[category] += item['total_price']
|
516 |
+
else:
|
517 |
+
category_totals[category] = item['total_price']
|
518 |
+
|
519 |
+
category_df = pd.DataFrame({
|
520 |
+
'الفئة': list(category_totals.keys()),
|
521 |
+
'المبلغ': list(category_totals.values())
|
522 |
+
})
|
523 |
+
|
524 |
+
fig = px.pie(
|
525 |
+
category_df,
|
526 |
+
values='المبلغ',
|
527 |
+
names='الفئة',
|
528 |
+
title='توزيع جدول الكميات حسب الفئة'
|
529 |
+
)
|
530 |
+
|
531 |
+
st.plotly_chart(fig, use_container_width=True)
|
532 |
+
|
533 |
+
with col2:
|
534 |
+
# ترتيب البنود حسب القيمة
|
535 |
+
top_items = sorted(st.session_state.bill_of_quantities, key=lambda x: x['total_price'], reverse=True)[:5]
|
536 |
+
|
537 |
+
top_items_df = pd.DataFrame({
|
538 |
+
'البند': [item['code'] + ' - ' + item['description'][:20] + '...' for item in top_items],
|
539 |
+
'القيمة': [item['total_price'] for item in top_items]
|
540 |
+
})
|
541 |
+
|
542 |
+
fig = px.bar(
|
543 |
+
top_items_df,
|
544 |
+
x='البند',
|
545 |
+
y='القيمة',
|
546 |
+
title='أعلى 5 بنود من حيث القيمة',
|
547 |
+
color='القيمة',
|
548 |
+
text_auto='.2s'
|
549 |
+
)
|
550 |
+
|
551 |
+
st.plotly_chart(fig, use_container_width=True)
|
552 |
+
|
553 |
+
# استيراد وتصدير جدول الكميات
|
554 |
+
st.markdown("#### استيراد وتصدير جدول الكميات")
|
555 |
+
|
556 |
+
col1, col2 = st.columns(2)
|
557 |
+
|
558 |
+
with col1:
|
559 |
+
if st.button("تصدير جدول الكميات إلى Excel", key="export_boq_button"):
|
560 |
+
# محاكاة تصدير البيانات
|
561 |
+
st.success("تم تصدير جدول الكميات إلى Excel بنجاح!")
|
562 |
+
|
563 |
+
with col2:
|
564 |
+
uploaded_file = st.file_uploader("استيراد جدول الكميات من Excel", type=["xlsx"], key="import_boq_file")
|
565 |
+
|
566 |
+
if uploaded_file is not None:
|
567 |
+
if st.button("استيراد البيانات", key="import_boq_button"):
|
568 |
+
# محاكاة استيراد البيانات
|
569 |
+
st.success("تم استيراد جدول الكميات بنجاح!")
|
570 |
+
|
571 |
+
def _render_cost_analysis_tab(self):
|
572 |
+
"""عرض تبويب تحليل التكاليف"""
|
573 |
+
|
574 |
+
st.markdown("### تحليل التكاليف")
|
575 |
+
|
576 |
+
# عرض تحليل التكاليف الحالي
|
577 |
+
st.markdown("#### قائمة التكاليف")
|
578 |
+
|
579 |
+
# تحويل قائمة التكاليف إلى DataFrame
|
580 |
+
cost_df = pd.DataFrame(st.session_state.cost_analysis)
|
581 |
+
|
582 |
+
# عرض التكاليف كجدول قابل للتعديل
|
583 |
+
edited_df = st.data_editor(
|
584 |
+
cost_df,
|
585 |
+
column_config={
|
586 |
+
"id": st.column_config.NumberColumn("الرقم", disabled=True),
|
587 |
+
"category": st.column_config.SelectboxColumn(
|
588 |
+
"الفئة",
|
589 |
+
options=["تكاليف مباشرة", "تكاليف غير مباشرة", "أرباح"]
|
590 |
+
),
|
591 |
+
"subcategory": st.column_config.TextColumn("الفئة الفرعية"),
|
592 |
+
"description": st.column_config.TextColumn("الوصف"),
|
593 |
+
"amount": st.column_config.NumberColumn("المبلغ (ريال)", min_value=0, format="%.2f"),
|
594 |
+
"percentage": st.column_config.NumberColumn("النسبة (%)", min_value=0, format="%.1f", disabled=True)
|
595 |
+
},
|
596 |
+
use_container_width=True,
|
597 |
+
hide_index=True,
|
598 |
+
num_rows="dynamic"
|
599 |
+
)
|
600 |
+
|
601 |
+
# حساب إجمالي التكاليف
|
602 |
+
total_amount = sum(item['amount'] for item in st.session_state.cost_analysis)
|
603 |
+
|
604 |
+
# تحديث النسبة المئوية لكل بند
|
605 |
+
for i, row in edited_df.iterrows():
|
606 |
+
edited_df.at[i, 'percentage'] = (row['amount'] / total_amount) * 100
|
607 |
+
|
608 |
+
# تحديث قائمة التكاليف
|
609 |
+
if not edited_df.equals(cost_df):
|
610 |
+
st.session_state.cost_analysis = edited_df.to_dict('records')
|
611 |
+
st.success("تم تحديث تحليل التكاليف بنجاح!")
|
612 |
+
|
613 |
+
# عرض إجمالي التكاليف
|
614 |
+
st.metric("إجمالي التكاليف", f"{total_amount:,.2f} ريال")
|
615 |
+
|
616 |
+
# إضافة تكلفة جديدة
|
617 |
+
st.markdown("#### إضافة تكلفة جديدة")
|
618 |
+
|
619 |
+
with st.form(key="add_cost_item_form"):
|
620 |
+
col1, col2 = st.columns(2)
|
621 |
+
|
622 |
+
with col1:
|
623 |
+
new_category = st.selectbox(
|
624 |
+
"الفئة",
|
625 |
+
["تكاليف مباشرة", "تكاليف غير مباشرة", "أرباح"],
|
626 |
+
key="new_cost_category"
|
627 |
+
)
|
628 |
+
new_subcategory = st.text_input("الفئة الفرعية", key="new_cost_subcategory")
|
629 |
+
|
630 |
+
with col2:
|
631 |
+
new_description = st.text_input("الوصف", key="new_cost_description")
|
632 |
+
new_amount = st.number_input("المبلغ (ريال)", min_value=0.0, key="new_cost_amount")
|
633 |
+
|
634 |
+
submit_button = st.form_submit_button("إضافة تكلفة")
|
635 |
+
|
636 |
+
if submit_button:
|
637 |
+
if new_description and new_subcategory:
|
638 |
+
# إنشاء معرف جديد
|
639 |
+
new_id = max([item['id'] for item in st.session_state.cost_analysis], default=0) + 1
|
640 |
+
|
641 |
+
# حساب النسبة المئوية
|
642 |
+
new_percentage = (new_amount / (total_amount + new_amount)) * 100
|
643 |
+
|
644 |
+
# إضافة التكلفة الجديدة
|
645 |
+
st.session_state.cost_analysis.append({
|
646 |
+
'id': new_id,
|
647 |
+
'category': new_category,
|
648 |
+
'subcategory': new_subcategory,
|
649 |
+
'description': new_description,
|
650 |
+
'amount': new_amount,
|
651 |
+
'percentage': new_percentage
|
652 |
+
})
|
653 |
+
|
654 |
+
# إعادة حساب النسب المئوية لجميع البنود
|
655 |
+
new_total = total_amount + new_amount
|
656 |
+
for item in st.session_state.cost_analysis:
|
657 |
+
item['percentage'] = (item['amount'] / new_total) * 100
|
658 |
+
|
659 |
+
st.success(f"تمت إضافة التكلفة '{new_description}' بنجاح!")
|
660 |
+
st.rerun()
|
661 |
+
else:
|
662 |
+
st.error("يرجى إدخال الفئة الفرعية والوصف.")
|
663 |
+
|
664 |
+
# تحليل التكاليف
|
665 |
+
st.markdown("#### تحليل التكاليف")
|
666 |
+
|
667 |
+
# تحليل التكاليف حسب الفئة
|
668 |
+
st.markdown("##### توزيع التكاليف حسب الفئة")
|
669 |
+
|
670 |
+
# تجميع البيانات حسب الفئة
|
671 |
+
category_totals = {}
|
672 |
+
|
673 |
+
for item in st.session_state.cost_analysis:
|
674 |
+
category = item['category']
|
675 |
+
if category in category_totals:
|
676 |
+
category_totals[category] += item['amount']
|
677 |
+
else:
|
678 |
+
category_totals[category] = item['amount']
|
679 |
+
|
680 |
+
category_df = pd.DataFrame({
|
681 |
+
'الفئة': list(category_totals.keys()),
|
682 |
+
'المبلغ': list(category_totals.values())
|
683 |
+
})
|
684 |
+
|
685 |
+
fig = px.pie(
|
686 |
+
category_df,
|
687 |
+
values='المبلغ',
|
688 |
+
names='الفئة',
|
689 |
+
title='توزيع التكاليف حسب الفئة',
|
690 |
+
color_discrete_sequence=px.colors.qualitative.Set3
|
691 |
+
)
|
692 |
+
|
693 |
+
st.plotly_chart(fig, use_container_width=True)
|
694 |
+
|
695 |
+
# تحليل التكاليف المباشرة
|
696 |
+
st.markdown("##### تحليل التكاليف المباشرة")
|
697 |
+
|
698 |
+
col1, col2 = st.columns(2)
|
699 |
+
|
700 |
+
with col1:
|
701 |
+
# تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
|
702 |
+
direct_subcategory_totals = {}
|
703 |
+
|
704 |
+
for item in st.session_state.cost_analysis:
|
705 |
+
if item['category'] == 'تكاليف مباشرة':
|
706 |
+
subcategory = item['subcategory']
|
707 |
+
if subcategory in direct_subcategory_totals:
|
708 |
+
direct_subcategory_totals[subcategory] += item['amount']
|
709 |
+
else:
|
710 |
+
direct_subcategory_totals[subcategory] = item['amount']
|
711 |
+
|
712 |
+
direct_subcategory_df = pd.DataFrame({
|
713 |
+
'الفئة الفرعية': list(direct_subcategory_totals.keys()),
|
714 |
+
'المبلغ': list(direct_subcategory_totals.values())
|
715 |
+
})
|
716 |
+
|
717 |
+
fig = px.pie(
|
718 |
+
direct_subcategory_df,
|
719 |
+
values='المبلغ',
|
720 |
+
names='الفئة الفرعية',
|
721 |
+
title='توزيع التكاليف المباشرة حسب الفئة الفرعية'
|
722 |
+
)
|
723 |
+
|
724 |
+
st.plotly_chart(fig, use_container_width=True)
|
725 |
+
|
726 |
+
with col2:
|
727 |
+
# تجميع البيانات حسب الوصف للتكاليف المباشرة
|
728 |
+
direct_description_totals = {}
|
729 |
+
|
730 |
+
for item in st.session_state.cost_analysis:
|
731 |
+
if item['category'] == 'تكاليف مباشرة':
|
732 |
+
description = item['description']
|
733 |
+
if description in direct_description_totals:
|
734 |
+
direct_description_totals[description] += item['amount']
|
735 |
+
else:
|
736 |
+
direct_description_totals[description] = item['amount']
|
737 |
+
|
738 |
+
direct_description_df = pd.DataFrame({
|
739 |
+
'الوصف': list(direct_description_totals.keys()),
|
740 |
+
'المبلغ': list(direct_description_totals.values())
|
741 |
+
})
|
742 |
+
|
743 |
+
# ترتيب البيانات تنازلياً حسب المبلغ
|
744 |
+
direct_description_df = direct_description_df.sort_values(by='المبلغ', ascending=False)
|
745 |
+
|
746 |
+
fig = px.bar(
|
747 |
+
direct_description_df,
|
748 |
+
x='الوصف',
|
749 |
+
y='المبلغ',
|
750 |
+
title='توزيع التكاليف المباشرة حسب البند',
|
751 |
+
color='المبلغ',
|
752 |
+
text_auto='.2s'
|
753 |
+
)
|
754 |
+
|
755 |
+
st.plotly_chart(fig, use_container_width=True)
|
756 |
+
|
757 |
+
# تحليل التكاليف غير المباشرة
|
758 |
+
st.markdown("##### تحليل التكاليف غير المباشرة")
|
759 |
+
|
760 |
+
col1, col2 = st.columns(2)
|
761 |
+
|
762 |
+
with col1:
|
763 |
+
# تجميع البيانات حسب الفئة الفرعية للتكاليف غير المباشرة
|
764 |
+
indirect_subcategory_totals = {}
|
765 |
+
|
766 |
+
for item in st.session_state.cost_analysis:
|
767 |
+
if item['category'] == 'تكاليف غير مباشرة':
|
768 |
+
subcategory = item['subcategory']
|
769 |
+
if subcategory in indirect_subcategory_totals:
|
770 |
+
indirect_subcategory_totals[subcategory] += item['amount']
|
771 |
+
else:
|
772 |
+
indirect_subcategory_totals[subcategory] = item['amount']
|
773 |
+
|
774 |
+
indirect_subcategory_df = pd.DataFrame({
|
775 |
+
'الفئة الفرعية': list(indirect_subcategory_totals.keys()),
|
776 |
+
'المبلغ': list(indirect_subcategory_totals.values())
|
777 |
+
})
|
778 |
+
|
779 |
+
fig = px.pie(
|
780 |
+
indirect_subcategory_df,
|
781 |
+
values='المبلغ',
|
782 |
+
names='الفئة الفرعية',
|
783 |
+
title='توزيع التكاليف غير المباشرة حسب الفئة الفرعية'
|
784 |
+
)
|
785 |
+
|
786 |
+
st.plotly_chart(fig, use_container_width=True)
|
787 |
+
|
788 |
+
with col2:
|
789 |
+
# تجميع البيانات حسب الوصف للتكاليف غير المباشرة
|
790 |
+
indirect_description_totals = {}
|
791 |
+
|
792 |
+
for item in st.session_state.cost_analysis:
|
793 |
+
if item['category'] == 'تكاليف غير مباشرة':
|
794 |
+
description = item['description']
|
795 |
+
if description in indirect_description_totals:
|
796 |
+
indirect_description_totals[description] += item['amount']
|
797 |
+
else:
|
798 |
+
indirect_description_totals[description] = item['amount']
|
799 |
+
|
800 |
+
indirect_description_df = pd.DataFrame({
|
801 |
+
'الوصف': list(indirect_description_totals.keys()),
|
802 |
+
'المبلغ': list(indirect_description_totals.values())
|
803 |
+
})
|
804 |
+
|
805 |
+
# ترتيب البيانات تنازلياً حسب المبلغ
|
806 |
+
indirect_description_df = indirect_description_df.sort_values(by='المبلغ', ascending=False)
|
807 |
+
|
808 |
+
fig = px.bar(
|
809 |
+
indirect_description_df,
|
810 |
+
x='الوصف',
|
811 |
+
y='المبلغ',
|
812 |
+
title='توزيع التكاليف غير المباشرة حسب البند',
|
813 |
+
color='المبلغ',
|
814 |
+
text_auto='.2s'
|
815 |
+
)
|
816 |
+
|
817 |
+
st.plotly_chart(fig, use_container_width=True)
|
818 |
+
|
819 |
+
# استيراد وتصدير تحليل التكاليف
|
820 |
+
st.markdown("#### استيراد وتصدير تحليل الت��اليف")
|
821 |
+
|
822 |
+
col1, col2 = st.columns(2)
|
823 |
+
|
824 |
+
with col1:
|
825 |
+
if st.button("تصدير تحليل التكاليف إلى Excel", key="export_cost_button"):
|
826 |
+
# محاكاة تصدير البيانات
|
827 |
+
st.success("تم تصدير تحليل التكاليف إلى Excel بنجاح!")
|
828 |
+
|
829 |
+
with col2:
|
830 |
+
uploaded_file = st.file_uploader("استيراد تحليل التكاليف من Excel", type=["xlsx"], key="import_cost_file")
|
831 |
+
|
832 |
+
if uploaded_file is not None:
|
833 |
+
if st.button("استيراد البيانات", key="import_cost_button"):
|
834 |
+
# محاكاة استيراد البيانات
|
835 |
+
st.success("تم استيراد تحليل التكاليف بنجاح!")
|
836 |
+
|
837 |
+
def _render_pricing_scenarios_tab(self):
|
838 |
+
"""عرض تبويب سيناريوهات التسعير"""
|
839 |
+
|
840 |
+
st.markdown("### سيناريوهات التسعير")
|
841 |
+
|
842 |
+
# عرض سيناريوهات التسعير الحالية
|
843 |
+
st.markdown("#### قائمة السيناريوهات")
|
844 |
+
|
845 |
+
# تحويل قائمة السيناريوهات إلى DataFrame
|
846 |
+
scenarios_df = pd.DataFrame(st.session_state.price_scenarios)
|
847 |
+
|
848 |
+
# عرض السيناريوهات كجدول قابل للتعديل
|
849 |
+
edited_df = st.data_editor(
|
850 |
+
scenarios_df,
|
851 |
+
column_config={
|
852 |
+
"id": st.column_config.NumberColumn("الرقم", disabled=True),
|
853 |
+
"name": st.column_config.TextColumn("اسم السيناريو"),
|
854 |
+
"description": st.column_config.TextColumn("الوصف"),
|
855 |
+
"total_cost": st.column_config.NumberColumn("إجمالي التكلفة (ريال)", min_value=0, format="%.2f"),
|
856 |
+
"profit_margin": st.column_config.NumberColumn("هامش الربح (%)", min_value=0, format="%.1f"),
|
857 |
+
"total_price": st.column_config.NumberColumn("السعر الإجمالي (ريال)", min_value=0, format="%.2f"),
|
858 |
+
"is_active": st.column_config.CheckboxColumn("نشط")
|
859 |
+
},
|
860 |
+
use_container_width=True,
|
861 |
+
hide_index=True,
|
862 |
+
num_rows="dynamic"
|
863 |
+
)
|
864 |
+
|
865 |
+
# تحديث قائمة السيناريوهات
|
866 |
+
if not edited_df.equals(scenarios_df):
|
867 |
+
# التأكد من وجود سيناريو نشط واحد فقط
|
868 |
+
active_count = sum(edited_df['is_active'])
|
869 |
+
if active_count != 1:
|
870 |
+
st.error("يجب أن يكون هناك سيناريو نشط واحد فقط.")
|
871 |
+
else:
|
872 |
+
st.session_state.price_scenarios = edited_df.to_dict('records')
|
873 |
+
st.success("تم تحديث سيناريوهات التسعير بنجاح!")
|
874 |
+
|
875 |
+
# إضافة سيناريو جديد
|
876 |
+
st.markdown("#### إضافة سيناريو جديد")
|
877 |
+
|
878 |
+
with st.form(key="add_scenario_form"):
|
879 |
+
col1, col2 = st.columns(2)
|
880 |
+
|
881 |
+
with col1:
|
882 |
+
new_name = st.text_input("اسم السيناريو", key="new_scenario_name")
|
883 |
+
new_description = st.text_area("الوصف", key="new_scenario_description")
|
884 |
+
|
885 |
+
with col2:
|
886 |
+
# حساب إجمالي التكاليف الحالي
|
887 |
+
total_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] != 'أرباح')
|
888 |
+
|
889 |
+
st.number_input("إجمالي التكلفة (ريال)", min_value=0.0, value=total_cost, key="new_scenario_total_cost", disabled=True)
|
890 |
+
new_profit_margin = st.number_input("هامش الربح (%)", min_value=0.0, max_value=100.0, value=10.0, key="new_scenario_profit_margin")
|
891 |
+
|
892 |
+
# حساب السعر الإجمالي
|
893 |
+
new_profit_amount = total_cost * (new_profit_margin / 100)
|
894 |
+
new_total_price = total_cost + new_profit_amount
|
895 |
+
|
896 |
+
st.number_input("السعر الإجمالي (ريال)", min_value=0.0, value=new_total_price, key="new_scenario_total_price", disabled=True)
|
897 |
+
new_is_active = st.checkbox("نشط", key="new_scenario_is_active")
|
898 |
+
|
899 |
+
submit_button = st.form_submit_button("إضافة سيناريو")
|
900 |
+
|
901 |
+
if submit_button:
|
902 |
+
if new_name:
|
903 |
+
# التحقق من حالة التنشيط
|
904 |
+
if new_is_active:
|
905 |
+
# إلغاء تنشيط جميع السيناريوهات الأخرى
|
906 |
+
for scenario in st.session_state.price_scenarios:
|
907 |
+
scenario['is_active'] = False
|
908 |
+
|
909 |
+
# إنشاء معرف جديد
|
910 |
+
new_id = max([item['id'] for item in st.session_state.price_scenarios], default=0) + 1
|
911 |
+
|
912 |
+
# إضافة السيناريو الجديد
|
913 |
+
st.session_state.price_scenarios.append({
|
914 |
+
'id': new_id,
|
915 |
+
'name': new_name,
|
916 |
+
'description': new_description,
|
917 |
+
'total_cost': total_cost,
|
918 |
+
'profit_margin': new_profit_margin,
|
919 |
+
'total_price': new_total_price,
|
920 |
+
'is_active': new_is_active
|
921 |
+
})
|
922 |
+
|
923 |
+
st.success(f"تمت إضافة السيناريو '{new_name}' بنجاح!")
|
924 |
+
st.rerun()
|
925 |
+
else:
|
926 |
+
st.error("يرجى إدخال اسم السيناريو.")
|
927 |
+
|
928 |
+
# تحليل السيناريوهات
|
929 |
+
st.markdown("#### تحليل السيناريوهات")
|
930 |
+
|
931 |
+
# مقارنة السيناريوهات
|
932 |
+
st.markdown("##### مقارنة السيناريوهات")
|
933 |
+
|
934 |
+
# إنشاء DataFrame للرسم البياني
|
935 |
+
scenarios_comparison_df = pd.DataFrame({
|
936 |
+
'السيناريو': [item['name'] for item in st.session_state.price_scenarios],
|
937 |
+
'التكلفة الإجمالية': [item['total_cost'] for item in st.session_state.price_scenarios],
|
938 |
+
'هامش الربح (%)': [item['profit_margin'] for item in st.session_state.price_scenarios],
|
939 |
+
'السعر الإجمالي': [item['total_price'] for item in st.session_state.price_scenarios],
|
940 |
+
'الحالة': ['نشط' if item['is_active'] else 'غير نشط' for item in st.session_state.price_scenarios]
|
941 |
+
})
|
942 |
+
|
943 |
+
# إنشاء رسم بياني شريطي مزدوج
|
944 |
+
fig = go.Figure()
|
945 |
+
|
946 |
+
# إضافة شريط للتكلفة الإجمالية
|
947 |
+
fig.add_trace(go.Bar(
|
948 |
+
x=scenarios_comparison_df['السيناريو'],
|
949 |
+
y=scenarios_comparison_df['التكلفة الإجمالية'],
|
950 |
+
name='التكلفة الإجمالية',
|
951 |
+
marker_color='indianred'
|
952 |
+
))
|
953 |
+
|
954 |
+
# إضافة شريط للسعر الإجمالي
|
955 |
+
fig.add_trace(go.Bar(
|
956 |
+
x=scenarios_comparison_df['السيناريو'],
|
957 |
+
y=scenarios_comparison_df['السعر الإجمالي'],
|
958 |
+
name='السعر الإجمالي',
|
959 |
+
marker_color='lightsalmon'
|
960 |
+
))
|
961 |
+
|
962 |
+
# إضافة خط لهامش الربح
|
963 |
+
fig.add_trace(go.Scatter(
|
964 |
+
x=scenarios_comparison_df['السيناريو'],
|
965 |
+
y=scenarios_comparison_df['هامش الربح (%)'] * 10000, # تكبير القيم لتظهر على الرسم البياني
|
966 |
+
name='هامش الربح (%)',
|
967 |
+
yaxis='y2',
|
968 |
+
line=dict(color='royalblue', width=4)
|
969 |
+
))
|
970 |
+
|
971 |
+
# تعديل تخطيط الرسم البياني
|
972 |
+
fig.update_layout(
|
973 |
+
title='مقارنة سيناريوهات التسعير',
|
974 |
+
xaxis_title='السيناريو',
|
975 |
+
yaxis_title='المبلغ (ريال)',
|
976 |
+
yaxis2=dict(
|
977 |
+
title='هامش الربح (%)',
|
978 |
+
titlefont=dict(color='royalblue'),
|
979 |
+
tickfont=dict(color='royalblue'),
|
980 |
+
overlaying='y',
|
981 |
+
side='right',
|
982 |
+
range=[0, 20]
|
983 |
+
),
|
984 |
+
barmode='group',
|
985 |
+
legend=dict(
|
986 |
+
x=0,
|
987 |
+
y=1.2,
|
988 |
+
orientation='h'
|
989 |
+
)
|
990 |
+
)
|
991 |
+
|
992 |
+
# تعديل النص على الأشرطة
|
993 |
+
fig.update_traces(
|
994 |
+
texttemplate='%{y:,.0f}',
|
995 |
+
textposition='outside'
|
996 |
+
)
|
997 |
+
|
998 |
+
st.plotly_chart(fig, use_container_width=True)
|
999 |
+
|
1000 |
+
# تحليل تأثير هامش الربح
|
1001 |
+
st.markdown("##### تحليل تأثير هامش الربح")
|
1002 |
+
|
1003 |
+
# إنشاء نطاق من هوامش الربح
|
1004 |
+
profit_margins = list(range(0, 21, 2)) # من 0% إلى 20% بزيادة 2%
|
1005 |
+
|
1006 |
+
# حساب السعر الإجمالي لكل هامش ربح
|
1007 |
+
total_cost = st.session_state.price_scenarios[0]['total_cost'] # استخدام التكلفة الإجمالية من السيناريو الأول
|
1008 |
+
total_prices = [total_cost * (1 + margin / 100) for margin in profit_margins]
|
1009 |
+
|
1010 |
+
# إنشاء DataFrame للرسم البياني
|
1011 |
+
profit_analysis_df = pd.DataFrame({
|
1012 |
+
'هامش الربح (%)': profit_margins,
|
1013 |
+
'السعر الإجمالي': total_prices
|
1014 |
+
})
|
1015 |
+
|
1016 |
+
# إنشاء رسم بياني خطي
|
1017 |
+
fig = px.line(
|
1018 |
+
profit_analysis_df,
|
1019 |
+
x='هامش الربح (%)',
|
1020 |
+
y='السعر الإجمالي',
|
1021 |
+
title='تأثير هامش الربح على السعر الإجمالي',
|
1022 |
+
markers=True
|
1023 |
+
)
|
1024 |
+
|
1025 |
+
# تعديل النص على النقاط
|
1026 |
+
fig.update_traces(
|
1027 |
+
texttemplate='%{y:,.0f}',
|
1028 |
+
textposition='top center'
|
1029 |
+
)
|
1030 |
+
|
1031 |
+
st.plotly_chart(fig, use_container_width=True)
|
1032 |
+
|
1033 |
+
# تحليل نقطة التعادل
|
1034 |
+
st.markdown("##### تحليل نقطة التعادل")
|
1035 |
+
|
1036 |
+
# افتراض تكاليف ثابتة ومتغيرة
|
1037 |
+
fixed_costs = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
|
1038 |
+
variable_costs = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
|
1039 |
+
|
1040 |
+
# افتراض سعر البيع من السيناريو النشط
|
1041 |
+
active_scenario = next((item for item in st.session_state.price_scenarios if item['is_active']), None)
|
1042 |
+
if active_scenario:
|
1043 |
+
selling_price = active_scenario['total_price']
|
1044 |
+
else:
|
1045 |
+
selling_price = st.session_state.price_scenarios[0]['total_price']
|
1046 |
+
|
1047 |
+
# حساب نقطة التعادل
|
1048 |
+
if selling_price > variable_costs:
|
1049 |
+
breakeven_point = fixed_costs / (selling_price - variable_costs)
|
1050 |
+
st.metric("نقطة التعادل", f"{breakeven_point:.2f} وحدة")
|
1051 |
+
|
1052 |
+
# إنشاء رسم بياني لنقطة التعادل
|
1053 |
+
units = list(range(0, int(breakeven_point * 2) + 1, max(1, int(breakeven_point / 10))))
|
1054 |
+
|
1055 |
+
total_costs = [fixed_costs + variable_costs * unit for unit in units]
|
1056 |
+
total_revenues = [selling_price * unit for unit in units]
|
1057 |
+
profits = [revenue - cost for revenue, cost in zip(total_revenues, total_costs)]
|
1058 |
+
|
1059 |
+
breakeven_df = pd.DataFrame({
|
1060 |
+
'الوحدات': units,
|
1061 |
+
'إجمالي التكاليف': total_costs,
|
1062 |
+
'إجمالي الإيرادات': total_revenues,
|
1063 |
+
'الربح': profits
|
1064 |
+
})
|
1065 |
+
|
1066 |
+
fig = go.Figure()
|
1067 |
+
|
1068 |
+
fig.add_trace(go.Scatter(
|
1069 |
+
x=breakeven_df['الوحدات'],
|
1070 |
+
y=breakeven_df['إجمالي التكاليف'],
|
1071 |
+
name='إجمالي التكاليف',
|
1072 |
+
line=dict(color='red', width=2)
|
1073 |
+
))
|
1074 |
+
|
1075 |
+
fig.add_trace(go.Scatter(
|
1076 |
+
x=breakeven_df['الوحدات'],
|
1077 |
+
y=breakeven_df['إجمالي الإيرادات'],
|
1078 |
+
name='إجمالي الإيرادات',
|
1079 |
+
line=dict(color='green', width=2)
|
1080 |
+
))
|
1081 |
+
|
1082 |
+
fig.add_trace(go.Scatter(
|
1083 |
+
x=breakeven_df['الوحدات'],
|
1084 |
+
y=breakeven_df['الربح'],
|
1085 |
+
name='الربح',
|
1086 |
+
line=dict(color='blue', width=2)
|
1087 |
+
))
|
1088 |
+
|
1089 |
+
# إضافة خط عمودي عند نقطة التعادل
|
1090 |
+
fig.add_vline(
|
1091 |
+
x=breakeven_point,
|
1092 |
+
line_dash="dash",
|
1093 |
+
line_color="black",
|
1094 |
+
annotation_text=f"نقطة التعادل: {breakeven_point:.2f}",
|
1095 |
+
annotation_position="top right"
|
1096 |
+
)
|
1097 |
+
|
1098 |
+
# إضافة خط أفقي عند الصفر
|
1099 |
+
fig.add_hline(
|
1100 |
+
y=0,
|
1101 |
+
line_dash="dash",
|
1102 |
+
line_color="gray"
|
1103 |
+
)
|
1104 |
+
|
1105 |
+
fig.update_layout(
|
1106 |
+
title='تحليل نقطة التعادل',
|
1107 |
+
xaxis_title='الوحدات',
|
1108 |
+
yaxis_title='المبلغ (ريال)'
|
1109 |
+
)
|
1110 |
+
|
1111 |
+
st.plotly_chart(fig, use_container_width=True)
|
1112 |
+
else:
|
1113 |
+
st.warning("لا يمكن حساب نقطة التعادل لأن سعر البيع أقل من التكاليف المتغيرة.")
|
1114 |
+
|
1115 |
+
def _render_competitive_analysis_tab(self):
|
1116 |
+
"""عرض تبويب المقارنة التنافسية"""
|
1117 |
+
|
1118 |
+
st.markdown("### المقارنة التنافسية")
|
1119 |
+
|
1120 |
+
# بيانات افتراضية للمنافسين
|
1121 |
+
competitors_data = [
|
1122 |
+
{
|
1123 |
+
'name': 'شركتنا',
|
1124 |
+
'price': 670000,
|
1125 |
+
'quality': 4.5,
|
1126 |
+
'delivery_time': 180,
|
1127 |
+
'experience': 8,
|
1128 |
+
'local_content': 85
|
1129 |
+
},
|
1130 |
+
{
|
1131 |
+
'name': 'المنافس أ',
|
1132 |
+
'price': 700000,
|
1133 |
+
'quality': 4.2,
|
1134 |
+
'delivery_time': 200,
|
1135 |
+
'experience': 10,
|
1136 |
+
'local_content': 75
|
1137 |
+
},
|
1138 |
+
{
|
1139 |
+
'name': 'المنافس ب',
|
1140 |
+
'price': 650000,
|
1141 |
+
'quality': 3.8,
|
1142 |
+
'delivery_time': 160,
|
1143 |
+
'experience': 5,
|
1144 |
+
'local_content': 90
|
1145 |
+
},
|
1146 |
+
{
|
1147 |
+
'name': 'المنافس ج',
|
1148 |
+
'price': 680000,
|
1149 |
+
'quality': 4.0,
|
1150 |
+
'delivery_time': 190,
|
1151 |
+
'experience': 12,
|
1152 |
+
'local_content': 80
|
1153 |
+
}
|
1154 |
+
]
|
1155 |
+
|
1156 |
+
# عرض بيانات المنافسين
|
1157 |
+
st.markdown("#### بيانات المنافسين")
|
1158 |
+
|
1159 |
+
competitors_df = pd.DataFrame(competitors_data)
|
1160 |
+
st.dataframe(competitors_df, use_container_width=True, hide_index=True)
|
1161 |
+
|
1162 |
+
# مقارنة الأسعار
|
1163 |
+
st.markdown("#### مقارنة الأسعار")
|
1164 |
+
|
1165 |
+
fig = px.bar(
|
1166 |
+
competitors_df,
|
1167 |
+
x='name',
|
1168 |
+
y='price',
|
1169 |
+
title='مقارنة الأسعار بين المنافسين',
|
1170 |
+
color='price',
|
1171 |
+
text_auto='.2s'
|
1172 |
+
)
|
1173 |
+
|
1174 |
+
fig.update_layout(
|
1175 |
+
xaxis_title='المنافس',
|
1176 |
+
yaxis_title='السعر (ريال)'
|
1177 |
+
)
|
1178 |
+
|
1179 |
+
st.plotly_chart(fig, use_container_width=True)
|
1180 |
+
|
1181 |
+
# مقارنة متعددة الأبعاد
|
1182 |
+
st.markdown("#### مقارنة متعددة الأبعاد")
|
1183 |
+
|
1184 |
+
# تحويل البيانات إلى تنسيق مناسب للرسم البياني الراداري
|
1185 |
+
categories = ['price', 'quality', 'delivery_time', 'experience', 'local_content']
|
1186 |
+
|
1187 |
+
# تطبيع البيانات (لجعل القيم بين 0 و 1)
|
1188 |
+
normalized_data = {}
|
1189 |
+
|
1190 |
+
for category in categories:
|
1191 |
+
if category == 'price' or category == 'delivery_time':
|
1192 |
+
# للسعر ووقت التسليم، القيمة الأقل أفضل
|
1193 |
+
min_val = min(item[category] for item in competitors_data)
|
1194 |
+
max_val = max(item[category] for item in competitors_data)
|
1195 |
+
normalized_data[category] = [(max_val - item[category]) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data]
|
1196 |
+
else:
|
1197 |
+
# للجودة والخبرة والمحتوى المحلي، القيمة الأعلى أفضل
|
1198 |
+
min_val = min(item[category] for item in competitors_data)
|
1199 |
+
max_val = max(item[category] for item in competitors_data)
|
1200 |
+
normalized_data[category] = [(item[category] - min_val) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data]
|
1201 |
+
|
1202 |
+
# إنشاء الرسم البياني الراداري
|
1203 |
+
fig = go.Figure()
|
1204 |
+
|
1205 |
+
for i, competitor in enumerate(competitors_data):
|
1206 |
+
fig.add_trace(go.Scatterpolar(
|
1207 |
+
r=[normalized_data[category][i] for category in categories],
|
1208 |
+
theta=['السعر', 'الجودة', 'وقت التسليم', 'الخبرة', 'المحتوى المحلي'],
|
1209 |
+
fill='toself',
|
1210 |
+
name=competitor['name']
|
1211 |
+
))
|
1212 |
+
|
1213 |
+
fig.update_layout(
|
1214 |
+
polar=dict(
|
1215 |
+
radialaxis=dict(
|
1216 |
+
visible=True,
|
1217 |
+
range=[0, 1]
|
1218 |
+
)
|
1219 |
+
),
|
1220 |
+
title='مقارنة متعددة الأبعاد بين المنافسين',
|
1221 |
+
showlegend=True
|
1222 |
+
)
|
1223 |
+
|
1224 |
+
st.plotly_chart(fig, use_container_width=True)
|
1225 |
+
|
1226 |
+
# تحليل نقاط القوة والضعف
|
1227 |
+
st.markdown("#### تحليل نقاط القوة والضعف")
|
1228 |
+
|
1229 |
+
# تحديد نقاط القوة والضعف لشركتنا
|
1230 |
+
our_company = competitors_data[0]
|
1231 |
+
|
1232 |
+
strengths = []
|
1233 |
+
weaknesses = []
|
1234 |
+
|
1235 |
+
# مقارنة السعر
|
1236 |
+
other_prices = [comp['price'] for comp in competitors_data[1:]]
|
1237 |
+
if our_company['price'] <= min(other_prices):
|
1238 |
+
strengths.append("السعر تنافسي جداً")
|
1239 |
+
elif our_company['price'] <= sum(other_prices) / len(other_prices):
|
1240 |
+
strengths.append("السعر تنافسي")
|
1241 |
+
else:
|
1242 |
+
weaknesses.append("السعر أعلى من المتوسط")
|
1243 |
+
|
1244 |
+
# مقارنة الجودة
|
1245 |
+
other_qualities = [comp['quality'] for comp in competitors_data[1:]]
|
1246 |
+
if our_company['quality'] >= max(other_qualities):
|
1247 |
+
strengths.append("الجودة ممتازة")
|
1248 |
+
elif our_company['quality'] >= sum(other_qualities) / len(other_qualities):
|
1249 |
+
strengths.append("الجودة جيدة")
|
1250 |
+
else:
|
1251 |
+
weaknesses.append("الجودة أقل من المتوسط")
|
1252 |
+
|
1253 |
+
# مقارنة وقت التسليم
|
1254 |
+
other_delivery_times = [comp['delivery_time'] for comp in competitors_data[1:]]
|
1255 |
+
if our_company['delivery_time'] <= min(other_delivery_times):
|
1256 |
+
strengths.append("وقت التسليم سريع جداً")
|
1257 |
+
elif our_company['delivery_time'] <= sum(other_delivery_times) / len(other_delivery_times):
|
1258 |
+
strengths.append("وقت التسليم جيد")
|
1259 |
+
else:
|
1260 |
+
weaknesses.append("وقت التسليم أطول من المتوسط")
|
1261 |
+
|
1262 |
+
# مقارنة الخبرة
|
1263 |
+
other_experiences = [comp['experience'] for comp in competitors_data[1:]]
|
1264 |
+
if our_company['experience'] >= max(other_experiences):
|
1265 |
+
strengths.append("خبرة واسعة جداً")
|
1266 |
+
elif our_company['experience'] >= sum(other_experiences) / len(other_experiences):
|
1267 |
+
strengths.append("خبرة جيدة")
|
1268 |
+
else:
|
1269 |
+
weaknesses.append("خبرة أقل من المتوسط")
|
1270 |
+
|
1271 |
+
# مقارنة المحتوى المحلي
|
1272 |
+
other_local_contents = [comp['local_content'] for comp in competitors_data[1:]]
|
1273 |
+
if our_company['local_content'] >= max(other_local_contents):
|
1274 |
+
strengths.append("محتوى محلي ممتاز")
|
1275 |
+
elif our_company['local_content'] >= sum(other_local_contents) / len(other_local_contents):
|
1276 |
+
strengths.append("محتوى محلي جيد")
|
1277 |
+
else:
|
1278 |
+
weaknesses.append("محتوى محلي أقل من المتوسط")
|
1279 |
+
|
1280 |
+
# عرض نقاط القوة والضعف
|
1281 |
+
col1, col2 = st.columns(2)
|
1282 |
+
|
1283 |
+
with col1:
|
1284 |
+
st.markdown("##### نقاط القوة")
|
1285 |
+
for strength in strengths:
|
1286 |
+
st.markdown(f"- {strength}")
|
1287 |
+
|
1288 |
+
with col2:
|
1289 |
+
st.markdown("##### نقاط الضعف")
|
1290 |
+
for weakness in weaknesses:
|
1291 |
+
st.markdown(f"- {weakness}")
|
1292 |
+
|
1293 |
+
# توصيات للتسعير
|
1294 |
+
st.markdown("#### توصيات للتسعير")
|
1295 |
+
|
1296 |
+
# تحديد التوصيات بناءً على المقارنة
|
1297 |
+
recommendations = []
|
1298 |
+
|
1299 |
+
# توصية بناءً على السعر
|
1300 |
+
avg_price = sum(comp['price'] for comp in competitors_data) / len(competitors_data)
|
1301 |
+
if our_company['price'] > avg_price:
|
1302 |
+
recommendations.append("النظر في تخفيض السعر للمنافسة بشكل أفضل.")
|
1303 |
+
|
1304 |
+
# توصية بناءً على الجودة
|
1305 |
+
if our_company['quality'] > sum(comp['quality'] for comp in competitors_data[1:]) / len(competitors_data[1:]):
|
1306 |
+
recommendations.append("التأكيد على جودة الخدمات في العروض التسويقية.")
|
1307 |
+
|
1308 |
+
# توصية بناءً على وقت التسليم
|
1309 |
+
if our_company['delivery_time'] < sum(comp['delivery_time'] for comp in competitors_data[1:]) / len(competitors_data[1:]):
|
1310 |
+
recommendations.append("التأكيد على سرعة التسليم كميزة تنافسية.")
|
1311 |
+
|
1312 |
+
# توصية بناءً على الخبرة
|
1313 |
+
if our_company['experience'] < max(comp['experience'] for comp in competitors_data[1:]):
|
1314 |
+
recommendations.append("تعزيز فريق العمل بخبرات إضافية.")
|
1315 |
+
|
1316 |
+
# توصية بناءً على المحتوى المحلي
|
1317 |
+
if our_company['local_content'] > sum(comp['local_content'] for comp in competitors_data[1:]) / len(competitors_data[1:]):
|
1318 |
+
recommendations.append("التأكيد على نسبة المحتوى المحلي العالية في العروض.")
|
1319 |
+
|
1320 |
+
# توصية عامة
|
1321 |
+
recommendations.append("مراجعة هيكل التكاليف بشكل دوري للحفاظ على القدرة التنافسية.")
|
1322 |
+
|
1323 |
+
# عرض التوصيات
|
1324 |
+
for recommendation in recommendations:
|
1325 |
+
st.markdown(f"- {recommendation}")
|
1326 |
+
|
1327 |
+
def _render_reports_tab(self):
|
1328 |
+
"""عرض تبويب التقارير"""
|
1329 |
+
|
1330 |
+
st.markdown("### التقارير")
|
1331 |
+
|
1332 |
+
# قائمة التقارير المتاحة
|
1333 |
+
reports = [
|
1334 |
+
"تقرير جدول الكميات",
|
1335 |
+
"تقرير تحليل التكاليف",
|
1336 |
+
"تقرير سيناريوهات التسعير",
|
1337 |
+
"تقرير المقارنة التنافسية",
|
1338 |
+
"تقرير ملخص التسعير"
|
1339 |
+
]
|
1340 |
+
|
1341 |
+
# اختيار التقرير
|
1342 |
+
selected_report = st.selectbox("اختر التقرير", reports)
|
1343 |
+
|
1344 |
+
# خيارات التصدير
|
1345 |
+
export_format = st.radio("صيغة التصدير", ["PDF", "Excel", "Word"])
|
1346 |
+
|
1347 |
+
# زر إنشاء التقرير
|
1348 |
+
if st.button("إنشاء التقرير"):
|
1349 |
+
st.success(f"تم إنشاء {selected_report} بصيغة {export_format} بنجاح!")
|
1350 |
+
|
1351 |
+
# عرض نموذج للتقرير
|
1352 |
+
st.markdown("#### نموذج التقرير")
|
1353 |
+
|
1354 |
+
if selected_report == "تقرير جدول الكميات":
|
1355 |
+
self._render_boq_report()
|
1356 |
+
elif selected_report == "تقرير تحليل التكاليف":
|
1357 |
+
self._render_cost_analysis_report()
|
1358 |
+
elif selected_report == "تقرير سيناريوهات التسعير":
|
1359 |
+
self._render_pricing_scenarios_report()
|
1360 |
+
elif selected_report == "تقرير المقارنة التنافسية":
|
1361 |
+
self._render_competitive_analysis_report()
|
1362 |
+
elif selected_report == "تقرير ملخص ��لتسعير":
|
1363 |
+
self._render_pricing_summary_report()
|
1364 |
+
|
1365 |
+
def _render_boq_report(self):
|
1366 |
+
"""عرض نموذج تقرير جدول الكميات"""
|
1367 |
+
|
1368 |
+
st.markdown("### تقرير جدول الكميات")
|
1369 |
+
st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d"))
|
1370 |
+
st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري")
|
1371 |
+
st.markdown("**رقم المناقصة:** T-2024-001")
|
1372 |
+
|
1373 |
+
st.markdown("#### جدول الكميات")
|
1374 |
+
|
1375 |
+
# عرض جدول الكميات
|
1376 |
+
boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
|
1377 |
+
st.dataframe(boq_df, use_container_width=True, hide_index=True)
|
1378 |
+
|
1379 |
+
# عرض إجمالي جدول الكميات
|
1380 |
+
total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities)
|
1381 |
+
st.metric("إجمالي جدول الكميات", f"{total_boq:,.2f} ريال")
|
1382 |
+
|
1383 |
+
# عرض توزيع البنود حسب الفئة
|
1384 |
+
st.markdown("#### توزيع البنود حسب الفئة")
|
1385 |
+
|
1386 |
+
# تجميع البيانات حسب الفئة
|
1387 |
+
category_totals = {}
|
1388 |
+
|
1389 |
+
for item in st.session_state.bill_of_quantities:
|
1390 |
+
category = item['category']
|
1391 |
+
if category in category_totals:
|
1392 |
+
category_totals[category] += item['total_price']
|
1393 |
+
else:
|
1394 |
+
category_totals[category] = item['total_price']
|
1395 |
+
|
1396 |
+
category_df = pd.DataFrame({
|
1397 |
+
'الفئة': list(category_totals.keys()),
|
1398 |
+
'المبلغ': list(category_totals.values())
|
1399 |
+
})
|
1400 |
+
|
1401 |
+
fig = px.pie(
|
1402 |
+
category_df,
|
1403 |
+
values='المبلغ',
|
1404 |
+
names='الفئة',
|
1405 |
+
title='توزيع جدول الكميات حسب الفئة'
|
1406 |
+
)
|
1407 |
+
|
1408 |
+
st.plotly_chart(fig, use_container_width=True)
|
1409 |
+
|
1410 |
+
def _render_cost_analysis_report(self):
|
1411 |
+
"""عرض نموذج تقرير تحليل التكاليف"""
|
1412 |
+
|
1413 |
+
st.markdown("### تقرير تحليل التكاليف")
|
1414 |
+
st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d"))
|
1415 |
+
st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري")
|
1416 |
+
st.markdown("**رقم المناقصة:** T-2024-001")
|
1417 |
+
|
1418 |
+
st.markdown("#### تحليل التكاليف")
|
1419 |
+
|
1420 |
+
# عرض تحليل التكاليف
|
1421 |
+
cost_df = pd.DataFrame(st.session_state.cost_analysis)
|
1422 |
+
st.dataframe(cost_df, use_container_width=True, hide_index=True)
|
1423 |
+
|
1424 |
+
# عرض إجمالي التكاليف
|
1425 |
+
total_cost = sum(item['amount'] for item in st.session_state.cost_analysis)
|
1426 |
+
st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال")
|
1427 |
+
|
1428 |
+
# عرض توزيع التكاليف حسب الفئة
|
1429 |
+
st.markdown("#### توزيع التكاليف حسب الفئة")
|
1430 |
+
|
1431 |
+
# تجميع البيانات حسب الفئة
|
1432 |
+
category_totals = {}
|
1433 |
+
|
1434 |
+
for item in st.session_state.cost_analysis:
|
1435 |
+
category = item['category']
|
1436 |
+
if category in category_totals:
|
1437 |
+
category_totals[category] += item['amount']
|
1438 |
+
else:
|
1439 |
+
category_totals[category] = item['amount']
|
1440 |
+
|
1441 |
+
category_df = pd.DataFrame({
|
1442 |
+
'الفئة': list(category_totals.keys()),
|
1443 |
+
'المبلغ': list(category_totals.values())
|
1444 |
+
})
|
1445 |
+
|
1446 |
+
fig = px.pie(
|
1447 |
+
category_df,
|
1448 |
+
values='المبلغ',
|
1449 |
+
names='الفئة',
|
1450 |
+
title='توزيع التكاليف حسب الفئة'
|
1451 |
+
)
|
1452 |
+
|
1453 |
+
st.plotly_chart(fig, use_container_width=True)
|
1454 |
+
|
1455 |
+
# عرض توزيع التكاليف المباشرة
|
1456 |
+
st.markdown("#### توزيع التكاليف المباشرة")
|
1457 |
+
|
1458 |
+
# تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
|
1459 |
+
direct_subcategory_totals = {}
|
1460 |
+
|
1461 |
+
for item in st.session_state.cost_analysis:
|
1462 |
+
if item['category'] == 'تكاليف مباشرة':
|
1463 |
+
subcategory = item['subcategory']
|
1464 |
+
if subcategory in direct_subcategory_totals:
|
1465 |
+
direct_subcategory_totals[subcategory] += item['amount']
|
1466 |
+
else:
|
1467 |
+
direct_subcategory_totals[subcategory] = item['amount']
|
1468 |
+
|
1469 |
+
direct_subcategory_df = pd.DataFrame({
|
1470 |
+
'الفئة الفرعية': list(direct_subcategory_totals.keys()),
|
1471 |
+
'المبلغ': list(direct_subcategory_totals.values())
|
1472 |
+
})
|
1473 |
+
|
1474 |
+
fig = px.bar(
|
1475 |
+
direct_subcategory_df,
|
1476 |
+
x='الفئة الفرعية',
|
1477 |
+
y='المبلغ',
|
1478 |
+
title='توزيع التكاليف المباشرة',
|
1479 |
+
color='الفئة الفرعية',
|
1480 |
+
text_auto='.2s'
|
1481 |
+
)
|
1482 |
+
|
1483 |
+
st.plotly_chart(fig, use_container_width=True)
|
1484 |
+
|
1485 |
+
def _render_pricing_scenarios_report(self):
|
1486 |
+
"""عرض نموذج تقرير سيناريوهات التسعير"""
|
1487 |
+
|
1488 |
+
st.markdown("### تقرير سيناريوهات التسعير")
|
1489 |
+
st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d"))
|
1490 |
+
st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري")
|
1491 |
+
st.markdown("**رقم المناقصة:** T-2024-001")
|
1492 |
+
|
1493 |
+
st.markdown("#### سيناريوهات التسعير")
|
1494 |
+
|
1495 |
+
# عرض سيناريوهات التسعير
|
1496 |
+
scenarios_df = pd.DataFrame(st.session_state.price_scenarios)
|
1497 |
+
st.dataframe(scenarios_df, use_container_width=True, hide_index=True)
|
1498 |
+
|
1499 |
+
# عرض السيناريو النشط
|
1500 |
+
active_scenario = next((item for item in st.session_state.price_scenarios if item['is_active']), None)
|
1501 |
+
if active_scenario:
|
1502 |
+
st.markdown(f"**السيناريو النشط:** {active_scenario['name']}")
|
1503 |
+
st.markdown(f"**السعر الإجمالي:** {active_scenario['total_price']:,.2f} ريال")
|
1504 |
+
st.markdown(f"**هامش الربح:** {active_scenario['profit_margin']:.1f}%")
|
1505 |
+
|
1506 |
+
# عرض مقارنة السيناريوهات
|
1507 |
+
st.markdown("#### مقارنة السيناريوهات")
|
1508 |
+
|
1509 |
+
# إنشاء DataFrame للرسم البياني
|
1510 |
+
scenarios_comparison_df = pd.DataFrame({
|
1511 |
+
'السيناريو': [item['name'] for item in st.session_state.price_scenarios],
|
1512 |
+
'التكلفة الإجمالية': [item['total_cost'] for item in st.session_state.price_scenarios],
|
1513 |
+
'هامش الربح (%)': [item['profit_margin'] for item in st.session_state.price_scenarios],
|
1514 |
+
'السعر الإجمالي': [item['total_price'] for item in st.session_state.price_scenarios],
|
1515 |
+
'الحالة': ['نشط' if item['is_active'] else 'غير نشط' for item in st.session_state.price_scenarios]
|
1516 |
+
})
|
1517 |
+
|
1518 |
+
# إنشاء رسم بياني شريطي مزدوج
|
1519 |
+
fig = go.Figure()
|
1520 |
+
|
1521 |
+
# إضافة شريط للتكلفة الإجمالية
|
1522 |
+
fig.add_trace(go.Bar(
|
1523 |
+
x=scenarios_comparison_df['السيناريو'],
|
1524 |
+
y=scenarios_comparison_df['التكلفة الإجمالية'],
|
1525 |
+
name='التكلفة الإجمالية',
|
1526 |
+
marker_color='indianred'
|
1527 |
+
))
|
1528 |
+
|
1529 |
+
# إضافة شريط للسعر الإجمالي
|
1530 |
+
fig.add_trace(go.Bar(
|
1531 |
+
x=scenarios_comparison_df['السيناريو'],
|
1532 |
+
y=scenarios_comparison_df['السعر الإجمالي'],
|
1533 |
+
name='السعر الإجمالي',
|
1534 |
+
marker_color='lightsalmon'
|
1535 |
+
))
|
1536 |
+
|
1537 |
+
# إضافة خط لهامش الربح
|
1538 |
+
fig.add_trace(go.Scatter(
|
1539 |
+
x=scenarios_comparison_df['السيناريو'],
|
1540 |
+
y=scenarios_comparison_df['هامش الربح (%)'] * 10000, # تكبير القيم لتظهر على الرسم البياني
|
1541 |
+
name='هامش الربح (%)',
|
1542 |
+
yaxis='y2',
|
1543 |
+
line=dict(color='royalblue', width=4)
|
1544 |
+
))
|
1545 |
+
|
1546 |
+
# تعديل تخطيط الرسم البياني
|
1547 |
+
fig.update_layout(
|
1548 |
+
title='مقارنة سيناريوهات التسعير',
|
1549 |
+
xaxis_title='السيناريو',
|
1550 |
+
yaxis_title='المبلغ (ريال)',
|
1551 |
+
yaxis2=dict(
|
1552 |
+
title='هامش الربح (%)',
|
1553 |
+
titlefont=dict(color='royalblue'),
|
1554 |
+
tickfont=dict(color='royalblue'),
|
1555 |
+
overlaying='y',
|
1556 |
+
side='right',
|
1557 |
+
range=[0, 20]
|
1558 |
+
),
|
1559 |
+
barmode='group',
|
1560 |
+
legend=dict(
|
1561 |
+
x=0,
|
1562 |
+
y=1.2,
|
1563 |
+
orientation='h'
|
1564 |
+
)
|
1565 |
+
)
|
1566 |
+
|
1567 |
+
# تعديل النص على الأشرطة
|
1568 |
+
fig.update_traces(
|
1569 |
+
texttemplate='%{y:,.0f}',
|
1570 |
+
textposition='outside'
|
1571 |
+
)
|
1572 |
+
|
1573 |
+
st.plotly_chart(fig, use_container_width=True)
|
1574 |
+
|
1575 |
+
def _render_competitive_analysis_report(self):
|
1576 |
+
"""عرض نموذج تقرير المقارنة التنافسية"""
|
1577 |
+
|
1578 |
+
st.markdown("### تقرير المقارنة التنافسية")
|
1579 |
+
st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d"))
|
1580 |
+
st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري")
|
1581 |
+
st.markdown("**رقم المناقصة:** T-2024-001")
|
1582 |
+
|
1583 |
+
# بيانات افتراضية للمنافسين
|
1584 |
+
competitors_data = [
|
1585 |
+
{
|
1586 |
+
'name': 'شركتنا',
|
1587 |
+
'price': 670000,
|
1588 |
+
'quality': 4.5,
|
1589 |
+
'delivery_time': 180,
|
1590 |
+
'experience': 8,
|
1591 |
+
'local_content': 85
|
1592 |
+
},
|
1593 |
+
{
|
1594 |
+
'name': 'المنافس أ',
|
1595 |
+
'price': 700000,
|
1596 |
+
'quality': 4.2,
|
1597 |
+
'delivery_time': 200,
|
1598 |
+
'experience': 10,
|
1599 |
+
'local_content': 75
|
1600 |
+
},
|
1601 |
+
{
|
1602 |
+
'name': 'المنافس ب',
|
1603 |
+
'price': 650000,
|
1604 |
+
'quality': 3.8,
|
1605 |
+
'delivery_time': 160,
|
1606 |
+
'experience': 5,
|
1607 |
+
'local_content': 90
|
1608 |
+
},
|
1609 |
+
{
|
1610 |
+
'name': 'المنافس ج',
|
1611 |
+
'price': 680000,
|
1612 |
+
'quality': 4.0,
|
1613 |
+
'delivery_time': 190,
|
1614 |
+
'experience': 12,
|
1615 |
+
'local_content': 80
|
1616 |
+
}
|
1617 |
+
]
|
1618 |
+
|
1619 |
+
# عرض بيانات المنافسين
|
1620 |
+
st.markdown("#### بيانات المنافسين")
|
1621 |
+
|
1622 |
+
competitors_df = pd.DataFrame(competitors_data)
|
1623 |
+
st.dataframe(competitors_df, use_container_width=True, hide_index=True)
|
1624 |
+
|
1625 |
+
# مقارنة الأسعار
|
1626 |
+
st.markdown("#### مقارنة الأسعار")
|
1627 |
+
|
1628 |
+
fig = px.bar(
|
1629 |
+
competitors_df,
|
1630 |
+
x='name',
|
1631 |
+
y='price',
|
1632 |
+
title='مقارنة الأسعار بين المنافسين',
|
1633 |
+
color='price',
|
1634 |
+
text_auto='.2s'
|
1635 |
+
)
|
1636 |
+
|
1637 |
+
fig.update_layout(
|
1638 |
+
xaxis_title='المنافس',
|
1639 |
+
yaxis_title='السعر (ريال)'
|
1640 |
+
)
|
1641 |
+
|
1642 |
+
st.plotly_chart(fig, use_container_width=True)
|
1643 |
+
|
1644 |
+
# مقارنة متعددة الأبعاد
|
1645 |
+
st.markdown("#### مقارنة متعددة الأبعاد")
|
1646 |
+
|
1647 |
+
# تحويل البيانات إلى تنسيق مناسب للرسم البياني الراداري
|
1648 |
+
categories = ['price', 'quality', 'delivery_time', 'experience', 'local_content']
|
1649 |
+
|
1650 |
+
# تطبيع البيانات (لجعل القيم بين 0 و 1)
|
1651 |
+
normalized_data = {}
|
1652 |
+
|
1653 |
+
for category in categories:
|
1654 |
+
if category == 'price' or category == 'delivery_time':
|
1655 |
+
# للسعر ووقت التسليم، القيمة الأقل أفضل
|
1656 |
+
min_val = min(item[category] for item in competitors_data)
|
1657 |
+
max_val = max(item[category] for item in competitors_data)
|
1658 |
+
normalized_data[category] = [(max_val - item[category]) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data]
|
1659 |
+
else:
|
1660 |
+
# للجودة والخبرة والمحتوى المحلي، القيمة الأعلى أفضل
|
1661 |
+
min_val = min(item[category] for item in competitors_data)
|
1662 |
+
max_val = max(item[category] for item in competitors_data)
|
1663 |
+
normalized_data[category] = [(item[category] - min_val) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data]
|
1664 |
+
|
1665 |
+
# إنشاء الرسم البياني الراداري
|
1666 |
+
fig = go.Figure()
|
1667 |
+
|
1668 |
+
for i, competitor in enumerate(competitors_data):
|
1669 |
+
fig.add_trace(go.Scatterpolar(
|
1670 |
+
r=[normalized_data[category][i] for category in categories],
|
1671 |
+
theta=['السعر', 'الجودة', 'وقت التسليم', 'الخبرة', 'المحتوى المحلي'],
|
1672 |
+
fill='toself',
|
1673 |
+
name=competitor['name']
|
1674 |
+
))
|
1675 |
+
|
1676 |
+
fig.update_layout(
|
1677 |
+
polar=dict(
|
1678 |
+
radialaxis=dict(
|
1679 |
+
visible=True,
|
1680 |
+
range=[0, 1]
|
1681 |
+
)
|
1682 |
+
),
|
1683 |
+
title='مقارنة متعددة الأبعاد بين المنافسين',
|
1684 |
+
showlegend=True
|
1685 |
+
)
|
1686 |
+
|
1687 |
+
st.plotly_chart(fig, use_container_width=True)
|
1688 |
+
|
1689 |
+
def _render_pricing_summary_report(self):
|
1690 |
+
"""عرض نموذج تقرير ملخص التسعير"""
|
1691 |
+
|
1692 |
+
st.markdown("### تقرير ملخص التسعير")
|
1693 |
+
st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d"))
|
1694 |
+
st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري")
|
1695 |
+
st.markdown("**رقم المناقصة:** T-2024-001")
|
1696 |
+
|
1697 |
+
# عرض ملخص التسعير
|
1698 |
+
st.markdown("#### ملخص التسعير")
|
1699 |
+
|
1700 |
+
# حساب إجمالي التكاليف
|
1701 |
+
total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
|
1702 |
+
total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
|
1703 |
+
total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
|
1704 |
+
total_cost = total_direct_cost + total_indirect_cost
|
1705 |
+
total_price = total_cost + total_profit
|
1706 |
+
|
1707 |
+
# إنشاء ج��ول ملخص
|
1708 |
+
summary_data = {
|
1709 |
+
'البند': ['التكاليف المباشرة', 'التكاليف غير المباشرة', 'إجمالي التكاليف', 'هامش الربح', 'السعر الإجمالي'],
|
1710 |
+
'المبلغ (ريال)': [total_direct_cost, total_indirect_cost, total_cost, total_profit, total_price],
|
1711 |
+
'النسبة (%)': [
|
1712 |
+
(total_direct_cost / total_price) * 100,
|
1713 |
+
(total_indirect_cost / total_price) * 100,
|
1714 |
+
(total_cost / total_price) * 100,
|
1715 |
+
(total_profit / total_price) * 100,
|
1716 |
+
100.0
|
1717 |
+
]
|
1718 |
+
}
|
1719 |
+
|
1720 |
+
summary_df = pd.DataFrame(summary_data)
|
1721 |
+
st.dataframe(summary_df, use_container_width=True, hide_index=True)
|
1722 |
+
|
1723 |
+
# عرض توزيع التكاليف
|
1724 |
+
st.markdown("#### توزيع التكاليف")
|
1725 |
+
|
1726 |
+
# إنشاء DataFrame للرسم البياني
|
1727 |
+
cost_distribution_df = pd.DataFrame({
|
1728 |
+
'البند': ['التكاليف المباشرة', 'التكاليف غير المباشرة', 'هامش الربح'],
|
1729 |
+
'المبلغ': [total_direct_cost, total_indirect_cost, total_profit]
|
1730 |
+
})
|
1731 |
+
|
1732 |
+
fig = px.pie(
|
1733 |
+
cost_distribution_df,
|
1734 |
+
values='المبلغ',
|
1735 |
+
names='البند',
|
1736 |
+
title='توزيع التكاليف والأرباح',
|
1737 |
+
color_discrete_sequence=px.colors.qualitative.Set3
|
1738 |
+
)
|
1739 |
+
|
1740 |
+
st.plotly_chart(fig, use_container_width=True)
|
1741 |
+
|
1742 |
+
# عرض ملخص السيناريو النشط
|
1743 |
+
st.markdown("#### السيناريو النشط")
|
1744 |
+
|
1745 |
+
active_scenario = next((item for item in st.session_state.price_scenarios if item['is_active']), None)
|
1746 |
+
if active_scenario:
|
1747 |
+
st.markdown(f"**اسم السيناريو:** {active_scenario['name']}")
|
1748 |
+
st.markdown(f"**الوصف:** {active_scenario['description']}")
|
1749 |
+
st.markdown(f"**إجمالي التكلفة:** {active_scenario['total_cost']:,.2f} ريال")
|
1750 |
+
st.markdown(f"**هامش الربح:** {active_scenario['profit_margin']:.1f}%")
|
1751 |
+
st.markdown(f"**السعر الإجمالي:** {active_scenario['total_price']:,.2f} ريال")
|
1752 |
+
|
1753 |
+
# عرض توصيات التسعير
|
1754 |
+
st.markdown("#### توصيات التسعير")
|
1755 |
+
|
1756 |
+
st.markdown("- مراجعة هيكل التكاليف بشكل دوري للحفاظ على القدرة التنافسية.")
|
1757 |
+
st.markdown("- التأكيد على جودة الخدمات في العروض التسويقية.")
|
1758 |
+
st.markdown("- التأكيد على نسبة المحتوى المحلي العالية في العروض.")
|
1759 |
+
st.markdown("- مراقبة أسعار المنافسين وتعديل الاستراتيجية التسعيرية عند الحاجة.")
|
1760 |
+
st.markdown("- تحليل نقاط القوة والضعف بشكل مستمر لتحسين العروض المستقبلية.")
|
modules/pricing/pricing_engine.py
ADDED
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة التسعير المتكامل لنظام إدارة المناقصات - Hybrid Face
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import logging
|
7 |
+
import threading
|
8 |
+
import datetime
|
9 |
+
import json
|
10 |
+
import math
|
11 |
+
from pathlib import Path
|
12 |
+
|
13 |
+
# تهيئة السجل
|
14 |
+
logging.basicConfig(
|
15 |
+
level=logging.INFO,
|
16 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
17 |
+
)
|
18 |
+
logger = logging.getLogger('pricing')
|
19 |
+
|
20 |
+
class PricingEngine:
|
21 |
+
"""محرك التسعير المتكامل"""
|
22 |
+
|
23 |
+
def __init__(self, config=None, db=None):
|
24 |
+
"""تهيئة محرك التسعير"""
|
25 |
+
self.config = config
|
26 |
+
self.db = db
|
27 |
+
self.pricing_in_progress = False
|
28 |
+
self.current_project = None
|
29 |
+
self.pricing_results = {}
|
30 |
+
|
31 |
+
# إنشاء مجلد التسعير إذا لم يكن موجوداً
|
32 |
+
if config and hasattr(config, 'EXPORTS_PATH'):
|
33 |
+
self.exports_path = Path(config.EXPORTS_PATH)
|
34 |
+
else:
|
35 |
+
self.exports_path = Path('data/exports')
|
36 |
+
|
37 |
+
if not self.exports_path.exists():
|
38 |
+
self.exports_path.mkdir(parents=True, exist_ok=True)
|
39 |
+
|
40 |
+
def calculate_pricing(self, project_id, strategy="comprehensive", callback=None):
|
41 |
+
"""حساب التسعير للمشروع"""
|
42 |
+
if self.pricing_in_progress:
|
43 |
+
logger.warning("هناك عملية تسعير جارية بالفعل")
|
44 |
+
return False
|
45 |
+
|
46 |
+
self.pricing_in_progress = True
|
47 |
+
self.current_project = project_id
|
48 |
+
self.pricing_results = {
|
49 |
+
"project_id": project_id,
|
50 |
+
"strategy": strategy,
|
51 |
+
"pricing_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
52 |
+
"status": "جاري التسعير",
|
53 |
+
"direct_costs": {},
|
54 |
+
"indirect_costs": {},
|
55 |
+
"risk_costs": {},
|
56 |
+
"summary": {}
|
57 |
+
}
|
58 |
+
|
59 |
+
# بدء التسعير في خيط منفصل
|
60 |
+
thread = threading.Thread(
|
61 |
+
target=self._calculate_pricing_thread,
|
62 |
+
args=(project_id, strategy, callback)
|
63 |
+
)
|
64 |
+
thread.daemon = True
|
65 |
+
thread.start()
|
66 |
+
|
67 |
+
return True
|
68 |
+
|
69 |
+
def _calculate_pricing_thread(self, project_id, strategy, callback):
|
70 |
+
"""خيط حساب التسعير"""
|
71 |
+
try:
|
72 |
+
# محاكاة جلب بيانات المشروع من قاعدة البيانات
|
73 |
+
project_data = self._get_project_data(project_id)
|
74 |
+
|
75 |
+
if not project_data:
|
76 |
+
logger.error(f"لم يتم العثور على بيانات المشروع: {project_id}")
|
77 |
+
self.pricing_results["status"] = "فشل التسعير"
|
78 |
+
self.pricing_results["error"] = "لم يتم العثور على بيانات المشروع"
|
79 |
+
return
|
80 |
+
|
81 |
+
# حساب التكاليف المباشرة
|
82 |
+
self._calculate_direct_costs(project_data)
|
83 |
+
|
84 |
+
# حساب التكاليف غير المباشرة
|
85 |
+
self._calculate_indirect_costs(project_data, strategy)
|
86 |
+
|
87 |
+
# حساب تكاليف المخاطر
|
88 |
+
self._calculate_risk_costs(project_data, strategy)
|
89 |
+
|
90 |
+
# حساب ملخص التسعير
|
91 |
+
self._calculate_pricing_summary(strategy)
|
92 |
+
|
93 |
+
# تحديث حالة التسعير
|
94 |
+
self.pricing_results["status"] = "اكتمل التسعير"
|
95 |
+
self.pricing_results["pricing_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
96 |
+
|
97 |
+
logger.info(f"اكتمل تسعير المشروع: {project_id}")
|
98 |
+
|
99 |
+
except Exception as e:
|
100 |
+
logger.error(f"خطأ في تسعير المشروع: {str(e)}")
|
101 |
+
self.pricing_results["status"] = "فشل التسعير"
|
102 |
+
self.pricing_results["error"] = str(e)
|
103 |
+
|
104 |
+
finally:
|
105 |
+
self.pricing_in_progress = False
|
106 |
+
|
107 |
+
# استدعاء دالة الاستجابة إذا تم توفيرها
|
108 |
+
if callback and callable(callback):
|
109 |
+
callback(self.pricing_results)
|
110 |
+
|
111 |
+
def _get_project_data(self, project_id):
|
112 |
+
"""الحصول على بيانات المشروع"""
|
113 |
+
# في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
|
114 |
+
# هنا نقوم بمحاكاة البيانات للتوضيح
|
115 |
+
|
116 |
+
return {
|
117 |
+
"id": project_id,
|
118 |
+
"name": "مشروع الطرق السريعة",
|
119 |
+
"client": "وزارة النقل",
|
120 |
+
"items": [
|
121 |
+
{"id": 1, "name": "أعمال الحفر", "unit": "م³", "quantity": 1500, "unit_cost": 45},
|
122 |
+
{"id": 2, "name": "أعمال الخرسانة", "unit": "م³", "quantity": 750, "unit_cost": 1200},
|
123 |
+
{"id": 3, "name": "أعمال الأسفلت", "unit": "م²", "quantity": 5000, "unit_cost": 120},
|
124 |
+
{"id": 4, "name": "أعمال الإنارة", "unit": "عدد", "quantity": 50, "unit_cost": 3500}
|
125 |
+
],
|
126 |
+
"resources": {
|
127 |
+
"materials": [
|
128 |
+
{"id": 1, "name": "أسمنت", "unit": "طن", "quantity": 300, "unit_cost": 950},
|
129 |
+
{"id": 2, "name": "حديد تسليح", "unit": "طن", "quantity": 120, "unit_cost": 3200},
|
130 |
+
{"id": 3, "name": "رمل", "unit": "م³", "quantity": 450, "unit_cost": 75},
|
131 |
+
{"id": 4, "name": "أسفلت", "unit": "طن", "quantity": 200, "unit_cost": 1800}
|
132 |
+
],
|
133 |
+
"equipment": [
|
134 |
+
{"id": 1, "name": "حفارة", "unit": "يوم", "quantity": 45, "unit_cost": 1500},
|
135 |
+
{"id": 2, "name": "لودر", "unit": "يوم", "quantity": 30, "unit_cost": 1200},
|
136 |
+
{"id": 3, "name": "شاحنة نقل", "unit": "يوم", "quantity": 60, "unit_cost": 800},
|
137 |
+
{"id": 4, "name": "خلاطة خرسانة", "unit": "يوم", "quantity": 40, "unit_cost": 600}
|
138 |
+
],
|
139 |
+
"labor": [
|
140 |
+
{"id": 1, "name": "عمال", "unit": "يوم", "quantity": 1200, "unit_cost": 150},
|
141 |
+
{"id": 2, "name": "فنيون", "unit": "يوم", "quantity": 600, "unit_cost": 300},
|
142 |
+
{"id": 3, "name": "مهندسون", "unit": "يوم", "quantity": 180, "unit_cost": 800}
|
143 |
+
]
|
144 |
+
},
|
145 |
+
"risks": [
|
146 |
+
{"id": 1, "name": "تأخر توريد المواد", "probability": "متوسط", "impact": "عالي", "cost_impact": 0.05},
|
147 |
+
{"id": 2, "name": "تغير أسعار المواد", "probability": "عالي", "impact": "عالي", "cost_impact": 0.08},
|
148 |
+
{"id": 3, "name": "ظروف جوية غير مواتية", "probability": "منخفض", "impact": "متوسط", "cost_impact": 0.03},
|
149 |
+
{"id": 4, "name": "نقص العمالة", "probability": "متوسط", "impact": "متوسط", "cost_impact": 0.04}
|
150 |
+
],
|
151 |
+
"project_duration": 180, # بالأيام
|
152 |
+
"location": "المنطقة الشرقية"
|
153 |
+
}
|
154 |
+
|
155 |
+
def _calculate_direct_costs(self, project_data):
|
156 |
+
"""حساب التكاليف المباشرة"""
|
157 |
+
# حساب تكاليف البنود
|
158 |
+
items_cost = 0
|
159 |
+
items_details = []
|
160 |
+
|
161 |
+
for item in project_data["items"]:
|
162 |
+
total_cost = item["quantity"] * item["unit_cost"]
|
163 |
+
items_cost += total_cost
|
164 |
+
|
165 |
+
items_details.append({
|
166 |
+
"id": item["id"],
|
167 |
+
"name": item["name"],
|
168 |
+
"unit": item["unit"],
|
169 |
+
"quantity": item["quantity"],
|
170 |
+
"unit_cost": item["unit_cost"],
|
171 |
+
"total_cost": total_cost
|
172 |
+
})
|
173 |
+
|
174 |
+
# حساب تكاليف الموارد
|
175 |
+
materials_cost = 0
|
176 |
+
equipment_cost = 0
|
177 |
+
labor_cost = 0
|
178 |
+
|
179 |
+
for material in project_data["resources"]["materials"]:
|
180 |
+
materials_cost += material["quantity"] * material["unit_cost"]
|
181 |
+
|
182 |
+
for equipment in project_data["resources"]["equipment"]:
|
183 |
+
equipment_cost += equipment["quantity"] * equipment["unit_cost"]
|
184 |
+
|
185 |
+
for labor in project_data["resources"]["labor"]:
|
186 |
+
labor_cost += labor["quantity"] * labor["unit_cost"]
|
187 |
+
|
188 |
+
resources_cost = materials_cost + equipment_cost + labor_cost
|
189 |
+
|
190 |
+
# تخزين نتائج التكاليف المباشرة
|
191 |
+
self.pricing_results["direct_costs"] = {
|
192 |
+
"items": {
|
193 |
+
"total": items_cost,
|
194 |
+
"details": items_details
|
195 |
+
},
|
196 |
+
"resources": {
|
197 |
+
"total": resources_cost,
|
198 |
+
"materials": materials_cost,
|
199 |
+
"equipment": equipment_cost,
|
200 |
+
"labor": labor_cost
|
201 |
+
},
|
202 |
+
"total_direct_costs": items_cost
|
203 |
+
}
|
204 |
+
|
205 |
+
def _calculate_indirect_costs(self, project_data, strategy):
|
206 |
+
"""حساب التكاليف غير المباشرة"""
|
207 |
+
direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
|
208 |
+
|
209 |
+
# تحديد نسب التكاليف غير المباشرة بناءً على استراتيجية التسعير
|
210 |
+
if strategy == "comprehensive":
|
211 |
+
overhead_rate = 0.15 # 15% نفقات عامة
|
212 |
+
profit_rate = 0.10 # 10% ربح
|
213 |
+
admin_rate = 0.05 # 5% تكاليف إدارية
|
214 |
+
elif strategy == "competitive":
|
215 |
+
overhead_rate = 0.12 # 12% نفقات عامة
|
216 |
+
profit_rate = 0.07 # 7% ربح
|
217 |
+
admin_rate = 0.04 # 4% تكاليف إدارية
|
218 |
+
else: # balanced
|
219 |
+
overhead_rate = 0.13 # 13% نفقات عامة
|
220 |
+
profit_rate = 0.08 # 8% ربح
|
221 |
+
admin_rate = 0.045 # 4.5% تكاليف إدارية
|
222 |
+
|
223 |
+
# حساب التكاليف غير المباشرة
|
224 |
+
overhead_cost = direct_costs * overhead_rate
|
225 |
+
profit_cost = direct_costs * profit_rate
|
226 |
+
admin_cost = direct_costs * admin_rate
|
227 |
+
|
228 |
+
# تكاليف إضافية
|
229 |
+
mobilization_cost = direct_costs * 0.03 # 3% تكاليف التجهيز
|
230 |
+
bonds_insurance_cost = direct_costs * 0.02 # 2% تكاليف الضمانات والتأمين
|
231 |
+
|
232 |
+
# إجمالي التكاليف غير المباشرة
|
233 |
+
total_indirect_costs = overhead_cost + profit_cost + admin_cost + mobilization_cost + bonds_insurance_cost
|
234 |
+
|
235 |
+
# تخزين نتائج التكاليف غير المباشرة
|
236 |
+
self.pricing_results["indirect_costs"] = {
|
237 |
+
"overhead": {
|
238 |
+
"rate": overhead_rate,
|
239 |
+
"cost": overhead_cost
|
240 |
+
},
|
241 |
+
"profit": {
|
242 |
+
"rate": profit_rate,
|
243 |
+
"cost": profit_cost
|
244 |
+
},
|
245 |
+
"administrative": {
|
246 |
+
"rate": admin_rate,
|
247 |
+
"cost": admin_cost
|
248 |
+
},
|
249 |
+
"mobilization": {
|
250 |
+
"rate": 0.03,
|
251 |
+
"cost": mobilization_cost
|
252 |
+
},
|
253 |
+
"bonds_insurance": {
|
254 |
+
"rate": 0.02,
|
255 |
+
"cost": bonds_insurance_cost
|
256 |
+
},
|
257 |
+
"total_indirect_costs": total_indirect_costs
|
258 |
+
}
|
259 |
+
|
260 |
+
def _calculate_risk_costs(self, project_data, strategy):
|
261 |
+
"""حساب تكاليف المخاطر"""
|
262 |
+
direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
|
263 |
+
|
264 |
+
# تحويل احتمالية وتأثير المخاطر إلى قيم رقمية
|
265 |
+
probability_map = {
|
266 |
+
"منخفض": 0.3,
|
267 |
+
"متوسط": 0.5,
|
268 |
+
"عالي": 0.7
|
269 |
+
}
|
270 |
+
|
271 |
+
impact_map = {
|
272 |
+
"منخفض": 0.3,
|
273 |
+
"متوسط": 0.5,
|
274 |
+
"عالي": 0.7
|
275 |
+
}
|
276 |
+
|
277 |
+
# حساب تكاليف المخاطر
|
278 |
+
risk_costs = []
|
279 |
+
total_risk_cost = 0
|
280 |
+
|
281 |
+
for risk in project_data["risks"]:
|
282 |
+
probability = probability_map.get(risk["probability"], 0.5)
|
283 |
+
impact = impact_map.get(risk["impact"], 0.5)
|
284 |
+
|
285 |
+
# حساب درجة المخاطرة
|
286 |
+
risk_score = probability * impact
|
287 |
+
|
288 |
+
# حساب تكلفة المخاطرة
|
289 |
+
risk_cost = direct_costs * risk["cost_impact"] * risk_score
|
290 |
+
|
291 |
+
# تعديل تكلفة المخاطرة بناءً على استراتيجية التسعير
|
292 |
+
if strategy == "comprehensive":
|
293 |
+
risk_cost_factor = 1.0 # تغطية كاملة للمخاطر
|
294 |
+
elif strategy == "competitive":
|
295 |
+
risk_cost_factor = 0.7 # تغطية جزئية للمخاطر
|
296 |
+
else: # balanced
|
297 |
+
risk_cost_factor = 0.85 # تغطية متوازنة للمخاطر
|
298 |
+
|
299 |
+
adjusted_risk_cost = risk_cost * risk_cost_factor
|
300 |
+
total_risk_cost += adjusted_risk_cost
|
301 |
+
|
302 |
+
risk_costs.append({
|
303 |
+
"id": risk["id"],
|
304 |
+
"name": risk["name"],
|
305 |
+
"probability": risk["probability"],
|
306 |
+
"impact": risk["impact"],
|
307 |
+
"risk_score": risk_score,
|
308 |
+
"cost_impact": risk["cost_impact"],
|
309 |
+
"risk_cost": risk_cost,
|
310 |
+
"adjusted_risk_cost": adjusted_risk_cost
|
311 |
+
})
|
312 |
+
|
313 |
+
# تخزين نتائج تكاليف المخاطر
|
314 |
+
self.pricing_results["risk_costs"] = {
|
315 |
+
"risks": risk_costs,
|
316 |
+
"total_risk_cost": total_risk_cost,
|
317 |
+
"strategy_factor": 1.0 if strategy == "comprehensive" else (0.7 if strategy == "competitive" else 0.85)
|
318 |
+
}
|
319 |
+
|
320 |
+
def _calculate_pricing_summary(self, strategy):
|
321 |
+
"""حساب ملخص التسعير"""
|
322 |
+
direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
|
323 |
+
indirect_costs = self.pricing_results["indirect_costs"]["total_indirect_costs"]
|
324 |
+
risk_costs = self.pricing_results["risk_costs"]["total_risk_cost"]
|
325 |
+
|
326 |
+
# حساب إجمالي التكاليف
|
327 |
+
total_costs = direct_costs + indirect_costs + risk_costs
|
328 |
+
|
329 |
+
# حساب ضريبة القيمة المضافة (15%)
|
330 |
+
vat = total_costs * 0.15
|
331 |
+
|
332 |
+
# حساب السعر النهائي
|
333 |
+
final_price = total_costs + vat
|
334 |
+
|
335 |
+
# تخزين ملخص التسعير
|
336 |
+
self.pricing_results["summary"] = {
|
337 |
+
"direct_costs": direct_costs,
|
338 |
+
"indirect_costs": indirect_costs,
|
339 |
+
"risk_costs": risk_costs,
|
340 |
+
"total_costs": total_costs,
|
341 |
+
"vat": {
|
342 |
+
"rate": 0.15,
|
343 |
+
"amount": vat
|
344 |
+
},
|
345 |
+
"final_price": final_price,
|
346 |
+
"strategy": strategy,
|
347 |
+
"pricing_notes": self._generate_pricing_notes(strategy)
|
348 |
+
}
|
349 |
+
|
350 |
+
def _generate_pricing_notes(self, strategy):
|
351 |
+
"""توليد ملاحظات التسعير"""
|
352 |
+
if strategy == "comprehensive":
|
353 |
+
return [
|
354 |
+
"تم تطبيق استراتيجية التسعير الشاملة التي تغطي جميع التكاليف والمخاطر",
|
355 |
+
"تم تضمين هامش ربح مناسب (10%) لضمان ربحية المشروع",
|
356 |
+
"تم تغطية جميع المخاطر المحتملة بشكل كامل",
|
357 |
+
"يوصى بمراجعة أسعار المواد قبل تقديم العرض النهائي"
|
358 |
+
]
|
359 |
+
elif strategy == "competitive":
|
360 |
+
return [
|
361 |
+
"تم تطبيق استراتيجية التسعير التنافسية لزيادة فرص الفوز بالمناقصة",
|
362 |
+
"تم تخفيض هامش الربح (7%) لتقديم سعر تنافسي",
|
363 |
+
"تم تغطية المخاطر بشكل جزئي، مما يتطلب إدارة مخاطر فعالة أثناء التنفيذ",
|
364 |
+
"يجب مراقبة التكاليف بدقة أثناء تنفيذ المشروع لضمان الربحية"
|
365 |
+
]
|
366 |
+
else: # balanced
|
367 |
+
return [
|
368 |
+
"تم تطبيق استراتيجية التسعير المتوازنة التي توازن بين الربحية والتنافسية",
|
369 |
+
"تم تضمين هامش ربح معقول (8%) يوازن بين الربحية والتنافسية",
|
370 |
+
"تم تغطية المخاطر الرئيسية بشكل مناسب",
|
371 |
+
"يوصى بمراجعة بنود التكلفة العالية قبل تقديم العرض النهائي"
|
372 |
+
]
|
373 |
+
|
374 |
+
def get_pricing_status(self):
|
375 |
+
"""الحصول على حالة التسعير الحالي"""
|
376 |
+
if not self.pricing_in_progress:
|
377 |
+
if not self.pricing_results:
|
378 |
+
return {"status": "لا يوجد تسعير جارٍ"}
|
379 |
+
else:
|
380 |
+
return {"status": self.pricing_results.get("status", "غير معروف")}
|
381 |
+
|
382 |
+
return {
|
383 |
+
"status": "جاري التسعير",
|
384 |
+
"project_id": self.current_project,
|
385 |
+
"start_time": self.pricing_results.get("pricing_start_time")
|
386 |
+
}
|
387 |
+
|
388 |
+
def get_pricing_results(self):
|
389 |
+
"""الحصول على نتائج التسعير"""
|
390 |
+
return self.pricing_results
|
391 |
+
|
392 |
+
def export_pricing_results(self, output_path=None):
|
393 |
+
"""تصدير نتائج التسعير إلى ملف JSON"""
|
394 |
+
if not self.pricing_results:
|
395 |
+
logger.warning("لا توجد نتائج تسعير للتصدير")
|
396 |
+
return None
|
397 |
+
|
398 |
+
if not output_path:
|
399 |
+
# إنشاء اسم ملف افتراضي
|
400 |
+
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
401 |
+
filename = f"pricing_results_{timestamp}.json"
|
402 |
+
output_path = os.path.join(self.exports_path, filename)
|
403 |
+
|
404 |
+
try:
|
405 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
406 |
+
json.dump(self.pricing_results, f, ensure_ascii=False, indent=4)
|
407 |
+
|
408 |
+
logger.info(f"تم تصدير نتائج التسعير إلى: {output_path}")
|
409 |
+
return output_path
|
410 |
+
|
411 |
+
except Exception as e:
|
412 |
+
logger.error(f"خطأ في تصدير نتائج التسعير: {str(e)}")
|
413 |
+
return None
|
414 |
+
|
415 |
+
def import_pricing_results(self, input_path):
|
416 |
+
"""استيراد نتائج التسعير من ملف JSON"""
|
417 |
+
if not os.path.exists(input_path):
|
418 |
+
logger.error(f"ملف نتائج التسعير غير موجود: {input_path}")
|
419 |
+
return False
|
420 |
+
|
421 |
+
try:
|
422 |
+
with open(input_path, 'r', encoding='utf-8') as f:
|
423 |
+
self.pricing_results = json.load(f)
|
424 |
+
|
425 |
+
logger.info(f"تم استيراد نتائج التسعير من: {input_path}")
|
426 |
+
return True
|
427 |
+
|
428 |
+
except Exception as e:
|
429 |
+
logger.error(f"خطأ في استيراد نتائج التسعير: {str(e)}")
|
430 |
+
return False
|
modules/project_management/project_management_app.py
ADDED
@@ -0,0 +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['name']}")
|
257 |
+
st.markdown(f"**رقم المناقصة**: {project['number']}")
|
258 |
+
st.markdown(f"**الجهة المالكة**: {project['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
ADDED
@@ -0,0 +1,1706 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة الموارد - التطبيق الرئيسي
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import matplotlib.pyplot as plt
|
9 |
+
import plotly.express as px
|
10 |
+
import plotly.graph_objects as go
|
11 |
+
from datetime import datetime, timedelta
|
12 |
+
import time
|
13 |
+
import io
|
14 |
+
import os
|
15 |
+
import json
|
16 |
+
import base64
|
17 |
+
from pathlib import Path
|
18 |
+
|
19 |
+
class ResourcesApp:
|
20 |
+
"""وحدة الموارد"""
|
21 |
+
|
22 |
+
def __init__(self):
|
23 |
+
"""تهيئة وحدة الموارد"""
|
24 |
+
|
25 |
+
# تهيئة حالة الجلسة
|
26 |
+
if 'resources_data' not in st.session_state:
|
27 |
+
# إنشاء بيانات افتراضية للموارد البشرية
|
28 |
+
np.random.seed(42)
|
29 |
+
|
30 |
+
# إنشاء بيانات الموظفين
|
31 |
+
n_employees = 50
|
32 |
+
employee_ids = [f"EMP-{i+1:03d}" for i in range(n_employees)]
|
33 |
+
employee_names = [
|
34 |
+
"أحمد محمد", "محمد علي", "علي إبراهيم", "إبراهيم خالد", "خالد عبدالله",
|
35 |
+
"عبدالله سعد", "سعد فهد", "فهد ناصر", "ناصر سلطان", "سلطان عمر",
|
36 |
+
"عمر يوسف", "يوسف عبدالرحمن", "عبدالرحمن حسن", "حسن أحمد", "أحمد عبدالعزيز",
|
37 |
+
"عبدالعزيز سعود", "سعود فيصل", "فيصل تركي", "تركي بندر", "بندر سلمان",
|
38 |
+
"سلمان محمد", "محمد عبدالله", "عبدالله فهد", "فهد سعد", "سعد خالد",
|
39 |
+
"خالد علي", "علي عمر", "عمر سعيد", "سعيد ماجد", "ماجد فارس",
|
40 |
+
"فارس نايف", "نايف سامي", "سامي راشد", "راشد وليد", "وليد هاني",
|
41 |
+
"هاني زياد", "زياد طارق", "طارق عادل", "عادل فراس", "فراس باسم",
|
42 |
+
"باسم جمال", "جمال كريم", "كريم نبيل", "نبيل هشام", "هشام عماد",
|
43 |
+
"عماد أيمن", "أيمن رامي", "رامي سمير", "سمير وائل", "وائل مازن"
|
44 |
+
]
|
45 |
+
employee_departments = np.random.choice(["الهندسة", "المشتريات", "المالية", "الموارد البشرية", "تقنية المعلومات", "التسويق", "المبيعات"], n_employees)
|
46 |
+
employee_positions = np.random.choice(["مدير", "مهندس", "محاسب", "مشرف", "أخصائي", "مساعد", "فني"], n_employees)
|
47 |
+
employee_skills = [
|
48 |
+
np.random.choice(["إدارة المشاريع", "التصميم الهندسي", "تحليل البيانات", "إدارة العقود", "التخطيط الاستراتيجي"],
|
49 |
+
size=np.random.randint(1, 4),
|
50 |
+
replace=False).tolist()
|
51 |
+
for _ in range(n_employees)
|
52 |
+
]
|
53 |
+
employee_experiences = np.random.randint(1, 20, n_employees)
|
54 |
+
employee_costs = np.random.randint(5000, 25000, n_employees)
|
55 |
+
employee_availabilities = np.random.choice([True, False], n_employees, p=[0.8, 0.2])
|
56 |
+
employee_ratings = np.random.uniform(3.0, 5.0, n_employees)
|
57 |
+
|
58 |
+
# إنشاء DataFrame للموظفين
|
59 |
+
employees_data = {
|
60 |
+
"رقم الموظف": employee_ids,
|
61 |
+
"اسم الموظف": employee_names,
|
62 |
+
"القسم": employee_departments,
|
63 |
+
"المنصب": employee_positions,
|
64 |
+
"المهارات": employee_skills,
|
65 |
+
"سنوات الخبرة": employee_experiences,
|
66 |
+
"التكلفة الشهرية": employee_costs,
|
67 |
+
"متاح": employee_availabilities,
|
68 |
+
"التقييم": employee_ratings
|
69 |
+
}
|
70 |
+
|
71 |
+
# إنشاء بيانات المعدات
|
72 |
+
n_equipment = 30
|
73 |
+
equipment_ids = [f"EQ-{i+1:03d}" for i in range(n_equipment)]
|
74 |
+
equipment_names = [
|
75 |
+
"حفارة كبيرة", "حفارة صغيرة", "جرافة", "شاحنة نقل", "رافعة كبيرة",
|
76 |
+
"رافعة متوسطة", "رافعة صغيرة", "خلاطة خرسانة", "مضخة خرسانة", "مولد كهرباء كبير",
|
77 |
+
"مولد كهرباء متوسط", "مولد كهرباء صغير", "ضاغط هواء", "آلة لحام", "معدات قياس",
|
78 |
+
"معدات اختبار", "سقالات", "قوالب خرسانية", "معدات سباكة", "معدات كهربائية",
|
79 |
+
"معدات تكييف", "معدات تدفئة", "معدات إضاءة", "معدات سلامة", "معدات إطفاء",
|
80 |
+
"سيارة نقل صغيرة", "سيارة نقل متوسطة", "سيارة نقل كبيرة", "معدات حفر يدوية", "معدات بناء يدوية"
|
81 |
+
]
|
82 |
+
equipment_types = np.random.choice(["حفر", "نقل", "رفع", "خرسانة", "كهرباء", "قياس", "بناء", "سلامة"], n_equipment)
|
83 |
+
equipment_costs = np.random.randint(500, 5000, n_equipment)
|
84 |
+
equipment_availabilities = np.random.choice([True, False], n_equipment, p=[0.7, 0.3])
|
85 |
+
equipment_conditions = np.random.choice(["ممتاز", "جيد", "متوسط", "سيء"], n_equipment, p=[0.4, 0.3, 0.2, 0.1])
|
86 |
+
equipment_locations = np.random.choice(["المستودع", "موقع المشروع 1", "موقع المشروع 2", "موقع المشروع 3", "في الصيانة"], n_equipment)
|
87 |
+
|
88 |
+
# إنشاء DataFrame للمعدات
|
89 |
+
equipment_data = {
|
90 |
+
"رقم المعدة": equipment_ids,
|
91 |
+
"اسم المعدة": equipment_names,
|
92 |
+
"النوع": equipment_types,
|
93 |
+
"التكلفة اليومية": equipment_costs,
|
94 |
+
"متاحة": equipment_availabilities,
|
95 |
+
"الحالة": equipment_conditions,
|
96 |
+
"الموقع": equipment_locations
|
97 |
+
}
|
98 |
+
|
99 |
+
# إنشاء بيانات المواد
|
100 |
+
n_materials = 40
|
101 |
+
material_ids = [f"MAT-{i+1:03d}" for i in range(n_materials)]
|
102 |
+
material_names = [
|
103 |
+
"خرسانة جاهزة", "حديد تسليح", "طابوق", "أسمنت", "رمل", "بحص", "خشب", "ألمنيوم", "زجاج", "دهان",
|
104 |
+
"سيراميك", "رخام", "جبس", "عازل مائي", "عازل حراري", "أنابيب PVC", "أسلاك كهربائية", "مفاتيح كهربائية",
|
105 |
+
"إنارة", "تكييف", "مصاعد", "أبواب خشبية", "أبواب حديدية", "نوافذ ألمنيوم", "نوافذ زجاجية",
|
106 |
+
"أرضيات خشبية", "أرضيات بلاط", "أرضيات رخام", "أرضيات سيراميك", "أرضيات بورسلين",
|
107 |
+
"دهان داخلي", "دهان خارجي", "مواد عزل", "مواد تشطيب", "مواد كهربائية", "مواد سباكة",
|
108 |
+
"مواد تكييف", "مواد إضاءة", "مواد سلامة", "مواد متنوعة"
|
109 |
+
]
|
110 |
+
material_units = np.random.choice(["م3", "طن", "م2", "كجم", "لتر", "قطعة", "متر"], n_materials)
|
111 |
+
material_quantities = np.random.randint(10, 1000, n_materials)
|
112 |
+
material_costs = np.random.randint(50, 5000, n_materials)
|
113 |
+
material_suppliers = np.random.choice(["المورد 1", "المورد 2", "المورد 3", "المورد 4", "المورد 5"], n_materials)
|
114 |
+
material_lead_times = np.random.randint(1, 30, n_materials)
|
115 |
+
|
116 |
+
# إنشاء DataFrame للمواد
|
117 |
+
materials_data = {
|
118 |
+
"رقم المادة": material_ids,
|
119 |
+
"اسم المادة": material_names,
|
120 |
+
"الوحدة": material_units,
|
121 |
+
"الكمية المتاحة": material_quantities,
|
122 |
+
"تكلفة الوحدة": material_costs,
|
123 |
+
"المورد": material_suppliers,
|
124 |
+
"مدة التوريد (يوم)": material_lead_times
|
125 |
+
}
|
126 |
+
|
127 |
+
# إنشاء بيانات المشاريع
|
128 |
+
n_projects = 10
|
129 |
+
project_ids = [f"PRJ-{i+1:03d}" for i in range(n_projects)]
|
130 |
+
project_names = [
|
131 |
+
"مشروع إنشاء مبنى إداري", "مشروع إنشاء مبنى سكني", "مشروع إنشاء مدرسة",
|
132 |
+
"مشروع إنشاء مستشفى", "مشروع تطوير طرق", "مشروع إنشاء جسر",
|
133 |
+
"مشروع بنية تحتية", "مشروع إنشاء مركز تجاري", "مشروع إنشاء فندق",
|
134 |
+
"مشروع إنشاء مصنع"
|
135 |
+
]
|
136 |
+
project_locations = np.random.choice(["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك"], n_projects)
|
137 |
+
project_start_dates = [
|
138 |
+
(datetime.now() - timedelta(days=np.random.randint(0, 180))).strftime("%Y-%m-%d")
|
139 |
+
for _ in range(n_projects)
|
140 |
+
]
|
141 |
+
project_end_dates = [
|
142 |
+
(datetime.strptime(start_date, "%Y-%m-%d") + timedelta(days=np.random.randint(180, 720))).strftime("%Y-%m-%d")
|
143 |
+
for start_date in project_start_dates
|
144 |
+
]
|
145 |
+
project_budgets = np.random.randint(1000000, 50000000, n_projects)
|
146 |
+
project_statuses = np.random.choice(["قيد التنفيذ", "مكتمل", "متوقف", "مخطط"], n_projects)
|
147 |
+
|
148 |
+
# إنشاء DataFrame للمشاريع
|
149 |
+
projects_data = {
|
150 |
+
"رقم المشروع": project_ids,
|
151 |
+
"اسم المشروع": project_names,
|
152 |
+
"الموقع": project_locations,
|
153 |
+
"تاريخ البدء": project_start_dates,
|
154 |
+
"تاريخ الانتهاء": project_end_dates,
|
155 |
+
"الميزانية": project_budgets,
|
156 |
+
"الحالة": project_statuses
|
157 |
+
}
|
158 |
+
|
159 |
+
# إنشاء بيانات تخصيص الموارد للمشاريع
|
160 |
+
n_allocations = 100
|
161 |
+
allocation_ids = [f"ALLOC-{i+1:03d}" for i in range(n_allocations)]
|
162 |
+
allocation_projects = np.random.choice(project_ids, n_allocations)
|
163 |
+
allocation_resource_types = np.random.choice(["موظف", "معدة", "مادة"], n_allocations)
|
164 |
+
allocation_resource_ids = []
|
165 |
+
for res_type in allocation_resource_types:
|
166 |
+
if res_type == "موظف":
|
167 |
+
allocation_resource_ids.append(np.random.choice(employee_ids))
|
168 |
+
elif res_type == "معدة":
|
169 |
+
allocation_resource_ids.append(np.random.choice(equipment_ids))
|
170 |
+
else:
|
171 |
+
allocation_resource_ids.append(np.random.choice(material_ids))
|
172 |
+
|
173 |
+
allocation_start_dates = [
|
174 |
+
(datetime.now() - timedelta(days=np.random.randint(0, 90))).strftime("%Y-%m-%d")
|
175 |
+
for _ in range(n_allocations)
|
176 |
+
]
|
177 |
+
allocation_end_dates = [
|
178 |
+
(datetime.strptime(start_date, "%Y-%m-%d") + timedelta(days=np.random.randint(30, 180))).strftime("%Y-%m-%d")
|
179 |
+
for start_date in allocation_start_dates
|
180 |
+
]
|
181 |
+
allocation_quantities = np.random.randint(1, 10, n_allocations)
|
182 |
+
allocation_costs = np.random.randint(5000, 50000, n_allocations)
|
183 |
+
|
184 |
+
# إنشاء DataFrame لتخصيص الموارد
|
185 |
+
allocations_data = {
|
186 |
+
"رقم التخصيص": allocation_ids,
|
187 |
+
"رقم المشروع": allocation_projects,
|
188 |
+
"نوع المورد": allocation_resource_types,
|
189 |
+
"رقم المورد": allocation_resource_ids,
|
190 |
+
"تاريخ البدء": allocation_start_dates,
|
191 |
+
"تاريخ الانتهاء": allocation_end_dates,
|
192 |
+
"الكمية": allocation_quantities,
|
193 |
+
"التكلفة": allocation_costs
|
194 |
+
}
|
195 |
+
|
196 |
+
# تخزين البيانات في حالة الجلسة
|
197 |
+
st.session_state.resources_data = {
|
198 |
+
"employees": pd.DataFrame(employees_data),
|
199 |
+
"equipment": pd.DataFrame(equipment_data),
|
200 |
+
"materials": pd.DataFrame(materials_data),
|
201 |
+
"projects": pd.DataFrame(projects_data),
|
202 |
+
"allocations": pd.DataFrame(allocations_data)
|
203 |
+
}
|
204 |
+
|
205 |
+
def render(self):
|
206 |
+
"""عرض واجهة وحدة الموارد"""
|
207 |
+
|
208 |
+
st.markdown("<h1 class='module-title'>وحدة الموارد</h1>", unsafe_allow_html=True)
|
209 |
+
|
210 |
+
tabs = st.tabs([
|
211 |
+
"لوحة المعلومات",
|
212 |
+
"الموارد البشرية",
|
213 |
+
"المعدات",
|
214 |
+
"المواد",
|
215 |
+
"تخصيص الموارد",
|
216 |
+
"تخطيط الموارد"
|
217 |
+
])
|
218 |
+
|
219 |
+
with tabs[0]:
|
220 |
+
self._render_dashboard_tab()
|
221 |
+
|
222 |
+
with tabs[1]:
|
223 |
+
self._render_human_resources_tab()
|
224 |
+
|
225 |
+
with tabs[2]:
|
226 |
+
self._render_equipment_tab()
|
227 |
+
|
228 |
+
with tabs[3]:
|
229 |
+
self._render_materials_tab()
|
230 |
+
|
231 |
+
with tabs[4]:
|
232 |
+
self._render_resource_allocation_tab()
|
233 |
+
|
234 |
+
with tabs[5]:
|
235 |
+
self._render_resource_planning_tab()
|
236 |
+
|
237 |
+
def _render_dashboard_tab(self):
|
238 |
+
"""عرض تبويب لوحة المعلومات"""
|
239 |
+
|
240 |
+
st.markdown("### لوحة معلومات الموارد")
|
241 |
+
|
242 |
+
# استخراج البيانات
|
243 |
+
employees_df = st.session_state.resources_data["employees"]
|
244 |
+
equipment_df = st.session_state.resources_data["equipment"]
|
245 |
+
materials_df = st.session_state.resources_data["materials"]
|
246 |
+
projects_df = st.session_state.resources_data["projects"]
|
247 |
+
allocations_df = st.session_state.resources_data["allocations"]
|
248 |
+
|
249 |
+
# عرض مؤشرات الأداء الرئيسية
|
250 |
+
st.markdown("#### مؤشرات الأداء الرئيسية")
|
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 |
+
total_materials = len(materials_df)
|
266 |
+
low_stock_materials = len(materials_df[materials_df["ا��كمية المتاحة"] < 50])
|
267 |
+
st.metric("المواد", f"{total_materials}", f"-{low_stock_materials} منخفضة المخزون")
|
268 |
+
|
269 |
+
with col4:
|
270 |
+
total_projects = len(projects_df)
|
271 |
+
active_projects = len(projects_df[projects_df["الحالة"] == "قيد التنفيذ"])
|
272 |
+
st.metric("المشاريع النشطة", f"{active_projects}/{total_projects}")
|
273 |
+
|
274 |
+
# عرض توزيع الموارد البشرية حسب القسم
|
275 |
+
st.markdown("#### توزيع الموارد البشرية حسب القسم")
|
276 |
+
|
277 |
+
dept_counts = employees_df["القسم"].value_counts().reset_index()
|
278 |
+
dept_counts.columns = ["القسم", "العدد"]
|
279 |
+
|
280 |
+
fig = px.pie(
|
281 |
+
dept_counts,
|
282 |
+
values="العدد",
|
283 |
+
names="القسم",
|
284 |
+
title="توزيع الموظفين حسب القسم",
|
285 |
+
color="القسم"
|
286 |
+
)
|
287 |
+
|
288 |
+
st.plotly_chart(fig, use_container_width=True)
|
289 |
+
|
290 |
+
# عرض توزيع المعدات حسب النوع
|
291 |
+
st.markdown("#### توزيع المعدات حسب النوع")
|
292 |
+
|
293 |
+
type_counts = equipment_df["النوع"].value_counts().reset_index()
|
294 |
+
type_counts.columns = ["النوع", "العدد"]
|
295 |
+
|
296 |
+
fig = px.bar(
|
297 |
+
type_counts,
|
298 |
+
x="النوع",
|
299 |
+
y="العدد",
|
300 |
+
title="توزيع المعدات حسب النوع",
|
301 |
+
color="النوع",
|
302 |
+
text_auto=True
|
303 |
+
)
|
304 |
+
|
305 |
+
st.plotly_chart(fig, use_container_width=True)
|
306 |
+
|
307 |
+
# عرض توزيع المواد حسب المورد
|
308 |
+
st.markdown("#### توزيع المواد حسب المورد")
|
309 |
+
|
310 |
+
supplier_counts = materials_df["المورد"].value_counts().reset_index()
|
311 |
+
supplier_counts.columns = ["المورد", "العدد"]
|
312 |
+
|
313 |
+
fig = px.pie(
|
314 |
+
supplier_counts,
|
315 |
+
values="العدد",
|
316 |
+
names="المورد",
|
317 |
+
title="توزيع المواد حسب المورد",
|
318 |
+
color="المورد"
|
319 |
+
)
|
320 |
+
|
321 |
+
st.plotly_chart(fig, use_container_width=True)
|
322 |
+
|
323 |
+
# عرض توزيع تكاليف الموارد
|
324 |
+
st.markdown("#### توزيع تكاليف الموارد")
|
325 |
+
|
326 |
+
# حساب إجمالي تكاليف الموظفين
|
327 |
+
total_employee_cost = employees_df["التكلفة الشهرية"].sum()
|
328 |
+
|
329 |
+
# حساب إجمالي تكاليف المعدات (افتراضياً لشهر واحد)
|
330 |
+
total_equipment_cost = equipment_df["التكلفة اليومية"].sum() * 30
|
331 |
+
|
332 |
+
# حساب إجمالي تكاليف المواد
|
333 |
+
total_material_cost = (materials_df["الكمية المتاحة"] * materials_df["تكلفة الوحدة"]).sum()
|
334 |
+
|
335 |
+
# إنشاء DataFrame لتوزيع التكاليف
|
336 |
+
cost_distribution = pd.DataFrame({
|
337 |
+
"نوع المورد": ["الموظفون", "المعدات", "المواد"],
|
338 |
+
"التكلفة": [total_employee_cost, total_equipment_cost, total_material_cost]
|
339 |
+
})
|
340 |
+
|
341 |
+
fig = px.pie(
|
342 |
+
cost_distribution,
|
343 |
+
values="التكلفة",
|
344 |
+
names="نوع المورد",
|
345 |
+
title="توزيع تكاليف الموارد",
|
346 |
+
color="نوع المورد",
|
347 |
+
color_discrete_map={
|
348 |
+
"الموظفون": "#3498db",
|
349 |
+
"المعدات": "#2ecc71",
|
350 |
+
"المواد": "#f39c12"
|
351 |
+
}
|
352 |
+
)
|
353 |
+
|
354 |
+
st.plotly_chart(fig, use_container_width=True)
|
355 |
+
|
356 |
+
# عرض تخصيص الموارد للمشاريع
|
357 |
+
st.markdown("#### تخصيص الموارد للمشاريع")
|
358 |
+
|
359 |
+
# حساب عدد الموارد المخصصة لكل مشروع
|
360 |
+
project_allocations = allocations_df["رقم المشروع"].value_counts().reset_index()
|
361 |
+
project_allocations.columns = ["رقم المشروع", "عدد الموارد المخصصة"]
|
362 |
+
|
363 |
+
# دمج بيانات المشاريع مع بيانات التخصيص
|
364 |
+
project_allocations = project_allocations.merge(
|
365 |
+
projects_df[["رقم المشروع", "اسم المشروع", "الحالة"]],
|
366 |
+
on="رقم المشروع",
|
367 |
+
how="left"
|
368 |
+
)
|
369 |
+
|
370 |
+
fig = px.bar(
|
371 |
+
project_allocations,
|
372 |
+
x="اسم المشروع",
|
373 |
+
y="عدد الموارد المخصصة",
|
374 |
+
title="تخصيص الموارد للمشاريع",
|
375 |
+
color="الحالة",
|
376 |
+
text_auto=True,
|
377 |
+
color_discrete_map={
|
378 |
+
"قيد التنفيذ": "#3498db",
|
379 |
+
"مكتمل": "#2ecc71",
|
380 |
+
"متوقف": "#e74c3c",
|
381 |
+
"مخطط": "#f39c12"
|
382 |
+
}
|
383 |
+
)
|
384 |
+
|
385 |
+
st.plotly_chart(fig, use_container_width=True)
|
386 |
+
|
387 |
+
# عرض توزيع أنواع الموارد المخصصة
|
388 |
+
st.markdown("#### توزيع أنواع الموارد المخصصة")
|
389 |
+
|
390 |
+
resource_type_counts = allocations_df["نوع المورد"].value_counts().reset_index()
|
391 |
+
resource_type_counts.columns = ["نوع المورد", "العدد"]
|
392 |
+
|
393 |
+
fig = px.pie(
|
394 |
+
resource_type_counts,
|
395 |
+
values="العدد",
|
396 |
+
names="نوع المورد",
|
397 |
+
title="توزيع أنواع الموارد المخصصة",
|
398 |
+
color="نوع المورد",
|
399 |
+
color_discrete_map={
|
400 |
+
"موظف": "#3498db",
|
401 |
+
"معدة": "#2ecc71",
|
402 |
+
"مادة": "#f39c12"
|
403 |
+
}
|
404 |
+
)
|
405 |
+
|
406 |
+
st.plotly_chart(fig, use_container_width=True)
|
407 |
+
|
408 |
+
def _render_human_resources_tab(self):
|
409 |
+
"""عرض تبويب الموارد البشرية"""
|
410 |
+
|
411 |
+
st.markdown("### إدارة الموارد البشرية")
|
412 |
+
|
413 |
+
# استخراج البيانات
|
414 |
+
employees_df = st.session_state.resources_data["employees"]
|
415 |
+
|
416 |
+
# عرض خيارات التصفية
|
417 |
+
st.markdown("#### خيارات التصفية")
|
418 |
+
|
419 |
+
col1, col2, col3 = st.columns(3)
|
420 |
+
|
421 |
+
with col1:
|
422 |
+
selected_departments = st.multiselect(
|
423 |
+
"القسم",
|
424 |
+
options=employees_df["القسم"].unique(),
|
425 |
+
default=employees_df["القسم"].unique()
|
426 |
+
)
|
427 |
+
|
428 |
+
with col2:
|
429 |
+
selected_positions = st.multiselect(
|
430 |
+
"المنصب",
|
431 |
+
options=employees_df["المنصب"].unique(),
|
432 |
+
default=employees_df["المنصب"].unique()
|
433 |
+
)
|
434 |
+
|
435 |
+
with col3:
|
436 |
+
availability_filter = st.selectbox(
|
437 |
+
"الإتاحة",
|
438 |
+
options=["الكل", "متاح فقط", "غير متاح فقط"]
|
439 |
+
)
|
440 |
+
|
441 |
+
# تطبيق التصفية
|
442 |
+
filtered_df = employees_df[
|
443 |
+
employees_df["القسم"].isin(selected_departments) &
|
444 |
+
employees_df["المنصب"].isin(selected_positions)
|
445 |
+
]
|
446 |
+
|
447 |
+
if availability_filter == "متاح فقط":
|
448 |
+
filtered_df = filtered_df[filtered_df["متاح"] == True]
|
449 |
+
elif availability_filter == "غير متاح فقط":
|
450 |
+
filtered_df = filtered_df[filtered_df["متاح"] == False]
|
451 |
+
|
452 |
+
# عرض البيانات المصفاة
|
453 |
+
st.markdown("#### قائمة الموظفين")
|
454 |
+
|
455 |
+
st.dataframe(
|
456 |
+
filtered_df,
|
457 |
+
column_config={
|
458 |
+
"رقم الموظف": st.column_config.TextColumn("رقم الموظف"),
|
459 |
+
"اسم الموظف": st.column_config.TextColumn("اسم الموظف"),
|
460 |
+
"القسم": st.column_config.TextColumn("القسم"),
|
461 |
+
"المنصب": st.column_config.TextColumn("المنصب"),
|
462 |
+
"المهارات": st.column_config.ListColumn("المهارات"),
|
463 |
+
"سنوات الخبرة": st.column_config.NumberColumn("سنوات الخبرة"),
|
464 |
+
"التكلفة الشهرية": st.column_config.NumberColumn("التكلفة الشهرية", format="%.2f ريال"),
|
465 |
+
"متاح": st.column_config.CheckboxColumn("متاح"),
|
466 |
+
"التقييم": st.column_config.ProgressColumn("التقييم", min_value=0, max_value=5)
|
467 |
+
},
|
468 |
+
use_container_width=True,
|
469 |
+
hide_index=True
|
470 |
+
)
|
471 |
+
|
472 |
+
# عرض إحصائيات الموارد البشرية
|
473 |
+
st.markdown("#### إحصائيات الموارد البشرية")
|
474 |
+
|
475 |
+
col1, col2, col3, col4 = st.columns(4)
|
476 |
+
|
477 |
+
with col1:
|
478 |
+
total_employees = len(filtered_df)
|
479 |
+
st.metric("إجمالي الموظفين", f"{total_employees}")
|
480 |
+
|
481 |
+
with col2:
|
482 |
+
available_employees = len(filtered_df[filtered_df["متاح"] == True])
|
483 |
+
availability_rate = available_employees / total_employees * 100 if total_employees > 0 else 0
|
484 |
+
st.metric("معدل الإتاحة", f"{availability_rate:.1f}%")
|
485 |
+
|
486 |
+
with col3:
|
487 |
+
avg_experience = filtered_df["سنوات الخبرة"].mean()
|
488 |
+
st.metric("متوسط سنوات الخبرة", f"{avg_experience:.1f} سنة")
|
489 |
+
|
490 |
+
with col4:
|
491 |
+
avg_cost = filtered_df["التكلفة الشهرية"].mean()
|
492 |
+
st.metric("متوسط التكلفة الشهرية", f"{avg_cost:.0f} ريال")
|
493 |
+
|
494 |
+
# عرض توزيع الموظفين حسب القسم
|
495 |
+
st.markdown("#### توزيع الموظفين حسب القسم")
|
496 |
+
|
497 |
+
dept_counts = filtered_df["القسم"].value_counts().reset_index()
|
498 |
+
dept_counts.columns = ["القسم", "العدد"]
|
499 |
+
|
500 |
+
fig = px.bar(
|
501 |
+
dept_counts,
|
502 |
+
x="القسم",
|
503 |
+
y="العدد",
|
504 |
+
title="توزيع الموظفين حسب القسم",
|
505 |
+
color="القسم",
|
506 |
+
text_auto=True
|
507 |
+
)
|
508 |
+
|
509 |
+
st.plotly_chart(fig, use_container_width=True)
|
510 |
+
|
511 |
+
# عرض توزيع الموظفين حسب المنصب
|
512 |
+
st.markdown("#### توزيع الموظفين حسب المنصب")
|
513 |
+
|
514 |
+
position_counts = filtered_df["المنصب"].value_counts().reset_index()
|
515 |
+
position_counts.columns = ["المنصب", "العدد"]
|
516 |
+
|
517 |
+
fig = px.pie(
|
518 |
+
position_counts,
|
519 |
+
values="العدد",
|
520 |
+
names="المنصب",
|
521 |
+
title="توزيع الموظفين حسب المنصب",
|
522 |
+
color="المنصب"
|
523 |
+
)
|
524 |
+
|
525 |
+
st.plotly_chart(fig, use_container_width=True)
|
526 |
+
|
527 |
+
# عرض توزيع الموظفين حسب سنوات الخبرة
|
528 |
+
st.markdown("#### توزيع الموظفين حسب سنوات الخبرة")
|
529 |
+
|
530 |
+
# إنشاء فئات لسنوات الخبرة
|
531 |
+
experience_bins = [0, 3, 5, 10, 15, 20]
|
532 |
+
experience_labels = ["أقل من 3 سنوات", "3-5 سنوات", "6-10 سنوات", "11-15 سنة", "أكثر من 15 سنة"]
|
533 |
+
|
534 |
+
filtered_df["فئة الخبرة"] = pd.cut(filtered_df["سنوات الخبرة"], bins=experience_bins, labels=experience_labels, right=False)
|
535 |
+
|
536 |
+
experience_counts = filtered_df["فئة الخبرة"].value_counts().reset_index()
|
537 |
+
experience_counts.columns = ["فئة الخبرة", "العدد"]
|
538 |
+
|
539 |
+
fig = px.bar(
|
540 |
+
experience_counts,
|
541 |
+
x="فئة الخبرة",
|
542 |
+
y="العدد",
|
543 |
+
title="توزيع الموظفين حسب سنوات الخبرة",
|
544 |
+
color="فئة الخبرة",
|
545 |
+
text_auto=True
|
546 |
+
)
|
547 |
+
|
548 |
+
st.plotly_chart(fig, use_container_width=True)
|
549 |
+
|
550 |
+
# عرض توزيع المهارات
|
551 |
+
st.markdown("#### توزيع المهارات")
|
552 |
+
|
553 |
+
# استخراج جميع المهارات
|
554 |
+
all_skills = []
|
555 |
+
for skills_list in filtered_df["المهارات"]:
|
556 |
+
all_skills.extend(skills_list)
|
557 |
+
|
558 |
+
skill_counts = pd.Series(all_skills).value_counts().reset_index()
|
559 |
+
skill_counts.columns = ["المهارة", "العدد"]
|
560 |
+
|
561 |
+
fig = px.bar(
|
562 |
+
skill_counts,
|
563 |
+
x="المهارة",
|
564 |
+
y="العدد",
|
565 |
+
title="توزيع المهارات",
|
566 |
+
color="المهارة",
|
567 |
+
text_auto=True
|
568 |
+
)
|
569 |
+
|
570 |
+
st.plotly_chart(fig, use_container_width=True)
|
571 |
+
|
572 |
+
# عرض العلاقة بين سنوات الخبرة والتكلفة
|
573 |
+
st.markdown("#### العلاقة بين سنوات الخبرة والتكلفة")
|
574 |
+
|
575 |
+
fig = px.scatter(
|
576 |
+
filtered_df,
|
577 |
+
x="سنوات الخبرة",
|
578 |
+
y="التكلفة الشهرية",
|
579 |
+
color="القسم",
|
580 |
+
size="التقييم",
|
581 |
+
hover_name="اسم الموظف",
|
582 |
+
hover_data=["المنصب", "متاح"],
|
583 |
+
title="العلاقة بين سنوات الخبرة والتكلفة الشهرية"
|
584 |
+
)
|
585 |
+
|
586 |
+
st.plotly_chart(fig, use_container_width=True)
|
587 |
+
|
588 |
+
# إضافة موظف جديد
|
589 |
+
st.markdown("#### إضافة موظف جديد")
|
590 |
+
|
591 |
+
with st.form("add_employee_form"):
|
592 |
+
col1, col2 = st.columns(2)
|
593 |
+
|
594 |
+
with col1:
|
595 |
+
new_employee_name = st.text_input("اسم الموظف")
|
596 |
+
new_employee_department = st.selectbox("القسم", options=employees_df["القسم"].unique())
|
597 |
+
new_employee_position = st.selectbox("المنصب", options=employees_df["المنصب"].unique())
|
598 |
+
new_employee_experience = st.number_input("سنوات الخبرة", min_value=0, max_value=40, value=5)
|
599 |
+
|
600 |
+
with col2:
|
601 |
+
new_employee_skills = st.multiselect(
|
602 |
+
"المهارات",
|
603 |
+
options=["إدارة المشاريع", "التصميم الهندسي", "تحليل البيانات", "إدارة العقود", "التخطيط الاستراتيجي", "إدارة الموارد", "إدارة المخاطر", "إدارة الجودة", "إدارة التكاليف", "إدارة الوقت"]
|
604 |
+
)
|
605 |
+
new_employee_cost = st.number_input("التكلفة الشهرية", min_value=3000, max_value=50000, value=10000)
|
606 |
+
new_employee_available = st.checkbox("متاح", value=True)
|
607 |
+
new_employee_rating = st.slider("التقييم", min_value=1.0, max_value=5.0, value=4.0, step=0.1)
|
608 |
+
|
609 |
+
submit_button = st.form_submit_button("إضافة موظف")
|
610 |
+
|
611 |
+
if submit_button:
|
612 |
+
if new_employee_name:
|
613 |
+
# إنشاء رقم موظف جديد
|
614 |
+
new_employee_id = f"EMP-{len(employees_df) + 1:03d}"
|
615 |
+
|
616 |
+
# إضافة الموظف الجديد
|
617 |
+
new_employee = pd.DataFrame({
|
618 |
+
"رقم الموظف": [new_employee_id],
|
619 |
+
"اسم الموظف": [new_employee_name],
|
620 |
+
"القسم": [new_employee_department],
|
621 |
+
"المنصب": [new_employee_position],
|
622 |
+
"المهارات": [new_employee_skills],
|
623 |
+
"سنوات الخبرة": [new_employee_experience],
|
624 |
+
"التكلفة الشهرية": [new_employee_cost],
|
625 |
+
"متاح": [new_employee_available],
|
626 |
+
"التقييم": [new_employee_rating]
|
627 |
+
})
|
628 |
+
|
629 |
+
# تحديث DataFrame الموظفين
|
630 |
+
st.session_state.resources_data["employees"] = pd.concat([employees_df, new_employee], ignore_index=True)
|
631 |
+
|
632 |
+
st.success(f"تم إضافة الموظف {new_employee_name} بنجاح!")
|
633 |
+
st.rerun()
|
634 |
+
else:
|
635 |
+
st.error("يرجى إدخال اسم الموظف")
|
636 |
+
|
637 |
+
def _render_equipment_tab(self):
|
638 |
+
"""عرض تبويب المعدات"""
|
639 |
+
|
640 |
+
st.markdown("### إدارة المعدات")
|
641 |
+
|
642 |
+
# استخراج البيانات
|
643 |
+
equipment_df = st.session_state.resources_data["equipment"]
|
644 |
+
|
645 |
+
# عرض خيارات التصفية
|
646 |
+
st.markdown("#### خيارات التصفية")
|
647 |
+
|
648 |
+
col1, col2, col3 = st.columns(3)
|
649 |
+
|
650 |
+
with col1:
|
651 |
+
selected_types = st.multiselect(
|
652 |
+
"النوع",
|
653 |
+
options=equipment_df["النوع"].unique(),
|
654 |
+
default=equipment_df["النوع"].unique()
|
655 |
+
)
|
656 |
+
|
657 |
+
with col2:
|
658 |
+
selected_conditions = st.multiselect(
|
659 |
+
"الحالة",
|
660 |
+
options=equipment_df["الحالة"].unique(),
|
661 |
+
default=equipment_df["الحالة"].unique()
|
662 |
+
)
|
663 |
+
|
664 |
+
with col3:
|
665 |
+
availability_filter = st.selectbox(
|
666 |
+
"الإتاحة",
|
667 |
+
options=["الكل", "متاحة فقط", "غير متاحة فقط"],
|
668 |
+
key="equipment_availability"
|
669 |
+
)
|
670 |
+
|
671 |
+
# تطبيق التصفية
|
672 |
+
filtered_df = equipment_df[
|
673 |
+
equipment_df["النوع"].isin(selected_types) &
|
674 |
+
equipment_df["الحالة"].isin(selected_conditions)
|
675 |
+
]
|
676 |
+
|
677 |
+
if availability_filter == "متاحة فقط":
|
678 |
+
filtered_df = filtered_df[filtered_df["متاحة"] == True]
|
679 |
+
elif availability_filter == "غير متاحة فقط":
|
680 |
+
filtered_df = filtered_df[filtered_df["متاحة"] == False]
|
681 |
+
|
682 |
+
# عرض البيانات المصفاة
|
683 |
+
st.markdown("#### قائمة المعدات")
|
684 |
+
|
685 |
+
st.dataframe(
|
686 |
+
filtered_df,
|
687 |
+
column_config={
|
688 |
+
"رقم المعدة": st.column_config.TextColumn("رقم المعدة"),
|
689 |
+
"اسم المعدة": st.column_config.TextColumn("اسم المعدة"),
|
690 |
+
"النوع": st.column_config.TextColumn("النوع"),
|
691 |
+
"التكلفة اليومية": st.column_config.NumberColumn("التكلفة اليومية", format="%.2f ريال"),
|
692 |
+
"متاحة": st.column_config.CheckboxColumn("متاحة"),
|
693 |
+
"الحالة": st.column_config.TextColumn("الحالة"),
|
694 |
+
"الموقع": st.column_config.TextColumn("الموقع")
|
695 |
+
},
|
696 |
+
use_container_width=True,
|
697 |
+
hide_index=True
|
698 |
+
)
|
699 |
+
|
700 |
+
# عرض إحصائيات المعدات
|
701 |
+
st.markdown("#### إحصائيات المعدات")
|
702 |
+
|
703 |
+
col1, col2, col3, col4 = st.columns(4)
|
704 |
+
|
705 |
+
with col1:
|
706 |
+
total_equipment = len(filtered_df)
|
707 |
+
st.metric("إجمالي المعدات", f"{total_equipment}")
|
708 |
+
|
709 |
+
with col2:
|
710 |
+
available_equipment = len(filtered_df[filtered_df["متاحة"] == True])
|
711 |
+
availability_rate = available_equipment / total_equipment * 100 if total_equipment > 0 else 0
|
712 |
+
st.metric("معدل الإتاحة", f"{availability_rate:.1f}%")
|
713 |
+
|
714 |
+
with col3:
|
715 |
+
good_condition = len(filtered_df[filtered_df["الحالة"].isin(["ممتاز", "جيد"])])
|
716 |
+
good_condition_rate = good_condition / total_equipment * 100 if total_equipment > 0 else 0
|
717 |
+
st.metric("معدل الحالة الجيدة", f"{good_condition_rate:.1f}%")
|
718 |
+
|
719 |
+
with col4:
|
720 |
+
avg_cost = filtered_df["التكلفة اليومية"].mean()
|
721 |
+
st.metric("متوسط التكلفة اليومية", f"{avg_cost:.0f} ريال")
|
722 |
+
|
723 |
+
# عرض توزيع المعدات حسب النوع
|
724 |
+
st.markdown("#### توزيع المعدات حسب النوع")
|
725 |
+
|
726 |
+
type_counts = filtered_df["النوع"].value_counts().reset_index()
|
727 |
+
type_counts.columns = ["النوع", "العدد"]
|
728 |
+
|
729 |
+
fig = px.bar(
|
730 |
+
type_counts,
|
731 |
+
x="النوع",
|
732 |
+
y="العدد",
|
733 |
+
title="توزيع المعدات حسب النوع",
|
734 |
+
color="النوع",
|
735 |
+
text_auto=True
|
736 |
+
)
|
737 |
+
|
738 |
+
st.plotly_chart(fig, use_container_width=True)
|
739 |
+
|
740 |
+
# عرض توزيع المعدات حسب الحالة
|
741 |
+
st.markdown("#### توزيع المعدات حسب الحالة")
|
742 |
+
|
743 |
+
condition_counts = filtered_df["الحالة"].value_counts().reset_index()
|
744 |
+
condition_counts.columns = ["الحالة", "العدد"]
|
745 |
+
|
746 |
+
fig = px.pie(
|
747 |
+
condition_counts,
|
748 |
+
values="العدد",
|
749 |
+
names="الحالة",
|
750 |
+
title="توزيع المعدات حسب الحالة",
|
751 |
+
color="الحالة",
|
752 |
+
color_discrete_map={
|
753 |
+
"ممتاز": "#2ecc71",
|
754 |
+
"جيد": "#3498db",
|
755 |
+
"متوسط": "#f39c12",
|
756 |
+
"سيء": "#e74c3c"
|
757 |
+
}
|
758 |
+
)
|
759 |
+
|
760 |
+
st.plotly_chart(fig, use_container_width=True)
|
761 |
+
|
762 |
+
# عرض توزيع المعدات حسب الموقع
|
763 |
+
st.markdown("#### توزيع المعدات حسب الموقع")
|
764 |
+
|
765 |
+
location_counts = filtered_df["الموقع"].value_counts().reset_index()
|
766 |
+
location_counts.columns = ["الموقع", "العدد"]
|
767 |
+
|
768 |
+
fig = px.bar(
|
769 |
+
location_counts,
|
770 |
+
x="الموقع",
|
771 |
+
y="العدد",
|
772 |
+
title="توزيع المعدات حسب الموقع",
|
773 |
+
color="الموقع",
|
774 |
+
text_auto=True
|
775 |
+
)
|
776 |
+
|
777 |
+
st.plotly_chart(fig, use_container_width=True)
|
778 |
+
|
779 |
+
# عرض العلاقة بين نوع المعدة والتكلفة
|
780 |
+
st.markdown("#### العلاقة بين نوع المعدة والتكلفة")
|
781 |
+
|
782 |
+
type_cost = filtered_df.groupby("النوع")["التكلفة اليومية"].mean().reset_index()
|
783 |
+
type_cost.columns = ["النوع", "متوسط التكلفة اليومية"]
|
784 |
+
|
785 |
+
fig = px.bar(
|
786 |
+
type_cost,
|
787 |
+
x="النوع",
|
788 |
+
y="متوسط التكلفة اليومية",
|
789 |
+
title="متوسط التكلفة اليومية حسب نوع المعدة",
|
790 |
+
color="النوع",
|
791 |
+
text_auto=".0f"
|
792 |
+
)
|
793 |
+
|
794 |
+
st.plotly_chart(fig, use_container_width=True)
|
795 |
+
|
796 |
+
# إضافة معدة جديدة
|
797 |
+
st.markdown("#### إضافة معدة جديدة")
|
798 |
+
|
799 |
+
with st.form("add_equipment_form"):
|
800 |
+
col1, col2 = st.columns(2)
|
801 |
+
|
802 |
+
with col1:
|
803 |
+
new_equipment_name = st.text_input("اسم المعدة")
|
804 |
+
new_equipment_type = st.selectbox("النوع", options=equipment_df["النوع"].unique())
|
805 |
+
new_equipment_cost = st.number_input("التكلفة اليومية", min_value=100, max_value=10000, value=1000)
|
806 |
+
|
807 |
+
with col2:
|
808 |
+
new_equipment_available = st.checkbox("متاحة", value=True)
|
809 |
+
new_equipment_condition = st.selectbox("الحالة", options=["ممتاز", "جيد", "متوسط", "سيء"])
|
810 |
+
new_equipment_location = st.selectbox("الموقع", options=equipment_df["الموقع"].unique())
|
811 |
+
|
812 |
+
submit_button = st.form_submit_button("إضافة معدة")
|
813 |
+
|
814 |
+
if submit_button:
|
815 |
+
if new_equipment_name:
|
816 |
+
# إنشاء رقم معدة جديد
|
817 |
+
new_equipment_id = f"EQ-{len(equipment_df) + 1:03d}"
|
818 |
+
|
819 |
+
# إضافة المعدة الجديدة
|
820 |
+
new_equipment = pd.DataFrame({
|
821 |
+
"رقم المعدة": [new_equipment_id],
|
822 |
+
"اسم المعدة": [new_equipment_name],
|
823 |
+
"النوع": [new_equipment_type],
|
824 |
+
"التكلفة اليومية": [new_equipment_cost],
|
825 |
+
"متاحة": [new_equipment_available],
|
826 |
+
"الحالة": [new_equipment_condition],
|
827 |
+
"الموقع": [new_equipment_location]
|
828 |
+
})
|
829 |
+
|
830 |
+
# تحديث DataFrame المعدات
|
831 |
+
st.session_state.resources_data["equipment"] = pd.concat([equipment_df, new_equipment], ignore_index=True)
|
832 |
+
|
833 |
+
st.success(f"تم إضافة المعدة {new_equipment_name} ب��جاح!")
|
834 |
+
st.rerun()
|
835 |
+
else:
|
836 |
+
st.error("يرجى إدخال اسم المعدة")
|
837 |
+
|
838 |
+
def _render_materials_tab(self):
|
839 |
+
"""عرض تبويب المواد"""
|
840 |
+
|
841 |
+
st.markdown("### إدارة المواد")
|
842 |
+
|
843 |
+
# استخراج البيانات
|
844 |
+
materials_df = st.session_state.resources_data["materials"]
|
845 |
+
|
846 |
+
# عرض خيارات التصفية
|
847 |
+
st.markdown("#### خيارات التصفية")
|
848 |
+
|
849 |
+
col1, col2, col3 = st.columns(3)
|
850 |
+
|
851 |
+
with col1:
|
852 |
+
selected_units = st.multiselect(
|
853 |
+
"الوحدة",
|
854 |
+
options=materials_df["الوحدة"].unique(),
|
855 |
+
default=materials_df["الوحدة"].unique()
|
856 |
+
)
|
857 |
+
|
858 |
+
with col2:
|
859 |
+
selected_suppliers = st.multiselect(
|
860 |
+
"المورد",
|
861 |
+
options=materials_df["المورد"].unique(),
|
862 |
+
default=materials_df["المورد"].unique()
|
863 |
+
)
|
864 |
+
|
865 |
+
with col3:
|
866 |
+
stock_filter = st.selectbox(
|
867 |
+
"المخزون",
|
868 |
+
options=["الكل", "منخفض المخزون", "مخزون كافي"]
|
869 |
+
)
|
870 |
+
|
871 |
+
# تطبيق التصفية
|
872 |
+
filtered_df = materials_df[
|
873 |
+
materials_df["الوحدة"].isin(selected_units) &
|
874 |
+
materials_df["المورد"].isin(selected_suppliers)
|
875 |
+
]
|
876 |
+
|
877 |
+
if stock_filter == "منخفض المخزون":
|
878 |
+
filtered_df = filtered_df[filtered_df["الكمية المتاحة"] < 50]
|
879 |
+
elif stock_filter == "مخزون كافي":
|
880 |
+
filtered_df = filtered_df[filtered_df["الكمية المتاحة"] >= 50]
|
881 |
+
|
882 |
+
# عرض البيانات المصفاة
|
883 |
+
st.markdown("#### قائمة المواد")
|
884 |
+
|
885 |
+
st.dataframe(
|
886 |
+
filtered_df,
|
887 |
+
column_config={
|
888 |
+
"رقم المادة": st.column_config.TextColumn("رقم المادة"),
|
889 |
+
"اسم المادة": st.column_config.TextColumn("اسم المادة"),
|
890 |
+
"الوحدة": st.column_config.TextColumn("الوحدة"),
|
891 |
+
"الكمية المتاحة": st.column_config.NumberColumn("الكمية المتاحة"),
|
892 |
+
"تكلفة الوحدة": st.column_config.NumberColumn("تكلفة الوحدة", format="%.2f ريال"),
|
893 |
+
"المورد": st.column_config.TextColumn("المورد"),
|
894 |
+
"مدة التوريد (يوم)": st.column_config.NumberColumn("مدة التوريد (يوم)")
|
895 |
+
},
|
896 |
+
use_container_width=True,
|
897 |
+
hide_index=True
|
898 |
+
)
|
899 |
+
|
900 |
+
# عرض إحصائيات المواد
|
901 |
+
st.markdown("#### إحصائيات المواد")
|
902 |
+
|
903 |
+
col1, col2, col3, col4 = st.columns(4)
|
904 |
+
|
905 |
+
with col1:
|
906 |
+
total_materials = len(filtered_df)
|
907 |
+
st.metric("إجمالي المواد", f"{total_materials}")
|
908 |
+
|
909 |
+
with col2:
|
910 |
+
low_stock_materials = len(filtered_df[filtered_df["الكمية المتاحة"] < 50])
|
911 |
+
low_stock_rate = low_stock_materials / total_materials * 100 if total_materials > 0 else 0
|
912 |
+
st.metric("نسبة المواد منخفضة المخزون", f"{low_stock_rate:.1f}%")
|
913 |
+
|
914 |
+
with col3:
|
915 |
+
avg_lead_time = filtered_df["مدة التوريد (يوم)"].mean()
|
916 |
+
st.metric("متوسط مدة التوريد", f"{avg_lead_time:.1f} يوم")
|
917 |
+
|
918 |
+
with col4:
|
919 |
+
total_inventory_value = (filtered_df["الكمية المتاحة"] * filtered_df["تكلفة الوحدة"]).sum()
|
920 |
+
st.metric("إجمالي قيمة المخزون", f"{total_inventory_value:,.0f} ريال")
|
921 |
+
|
922 |
+
# عرض توزيع المواد حسب المورد
|
923 |
+
st.markdown("#### توزيع المواد حسب المورد")
|
924 |
+
|
925 |
+
supplier_counts = filtered_df["المورد"].value_counts().reset_index()
|
926 |
+
supplier_counts.columns = ["المورد", "العدد"]
|
927 |
+
|
928 |
+
fig = px.pie(
|
929 |
+
supplier_counts,
|
930 |
+
values="العدد",
|
931 |
+
names="المورد",
|
932 |
+
title="توزيع المواد حسب المورد",
|
933 |
+
color="المورد"
|
934 |
+
)
|
935 |
+
|
936 |
+
st.plotly_chart(fig, use_container_width=True)
|
937 |
+
|
938 |
+
# عرض توزيع المواد حسب الوحدة
|
939 |
+
st.markdown("#### توزيع المواد حسب الوحدة")
|
940 |
+
|
941 |
+
unit_counts = filtered_df["الوحدة"].value_counts().reset_index()
|
942 |
+
unit_counts.columns = ["الوحدة", "العدد"]
|
943 |
+
|
944 |
+
fig = px.bar(
|
945 |
+
unit_counts,
|
946 |
+
x="الوحدة",
|
947 |
+
y="العدد",
|
948 |
+
title="توزيع المواد حسب الوحدة",
|
949 |
+
color="الوحدة",
|
950 |
+
text_auto=True
|
951 |
+
)
|
952 |
+
|
953 |
+
st.plotly_chart(fig, use_container_width=True)
|
954 |
+
|
955 |
+
# عرض المواد منخفضة المخزون
|
956 |
+
st.markdown("#### المواد منخفضة المخزون")
|
957 |
+
|
958 |
+
low_stock_df = filtered_df[filtered_df["الكمية المتاحة"] < 50].sort_values("الكمية المتاحة")
|
959 |
+
|
960 |
+
if not low_stock_df.empty:
|
961 |
+
fig = px.bar(
|
962 |
+
low_stock_df,
|
963 |
+
x="اسم المادة",
|
964 |
+
y="الكمية المتاحة",
|
965 |
+
title="المواد منخفضة المخزون",
|
966 |
+
color="الكمية المتاحة",
|
967 |
+
color_continuous_scale="Reds_r",
|
968 |
+
text_auto=True
|
969 |
+
)
|
970 |
+
|
971 |
+
st.plotly_chart(fig, use_container_width=True)
|
972 |
+
else:
|
973 |
+
st.info("لا توجد مواد منخفضة المخزون")
|
974 |
+
|
975 |
+
# عرض العلاقة بين مدة التوريد والمورد
|
976 |
+
st.markdown("#### العلاقة بين مدة التوريد والمورد")
|
977 |
+
|
978 |
+
supplier_lead_time = filtered_df.groupby("المورد")["مدة التوريد (يوم)"].mean().reset_index()
|
979 |
+
supplier_lead_time.columns = ["المورد", "متوسط مدة التوريد (يوم)"]
|
980 |
+
|
981 |
+
fig = px.bar(
|
982 |
+
supplier_lead_time,
|
983 |
+
x="المورد",
|
984 |
+
y="متوسط مدة التوريد (يوم)",
|
985 |
+
title="متوسط مدة التوريد حسب المورد",
|
986 |
+
color="المورد",
|
987 |
+
text_auto=".1f"
|
988 |
+
)
|
989 |
+
|
990 |
+
st.plotly_chart(fig, use_container_width=True)
|
991 |
+
|
992 |
+
# إضافة مادة جديدة
|
993 |
+
st.markdown("#### إضافة مادة جديدة")
|
994 |
+
|
995 |
+
with st.form("add_material_form"):
|
996 |
+
col1, col2 = st.columns(2)
|
997 |
+
|
998 |
+
with col1:
|
999 |
+
new_material_name = st.text_input("اسم المادة")
|
1000 |
+
new_material_unit = st.selectbox("الوحدة", options=materials_df["الوحدة"].unique())
|
1001 |
+
new_material_quantity = st.number_input("الكمية المتاحة", min_value=0, max_value=10000, value=100)
|
1002 |
+
|
1003 |
+
with col2:
|
1004 |
+
new_material_cost = st.number_input("تكلفة الوحدة", min_value=1, max_value=10000, value=100)
|
1005 |
+
new_material_supplier = st.selectbox("المورد", options=materials_df["المورد"].unique())
|
1006 |
+
new_material_lead_time = st.number_input("مدة التوريد (يوم)", min_value=1, max_value=90, value=7)
|
1007 |
+
|
1008 |
+
submit_button = st.form_submit_button("إضافة مادة")
|
1009 |
+
|
1010 |
+
if submit_button:
|
1011 |
+
if new_material_name:
|
1012 |
+
# إنشاء رقم مادة جديد
|
1013 |
+
new_material_id = f"MAT-{len(materials_df) + 1:03d}"
|
1014 |
+
|
1015 |
+
# إضافة المادة الجديدة
|
1016 |
+
new_material = pd.DataFrame({
|
1017 |
+
"رقم المادة": [new_material_id],
|
1018 |
+
"اسم المادة": [new_material_name],
|
1019 |
+
"الوحدة": [new_material_unit],
|
1020 |
+
"الكمية المتاحة": [new_material_quantity],
|
1021 |
+
"تكلفة الوحدة": [new_material_cost],
|
1022 |
+
"المورد": [new_material_supplier],
|
1023 |
+
"مدة التوريد (يوم)": [new_material_lead_time]
|
1024 |
+
})
|
1025 |
+
|
1026 |
+
# تحديث DataFrame المواد
|
1027 |
+
st.session_state.resources_data["materials"] = pd.concat([materials_df, new_material], ignore_index=True)
|
1028 |
+
|
1029 |
+
st.success(f"تم إضافة المادة {new_material_name} بنجاح!")
|
1030 |
+
st.rerun()
|
1031 |
+
else:
|
1032 |
+
st.error("يرجى إدخال اسم المادة")
|
1033 |
+
|
1034 |
+
# طلب مواد
|
1035 |
+
st.markdown("#### طلب مواد")
|
1036 |
+
|
1037 |
+
with st.form("order_materials_form"):
|
1038 |
+
col1, col2, col3 = st.columns(3)
|
1039 |
+
|
1040 |
+
with col1:
|
1041 |
+
material_to_order = st.selectbox("المادة", options=materials_df["اسم المادة"].unique())
|
1042 |
+
|
1043 |
+
with col2:
|
1044 |
+
order_quantity = st.number_input("الكمية المطلوبة", min_value=1, max_value=1000, value=50)
|
1045 |
+
|
1046 |
+
with col3:
|
1047 |
+
order_date = st.date_input("تاريخ الطلب", value=datetime.now())
|
1048 |
+
|
1049 |
+
submit_button = st.form_submit_button("طلب المادة")
|
1050 |
+
|
1051 |
+
if submit_button:
|
1052 |
+
# محاكاة طلب المادة
|
1053 |
+
st.success(f"تم طلب {order_quantity} {materials_df[materials_df['اسم المادة'] == material_to_order]['الوحدة'].values[0]} من {material_to_order} بنجاح!")
|
1054 |
+
|
1055 |
+
# عرض تفاصيل الطلب
|
1056 |
+
material_info = materials_df[materials_df["اسم المادة"] == material_to_order].iloc[0]
|
1057 |
+
lead_time = material_info["مدة التوريد (يوم)"]
|
1058 |
+
expected_delivery = order_date + timedelta(days=lead_time)
|
1059 |
+
|
1060 |
+
st.info(f"تاريخ التسليم المتوقع: {expected_delivery.strftime('%Y-%m-%d')}")
|
1061 |
+
|
1062 |
+
# حساب التكلفة الإجمالية
|
1063 |
+
unit_cost = material_info["تكلفة الوحدة"]
|
1064 |
+
total_cost = unit_cost * order_quantity
|
1065 |
+
|
1066 |
+
st.metric("التكلفة الإجمالية", f"{total_cost:,.2f} ريال")
|
1067 |
+
|
1068 |
+
def _render_resource_allocation_tab(self):
|
1069 |
+
"""عرض تبويب تخصيص الموارد"""
|
1070 |
+
|
1071 |
+
st.markdown("### تخصيص الموارد")
|
1072 |
+
|
1073 |
+
# استخراج البيانات
|
1074 |
+
employees_df = st.session_state.resources_data["employees"]
|
1075 |
+
equipment_df = st.session_state.resources_data["equipment"]
|
1076 |
+
materials_df = st.session_state.resources_data["materials"]
|
1077 |
+
projects_df = st.session_state.resources_data["projects"]
|
1078 |
+
allocations_df = st.session_state.resources_data["allocations"]
|
1079 |
+
|
1080 |
+
# عرض خيارات التصفية
|
1081 |
+
st.markdown("#### خيارات التصفية")
|
1082 |
+
|
1083 |
+
col1, col2 = st.columns(2)
|
1084 |
+
|
1085 |
+
with col1:
|
1086 |
+
selected_projects = st.multiselect(
|
1087 |
+
"المشروع",
|
1088 |
+
options=projects_df["اسم المشروع"].unique(),
|
1089 |
+
default=projects_df["اسم المشروع"].unique()
|
1090 |
+
)
|
1091 |
+
|
1092 |
+
with col2:
|
1093 |
+
selected_resource_types = st.multiselect(
|
1094 |
+
"نوع المورد",
|
1095 |
+
options=allocations_df["نوع المورد"].unique(),
|
1096 |
+
default=allocations_df["نوع المورد"].unique()
|
1097 |
+
)
|
1098 |
+
|
1099 |
+
# تحويل أسماء المشاريع إلى أرقام المشاريع
|
1100 |
+
selected_project_ids = projects_df[projects_df["اسم المشروع"].isin(selected_projects)]["رقم المشروع"].tolist()
|
1101 |
+
|
1102 |
+
# تطبيق التصفية
|
1103 |
+
filtered_df = allocations_df[
|
1104 |
+
allocations_df["رقم المشروع"].isin(selected_project_ids) &
|
1105 |
+
allocations_df["نوع المورد"].isin(selected_resource_types)
|
1106 |
+
]
|
1107 |
+
|
1108 |
+
# دمج البيانات مع بيانات المشاريع
|
1109 |
+
merged_df = filtered_df.merge(
|
1110 |
+
projects_df[["رقم المشروع", "اسم المشروع"]],
|
1111 |
+
on="رقم المشروع",
|
1112 |
+
how="left"
|
1113 |
+
)
|
1114 |
+
|
1115 |
+
# إضافة أسماء الموارد
|
1116 |
+
merged_df["اسم المورد"] = ""
|
1117 |
+
|
1118 |
+
for i, row in merged_df.iterrows():
|
1119 |
+
if row["نوع المورد"] == "موظف":
|
1120 |
+
resource_name = employees_df[employees_df["رقم الموظف"] == row["رقم المورد"]]["اسم الموظف"].values
|
1121 |
+
if len(resource_name) > 0:
|
1122 |
+
merged_df.at[i, "اسم المورد"] = resource_name[0]
|
1123 |
+
elif row["نوع المورد"] == "معدة":
|
1124 |
+
resource_name = equipment_df[equipment_df["رقم المعدة"] == row["رقم المورد"]]["اسم المعدة"].values
|
1125 |
+
if len(resource_name) > 0:
|
1126 |
+
merged_df.at[i, "اسم المورد"] = resource_name[0]
|
1127 |
+
elif row["نوع المورد"] == "مادة":
|
1128 |
+
resource_name = materials_df[materials_df["رقم المادة"] == row["رقم المورد"]]["اسم المادة"].values
|
1129 |
+
if len(resource_name) > 0:
|
1130 |
+
merged_df.at[i, "اسم المورد"] = resource_name[0]
|
1131 |
+
|
1132 |
+
# عرض البيانات المصفاة
|
1133 |
+
st.markdown("#### قائمة تخصيص الموارد")
|
1134 |
+
|
1135 |
+
display_df = merged_df[["رقم التخصيص", "اسم المشروع", "نوع المورد", "اسم المورد", "تاريخ البدء", "تاريخ الانتهاء", "الكمية", "التكلفة"]]
|
1136 |
+
|
1137 |
+
st.dataframe(
|
1138 |
+
display_df,
|
1139 |
+
column_config={
|
1140 |
+
"رقم التخصيص": st.column_config.TextColumn("رقم التخصيص"),
|
1141 |
+
"اسم المشروع": st.column_config.TextColumn("اسم المشروع"),
|
1142 |
+
"نوع المورد": st.column_config.TextColumn("نوع المورد"),
|
1143 |
+
"اسم المورد": st.column_config.TextColumn("اسم المورد"),
|
1144 |
+
"تاريخ البدء": st.column_config.DateColumn("تاريخ البدء"),
|
1145 |
+
"تاريخ الانتهاء": st.column_config.DateColumn("تاريخ الانتهاء"),
|
1146 |
+
"الكمية": st.column_config.NumberColumn("الكمية"),
|
1147 |
+
"التكلفة": st.column_config.NumberColumn("التكلفة", format="%.2f ريال")
|
1148 |
+
},
|
1149 |
+
use_container_width=True,
|
1150 |
+
hide_index=True
|
1151 |
+
)
|
1152 |
+
|
1153 |
+
# عرض إحصائيات تخصيص الموارد
|
1154 |
+
st.markdown("#### إحصائيات تخصيص الموارد")
|
1155 |
+
|
1156 |
+
col1, col2, col3, col4 = st.columns(4)
|
1157 |
+
|
1158 |
+
with col1:
|
1159 |
+
total_allocations = len(merged_df)
|
1160 |
+
st.metric("إجمالي التخصيصات", f"{total_allocations}")
|
1161 |
+
|
1162 |
+
with col2:
|
1163 |
+
total_cost = merged_df["التكلفة"].sum()
|
1164 |
+
st.metric("إجمالي التكلفة", f"{total_cost:,.0f} ريال")
|
1165 |
+
|
1166 |
+
with col3:
|
1167 |
+
avg_duration = (pd.to_datetime(merged_df["تاريخ الانتهاء"]) - pd.to_datetime(merged_df["تاريخ البدء"])).mean().days
|
1168 |
+
st.metric("متوسط مدة التخصيص", f"{avg_duration:.0f} يوم")
|
1169 |
+
|
1170 |
+
with col4:
|
1171 |
+
resource_types = merged_df["نوع المورد"].value_counts()
|
1172 |
+
most_common_type = resource_types.index[0] if not resource_types.empty else ""
|
1173 |
+
st.metric("أكثر أنواع الموارد تخصيصاً", f"{most_common_type}")
|
1174 |
+
|
1175 |
+
# عرض توزيع تخصيص الموارد حسب المشروع
|
1176 |
+
st.markdown("#### توزيع تخصيص الموارد حسب المشروع")
|
1177 |
+
|
1178 |
+
project_allocations = merged_df.groupby("اسم المشروع").size().reset_index()
|
1179 |
+
project_allocations.columns = ["اسم المشروع", "عدد التخصيصات"]
|
1180 |
+
|
1181 |
+
fig = px.bar(
|
1182 |
+
project_allocations,
|
1183 |
+
x="اسم المشروع",
|
1184 |
+
y="عدد التخصيصات",
|
1185 |
+
title="توزيع تخصيص الموارد حسب المشروع",
|
1186 |
+
color="اسم المشروع",
|
1187 |
+
text_auto=True
|
1188 |
+
)
|
1189 |
+
|
1190 |
+
st.plotly_chart(fig, use_container_width=True)
|
1191 |
+
|
1192 |
+
# عرض توزيع تخصيص الموارد حسب نوع المورد
|
1193 |
+
st.markdown("#### توزيع تخصيص الموارد حسب نوع المورد")
|
1194 |
+
|
1195 |
+
resource_type_allocations = merged_df.groupby("نوع المورد").size().reset_index()
|
1196 |
+
resource_type_allocations.columns = ["نوع المورد", "عدد التخصيصات"]
|
1197 |
+
|
1198 |
+
fig = px.pie(
|
1199 |
+
resource_type_allocations,
|
1200 |
+
values="عدد التخصيصات",
|
1201 |
+
names="نوع المورد",
|
1202 |
+
title="توزيع تخصيص الموارد حسب نوع المورد",
|
1203 |
+
color="نوع المورد",
|
1204 |
+
color_discrete_map={
|
1205 |
+
"موظف": "#3498db",
|
1206 |
+
"معدة": "#2ecc71",
|
1207 |
+
"مادة": "#f39c12"
|
1208 |
+
}
|
1209 |
+
)
|
1210 |
+
|
1211 |
+
st.plotly_chart(fig, use_container_width=True)
|
1212 |
+
|
1213 |
+
# عرض توزيع تكاليف الموارد حسب المشروع
|
1214 |
+
st.markdown("#### توزيع تكاليف الموارد حسب المشروع")
|
1215 |
+
|
1216 |
+
project_costs = merged_df.groupby("اسم المشروع")["التكلفة"].sum().reset_index()
|
1217 |
+
project_costs.columns = ["اسم المشروع", "إجمالي التكلفة"]
|
1218 |
+
|
1219 |
+
fig = px.bar(
|
1220 |
+
project_costs,
|
1221 |
+
x="اسم المشروع",
|
1222 |
+
y="إجمالي التكلفة",
|
1223 |
+
title="توزيع تكاليف الموارد حسب المشروع",
|
1224 |
+
color="اسم المشروع",
|
1225 |
+
text_auto=".0f"
|
1226 |
+
)
|
1227 |
+
|
1228 |
+
st.plotly_chart(fig, use_container_width=True)
|
1229 |
+
|
1230 |
+
# عرض توزيع تكاليف الموارد حسب نوع المورد
|
1231 |
+
st.markdown("#### توزيع تكاليف الموارد حسب نوع المورد")
|
1232 |
+
|
1233 |
+
resource_type_costs = merged_df.groupby("نوع المورد")["التكلفة"].sum().reset_index()
|
1234 |
+
resource_type_costs.columns = ["نوع المورد", "إجمالي التكلفة"]
|
1235 |
+
|
1236 |
+
fig = px.pie(
|
1237 |
+
resource_type_costs,
|
1238 |
+
values="إجمالي التكلفة",
|
1239 |
+
names="نوع المورد",
|
1240 |
+
title="توزيع تكاليف الموارد حسب نوع المورد",
|
1241 |
+
color="نوع المورد",
|
1242 |
+
color_discrete_map={
|
1243 |
+
"موظف": "#3498db",
|
1244 |
+
"معدة": "#2ecc71",
|
1245 |
+
"مادة": "#f39c12"
|
1246 |
+
}
|
1247 |
+
)
|
1248 |
+
|
1249 |
+
st.plotly_chart(fig, use_container_width=True)
|
1250 |
+
|
1251 |
+
# إضافة تخصيص جديد
|
1252 |
+
st.markdown("#### إضافة تخصيص جديد")
|
1253 |
+
|
1254 |
+
with st.form("add_allocation_form"):
|
1255 |
+
col1, col2 = st.columns(2)
|
1256 |
+
|
1257 |
+
with col1:
|
1258 |
+
new_allocation_project = st.selectbox("المشروع", options=projects_df["اسم المشروع"].unique())
|
1259 |
+
new_allocation_resource_type = st.selectbox("نوع المورد", options=["موظف", "معدة", "ما��ة"])
|
1260 |
+
|
1261 |
+
# تحديد خيارات الموارد بناءً على النوع
|
1262 |
+
if new_allocation_resource_type == "موظف":
|
1263 |
+
resource_options = employees_df[employees_df["متاح"] == True]["اسم الموظف"].unique()
|
1264 |
+
elif new_allocation_resource_type == "معدة":
|
1265 |
+
resource_options = equipment_df[equipment_df["متاحة"] == True]["اسم المعدة"].unique()
|
1266 |
+
else:
|
1267 |
+
resource_options = materials_df["اسم المادة"].unique()
|
1268 |
+
|
1269 |
+
new_allocation_resource = st.selectbox("المورد", options=resource_options)
|
1270 |
+
|
1271 |
+
with col2:
|
1272 |
+
new_allocation_start_date = st.date_input("تاريخ البدء", value=datetime.now())
|
1273 |
+
new_allocation_end_date = st.date_input("تاريخ الانتهاء", value=datetime.now() + timedelta(days=30))
|
1274 |
+
new_allocation_quantity = st.number_input("الكمية", min_value=1, max_value=100, value=1)
|
1275 |
+
|
1276 |
+
submit_button = st.form_submit_button("إضافة تخصيص")
|
1277 |
+
|
1278 |
+
if submit_button:
|
1279 |
+
# التحقق من صحة التواريخ
|
1280 |
+
if new_allocation_end_date <= new_allocation_start_date:
|
1281 |
+
st.error("يجب أن يكون تاريخ الانتهاء بعد تاريخ البدء")
|
1282 |
+
else:
|
1283 |
+
# الحصول على رقم المشروع
|
1284 |
+
project_id = projects_df[projects_df["اسم المشروع"] == new_allocation_project]["رقم المشروع"].values[0]
|
1285 |
+
|
1286 |
+
# الحصول على رقم المورد
|
1287 |
+
if new_allocation_resource_type == "موظف":
|
1288 |
+
resource_id = employees_df[employees_df["اسم الموظف"] == new_allocation_resource]["رقم الموظف"].values[0]
|
1289 |
+
# حساب التكلفة
|
1290 |
+
cost = employees_df[employees_df["رقم الموظف"] == resource_id]["التكلفة الشهرية"].values[0] * new_allocation_quantity
|
1291 |
+
elif new_allocation_resource_type == "معدة":
|
1292 |
+
resource_id = equipment_df[equipment_df["اسم المعدة"] == new_allocation_resource]["رقم المعدة"].values[0]
|
1293 |
+
# حساب التكلفة
|
1294 |
+
days = (new_allocation_end_date - new_allocation_start_date).days
|
1295 |
+
cost = equipment_df[equipment_df["رقم المعدة"] == resource_id]["التكلفة اليومية"].values[0] * days * new_allocation_quantity
|
1296 |
+
else:
|
1297 |
+
resource_id = materials_df[materials_df["اسم المادة"] == new_allocation_resource]["رقم المادة"].values[0]
|
1298 |
+
# حساب التكلفة
|
1299 |
+
cost = materials_df[materials_df["رقم المادة"] == resource_id]["تكلفة الوحدة"].values[0] * new_allocation_quantity
|
1300 |
+
|
1301 |
+
# إنشاء رقم تخصيص جديد
|
1302 |
+
new_allocation_id = f"ALLOC-{len(allocations_df) + 1:03d}"
|
1303 |
+
|
1304 |
+
# إضافة التخصيص الجديد
|
1305 |
+
new_allocation = pd.DataFrame({
|
1306 |
+
"رقم التخصيص": [new_allocation_id],
|
1307 |
+
"رقم المشروع": [project_id],
|
1308 |
+
"نوع المورد": [new_allocation_resource_type],
|
1309 |
+
"رقم المورد": [resource_id],
|
1310 |
+
"تاريخ البدء": [new_allocation_start_date.strftime("%Y-%m-%d")],
|
1311 |
+
"تاريخ الانتهاء": [new_allocation_end_date.strftime("%Y-%m-%d")],
|
1312 |
+
"الكمية": [new_allocation_quantity],
|
1313 |
+
"التكلفة": [cost]
|
1314 |
+
})
|
1315 |
+
|
1316 |
+
# تحديث DataFrame التخصيصات
|
1317 |
+
st.session_state.resources_data["allocations"] = pd.concat([allocations_df, new_allocation], ignore_index=True)
|
1318 |
+
|
1319 |
+
# تحديث حالة المورد إذا كان موظف أو معدة
|
1320 |
+
if new_allocation_resource_type == "موظف":
|
1321 |
+
employees_idx = employees_df[employees_df["رقم الموظف"] == resource_id].index
|
1322 |
+
st.session_state.resources_data["employees"].at[employees_idx[0], "متاح"] = False
|
1323 |
+
elif new_allocation_resource_type == "معدة":
|
1324 |
+
equipment_idx = equipment_df[equipment_df["رقم المعدة"] == resource_id].index
|
1325 |
+
st.session_state.resources_data["equipment"].at[equipment_idx[0], "متاحة"] = False
|
1326 |
+
elif new_allocation_resource_type == "مادة":
|
1327 |
+
materials_idx = materials_df[materials_df["رقم المادة"] == resource_id].index
|
1328 |
+
current_quantity = st.session_state.resources_data["materials"].at[materials_idx[0], "الكمية المتاحة"]
|
1329 |
+
st.session_state.resources_data["materials"].at[materials_idx[0], "الكمية المتاحة"] = max(0, current_quantity - new_allocation_quantity)
|
1330 |
+
|
1331 |
+
st.success(f"تم إضافة تخصيص {new_allocation_resource_type} ({new_allocation_resource}) لمشروع {new_allocation_project} بنجاح!")
|
1332 |
+
st.rerun()
|
1333 |
+
|
1334 |
+
def _render_resource_planning_tab(self):
|
1335 |
+
"""عرض تبويب تخطيط الموارد"""
|
1336 |
+
|
1337 |
+
st.markdown("### تخطيط الموارد")
|
1338 |
+
|
1339 |
+
# استخراج البيانات
|
1340 |
+
employees_df = st.session_state.resources_data["employees"]
|
1341 |
+
equipment_df = st.session_state.resources_data["equipment"]
|
1342 |
+
materials_df = st.session_state.resources_data["materials"]
|
1343 |
+
projects_df = st.session_state.resources_data["projects"]
|
1344 |
+
allocations_df = st.session_state.resources_data["allocations"]
|
1345 |
+
|
1346 |
+
# عرض المشاريع القادمة
|
1347 |
+
st.markdown("#### المشاريع القادمة")
|
1348 |
+
|
1349 |
+
upcoming_projects = projects_df[projects_df["الحالة"] == "مخطط"].sort_values("تاريخ البدء")
|
1350 |
+
|
1351 |
+
if not upcoming_projects.empty:
|
1352 |
+
st.dataframe(
|
1353 |
+
upcoming_projects,
|
1354 |
+
column_config={
|
1355 |
+
"رقم المشروع": st.column_config.TextColumn("رقم المشروع"),
|
1356 |
+
"اسم المشروع": st.column_config.TextColumn("اسم المشروع"),
|
1357 |
+
"الموقع": st.column_config.TextColumn("الموقع"),
|
1358 |
+
"تاريخ البدء": st.column_config.DateColumn("تاريخ البدء"),
|
1359 |
+
"تاريخ الانتهاء": st.column_config.DateColumn("تاريخ الانتهاء"),
|
1360 |
+
"الميزانية": st.column_config.NumberColumn("الميزانية", format="%.2f ريال"),
|
1361 |
+
"الحالة": st.column_config.TextColumn("الحالة")
|
1362 |
+
},
|
1363 |
+
use_container_width=True,
|
1364 |
+
hide_index=True
|
1365 |
+
)
|
1366 |
+
else:
|
1367 |
+
st.info("لا توجد مشاريع قادمة")
|
1368 |
+
|
1369 |
+
# عرض توافر الموارد
|
1370 |
+
st.markdown("#### توافر الموارد")
|
1371 |
+
|
1372 |
+
col1, col2, col3 = st.columns(3)
|
1373 |
+
|
1374 |
+
with col1:
|
1375 |
+
total_employees = len(employees_df)
|
1376 |
+
available_employees = len(employees_df[employees_df["متاح"] == True])
|
1377 |
+
availability_rate = available_employees / total_employees * 100 if total_employees > 0 else 0
|
1378 |
+
|
1379 |
+
st.metric("الموظفون المتاحون", f"{available_employees}/{total_employees}", f"{availability_rate:.1f}%")
|
1380 |
+
|
1381 |
+
# عرض توزيع توافر الموظفين
|
1382 |
+
availability_data = pd.DataFrame({
|
1383 |
+
"الحالة": ["متاح", "غير متاح"],
|
1384 |
+
"العدد": [available_employees, total_employees - available_employees]
|
1385 |
+
})
|
1386 |
+
|
1387 |
+
fig = px.pie(
|
1388 |
+
availability_data,
|
1389 |
+
values="العدد",
|
1390 |
+
names="الحالة",
|
1391 |
+
title="توافر الموظفين",
|
1392 |
+
color="الحالة",
|
1393 |
+
color_discrete_map={
|
1394 |
+
"متاح": "#2ecc71",
|
1395 |
+
"غير متاح": "#e74c3c"
|
1396 |
+
}
|
1397 |
+
)
|
1398 |
+
|
1399 |
+
st.plotly_chart(fig, use_container_width=True)
|
1400 |
+
|
1401 |
+
with col2:
|
1402 |
+
total_equipment = len(equipment_df)
|
1403 |
+
available_equipment = len(equipment_df[equipment_df["متاحة"] == True])
|
1404 |
+
availability_rate = available_equipment / total_equipment * 100 if total_equipment > 0 else 0
|
1405 |
+
|
1406 |
+
st.metric("المعدات المتاحة", f"{available_equipment}/{total_equipment}", f"{availability_rate:.1f}%")
|
1407 |
+
|
1408 |
+
# عرض توزيع توافر المعدات
|
1409 |
+
availability_data = pd.DataFrame({
|
1410 |
+
"الحالة": ["متاحة", "غير متاحة"],
|
1411 |
+
"العدد": [available_equipment, total_equipment - available_equipment]
|
1412 |
+
})
|
1413 |
+
|
1414 |
+
fig = px.pie(
|
1415 |
+
availability_data,
|
1416 |
+
values="العدد",
|
1417 |
+
names="الحالة",
|
1418 |
+
title="توافر المعدات",
|
1419 |
+
color="الحالة",
|
1420 |
+
color_discrete_map={
|
1421 |
+
"متاحة": "#2ecc71",
|
1422 |
+
"غير متاحة": "#e74c3c"
|
1423 |
+
}
|
1424 |
+
)
|
1425 |
+
|
1426 |
+
st.plotly_chart(fig, use_container_width=True)
|
1427 |
+
|
1428 |
+
with col3:
|
1429 |
+
total_materials = len(materials_df)
|
1430 |
+
low_stock_materials = len(materials_df[materials_df["الكمية المتاحة"] < 50])
|
1431 |
+
low_stock_rate = low_stock_materials / total_materials * 100 if total_materials > 0 else 0
|
1432 |
+
|
1433 |
+
st.metric("المواد منخفضة المخزون", f"{low_stock_materials}/{total_materials}", f"{low_stock_rate:.1f}%")
|
1434 |
+
|
1435 |
+
# عرض توزيع حالة المخزون
|
1436 |
+
stock_data = pd.DataFrame({
|
1437 |
+
"حالة المخزون": ["مخزون كافي", "مخزون منخفض"],
|
1438 |
+
"العدد": [total_materials - low_stock_materials, low_stock_materials]
|
1439 |
+
})
|
1440 |
+
|
1441 |
+
fig = px.pie(
|
1442 |
+
stock_data,
|
1443 |
+
values="العدد",
|
1444 |
+
names="حالة المخزون",
|
1445 |
+
title="حالة مخزون المواد",
|
1446 |
+
color="حالة المخزون",
|
1447 |
+
color_discrete_map={
|
1448 |
+
"مخزون كافي": "#2ecc71",
|
1449 |
+
"مخزون منخفض": "#e74c3c"
|
1450 |
+
}
|
1451 |
+
)
|
1452 |
+
|
1453 |
+
st.plotly_chart(fig, use_container_width=True)
|
1454 |
+
|
1455 |
+
# عرض جدول زمني للموارد
|
1456 |
+
st.markdown("#### الجدول الزمني للموارد")
|
1457 |
+
|
1458 |
+
# إنشاء DataFrame للجدول الزمني
|
1459 |
+
timeline_df = allocations_df.copy()
|
1460 |
+
timeline_df["تاريخ البدء"] = pd.to_datetime(timeline_df["تاريخ البدء"])
|
1461 |
+
timeline_df["تاريخ الانتهاء"] = pd.to_datetime(timeline_df["تاريخ الانتهاء"])
|
1462 |
+
|
1463 |
+
# دمج البيانات مع بيانات المشاريع
|
1464 |
+
timeline_df = timeline_df.merge(
|
1465 |
+
projects_df[["رقم المشروع", "اسم المشروع"]],
|
1466 |
+
on="رقم المشروع",
|
1467 |
+
how="left"
|
1468 |
+
)
|
1469 |
+
|
1470 |
+
# إضافة أسماء الموارد
|
1471 |
+
timeline_df["اسم المورد"] = ""
|
1472 |
+
|
1473 |
+
for i, row in timeline_df.iterrows():
|
1474 |
+
if row["نوع المورد"] == "موظف":
|
1475 |
+
resource_name = employees_df[employees_df["رقم الموظف"] == row["رقم المورد"]]["اسم الموظف"].values
|
1476 |
+
if len(resource_name) > 0:
|
1477 |
+
timeline_df.at[i, "اسم المورد"] = resource_name[0]
|
1478 |
+
elif row["نوع المورد"] == "معدة":
|
1479 |
+
resource_name = equipment_df[equipment_df["رقم المعدة"] == row["رقم المورد"]]["اسم المعدة"].values
|
1480 |
+
if len(resource_name) > 0:
|
1481 |
+
timeline_df.at[i, "اسم المورد"] = resource_name[0]
|
1482 |
+
elif row["نوع المورد"] == "مادة":
|
1483 |
+
resource_name = materials_df[materials_df["رقم المادة"] == row["رقم المورد"]]["اسم المادة"].values
|
1484 |
+
if len(resource_name) > 0:
|
1485 |
+
timeline_df.at[i, "اسم المورد"] = resource_name[0]
|
1486 |
+
|
1487 |
+
# إنشاء رسم بياني للجدول الزمني
|
1488 |
+
fig = px.timeline(
|
1489 |
+
timeline_df,
|
1490 |
+
x_start="تاريخ البدء",
|
1491 |
+
x_end="تاريخ الانتهاء",
|
1492 |
+
y="اسم المورد",
|
1493 |
+
color="اسم المشروع",
|
1494 |
+
hover_name="اسم المشروع",
|
1495 |
+
hover_data=["نوع المورد", "الكمية", "التكلفة"],
|
1496 |
+
title="الجدول الزمني لتخصيص الموارد"
|
1497 |
+
)
|
1498 |
+
|
1499 |
+
fig.update_yaxes(autorange="reversed")
|
1500 |
+
|
1501 |
+
st.plotly_chart(fig, use_container_width=True)
|
1502 |
+
|
1503 |
+
# عرض توقعات الاحتياجات المستقبلية
|
1504 |
+
st.markdown("#### توقعات الاحتياجات المستقبلية")
|
1505 |
+
|
1506 |
+
# اختيار المشروع للتخطيط
|
1507 |
+
project_for_planning = st.selectbox("اختر المشروع للتخطيط", options=upcoming_projects["اسم المشروع"] if not upcoming_projects.empty else ["لا توجد مشاريع قادمة"])
|
1508 |
+
|
1509 |
+
if project_for_planning != "لا توجد مشاريع قادمة":
|
1510 |
+
# الحصول على بيانات المشروع
|
1511 |
+
project_data = upcoming_projects[upcoming_projects["اسم المشروع"] == project_for_planning].iloc[0]
|
1512 |
+
|
1513 |
+
st.markdown(f"**تاريخ البدء:** {project_data['تاريخ البدء']}")
|
1514 |
+
st.markdown(f"**تاريخ الانتهاء:** {project_data['تاريخ الانتهاء']}")
|
1515 |
+
st.markdown(f"**الميزانية:** {project_data['الميزانية']:,.0f} ريال")
|
1516 |
+
|
1517 |
+
# تقدير الاحتياجات بناءً على المشاريع المماثلة
|
1518 |
+
st.markdown("##### تقدير الاحتياجات بناءً على المشاريع المماثلة")
|
1519 |
+
|
1520 |
+
# محاكاة تقدير الاحتياجات
|
1521 |
+
estimated_resources = {
|
1522 |
+
"الموظ��ون": {
|
1523 |
+
"مهندس": 5,
|
1524 |
+
"فني": 10,
|
1525 |
+
"مشرف": 2,
|
1526 |
+
"محاسب": 1,
|
1527 |
+
"مساعد": 3
|
1528 |
+
},
|
1529 |
+
"المعدات": {
|
1530 |
+
"حفارة كبيرة": 1,
|
1531 |
+
"حفارة صغيرة": 2,
|
1532 |
+
"شاحنة نقل": 3,
|
1533 |
+
"رافعة متوسطة": 1,
|
1534 |
+
"خلاطة خرسانة": 2
|
1535 |
+
},
|
1536 |
+
"المواد": {
|
1537 |
+
"خرسانة جاهزة": 500,
|
1538 |
+
"حديد تسليح": 200,
|
1539 |
+
"طابوق": 10000,
|
1540 |
+
"أسمنت": 1000,
|
1541 |
+
"رمل": 300
|
1542 |
+
}
|
1543 |
+
}
|
1544 |
+
|
1545 |
+
# عرض تقدير الاحتياجات
|
1546 |
+
col1, col2, col3 = st.columns(3)
|
1547 |
+
|
1548 |
+
with col1:
|
1549 |
+
st.markdown("**الموظفون المطلوبون:**")
|
1550 |
+
for position, count in estimated_resources["الموظفون"].items():
|
1551 |
+
st.markdown(f"- {position}: {count}")
|
1552 |
+
|
1553 |
+
# التحقق من توافر الموظفين
|
1554 |
+
available_positions = {}
|
1555 |
+
for position, count in estimated_resources["الموظفون"].items():
|
1556 |
+
available_count = len(employees_df[(employees_df["المنصب"] == position) & (employees_df["متاح"] == True)])
|
1557 |
+
available_positions[position] = available_count
|
1558 |
+
|
1559 |
+
if available_count < count:
|
1560 |
+
st.warning(f"نقص في {position}: متاح {available_count}/{count}")
|
1561 |
+
else:
|
1562 |
+
st.success(f"متوفر: {available_count}/{count}")
|
1563 |
+
|
1564 |
+
with col2:
|
1565 |
+
st.markdown("**المعدات المطلوبة:**")
|
1566 |
+
for equipment_name, count in estimated_resources["المعدات"].items():
|
1567 |
+
st.markdown(f"- {equipment_name}: {count}")
|
1568 |
+
|
1569 |
+
# التحقق من توافر المعدات
|
1570 |
+
available_equipment = {}
|
1571 |
+
for equipment_name, count in estimated_resources["المعدات"].items():
|
1572 |
+
available_count = len(equipment_df[(equipment_df["اسم المعدة"] == equipment_name) & (equipment_df["متاحة"] == True)])
|
1573 |
+
available_equipment[equipment_name] = available_count
|
1574 |
+
|
1575 |
+
if available_count < count:
|
1576 |
+
st.warning(f"نقص في {equipment_name}: متاح {available_count}/{count}")
|
1577 |
+
else:
|
1578 |
+
st.success(f"متوفر: {available_count}/{count}")
|
1579 |
+
|
1580 |
+
with col3:
|
1581 |
+
st.markdown("**المواد المطلوبة:**")
|
1582 |
+
for material_name, quantity in estimated_resources["المواد"].items():
|
1583 |
+
st.markdown(f"- {material_name}: {quantity}")
|
1584 |
+
|
1585 |
+
# التحقق من توافر المواد
|
1586 |
+
available_materials = {}
|
1587 |
+
for material_name, quantity in estimated_resources["المواد"].items():
|
1588 |
+
available_quantity = materials_df[materials_df["اسم المادة"] == material_name]["الكمية المتاحة"].sum()
|
1589 |
+
available_materials[material_name] = available_quantity
|
1590 |
+
|
1591 |
+
if available_quantity < quantity:
|
1592 |
+
st.warning(f"نقص في {material_name}: متاح {available_quantity}/{quantity}")
|
1593 |
+
else:
|
1594 |
+
st.success(f"متوفر: {available_quantity}/{quantity}")
|
1595 |
+
|
1596 |
+
# عرض تقدير التكاليف
|
1597 |
+
st.markdown("##### تقدير تكاليف الموارد")
|
1598 |
+
|
1599 |
+
# حساب تكاليف الموظفين
|
1600 |
+
employee_costs = 0
|
1601 |
+
for position, count in estimated_resources["الموظفون"].items():
|
1602 |
+
avg_cost = employees_df[employees_df["المنصب"] == position]["التكلفة الشهرية"].mean()
|
1603 |
+
# افتراض مدة المشروع 6 أشهر
|
1604 |
+
employee_costs += avg_cost * count * 6
|
1605 |
+
|
1606 |
+
# حساب تكاليف المعدات
|
1607 |
+
equipment_costs = 0
|
1608 |
+
for equipment_name, count in estimated_resources["المعدات"].items():
|
1609 |
+
avg_cost = equipment_df[equipment_df["اسم المعدة"] == equipment_name]["التكلفة اليومية"].mean()
|
1610 |
+
# افتراض مدة المشروع 180 يوم
|
1611 |
+
equipment_costs += avg_cost * count * 180
|
1612 |
+
|
1613 |
+
# حساب تكاليف المواد
|
1614 |
+
material_costs = 0
|
1615 |
+
for material_name, quantity in estimated_resources["المواد"].items():
|
1616 |
+
avg_cost = materials_df[materials_df["ا��م المادة"] == material_name]["تكلفة الوحدة"].mean()
|
1617 |
+
material_costs += avg_cost * quantity
|
1618 |
+
|
1619 |
+
# إجمالي التكاليف
|
1620 |
+
total_costs = employee_costs + equipment_costs + material_costs
|
1621 |
+
|
1622 |
+
# عرض التكاليف
|
1623 |
+
col1, col2, col3, col4 = st.columns(4)
|
1624 |
+
|
1625 |
+
with col1:
|
1626 |
+
st.metric("تكاليف الموظفين", f"{employee_costs:,.0f} ريال")
|
1627 |
+
|
1628 |
+
with col2:
|
1629 |
+
st.metric("تكاليف المعدات", f"{equipment_costs:,.0f} ريال")
|
1630 |
+
|
1631 |
+
with col3:
|
1632 |
+
st.metric("تكاليف المواد", f"{material_costs:,.0f} ريال")
|
1633 |
+
|
1634 |
+
with col4:
|
1635 |
+
st.metric("إجمالي التكاليف", f"{total_costs:,.0f} ريال")
|
1636 |
+
|
1637 |
+
# عرض توزيع التكاليف
|
1638 |
+
cost_distribution = pd.DataFrame({
|
1639 |
+
"نوع التكلفة": ["تكاليف الموظفين", "تكاليف المعدات", "تكاليف المواد"],
|
1640 |
+
"التكلفة": [employee_costs, equipment_costs, material_costs]
|
1641 |
+
})
|
1642 |
+
|
1643 |
+
fig = px.pie(
|
1644 |
+
cost_distribution,
|
1645 |
+
values="التكلفة",
|
1646 |
+
names="نوع التكلفة",
|
1647 |
+
title="توزيع تكاليف الموارد",
|
1648 |
+
color="نوع التكلفة",
|
1649 |
+
color_discrete_map={
|
1650 |
+
"تكاليف الموظفين": "#3498db",
|
1651 |
+
"تكاليف المعدات": "#2ecc71",
|
1652 |
+
"تكاليف المواد": "#f39c12"
|
1653 |
+
}
|
1654 |
+
)
|
1655 |
+
|
1656 |
+
st.plotly_chart(fig, use_container_width=True)
|
1657 |
+
|
1658 |
+
# عرض توصيات لتخطيط الموارد
|
1659 |
+
st.markdown("##### توصيات لتخطيط الموارد")
|
1660 |
+
|
1661 |
+
recommendations = []
|
1662 |
+
|
1663 |
+
# توصيات للموظفين
|
1664 |
+
for position, count in estimated_resources["الموظفون"].items():
|
1665 |
+
available_count = available_positions[position]
|
1666 |
+
if available_count < count:
|
1667 |
+
recommendations.append(f"توظيف {count - available_count} {position} إضافي")
|
1668 |
+
|
1669 |
+
# توصيات للمعدات
|
1670 |
+
for equipment_name, count in estimated_resources["المعدات"].items():
|
1671 |
+
available_count = available_equipment[equipment_name]
|
1672 |
+
if available_count < count:
|
1673 |
+
recommendations.append(f"استئجار {count - available_count} {equipment_name} إضافية")
|
1674 |
+
|
1675 |
+
# توصيات للمواد
|
1676 |
+
for material_name, quantity in estimated_resources["المواد"].items():
|
1677 |
+
available_quantity = available_materials[material_name]
|
1678 |
+
if available_quantity < quantity:
|
1679 |
+
recommendations.append(f"شراء {quantity - available_quantity} وحدة إضافية من {material_name}")
|
1680 |
+
|
1681 |
+
if recommendations:
|
1682 |
+
for recommendation in recommendations:
|
1683 |
+
st.markdown(f"- {recommendation}")
|
1684 |
+
else:
|
1685 |
+
st.success("جميع الموارد المطلوبة متوفرة")
|
1686 |
+
|
1687 |
+
# زر لإنشاء خطة الموارد
|
1688 |
+
if st.button("إنشاء خطة الموارد"):
|
1689 |
+
st.success("تم إنشاء خطة الموارد بنجاح!")
|
1690 |
+
|
1691 |
+
# عرض ملخص الخطة
|
1692 |
+
st.markdown("##### ملخص خطة الموارد")
|
1693 |
+
st.markdown(f"**المشروع:** {project_for_planning}")
|
1694 |
+
st.markdown(f"**تاريخ البدء:** {project_data['تاريخ البدء']}")
|
1695 |
+
st.markdown(f"**تاريخ الانتهاء:** {project_data['تاريخ الانتهاء']}")
|
1696 |
+
st.markdown(f"**إجمالي تكاليف الموارد:** {total_costs:,.0f} ريال")
|
1697 |
+
st.markdown(f"**نسبة تكاليف الموارد من الميزانية:** {total_costs / project_data['الميزانية'] * 100:.1f}%")
|
1698 |
+
|
1699 |
+
if recommendations:
|
1700 |
+
st.markdown("**الإجراءات المطلوبة:**")
|
1701 |
+
for recommendation in recommendations:
|
1702 |
+
st.markdown(f"- {recommendation}")
|
1703 |
+
else:
|
1704 |
+
st.markdown("**الإجراءات المطلوبة:** لا توجد إجراءات مطلوبة، جميع الموارد متوفرة")
|
1705 |
+
else:
|
1706 |
+
st.info("لا توجد مشاريع قادمة للتخطيط")
|
modules/risk_analysis/risk_analyzer.py
ADDED
@@ -0,0 +1,585 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة تحليل المخاطر لنظام إدارة المناقصات - Hybrid Face
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import logging
|
7 |
+
import threading
|
8 |
+
import datetime
|
9 |
+
import json
|
10 |
+
import math
|
11 |
+
from pathlib import Path
|
12 |
+
|
13 |
+
# تهيئة السجل
|
14 |
+
logging.basicConfig(
|
15 |
+
level=logging.INFO,
|
16 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
17 |
+
)
|
18 |
+
logger = logging.getLogger('risk_analysis')
|
19 |
+
|
20 |
+
class RiskAnalyzer:
|
21 |
+
"""محلل المخاطر"""
|
22 |
+
|
23 |
+
def __init__(self, config=None, db=None):
|
24 |
+
"""تهيئة محلل المخاطر"""
|
25 |
+
self.config = config
|
26 |
+
self.db = db
|
27 |
+
self.analysis_in_progress = False
|
28 |
+
self.current_project = None
|
29 |
+
self.analysis_results = {}
|
30 |
+
|
31 |
+
# إنشاء مجلد التحليل إذا لم يكن موجوداً
|
32 |
+
if config and hasattr(config, 'EXPORTS_PATH'):
|
33 |
+
self.exports_path = Path(config.EXPORTS_PATH)
|
34 |
+
else:
|
35 |
+
self.exports_path = Path('data/exports')
|
36 |
+
|
37 |
+
if not self.exports_path.exists():
|
38 |
+
self.exports_path.mkdir(parents=True, exist_ok=True)
|
39 |
+
|
40 |
+
def analyze_risks(self, project_id, method="comprehensive", callback=None):
|
41 |
+
"""تحليل مخاطر المشروع"""
|
42 |
+
if self.analysis_in_progress:
|
43 |
+
logger.warning("هناك عملية تحليل مخاطر جارية بالفعل")
|
44 |
+
return False
|
45 |
+
|
46 |
+
self.analysis_in_progress = True
|
47 |
+
self.current_project = project_id
|
48 |
+
self.analysis_results = {
|
49 |
+
"project_id": project_id,
|
50 |
+
"method": method,
|
51 |
+
"analysis_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
52 |
+
"status": "جاري التحليل",
|
53 |
+
"identified_risks": [],
|
54 |
+
"risk_categories": {},
|
55 |
+
"risk_matrix": {},
|
56 |
+
"mitigation_strategies": [],
|
57 |
+
"summary": {}
|
58 |
+
}
|
59 |
+
|
60 |
+
# بدء التحليل في خيط منفصل
|
61 |
+
thread = threading.Thread(
|
62 |
+
target=self._analyze_risks_thread,
|
63 |
+
args=(project_id, method, callback)
|
64 |
+
)
|
65 |
+
thread.daemon = True
|
66 |
+
thread.start()
|
67 |
+
|
68 |
+
return True
|
69 |
+
|
70 |
+
def _analyze_risks_thread(self, project_id, method, callback):
|
71 |
+
"""خيط تحليل المخاطر"""
|
72 |
+
try:
|
73 |
+
# محاكاة جلب بيانات المشروع من قاعدة البيانات
|
74 |
+
project_data = self._get_project_data(project_id)
|
75 |
+
|
76 |
+
if not project_data:
|
77 |
+
logger.error(f"لم يتم العثور على بيانات المشروع: {project_id}")
|
78 |
+
self.analysis_results["status"] = "فشل التحليل"
|
79 |
+
self.analysis_results["error"] = "لم يتم العثور على بيانات المشروع"
|
80 |
+
return
|
81 |
+
|
82 |
+
# تحديد المخاطر
|
83 |
+
self._identify_risks(project_data, method)
|
84 |
+
|
85 |
+
# تصنيف المخاطر
|
86 |
+
self._categorize_risks()
|
87 |
+
|
88 |
+
# إنشاء مصفوفة المخاطر
|
89 |
+
self._create_risk_matrix()
|
90 |
+
|
91 |
+
# تطوير استراتيجيات التخفيف
|
92 |
+
self._develop_mitigation_strategies(method)
|
93 |
+
|
94 |
+
# إنشاء ملخص التحليل
|
95 |
+
self._create_analysis_summary(method)
|
96 |
+
|
97 |
+
# تحديث حالة التحليل
|
98 |
+
self.analysis_results["status"] = "اكتمل التحليل"
|
99 |
+
self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
100 |
+
|
101 |
+
logger.info(f"اكتمل تحليل مخاطر المشروع: {project_id}")
|
102 |
+
|
103 |
+
except Exception as e:
|
104 |
+
logger.error(f"خطأ في تحليل مخاطر المشروع: {str(e)}")
|
105 |
+
self.analysis_results["status"] = "فشل التحليل"
|
106 |
+
self.analysis_results["error"] = str(e)
|
107 |
+
|
108 |
+
finally:
|
109 |
+
self.analysis_in_progress = False
|
110 |
+
|
111 |
+
# استدعاء دالة الاستجابة إذا تم توفيرها
|
112 |
+
if callback and callable(callback):
|
113 |
+
callback(self.analysis_results)
|
114 |
+
|
115 |
+
def _get_project_data(self, project_id):
|
116 |
+
"""الحصول على بيانات المشروع"""
|
117 |
+
# في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
|
118 |
+
# هنا نقوم بمحاكاة البيانات للتوضيح
|
119 |
+
|
120 |
+
return {
|
121 |
+
"id": project_id,
|
122 |
+
"name": "مشروع الطرق السريعة",
|
123 |
+
"client": "وزارة النقل",
|
124 |
+
"description": "إنشاء طرق سريعة بطول 50 كم في المنطقة الشرقية",
|
125 |
+
"start_date": "2025-05-01",
|
126 |
+
"end_date": "2025-11-30",
|
127 |
+
"status": "تخطيط",
|
128 |
+
"budget": 50000000,
|
129 |
+
"location": "المنطقة الشرقية",
|
130 |
+
"project_type": "بنية تحتية",
|
131 |
+
"complexity": "متوسط",
|
132 |
+
"existing_risks": [
|
133 |
+
{"id": 1, "name": "تأخر توريد المواد", "probability": "متوسط", "impact": "عالي", "category": "توريد"},
|
134 |
+
{"id": 2, "name": "تغير أسعار المواد", "probability": "عالي", "impact": "عالي", "category": "مالي"},
|
135 |
+
{"id": 3, "name": "ظروف جوية غير مواتية", "probability": "منخفض", "impact": "متوسط", "category": "بيئي"},
|
136 |
+
{"id": 4, "name": "نقص العمالة", "probability": "متوسط", "impact": "متوسط", "category": "موارد بشرية"}
|
137 |
+
]
|
138 |
+
}
|
139 |
+
|
140 |
+
def _identify_risks(self, project_data, method):
|
141 |
+
"""تحديد المخاطر"""
|
142 |
+
# دمج المخاطر الموجودة
|
143 |
+
identified_risks = []
|
144 |
+
for risk in project_data["existing_risks"]:
|
145 |
+
identified_risks.append({
|
146 |
+
"id": risk["id"],
|
147 |
+
"name": risk["name"],
|
148 |
+
"description": f"مخاطر {risk['name']} في المشروع",
|
149 |
+
"category": risk["category"],
|
150 |
+
"probability": risk["probability"],
|
151 |
+
"impact": risk["impact"],
|
152 |
+
"risk_score": self._calculate_risk_score(risk["probability"], risk["impact"]),
|
153 |
+
"source": "existing"
|
154 |
+
})
|
155 |
+
|
156 |
+
# إضافة مخاطر إضافية بناءً على نوع المشروع وموقعه وتعقيده
|
157 |
+
additional_risks = self._generate_additional_risks(project_data, method)
|
158 |
+
identified_risks.extend(additional_risks)
|
159 |
+
|
160 |
+
# تخزين المخاطر المحددة
|
161 |
+
self.analysis_results["identified_risks"] = identified_risks
|
162 |
+
|
163 |
+
def _generate_additional_risks(self, project_data, method):
|
164 |
+
"""توليد مخاطر إضافية بناءً على بيانات المشروع"""
|
165 |
+
additional_risks = []
|
166 |
+
|
167 |
+
# مخاطر مرتبطة بنوع المشروع
|
168 |
+
if project_data["project_type"] == "بنية تحتية":
|
169 |
+
additional_risks.extend([
|
170 |
+
{
|
171 |
+
"id": 101,
|
172 |
+
"name": "مشاكل جيوتقنية",
|
173 |
+
"description": "مشاكل غير متوقعة في التربة أو الظروف الجيولوجية",
|
174 |
+
"category": "فني",
|
175 |
+
"probability": "متوسط",
|
176 |
+
"impact": "عالي",
|
177 |
+
"risk_score": self._calculate_risk_score("متوسط", "عالي"),
|
178 |
+
"source": "generated"
|
179 |
+
},
|
180 |
+
{
|
181 |
+
"id": 102,
|
182 |
+
"name": "تعارض مع مرافق قائمة",
|
183 |
+
"description": "تعارض أعمال الحفر مع خطوط المرافق القائمة (كهرباء، مياه، اتصالات)",
|
184 |
+
"category": "فني",
|
185 |
+
"probability": "متوسط",
|
186 |
+
"impact": "متوسط",
|
187 |
+
"risk_score": self._calculate_risk_score("متوسط", "متوسط"),
|
188 |
+
"source": "generated"
|
189 |
+
}
|
190 |
+
])
|
191 |
+
|
192 |
+
# مخاطر مرتبطة بالموقع
|
193 |
+
if project_data["location"] == "المنطقة الشرقية":
|
194 |
+
additional_risks.extend([
|
195 |
+
{
|
196 |
+
"id": 201,
|
197 |
+
"name": "ارتفاع درجات الحرارة",
|
198 |
+
"description": "تأثير ارتفاع درجات الحرارة على إنتاجية العمل وجودة المواد",
|
199 |
+
"category": "بيئي",
|
200 |
+
"probability": "عالي",
|
201 |
+
"impact": "متوسط",
|
202 |
+
"risk_score": self._calculate_risk_score("عالي", "متوسط"),
|
203 |
+
"source": "generated"
|
204 |
+
},
|
205 |
+
{
|
206 |
+
"id": 202,
|
207 |
+
"name": "رطوبة عالية",
|
208 |
+
"description": "تأثير الرطوبة العالية على جودة المواد وتقنيات البناء",
|
209 |
+
"category": "بيئي",
|
210 |
+
"probability": "عالي",
|
211 |
+
"impact": "منخفض",
|
212 |
+
"risk_score": self._calculate_risk_score("عالي", "منخفض"),
|
213 |
+
"source": "generated"
|
214 |
+
}
|
215 |
+
])
|
216 |
+
|
217 |
+
# مخاطر مرتبطة بتعقيد المشروع
|
218 |
+
if project_data["complexity"] in ["متوسط", "عالي"]:
|
219 |
+
additional_risks.extend([
|
220 |
+
{
|
221 |
+
"id": 301,
|
222 |
+
"name": "تغييرات في نطاق العمل",
|
223 |
+
"description": "طلبات تغيير من العميل أو تعديلات في متطلبات المشروع",
|
224 |
+
"category": "إداري",
|
225 |
+
"probability": "عالي",
|
226 |
+
"impact": "عالي",
|
227 |
+
"risk_score": self._calculate_risk_score("عالي", "عالي"),
|
228 |
+
"source": "generated"
|
229 |
+
},
|
230 |
+
{
|
231 |
+
"id": 302,
|
232 |
+
"name": "تأخر الموافقات",
|
233 |
+
"description": "تأخر الحصول على الموافقات والتصاريح اللازمة",
|
234 |
+
"category": "تنظيمي",
|
235 |
+
"probability": "متوسط",
|
236 |
+
"impact": "عالي",
|
237 |
+
"risk_score": self._calculate_risk_score("متوسط", "عالي"),
|
238 |
+
"source": "generated"
|
239 |
+
}
|
240 |
+
])
|
241 |
+
|
242 |
+
# إضافة مخاطر إضافية إذا كانت طريقة التحليل شاملة
|
243 |
+
if method == "comprehensive":
|
244 |
+
additional_risks.extend([
|
245 |
+
{
|
246 |
+
"id": 401,
|
247 |
+
"name": "مخاطر الأمن السيبراني",
|
248 |
+
"description": "مخاطر الهجمات الإلكترونية على أنظمة إدارة المشروع",
|
249 |
+
"category": "تقني",
|
250 |
+
"probability": "منخفض",
|
251 |
+
"impact": "عالي",
|
252 |
+
"risk_score": self._calculate_risk_score("منخفض", "عالي"),
|
253 |
+
"source": "generated"
|
254 |
+
},
|
255 |
+
{
|
256 |
+
"id": 402,
|
257 |
+
"name": "تغيرات في اللوائح والأنظمة",
|
258 |
+
"description": "تغييرات في اللوائح والأنظمة الحكومية المتعلقة بالمشروع",
|
259 |
+
"category": "تنظيمي",
|
260 |
+
"probability": "منخفض",
|
261 |
+
"impact": "متوسط",
|
262 |
+
"risk_score": self._calculate_risk_score("منخفض", "متوسط"),
|
263 |
+
"source": "generated"
|
264 |
+
},
|
265 |
+
{
|
266 |
+
"id": 403,
|
267 |
+
"name": "مخاطر العملة والتضخم",
|
268 |
+
"description": "تقلبات أسعار العملة ومعدلات التضخم",
|
269 |
+
"category": "مالي",
|
270 |
+
"probability": "متوسط",
|
271 |
+
"impact": "متوسط",
|
272 |
+
"risk_score": self._calculate_risk_score("متوسط", "متوسط"),
|
273 |
+
"source": "generated"
|
274 |
+
}
|
275 |
+
])
|
276 |
+
|
277 |
+
return additional_risks
|
278 |
+
|
279 |
+
def _calculate_risk_score(self, probability, impact):
|
280 |
+
"""حساب درجة المخاطرة"""
|
281 |
+
probability_map = {
|
282 |
+
"منخفض": 1,
|
283 |
+
"متوسط": 2,
|
284 |
+
"عالي": 3
|
285 |
+
}
|
286 |
+
|
287 |
+
impact_map = {
|
288 |
+
"منخفض": 1,
|
289 |
+
"متوسط": 2,
|
290 |
+
"عالي": 3
|
291 |
+
}
|
292 |
+
|
293 |
+
prob_value = probability_map.get(probability, 1)
|
294 |
+
impact_value = impact_map.get(impact, 1)
|
295 |
+
|
296 |
+
return prob_value * impact_value
|
297 |
+
|
298 |
+
def _categorize_risks(self):
|
299 |
+
"""تصنيف المخاطر"""
|
300 |
+
categories = {}
|
301 |
+
|
302 |
+
for risk in self.analysis_results["identified_risks"]:
|
303 |
+
category = risk["category"]
|
304 |
+
|
305 |
+
if category not in categories:
|
306 |
+
categories[category] = {
|
307 |
+
"count": 0,
|
308 |
+
"risks": [],
|
309 |
+
"avg_score": 0,
|
310 |
+
"max_score": 0
|
311 |
+
}
|
312 |
+
|
313 |
+
categories[category]["count"] += 1
|
314 |
+
categories[category]["risks"].append(risk)
|
315 |
+
categories[category]["max_score"] = max(categories[category]["max_score"], risk["risk_score"])
|
316 |
+
|
317 |
+
# حساب متوسط درجة المخاطرة لكل فئة
|
318 |
+
for category in categories:
|
319 |
+
total_score = sum(risk["risk_score"] for risk in categories[category]["risks"])
|
320 |
+
categories[category]["avg_score"] = total_score / categories[category]["count"]
|
321 |
+
|
322 |
+
# ترتيب الفئات حسب متوسط درجة المخاطرة
|
323 |
+
sorted_categories = dict(sorted(
|
324 |
+
categories.items(),
|
325 |
+
key=lambda item: item[1]["avg_score"],
|
326 |
+
reverse=True
|
327 |
+
))
|
328 |
+
|
329 |
+
self.analysis_results["risk_categories"] = sorted_categories
|
330 |
+
|
331 |
+
def _create_risk_matrix(self):
|
332 |
+
"""إنشاء مصفوفة المخاطر"""
|
333 |
+
matrix = {
|
334 |
+
"high_impact": {
|
335 |
+
"high_probability": [],
|
336 |
+
"medium_probability": [],
|
337 |
+
"low_probability": []
|
338 |
+
},
|
339 |
+
"medium_impact": {
|
340 |
+
"high_probability": [],
|
341 |
+
"medium_probability": [],
|
342 |
+
"low_probability": []
|
343 |
+
},
|
344 |
+
"low_impact": {
|
345 |
+
"high_probability": [],
|
346 |
+
"medium_probability": [],
|
347 |
+
"low_probability": []
|
348 |
+
}
|
349 |
+
}
|
350 |
+
|
351 |
+
for risk in self.analysis_results["identified_risks"]:
|
352 |
+
impact = risk["impact"].lower()
|
353 |
+
probability = risk["probability"].lower()
|
354 |
+
|
355 |
+
impact_key = f"{impact}_impact"
|
356 |
+
probability_key = f"{probability}_probability"
|
357 |
+
|
358 |
+
if impact_key in matrix and probability_key in matrix[impact_key]:
|
359 |
+
matrix[impact_key][probability_key].append(risk)
|
360 |
+
|
361 |
+
self.analysis_results["risk_matrix"] = matrix
|
362 |
+
|
363 |
+
def _develop_mitigation_strategies(self, method):
|
364 |
+
"""تطوير استراتيجيات التخفيف"""
|
365 |
+
mitigation_strategies = []
|
366 |
+
|
367 |
+
for risk in self.analysis_results["identified_risks"]:
|
368 |
+
strategy = self._generate_mitigation_strategy(risk, method)
|
369 |
+
mitigation_strategies.append(strategy)
|
370 |
+
|
371 |
+
self.analysis_results["mitigation_strategies"] = mitigation_strategies
|
372 |
+
|
373 |
+
def _generate_mitigation_strategy(self, risk, method):
|
374 |
+
"""توليد استراتيجية تخفيف للمخاطرة"""
|
375 |
+
strategy = {
|
376 |
+
"risk_id": risk["id"],
|
377 |
+
"risk_name": risk["name"],
|
378 |
+
"risk_score": risk["risk_score"],
|
379 |
+
"strategy_type": "",
|
380 |
+
"actions": [],
|
381 |
+
"responsible": "",
|
382 |
+
"timeline": "",
|
383 |
+
"cost_impact": 0
|
384 |
+
}
|
385 |
+
|
386 |
+
# تحديد نوع الاستراتيجية بناءً على درجة المخاطرة
|
387 |
+
if risk["risk_score"] >= 6: # مخاطر عالية
|
388 |
+
strategy["strategy_type"] = "تجنب"
|
389 |
+
strategy["responsible"] = "مدير المشروع"
|
390 |
+
strategy["timeline"] = "فوري"
|
391 |
+
strategy["cost_impact"] = "عالي"
|
392 |
+
elif risk["risk_score"] >= 3: # مخاطر متوسطة
|
393 |
+
strategy["strategy_type"] = "تخفيف"
|
394 |
+
strategy["responsible"] = "مشرف القسم المعني"
|
395 |
+
strategy["timeline"] = "خلال أسبوعين"
|
396 |
+
strategy["cost_impact"] = "متوسط"
|
397 |
+
else: # مخاطر منخفضة
|
398 |
+
strategy["strategy_type"] = "قبول"
|
399 |
+
strategy["responsible"] = "فريق المشروع"
|
400 |
+
strategy["timeline"] = "مراقبة مستمرة"
|
401 |
+
strategy["cost_impact"] = "منخفض"
|
402 |
+
|
403 |
+
# توليد إجراءات التخفيف بناءً على نوع المخاطرة وفئتها
|
404 |
+
if risk["category"] == "توريد":
|
405 |
+
strategy["actions"] = [
|
406 |
+
"تحديد موردين بدلاء",
|
407 |
+
"وضع جدول زمني للتوريد مع هوامش زمنية",
|
408 |
+
"التعاقد المسبق على المواد الرئيسية"
|
409 |
+
]
|
410 |
+
elif risk["category"] == "مالي":
|
411 |
+
strategy["actions"] = [
|
412 |
+
"تضمين بند تعديل الأسعار في العقود",
|
413 |
+
"وضع ميزانية احتياطية",
|
414 |
+
"التحوط ضد تقلبات الأسعار"
|
415 |
+
]
|
416 |
+
elif risk["category"] == "بيئي":
|
417 |
+
strategy["actions"] = [
|
418 |
+
"وضع خطة للطوارئ الجوية",
|
419 |
+
"جدولة الأنشطة الحساسة في الأوقات المناسبة",
|
420 |
+
"توفير معدات وقاية مناسبة"
|
421 |
+
]
|
422 |
+
elif risk["category"] == "موارد بشرية":
|
423 |
+
strategy["actions"] = [
|
424 |
+
"التعاقد المسبق مع مقاولي الباطن",
|
425 |
+
"وضع خطة لتدريب وتأهيل العمالة",
|
426 |
+
"تحفيز العاملين للحفاظ عليهم"
|
427 |
+
]
|
428 |
+
elif risk["category"] == "فني":
|
429 |
+
strategy["actions"] = [
|
430 |
+
"إجراء دراسات فنية إضافية",
|
431 |
+
"الاستعانة بخبراء متخصصين",
|
432 |
+
"وضع خطط بديلة للحلول الفنية"
|
433 |
+
]
|
434 |
+
elif risk["category"] == "إداري":
|
435 |
+
strategy["actions"] = [
|
436 |
+
"تحديد نطاق العمل بدقة في العقود",
|
437 |
+
"وضع إجراءات واضحة لإدارة التغيير",
|
438 |
+
"التواصل المستمر مع العميل"
|
439 |
+
]
|
440 |
+
elif risk["category"] == "تنظيمي":
|
441 |
+
strategy["actions"] = [
|
442 |
+
"متابعة التغييرات في اللوائح والأنظمة",
|
443 |
+
"التنسيق المبكر مع الجهات المعنية",
|
444 |
+
"تعيين مستشار قانوني للمشروع"
|
445 |
+
]
|
446 |
+
else:
|
447 |
+
strategy["actions"] = [
|
448 |
+
"مراقبة المخاطر بشكل دوري",
|
449 |
+
"وضع خطة استجابة",
|
450 |
+
"تخصيص موارد احتياطية"
|
451 |
+
]
|
452 |
+
|
453 |
+
# إضافة إجراءات إضافية إذا كانت طريقة التحليل شاملة
|
454 |
+
if method == "comprehensive" and len(strategy["actions"]) < 5:
|
455 |
+
strategy["actions"].append("إجراء مراجعات دورية لفعالية استراتيجية التخفيف")
|
456 |
+
strategy["actions"].append("توثيق الدروس المستفادة لتحسين إدارة المخاطر المستقبلية")
|
457 |
+
|
458 |
+
return strategy
|
459 |
+
|
460 |
+
def _create_analysis_summary(self, method):
|
461 |
+
"""إنشاء ملخص التحليل"""
|
462 |
+
identified_risks = self.analysis_results["identified_risks"]
|
463 |
+
|
464 |
+
# حساب إحصائيات المخاطر
|
465 |
+
total_risks = len(identified_risks)
|
466 |
+
high_risks = sum(1 for risk in identified_risks if risk["risk_score"] >= 6)
|
467 |
+
medium_risks = sum(1 for risk in identified_risks if 3 <= risk["risk_score"] < 6)
|
468 |
+
low_risks = sum(1 for risk in identified_risks if risk["risk_score"] < 3)
|
469 |
+
|
470 |
+
# حساب متوسط درجة المخاطرة
|
471 |
+
avg_risk_score = sum(risk["risk_score"] for risk in identified_risks) / total_risks if total_risks > 0 else 0
|
472 |
+
|
473 |
+
# تحديد أعلى فئات المخاطر
|
474 |
+
categories = self.analysis_results["risk_categories"]
|
475 |
+
top_categories = list(categories.keys())[:3] if len(categories) >= 3 else list(categories.keys())
|
476 |
+
|
477 |
+
# تحديد المخاطر الحرجة
|
478 |
+
critical_risks = [risk for risk in identified_risks if risk["risk_score"] >= 6]
|
479 |
+
critical_risks = sorted(critical_risks, key=lambda x: x["risk_score"], reverse=True)
|
480 |
+
top_critical_risks = critical_risks[:3] if len(critical_risks) >= 3 else critical_risks
|
481 |
+
|
482 |
+
# إنشاء ملخص التحليل
|
483 |
+
summary = {
|
484 |
+
"total_risks": total_risks,
|
485 |
+
"risk_distribution": {
|
486 |
+
"high_risks": high_risks,
|
487 |
+
"medium_risks": medium_risks,
|
488 |
+
"low_risks": low_risks
|
489 |
+
},
|
490 |
+
"avg_risk_score": avg_risk_score,
|
491 |
+
"top_risk_categories": top_categories,
|
492 |
+
"critical_risks": [risk["name"] for risk in top_critical_risks],
|
493 |
+
"overall_risk_level": self._determine_overall_risk_level(avg_risk_score, high_risks, total_risks),
|
494 |
+
"recommendations": self._generate_recommendations(method, high_risks, avg_risk_score)
|
495 |
+
}
|
496 |
+
|
497 |
+
self.analysis_results["summary"] = summary
|
498 |
+
|
499 |
+
def _determine_overall_risk_level(self, avg_risk_score, high_risks, total_risks):
|
500 |
+
"""تحديد مستوى المخاطرة الإجمالي"""
|
501 |
+
if avg_risk_score >= 5 or (high_risks / total_risks >= 0.3 if total_risks > 0 else False):
|
502 |
+
return "عالي"
|
503 |
+
elif avg_risk_score >= 3 or (high_risks / total_risks >= 0.1 if total_risks > 0 else False):
|
504 |
+
return "متوسط"
|
505 |
+
else:
|
506 |
+
return "منخفض"
|
507 |
+
|
508 |
+
def _generate_recommendations(self, method, high_risks, avg_risk_score):
|
509 |
+
"""توليد توصيات بناءً على نتائج التحليل"""
|
510 |
+
recommendations = []
|
511 |
+
|
512 |
+
if high_risks > 0:
|
513 |
+
recommendations.append("التركيز على استراتيجيات تخفيف المخاطر عالية الدرجة")
|
514 |
+
|
515 |
+
if avg_risk_score >= 4:
|
516 |
+
recommendations.append("إجراء مراجعة شاملة لخطة إدارة المخاطر بشكل دوري")
|
517 |
+
recommendations.append("تخصيص ميزانية احتياطية كافية للتعامل مع المخاطر المحتملة")
|
518 |
+
|
519 |
+
recommendations.append("توثيق المخاطر واستراتيجيات التخفيف في سجل المخاطر")
|
520 |
+
recommendations.append("تعيين مسؤولين محددين لمتابعة تنفيذ استراتيجيات التخفيف")
|
521 |
+
|
522 |
+
if method == "comprehensive":
|
523 |
+
recommendations.append("إجراء تحليل كمي للمخاطر لتقدير التأثير المالي والزمني")
|
524 |
+
recommendations.append("تطوير مؤشرات إنذار مبكر لرصد المخاطر قبل حدوثها")
|
525 |
+
recommendations.append("إشراك أصحاب المصلحة في عملية تحديد وتقييم المخاطر")
|
526 |
+
|
527 |
+
return recommendations
|
528 |
+
|
529 |
+
def get_analysis_status(self):
|
530 |
+
"""الحصول على حالة التحليل الحالي"""
|
531 |
+
if not self.analysis_in_progress:
|
532 |
+
if not self.analysis_results:
|
533 |
+
return {"status": "لا يوجد تحليل جارٍ"}
|
534 |
+
else:
|
535 |
+
return {"status": self.analysis_results.get("status", "غير معروف")}
|
536 |
+
|
537 |
+
return {
|
538 |
+
"status": "جاري التحليل",
|
539 |
+
"project_id": self.current_project,
|
540 |
+
"start_time": self.analysis_results.get("analysis_start_time")
|
541 |
+
}
|
542 |
+
|
543 |
+
def get_analysis_results(self):
|
544 |
+
"""الحصول على نتائج التحليل"""
|
545 |
+
return self.analysis_results
|
546 |
+
|
547 |
+
def export_analysis_results(self, output_path=None):
|
548 |
+
"""تصدير نتائج التحليل إلى ملف JSON"""
|
549 |
+
if not self.analysis_results:
|
550 |
+
logger.warning("لا توجد نتائج تحليل للتصدير")
|
551 |
+
return None
|
552 |
+
|
553 |
+
if not output_path:
|
554 |
+
# إنشاء اسم ملف افتراضي
|
555 |
+
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
556 |
+
filename = f"risk_analysis_results_{timestamp}.json"
|
557 |
+
output_path = os.path.join(self.exports_path, filename)
|
558 |
+
|
559 |
+
try:
|
560 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
561 |
+
json.dump(self.analysis_results, f, ensure_ascii=False, indent=4)
|
562 |
+
|
563 |
+
logger.info(f"تم تصدير نتائج تحليل المخاطر إلى: {output_path}")
|
564 |
+
return output_path
|
565 |
+
|
566 |
+
except Exception as e:
|
567 |
+
logger.error(f"خطأ في تصدير نتائج تحليل المخاطر: {str(e)}")
|
568 |
+
return None
|
569 |
+
|
570 |
+
def import_analysis_results(self, input_path):
|
571 |
+
"""استيراد نتائج التحليل من ملف JSON"""
|
572 |
+
if not os.path.exists(input_path):
|
573 |
+
logger.error(f"ملف نتائج التحليل غير موجود: {input_path}")
|
574 |
+
return False
|
575 |
+
|
576 |
+
try:
|
577 |
+
with open(input_path, 'r', encoding='utf-8') as f:
|
578 |
+
self.analysis_results = json.load(f)
|
579 |
+
|
580 |
+
logger.info(f"تم استيراد نتائج تحليل المخاطر من: {input_path}")
|
581 |
+
return True
|
582 |
+
|
583 |
+
except Exception as e:
|
584 |
+
logger.error(f"خطأ في استيراد نتائج تحليل المخاطر: {str(e)}")
|
585 |
+
return False
|
modules/translation/translation_app.py
ADDED
@@ -0,0 +1,936 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة الترجمة - نظام تحليل المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import os
|
9 |
+
import sys
|
10 |
+
from pathlib import Path
|
11 |
+
import re
|
12 |
+
import datetime
|
13 |
+
|
14 |
+
# إضافة مسار المشروع للنظام
|
15 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
16 |
+
|
17 |
+
# استيراد محسن واجهة المستخدم
|
18 |
+
from styling.enhanced_ui import UIEnhancer
|
19 |
+
|
20 |
+
class TranslationApp:
|
21 |
+
"""تطبيق الترجمة"""
|
22 |
+
|
23 |
+
def __init__(self):
|
24 |
+
"""تهيئة تطبيق الترجمة"""
|
25 |
+
self.ui = UIEnhancer(page_title="الترجمة - نظام تحليل المناقصات", page_icon="🌐")
|
26 |
+
self.ui.apply_theme_colors()
|
27 |
+
|
28 |
+
# قائمة اللغات المدعومة
|
29 |
+
self.supported_languages = {
|
30 |
+
"ar": "العربية",
|
31 |
+
"en": "الإنجليزية",
|
32 |
+
"fr": "الفرنسية",
|
33 |
+
"de": "الألمانية",
|
34 |
+
"es": "الإسبانية",
|
35 |
+
"it": "الإيطالية",
|
36 |
+
"zh": "الصينية",
|
37 |
+
"ja": "اليابانية",
|
38 |
+
"ru": "الروسية",
|
39 |
+
"tr": "التركية"
|
40 |
+
}
|
41 |
+
|
42 |
+
# بيانات نموذجية للمصطلحات الفنية
|
43 |
+
self.technical_terms = [
|
44 |
+
{"ar": "كراسة الشروط", "en": "Terms and Conditions Document", "category": "مستندات"},
|
45 |
+
{"ar": "جدول الكميات", "en": "Bill of Quantities (BOQ)", "category": "مستندات"},
|
46 |
+
{"ar": "المواصفات الفنية", "en": "Technical Specifications", "category": "مستندات"},
|
47 |
+
{"ar": "ضمان ابتدائي", "en": "Bid Bond", "category": "ضمانات"},
|
48 |
+
{"ar": "ضمان حسن التنفيذ", "en": "Performance Bond", "category": "ضمانات"},
|
49 |
+
{"ar": "ضمان دفعة مقدمة", "en": "Advance Payment Guarantee", "category": "ضمانات"},
|
50 |
+
{"ar": "ضمان صيانة", "en": "Maintenance Bond", "category": "ضمانات"},
|
51 |
+
{"ar": "مناقصة عامة", "en": "Public Tender", "category": "أنواع المناقصات"},
|
52 |
+
{"ar": "مناقصة محدودة", "en": "Limited Tender", "category": "أنواع المناقصات"},
|
53 |
+
{"ar": "منافسة", "en": "Competition", "category": "أنواع المناقصات"},
|
54 |
+
{"ar": "أمر شراء", "en": "Purchase Order", "category": "عقود"},
|
55 |
+
{"ar": "عقد إطاري", "en": "Framework Agreement", "category": "عقود"},
|
56 |
+
{"ar": "عقد زمني", "en": "Time-based Contract", "category": "عقود"},
|
57 |
+
{"ar": "عقد تسليم مفتاح", "en": "Turnkey Contract", "category": "عقود"},
|
58 |
+
{"ar": "مقاول من الباطن", "en": "Subcontractor", "category": "أطراف"},
|
59 |
+
{"ar": "استشاري", "en": "Consultant", "category": "أطراف"},
|
60 |
+
{"ar": "مالك المشروع", "en": "Project Owner", "category": "أطراف"},
|
61 |
+
{"ar": "مدير المشروع", "en": "Project Manager", "category": "أطراف"},
|
62 |
+
{"ar": "مهندس الموقع", "en": "Site Engineer", "category": "أطراف"},
|
63 |
+
{"ar": "مراقب الجودة", "en": "Quality Control", "category": "أطراف"},
|
64 |
+
{"ar": "أعمال مدنية", "en": "Civil Works", "category": "أعمال"},
|
65 |
+
{"ar": "أعمال كهربائية", "en": "Electrical Works", "category": "أعمال"},
|
66 |
+
{"ar": "أعمال ميكانيكية", "en": "Mechanical Works", "category": "أعمال"},
|
67 |
+
{"ar": "أعمال معمارية", "en": "Architectural Works", "category": "أعمال"},
|
68 |
+
{"ar": "أعمال تشطيبات", "en": "Finishing Works", "category": "أعمال"},
|
69 |
+
{"ar": "غرامة تأخير", "en": "Delay Penalty", "category": "شروط"},
|
70 |
+
{"ar": "مدة التنفيذ", "en": "Execution Period", "category": "شروط"},
|
71 |
+
{"ar": "فترة الضمان", "en": "Warranty Period", "category": "شروط"},
|
72 |
+
{"ar": "شروط الدفع", "en": "Payment Terms", "category": "شروط"},
|
73 |
+
{"ar": "تسوية النزاعات", "en": "Dispute Resolution", "category": "شروط"}
|
74 |
+
]
|
75 |
+
|
76 |
+
# بيانات نموذجية للمستندات المترجمة
|
77 |
+
self.translated_documents = [
|
78 |
+
{
|
79 |
+
"id": "TD001",
|
80 |
+
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
81 |
+
"source_language": "ar",
|
82 |
+
"target_language": "en",
|
83 |
+
"original_file": "specs_v2.0_ar.pdf",
|
84 |
+
"translated_file": "specs_v2.0_en.pdf",
|
85 |
+
"translation_date": "2025-03-15",
|
86 |
+
"translated_by": "أحمد محمد",
|
87 |
+
"status": "مكتمل",
|
88 |
+
"pages": 52,
|
89 |
+
"related_entity": "T-2025-001"
|
90 |
+
},
|
91 |
+
{
|
92 |
+
"id": "TD002",
|
93 |
+
"name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
|
94 |
+
"source_language": "ar",
|
95 |
+
"target_language": "en",
|
96 |
+
"original_file": "boq_v1.1_ar.xlsx",
|
97 |
+
"translated_file": "boq_v1.1_en.xlsx",
|
98 |
+
"translation_date": "2025-02-25",
|
99 |
+
"translated_by": "سارة عبدالله",
|
100 |
+
"status": "مكتمل",
|
101 |
+
"pages": 22,
|
102 |
+
"related_entity": "T-2025-001"
|
103 |
+
},
|
104 |
+
{
|
105 |
+
"id": "TD003",
|
106 |
+
"name": "المخططات - مناقصة إنشاء مبنى إداري",
|
107 |
+
"source_language": "ar",
|
108 |
+
"target_language": "en",
|
109 |
+
"original_file": "drawings_v2.0_ar.pdf",
|
110 |
+
"translated_file": "drawings_v2.0_en.pdf",
|
111 |
+
"translation_date": "2025-03-20",
|
112 |
+
"translated_by": "محمد علي",
|
113 |
+
"status": "مكتمل",
|
114 |
+
"pages": 35,
|
115 |
+
"related_entity": "T-2025-001"
|
116 |
+
},
|
117 |
+
{
|
118 |
+
"id": "TD004",
|
119 |
+
"name": "كراسة الشروط - مناقصة صيانة طرق",
|
120 |
+
"source_language": "ar",
|
121 |
+
"target_language": "en",
|
122 |
+
"original_file": "specs_v1.1_ar.pdf",
|
123 |
+
"translated_file": "specs_v1.1_en.pdf",
|
124 |
+
"translation_date": "2025-03-25",
|
125 |
+
"translated_by": "فاطمة أحمد",
|
126 |
+
"status": "مكتمل",
|
127 |
+
"pages": 34,
|
128 |
+
"related_entity": "T-2025-002"
|
129 |
+
},
|
130 |
+
{
|
131 |
+
"id": "TD005",
|
132 |
+
"name": "جدول الكميات - مناقصة صيانة طرق",
|
133 |
+
"source_language": "ar",
|
134 |
+
"target_language": "en",
|
135 |
+
"original_file": "boq_v1.0_ar.xlsx",
|
136 |
+
"translated_file": "boq_v1.0_en.xlsx",
|
137 |
+
"translation_date": "2025-03-10",
|
138 |
+
"translated_by": "خالد عمر",
|
139 |
+
"status": "مكتمل",
|
140 |
+
"pages": 15,
|
141 |
+
"related_entity": "T-2025-002"
|
142 |
+
},
|
143 |
+
{
|
144 |
+
"id": "TD006",
|
145 |
+
"name": "كراسة الشروط - مناقصة توريد معدات",
|
146 |
+
"source_language": "en",
|
147 |
+
"target_language": "ar",
|
148 |
+
"original_file": "specs_v1.0_en.pdf",
|
149 |
+
"translated_file": "specs_v1.0_ar.pdf",
|
150 |
+
"translation_date": "2025-02-15",
|
151 |
+
"translated_by": "أحمد محمد",
|
152 |
+
"status": "مكتمل",
|
153 |
+
"pages": 28,
|
154 |
+
"related_entity": "T-2025-003"
|
155 |
+
},
|
156 |
+
{
|
157 |
+
"id": "TD007",
|
158 |
+
"name": "عقد توريد - مناقصة توريد معدات",
|
159 |
+
"source_language": "en",
|
160 |
+
"target_language": "ar",
|
161 |
+
"original_file": "contract_v1.0_en.pdf",
|
162 |
+
"translated_file": "contract_v1.0_ar.pdf",
|
163 |
+
"translation_date": "2025-03-05",
|
164 |
+
"translated_by": "سارة عبدالله",
|
165 |
+
"status": "مكتمل",
|
166 |
+
"pages": 20,
|
167 |
+
"related_entity": "T-2025-003"
|
168 |
+
},
|
169 |
+
{
|
170 |
+
"id": "TD008",
|
171 |
+
"name": "كراسة الشروط - مناقصة تجهيز مختبرات",
|
172 |
+
"source_language": "ar",
|
173 |
+
"target_language": "en",
|
174 |
+
"original_file": "specs_v1.0_ar.pdf",
|
175 |
+
"translated_file": "specs_v1.0_en.pdf",
|
176 |
+
"translation_date": "2025-03-28",
|
177 |
+
"translated_by": "محمد علي",
|
178 |
+
"status": "قيد التنفيذ",
|
179 |
+
"pages": 30,
|
180 |
+
"related_entity": "T-2025-004"
|
181 |
+
}
|
182 |
+
]
|
183 |
+
|
184 |
+
# بيانات نموذجية للنصوص المترجمة
|
185 |
+
self.sample_translations = {
|
186 |
+
"text1": {
|
187 |
+
"ar": """
|
188 |
+
# كراسة الشروط والمواصفات
|
189 |
+
## مناقصة إنشاء مبنى إداري
|
190 |
+
|
191 |
+
### 1. مقدمة
|
192 |
+
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
|
193 |
+
|
194 |
+
### 2. نطاق العمل
|
195 |
+
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
|
196 |
+
- أعمال الهيكل الإنشائي
|
197 |
+
- أعمال التشطيبات الداخلية والخارجية
|
198 |
+
- أعمال الكهرباء والميكانيكا
|
199 |
+
- أعمال تنسيق الموقع
|
200 |
+
- أعمال أنظمة الأمن والسلامة
|
201 |
+
- أعمال أنظمة المباني الذكية
|
202 |
+
""",
|
203 |
+
|
204 |
+
"en": """
|
205 |
+
# Terms and Conditions Document
|
206 |
+
## Administrative Building Construction Tender
|
207 |
+
|
208 |
+
### 1. Introduction
|
209 |
+
Peninsula Contracting Company invites specialized companies to submit their offers for the implementation of an administrative building construction project in Riyadh.
|
210 |
+
|
211 |
+
### 2. Scope of Work
|
212 |
+
The scope of work includes the design and implementation of a 6-floor administrative building with a total area of 6000 square meters, including:
|
213 |
+
- Structural works
|
214 |
+
- Interior and exterior finishing works
|
215 |
+
- Electrical and mechanical works
|
216 |
+
- Site coordination works
|
217 |
+
- Security and safety systems works
|
218 |
+
- Smart building systems works
|
219 |
+
"""
|
220 |
+
},
|
221 |
+
|
222 |
+
"text2": {
|
223 |
+
"ar": """
|
224 |
+
### 3. المواصفات الفنية
|
225 |
+
#### 3.1 أعمال الخرسانة
|
226 |
+
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
|
227 |
+
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
228 |
+
- يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
|
229 |
+
|
230 |
+
#### 3.2 أعمال التشطيبات
|
231 |
+
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
232 |
+
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
233 |
+
- يجب استخدام زجاج عاكس للحرارة للواجهات
|
234 |
+
- يجب استخدام مواد صديقة للبيئة
|
235 |
+
""",
|
236 |
+
|
237 |
+
"en": """
|
238 |
+
### 3. Technical Specifications
|
239 |
+
#### 3.1 Concrete Works
|
240 |
+
- Reinforced concrete must have a strength of not less than 40 Newton/mm²
|
241 |
+
- Reinforcement steel must comply with Saudi specifications
|
242 |
+
- Concrete additives must be used to increase its resistance to weather conditions
|
243 |
+
|
244 |
+
#### 3.2 Finishing Works
|
245 |
+
- High-quality materials must be used for interior finishes
|
246 |
+
- Exterior facades must be weather-resistant
|
247 |
+
- Heat-reflective glass must be used for facades
|
248 |
+
- Environmentally friendly materials must be used
|
249 |
+
"""
|
250 |
+
}
|
251 |
+
}
|
252 |
+
|
253 |
+
def run(self):
|
254 |
+
"""تشغيل تطبيق الترجمة"""
|
255 |
+
# إنشاء قائمة العناصر
|
256 |
+
menu_items = [
|
257 |
+
{"name": "لوحة المعلومات", "icon": "house"},
|
258 |
+
{"name": "المناقصات والعقود", "icon": "file-text"},
|
259 |
+
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
260 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
261 |
+
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
262 |
+
{"name": "الموارد والتكاليف", "icon": "people"},
|
263 |
+
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
264 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
265 |
+
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
266 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
267 |
+
{"name": "الإشعارات", "icon": "bell"},
|
268 |
+
{"name": "مقارنة المستندات", "icon": "files"},
|
269 |
+
{"name": "الترجمة", "icon": "translate"},
|
270 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
271 |
+
{"name": "التقارير", "icon": "bar-chart"},
|
272 |
+
{"name": "الإعدادات", "icon": "gear"}
|
273 |
+
]
|
274 |
+
|
275 |
+
# إنشاء الشريط الجانبي
|
276 |
+
selected = self.ui.create_sidebar(menu_items)
|
277 |
+
|
278 |
+
# إنشاء ترويسة الصفحة
|
279 |
+
self.ui.create_header("الترجمة", "أدوات ترجمة المستندات والنصوص")
|
280 |
+
|
281 |
+
# إنشاء علامات تبويب للوظائف المختلفة
|
282 |
+
tabs = st.tabs(["ترجمة النصوص", "ترجمة المستندات", "قاموس المصطلحات", "المستندات المترجمة"])
|
283 |
+
|
284 |
+
# علامة تبويب ترجمة النصوص
|
285 |
+
with tabs[0]:
|
286 |
+
self.translate_text()
|
287 |
+
|
288 |
+
# علامة تبويب ترجمة المستندات
|
289 |
+
with tabs[1]:
|
290 |
+
self.translate_documents()
|
291 |
+
|
292 |
+
# علامة تبويب قاموس المصطلحات
|
293 |
+
with tabs[2]:
|
294 |
+
self.technical_terms_dictionary()
|
295 |
+
|
296 |
+
# علامة تبويب المستندات المترجمة
|
297 |
+
with tabs[3]:
|
298 |
+
self.show_translated_documents()
|
299 |
+
|
300 |
+
def translate_text(self):
|
301 |
+
"""ترجمة النصوص"""
|
302 |
+
st.markdown("### ترجمة النصوص")
|
303 |
+
|
304 |
+
# اختيار لغات الترجمة
|
305 |
+
col1, col2 = st.columns(2)
|
306 |
+
|
307 |
+
with col1:
|
308 |
+
source_language = st.selectbox(
|
309 |
+
"لغة المصدر",
|
310 |
+
options=list(self.supported_languages.keys()),
|
311 |
+
format_func=lambda x: self.supported_languages[x],
|
312 |
+
index=0 # العربية كلغة افتراضية
|
313 |
+
)
|
314 |
+
|
315 |
+
with col2:
|
316 |
+
# استبعاد لغة المصدر من خيارات لغة الهدف
|
317 |
+
target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
|
318 |
+
target_language = st.selectbox(
|
319 |
+
"لغة الهدف",
|
320 |
+
options=list(target_languages.keys()),
|
321 |
+
format_func=lambda x: self.supported_languages[x],
|
322 |
+
index=0 # أول لغة متاحة
|
323 |
+
)
|
324 |
+
|
325 |
+
# خيارات الترجمة
|
326 |
+
st.markdown("#### خيارات الترجمة")
|
327 |
+
|
328 |
+
col1, col2, col3 = st.columns(3)
|
329 |
+
|
330 |
+
with col1:
|
331 |
+
translation_engine = st.radio(
|
332 |
+
"محرك الترجمة",
|
333 |
+
options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"]
|
334 |
+
)
|
335 |
+
|
336 |
+
with col2:
|
337 |
+
use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True)
|
338 |
+
|
339 |
+
with col3:
|
340 |
+
preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True)
|
341 |
+
|
342 |
+
# إدخال النص المراد ترجمته
|
343 |
+
st.markdown("#### النص المراد ترجمته")
|
344 |
+
|
345 |
+
# إضافة أمثلة نصية
|
346 |
+
examples = st.expander("أمثلة نصية")
|
347 |
+
with examples:
|
348 |
+
if st.button("مثال 1: مقدمة كراسة الشروط"):
|
349 |
+
source_text = self.sample_translations["text1"][source_language] if source_language in self.sample_translations["text1"] else self.sample_translations["text1"]["ar"]
|
350 |
+
elif st.button("مثال 2: المواصفات الفنية"):
|
351 |
+
source_text = self.sample_translations["text2"][source_language] if source_language in self.sample_translations["text2"] else self.sample_translations["text2"]["ar"]
|
352 |
+
else:
|
353 |
+
source_text = ""
|
354 |
+
|
355 |
+
if "source_text" not in locals():
|
356 |
+
source_text = ""
|
357 |
+
|
358 |
+
source_text = st.text_area(
|
359 |
+
"أدخل النص المراد ترجمته",
|
360 |
+
value=source_text,
|
361 |
+
height=200
|
362 |
+
)
|
363 |
+
|
364 |
+
# زر الترجمة
|
365 |
+
if st.button("ترجمة النص", use_container_width=True):
|
366 |
+
if not source_text:
|
367 |
+
st.error("يرجى إدخال النص المراد ترجمته")
|
368 |
+
else:
|
369 |
+
# في تطبيق حقيقي، سيتم استدعاء واجهة برمجة التطبيقات للترجمة
|
370 |
+
# هنا نستخدم النصوص النموذجية المحددة مسبقاً للعرض
|
371 |
+
|
372 |
+
with st.spinner("جاري الترجمة..."):
|
373 |
+
# محاكاة تأخير الترجمة
|
374 |
+
import time
|
375 |
+
time.sleep(1)
|
376 |
+
|
377 |
+
# التحقق من وجود ترجمة نموذجية
|
378 |
+
if source_language == "ar" and target_language == "en" and source_text.strip() in [self.sample_translations["text1"]["ar"].strip(), self.sample_translations["text2"]["ar"].strip()]:
|
379 |
+
if source_text.strip() == self.sample_translations["text1"]["ar"].strip():
|
380 |
+
translated_text = self.sample_translations["text1"]["en"]
|
381 |
+
else:
|
382 |
+
translated_text = self.sample_translations["text2"]["en"]
|
383 |
+
elif source_language == "en" and target_language == "ar" and source_text.strip() in [self.sample_translations["text1"]["en"].strip(), self.sample_translations["text2"]["en"].strip()]:
|
384 |
+
if source_text.strip() == self.sample_translations["text1"]["en"].strip():
|
385 |
+
translated_text = self.sample_translations["text1"]["ar"]
|
386 |
+
else:
|
387 |
+
translated_text = self.sample_translations["text2"]["ar"]
|
388 |
+
else:
|
389 |
+
# ترجمة نموذجية للعرض فقط
|
390 |
+
translated_text = f"[هذا نص مترجم نموذجي من {self.supported_languages[source_language]} إلى {self.supported_languages[target_language]}]\n\n{source_text}"
|
391 |
+
|
392 |
+
# عرض النص المترجم
|
393 |
+
st.markdown("#### النص المترجم")
|
394 |
+
st.text_area(
|
395 |
+
"النص المترجم",
|
396 |
+
value=translated_text,
|
397 |
+
height=200
|
398 |
+
)
|
399 |
+
|
400 |
+
# أزرار إضافية
|
401 |
+
col1, col2, col3 = st.columns(3)
|
402 |
+
|
403 |
+
with col1:
|
404 |
+
if st.button("نسخ النص المترجم", use_container_width=True):
|
405 |
+
st.success("تم نسخ النص المترجم إلى الحافظة")
|
406 |
+
|
407 |
+
with col2:
|
408 |
+
if st.button("حفظ الترجمة", use_container_width=True):
|
409 |
+
st.success("تم حفظ الترجمة بنجاح")
|
410 |
+
|
411 |
+
with col3:
|
412 |
+
if st.button("تصدير كملف", use_container_width=True):
|
413 |
+
st.success("تم تصدير الترجمة كملف بنجاح")
|
414 |
+
|
415 |
+
# عرض إحصائيات الترجمة
|
416 |
+
st.markdown("#### إحصائيات الترجمة")
|
417 |
+
|
418 |
+
col1, col2, col3, col4 = st.columns(4)
|
419 |
+
|
420 |
+
with col1:
|
421 |
+
self.ui.create_metric_card(
|
422 |
+
"عدد الكلمات",
|
423 |
+
str(len(source_text.split())),
|
424 |
+
None,
|
425 |
+
self.ui.COLORS['primary']
|
426 |
+
)
|
427 |
+
|
428 |
+
with col2:
|
429 |
+
self.ui.create_metric_card(
|
430 |
+
"عدد الأحرف",
|
431 |
+
str(len(source_text)),
|
432 |
+
None,
|
433 |
+
self.ui.COLORS['secondary']
|
434 |
+
)
|
435 |
+
|
436 |
+
with col3:
|
437 |
+
self.ui.create_metric_card(
|
438 |
+
"وقت الترجمة",
|
439 |
+
"1.2 ثانية",
|
440 |
+
None,
|
441 |
+
self.ui.COLORS['success']
|
442 |
+
)
|
443 |
+
|
444 |
+
with col4:
|
445 |
+
self.ui.create_metric_card(
|
446 |
+
"المصطلحات الفنية",
|
447 |
+
"5",
|
448 |
+
None,
|
449 |
+
self.ui.COLORS['accent']
|
450 |
+
)
|
451 |
+
|
452 |
+
def translate_documents(self):
|
453 |
+
"""ترجمة المستندات"""
|
454 |
+
st.markdown("### ترجمة المستندات")
|
455 |
+
|
456 |
+
# اختيار لغات الترجمة
|
457 |
+
col1, col2 = st.columns(2)
|
458 |
+
|
459 |
+
with col1:
|
460 |
+
source_language = st.selectbox(
|
461 |
+
"لغة المصدر",
|
462 |
+
options=list(self.supported_languages.keys()),
|
463 |
+
format_func=lambda x: self.supported_languages[x],
|
464 |
+
index=0, # العربية كلغة افتراضية
|
465 |
+
key="doc_source_lang"
|
466 |
+
)
|
467 |
+
|
468 |
+
with col2:
|
469 |
+
# استبعاد لغة المصدر من خيارات لغة الهدف
|
470 |
+
target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
|
471 |
+
target_language = st.selectbox(
|
472 |
+
"لغة الهدف",
|
473 |
+
options=list(target_languages.keys()),
|
474 |
+
format_func=lambda x: self.supported_languages[x],
|
475 |
+
index=0, # أول لغة متاحة
|
476 |
+
key="doc_target_lang"
|
477 |
+
)
|
478 |
+
|
479 |
+
# تحميل المستند
|
480 |
+
st.markdown("#### تحميل المستند")
|
481 |
+
|
482 |
+
uploaded_file = st.file_uploader("اختر المستند المراد ترجمته", type=["pdf", "docx", "xlsx", "txt"])
|
483 |
+
|
484 |
+
if uploaded_file is not None:
|
485 |
+
st.success(f"تم تحميل الملف: {uploaded_file.name}")
|
486 |
+
|
487 |
+
# عرض معلومات الملف
|
488 |
+
file_details = {
|
489 |
+
"اسم الملف": uploaded_file.name,
|
490 |
+
"نوع الملف": uploaded_file.type,
|
491 |
+
"حجم الملف": f"{uploaded_file.size / 1024:.1f} كيلوبايت"
|
492 |
+
}
|
493 |
+
|
494 |
+
st.json(file_details)
|
495 |
+
|
496 |
+
# خيارات الترجمة
|
497 |
+
st.markdown("#### خيارات الترجمة")
|
498 |
+
|
499 |
+
col1, col2 = st.columns(2)
|
500 |
+
|
501 |
+
with col1:
|
502 |
+
translation_engine = st.radio(
|
503 |
+
"محرك الترجمة",
|
504 |
+
options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"],
|
505 |
+
key="doc_engine"
|
506 |
+
)
|
507 |
+
|
508 |
+
use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True, key="doc_terms")
|
509 |
+
|
510 |
+
with col2:
|
511 |
+
preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True, key="doc_format")
|
512 |
+
|
513 |
+
translate_images = st.checkbox("ترجمة النصوص في الصور", value=False)
|
514 |
+
|
515 |
+
maintain_layout = st.checkbox("الحفاظ على تخطيط المستند", value=True)
|
516 |
+
|
517 |
+
# معلومات إضافية
|
518 |
+
st.markdown("#### معلومات إضافية")
|
519 |
+
|
520 |
+
col1, col2 = st.columns(2)
|
521 |
+
|
522 |
+
with col1:
|
523 |
+
document_name = st.text_input("اسم المستند")
|
524 |
+
|
525 |
+
with col2:
|
526 |
+
related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة أو المشروع)")
|
527 |
+
|
528 |
+
# زر بدء الترجمة
|
529 |
+
if st.button("بدء ترجمة المستند", use_container_width=True):
|
530 |
+
if uploaded_file is None:
|
531 |
+
st.error("يرجى تحميل المستند المراد ترجمته")
|
532 |
+
else:
|
533 |
+
# في تطبيق حقيقي، سيتم إرسال المستند إلى خدمة الترجمة
|
534 |
+
# هنا نعرض محاكاة لعملية الترجمة
|
535 |
+
|
536 |
+
progress_bar = st.progress(0)
|
537 |
+
status_text = st.empty()
|
538 |
+
|
539 |
+
# محاكاة تقدم الترجمة
|
540 |
+
import time
|
541 |
+
for i in range(101):
|
542 |
+
progress_bar.progress(i)
|
543 |
+
|
544 |
+
if i < 10:
|
545 |
+
status_text.text("جاري تحليل المستند...")
|
546 |
+
elif i < 30:
|
547 |
+
status_text.text("جاري استخراج النصوص...")
|
548 |
+
elif i < 70:
|
549 |
+
status_text.text("جاري ترجمة المحتوى...")
|
550 |
+
elif i < 90:
|
551 |
+
status_text.text("جاري إعادة بناء المستند...")
|
552 |
+
else:
|
553 |
+
status_text.text("جاري إنهاء الترجمة...")
|
554 |
+
|
555 |
+
time.sleep(0.05)
|
556 |
+
|
557 |
+
# عرض نتيجة الترجمة
|
558 |
+
st.success("تمت ترجمة المستند بنجاح!")
|
559 |
+
|
560 |
+
# إنشاء اسم الملف المترجم
|
561 |
+
file_name_parts = uploaded_file.name.split('.')
|
562 |
+
translated_file_name = f"{'.'.join(file_name_parts[:-1])}_{target_language}.{file_name_parts[-1]}"
|
563 |
+
|
564 |
+
# عرض معلومات الملف المترجم
|
565 |
+
st.markdown("#### معلومات الملف المترجم")
|
566 |
+
|
567 |
+
col1, col2 = st.columns(2)
|
568 |
+
|
569 |
+
with col1:
|
570 |
+
st.markdown(f"**اسم الملف:** {translated_file_name}")
|
571 |
+
st.markdown(f"**لغة المصدر:** {self.supported_languages[source_language]}")
|
572 |
+
st.markdown(f"**لغة الهدف:** {self.supported_languages[target_language]}")
|
573 |
+
|
574 |
+
with col2:
|
575 |
+
st.markdown(f"**محرك الترجمة:** {translation_engine}")
|
576 |
+
st.markdown(f"**تاريخ الترجمة:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
|
577 |
+
st.markdown(f"**حالة الترجمة:** مكتمل")
|
578 |
+
|
579 |
+
# أزرار إضافية
|
580 |
+
col1, col2, col3 = st.columns(3)
|
581 |
+
|
582 |
+
with col1:
|
583 |
+
if st.button("تنزيل الملف المترجم", use_container_width=True):
|
584 |
+
st.success("تم بدء تنزيل الملف المترجم")
|
585 |
+
|
586 |
+
with col2:
|
587 |
+
if st.button("حفظ في المستندات المترجمة", use_container_width=True):
|
588 |
+
st.success("تم حفظ الملف في المستندات المترجمة")
|
589 |
+
|
590 |
+
with col3:
|
591 |
+
if st.button("مشاركة الملف", use_container_width=True):
|
592 |
+
st.success("تم نسخ رابط مشاركة الملف")
|
593 |
+
|
594 |
+
# عرض إحصائيات الترجمة
|
595 |
+
st.markdown("#### إحصائيات الترجمة")
|
596 |
+
|
597 |
+
col1, col2, col3, col4 = st.columns(4)
|
598 |
+
|
599 |
+
with col1:
|
600 |
+
self.ui.create_metric_card(
|
601 |
+
"عدد الصفحات",
|
602 |
+
"12",
|
603 |
+
None,
|
604 |
+
self.ui.COLORS['primary']
|
605 |
+
)
|
606 |
+
|
607 |
+
with col2:
|
608 |
+
self.ui.create_metric_card(
|
609 |
+
"عدد الكلمات",
|
610 |
+
"2,450",
|
611 |
+
None,
|
612 |
+
self.ui.COLORS['secondary']
|
613 |
+
)
|
614 |
+
|
615 |
+
with col3:
|
616 |
+
self.ui.create_metric_card(
|
617 |
+
"وقت الترجمة",
|
618 |
+
"45 ثانية",
|
619 |
+
None,
|
620 |
+
self.ui.COLORS['success']
|
621 |
+
)
|
622 |
+
|
623 |
+
with col4:
|
624 |
+
self.ui.create_metric_card(
|
625 |
+
"المصطلحات الفنية",
|
626 |
+
"28",
|
627 |
+
None,
|
628 |
+
self.ui.COLORS['accent']
|
629 |
+
)
|
630 |
+
|
631 |
+
def technical_terms_dictionary(self):
|
632 |
+
"""قاموس المصطلحات الفنية"""
|
633 |
+
st.markdown("### قاموس المصطلحات الفنية")
|
634 |
+
|
635 |
+
# إضافة مصطلح جديد
|
636 |
+
with st.expander("إضافة مصطلح جديد"):
|
637 |
+
with st.form("add_term_form"):
|
638 |
+
col1, col2, col3 = st.columns(3)
|
639 |
+
|
640 |
+
with col1:
|
641 |
+
term_ar = st.text_input("المصطلح بالعربية")
|
642 |
+
|
643 |
+
with col2:
|
644 |
+
term_en = st.text_input("المصطلح بالإنجليزية")
|
645 |
+
|
646 |
+
with col3:
|
647 |
+
term_category = st.selectbox(
|
648 |
+
"الفئة",
|
649 |
+
options=["مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
|
650 |
+
)
|
651 |
+
|
652 |
+
# زر إضافة المصطلح
|
653 |
+
submit_button = st.form_submit_button("إضافة المصطلح")
|
654 |
+
|
655 |
+
if submit_button:
|
656 |
+
if not term_ar or not term_en:
|
657 |
+
st.error("يرجى ملء جميع الحقول المطلوبة")
|
658 |
+
else:
|
659 |
+
# في تطبيق حقيقي، سيتم إضافة المصطلح إلى قاعدة البيانات
|
660 |
+
st.success("تمت إضافة المصطلح بنجاح")
|
661 |
+
|
662 |
+
# البحث في المصطلحات
|
663 |
+
st.markdown("#### البحث في المصطلحات")
|
664 |
+
|
665 |
+
col1, col2, col3 = st.columns(3)
|
666 |
+
|
667 |
+
with col1:
|
668 |
+
search_term = st.text_input("البحث عن مصطلح")
|
669 |
+
|
670 |
+
with col2:
|
671 |
+
search_language = st.radio(
|
672 |
+
"لغة البحث",
|
673 |
+
options=["الكل", "العربية", "الإنجليزية"],
|
674 |
+
horizontal=True
|
675 |
+
)
|
676 |
+
|
677 |
+
with col3:
|
678 |
+
category_filter = st.selectbox(
|
679 |
+
"تصفية حسب الفئة",
|
680 |
+
options=["الكل", "مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
|
681 |
+
)
|
682 |
+
|
683 |
+
# تطبيق الفلاتر
|
684 |
+
filtered_terms = self.technical_terms
|
685 |
+
|
686 |
+
if search_term:
|
687 |
+
if search_language == "العربية":
|
688 |
+
filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower()]
|
689 |
+
elif search_language == "الإنجليزية":
|
690 |
+
filtered_terms = [term for term in filtered_terms if search_term.lower() in term["en"].lower()]
|
691 |
+
else:
|
692 |
+
filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower() or search_term.lower() in term["en"].lower()]
|
693 |
+
|
694 |
+
if category_filter != "الكل":
|
695 |
+
filtered_terms = [term for term in filtered_terms if term["category"] == category_filter]
|
696 |
+
|
697 |
+
# عرض المصطلحات
|
698 |
+
st.markdown("#### المصطلحات الفنية")
|
699 |
+
|
700 |
+
if not filtered_terms:
|
701 |
+
st.info("لا توجد مصطلحات تطابق معايير البحث")
|
702 |
+
else:
|
703 |
+
# تحويل البيانات إلى DataFrame
|
704 |
+
terms_df = pd.DataFrame(filtered_terms)
|
705 |
+
|
706 |
+
# إعادة تسمية الأعمدة
|
707 |
+
terms_df = terms_df.rename(columns={
|
708 |
+
"ar": "المصطلح بالعربية",
|
709 |
+
"en": "المصطلح بالإنجليزية",
|
710 |
+
"category": "الفئة"
|
711 |
+
})
|
712 |
+
|
713 |
+
# عرض الجدول
|
714 |
+
st.dataframe(
|
715 |
+
terms_df,
|
716 |
+
use_container_width=True,
|
717 |
+
hide_index=True
|
718 |
+
)
|
719 |
+
|
720 |
+
# أزرار إضافية
|
721 |
+
col1, col2 = st.columns([1, 5])
|
722 |
+
|
723 |
+
with col1:
|
724 |
+
if st.button("تصدير القاموس", use_container_width=True):
|
725 |
+
st.success("تم تصدير القاموس بنجاح")
|
726 |
+
|
727 |
+
# عرض إحصائيات القاموس
|
728 |
+
st.markdown("#### إحصائيات القاموس")
|
729 |
+
|
730 |
+
# حساب عدد المصطلحات في كل فئة
|
731 |
+
category_counts = {}
|
732 |
+
for term in self.technical_terms:
|
733 |
+
if term["category"] not in category_counts:
|
734 |
+
category_counts[term["category"]] = 0
|
735 |
+
category_counts[term["category"]] += 1
|
736 |
+
|
737 |
+
# عرض الإحصائيات
|
738 |
+
col1, col2 = st.columns(2)
|
739 |
+
|
740 |
+
with col1:
|
741 |
+
st.markdown("##### عدد المصطلحات حسب الفئة")
|
742 |
+
|
743 |
+
# تحويل البيانات إلى DataFrame
|
744 |
+
category_df = pd.DataFrame({
|
745 |
+
"الفئة": list(category_counts.keys()),
|
746 |
+
"العدد": list(category_counts.values())
|
747 |
+
})
|
748 |
+
|
749 |
+
# عرض الرسم البياني
|
750 |
+
st.bar_chart(category_df.set_index("الفئة"))
|
751 |
+
|
752 |
+
with col2:
|
753 |
+
st.markdown("##### إحصائيات عامة")
|
754 |
+
|
755 |
+
total_terms = len(self.technical_terms)
|
756 |
+
categories_count = len(category_counts)
|
757 |
+
|
758 |
+
st.markdown(f"**إجمالي المصطلحات:** {total_terms}")
|
759 |
+
st.markdown(f"**عدد الفئات:** {categories_count}")
|
760 |
+
st.markdown(f"**متوسط المصطلحات لكل فئة:** {total_terms / categories_count:.1f}")
|
761 |
+
st.markdown(f"**آخر تحديث للقاموس:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
|
762 |
+
|
763 |
+
def show_translated_documents(self):
|
764 |
+
"""عرض المستندات المترجمة"""
|
765 |
+
st.markdown("### المستندات المترجمة")
|
766 |
+
|
767 |
+
# إنشاء فلاتر للمستندات
|
768 |
+
col1, col2, col3 = st.columns(3)
|
769 |
+
|
770 |
+
with col1:
|
771 |
+
entity_filter = st.selectbox(
|
772 |
+
"تصفية حسب الكيان",
|
773 |
+
options=["الكل"] + list(set([doc["related_entity"] for doc in self.translated_documents]))
|
774 |
+
)
|
775 |
+
|
776 |
+
with col2:
|
777 |
+
language_pair_filter = st.selectbox(
|
778 |
+
"تصفية حسب زوج اللغات",
|
779 |
+
options=["الكل"] + list(set([f"{doc['source_language']} -> {doc['target_language']}" for doc in self.translated_documents]))
|
780 |
+
)
|
781 |
+
|
782 |
+
with col3:
|
783 |
+
status_filter = st.selectbox(
|
784 |
+
"تصفية حسب الحالة",
|
785 |
+
options=["الكل", "مكتمل", "قيد التنفيذ"]
|
786 |
+
)
|
787 |
+
|
788 |
+
# تطبيق الفلاتر
|
789 |
+
filtered_docs = self.translated_documents
|
790 |
+
|
791 |
+
if entity_filter != "الكل":
|
792 |
+
filtered_docs = [doc for doc in filtered_docs if doc["related_entity"] == entity_filter]
|
793 |
+
|
794 |
+
if language_pair_filter != "الكل":
|
795 |
+
source_lang, target_lang = language_pair_filter.split(" -> ")
|
796 |
+
filtered_docs = [doc for doc in filtered_docs if doc["source_language"] == source_lang and doc["target_language"] == target_lang]
|
797 |
+
|
798 |
+
if status_filter != "الكل":
|
799 |
+
filtered_docs = [doc for doc in filtered_docs if doc["status"] == status_filter]
|
800 |
+
|
801 |
+
# عرض المستندات المترجمة
|
802 |
+
if not filtered_docs:
|
803 |
+
st.info("لا توجد مستندات مترجمة تطابق معايير التصفية")
|
804 |
+
else:
|
805 |
+
# تحويل البيانات إلى DataFrame
|
806 |
+
docs_df = pd.DataFrame(filtered_docs)
|
807 |
+
|
808 |
+
# تحويل رموز اللغات إلى أسماء اللغات
|
809 |
+
docs_df["source_language"] = docs_df["source_language"].map(self.supported_languages)
|
810 |
+
docs_df["target_language"] = docs_df["target_language"].map(self.supported_languages)
|
811 |
+
|
812 |
+
# إعادة ترتيب الأعمدة وتغيير أسمائها
|
813 |
+
display_df = docs_df[[
|
814 |
+
"id", "name", "source_language", "target_language", "translation_date", "status", "pages", "related_entity"
|
815 |
+
]].rename(columns={
|
816 |
+
"id": "الرقم",
|
817 |
+
"name": "اسم المستند",
|
818 |
+
"source_language": "لغة المصدر",
|
819 |
+
"target_language": "لغة الهدف",
|
820 |
+
"translation_date": "تاريخ الترجمة",
|
821 |
+
"status": "الحالة",
|
822 |
+
"pages": "عدد الصفحات",
|
823 |
+
"related_entity": "الكيان المرتبط"
|
824 |
+
})
|
825 |
+
|
826 |
+
# عرض الجدول
|
827 |
+
st.dataframe(
|
828 |
+
display_df,
|
829 |
+
use_container_width=True,
|
830 |
+
hide_index=True
|
831 |
+
)
|
832 |
+
|
833 |
+
# عرض تفاصيل المستند المحدد
|
834 |
+
st.markdown("#### تفاصيل المستند المترجم")
|
835 |
+
|
836 |
+
selected_doc_id = st.selectbox(
|
837 |
+
"اختر مستنداً لعرض التفاصيل",
|
838 |
+
options=[doc["id"] for doc in filtered_docs],
|
839 |
+
format_func=lambda x: next((f"{doc['id']} - {doc['name']}" for doc in filtered_docs if doc["id"] == x), "")
|
840 |
+
)
|
841 |
+
|
842 |
+
# العثور على المستند المحدد
|
843 |
+
selected_doc = next((doc for doc in filtered_docs if doc["id"] == selected_doc_id), None)
|
844 |
+
|
845 |
+
if selected_doc:
|
846 |
+
col1, col2 = st.columns(2)
|
847 |
+
|
848 |
+
with col1:
|
849 |
+
st.markdown(f"**اسم المستند:** {selected_doc['name']}")
|
850 |
+
st.markdown(f"**لغة المصدر:** {self.supported_languages[selected_doc['source_language']]}")
|
851 |
+
st.markdown(f"**لغة الهدف:** {self.supported_languages[selected_doc['target_language']]}")
|
852 |
+
st.markdown(f"**تاريخ الترجمة:** {selected_doc['translation_date']}")
|
853 |
+
|
854 |
+
with col2:
|
855 |
+
st.markdown(f"**الملف الأصلي:** {selected_doc['original_file']}")
|
856 |
+
st.markdown(f"**الملف المترجم:** {selected_doc['translated_file']}")
|
857 |
+
st.markdown(f"**المترجم:** {selected_doc['translated_by']}")
|
858 |
+
st.markdown(f"**الحالة:** {selected_doc['status']}")
|
859 |
+
|
860 |
+
# أزرار الإجراءات
|
861 |
+
col1, col2, col3 = st.columns(3)
|
862 |
+
|
863 |
+
with col1:
|
864 |
+
if st.button("تنزيل الملف الأصلي", use_container_width=True):
|
865 |
+
st.success("تم بدء تنزيل الملف الأصلي")
|
866 |
+
|
867 |
+
with col2:
|
868 |
+
if st.button("تنزيل الملف المترجم", use_container_width=True):
|
869 |
+
st.success("تم بدء تنزيل الملف المترجم")
|
870 |
+
|
871 |
+
with col3:
|
872 |
+
if st.button("مشاركة الملف المترجم", use_container_width=True):
|
873 |
+
st.success("تم نسخ رابط مشاركة الملف المترجم")
|
874 |
+
|
875 |
+
# عرض إحصائيات الترجمة
|
876 |
+
st.markdown("#### إحصائيات الترجمة")
|
877 |
+
|
878 |
+
col1, col2, col3 = st.columns(3)
|
879 |
+
|
880 |
+
with col1:
|
881 |
+
# إحصائيات حسب زوج اللغات
|
882 |
+
language_pairs = {}
|
883 |
+
for doc in self.translated_documents:
|
884 |
+
pair = f"{self.supported_languages[doc['source_language']]} -> {self.supported_languages[doc['target_language']]}"
|
885 |
+
if pair not in language_pairs:
|
886 |
+
language_pairs[pair] = 0
|
887 |
+
language_pairs[pair] += 1
|
888 |
+
|
889 |
+
st.markdown("##### المستندات حسب زوج اللغات")
|
890 |
+
|
891 |
+
# تحويل البيانات إلى DataFrame
|
892 |
+
language_df = pd.DataFrame({
|
893 |
+
"زوج اللغات": list(language_pairs.keys()),
|
894 |
+
"العدد": list(language_pairs.values())
|
895 |
+
})
|
896 |
+
|
897 |
+
# عرض الرسم البياني
|
898 |
+
st.bar_chart(language_df.set_index("زوج اللغات"))
|
899 |
+
|
900 |
+
with col2:
|
901 |
+
# إحصائيات حسب الكيان المرتبط
|
902 |
+
entity_counts = {}
|
903 |
+
for doc in self.translated_documents:
|
904 |
+
if doc["related_entity"] not in entity_counts:
|
905 |
+
entity_counts[doc["related_entity"]] = 0
|
906 |
+
entity_counts[doc["related_entity"]] += 1
|
907 |
+
|
908 |
+
st.markdown("##### المستندات حسب الكيان المرتبط")
|
909 |
+
|
910 |
+
# تحويل البيانات إلى DataFrame
|
911 |
+
entity_df = pd.DataFrame({
|
912 |
+
"الكيان المرتبط": list(entity_counts.keys()),
|
913 |
+
"العدد": list(entity_counts.values())
|
914 |
+
})
|
915 |
+
|
916 |
+
# عرض الرسم البياني
|
917 |
+
st.bar_chart(entity_df.set_index("الكيان المرتبط"))
|
918 |
+
|
919 |
+
with col3:
|
920 |
+
# إحصائيات عامة
|
921 |
+
total_docs = len(self.translated_documents)
|
922 |
+
completed_docs = len([doc for doc in self.translated_documents if doc["status"] == "مكتمل"])
|
923 |
+
in_progress_docs = len([doc for doc in self.translated_documents if doc["status"] == "قيد التنفيذ"])
|
924 |
+
total_pages = sum([doc["pages"] for doc in self.translated_documents])
|
925 |
+
|
926 |
+
st.markdown("##### إحصائيات عامة")
|
927 |
+
st.markdown(f"**إجمالي المستندات المترجمة:** {total_docs}")
|
928 |
+
st.markdown(f"**المستندات المكتملة:** {completed_docs}")
|
929 |
+
st.markdown(f"**المستندات قيد التنفيذ:** {in_progress_docs}")
|
930 |
+
st.markdown(f"**إجمالي الصفحات المترجمة:** {total_pages}")
|
931 |
+
st.markdown(f"**متوسط الصفحات لكل مستند:** {total_pages / total_docs:.1f}")
|
932 |
+
|
933 |
+
# تشغيل التطبيق
|
934 |
+
if __name__ == "__main__":
|
935 |
+
translation_app = TranslationApp()
|
936 |
+
translation_app.run()
|
requirements.txt
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# الاعتماديات الأساسية
|
2 |
+
streamlit==1.32.0
|
3 |
+
pandas==2.2.0
|
4 |
+
numpy==1.26.3
|
5 |
+
matplotlib==3.8.2
|
6 |
+
seaborn==0.13.1
|
7 |
+
plotly==5.18.0
|
8 |
+
|
9 |
+
# معالجة البيانات
|
10 |
+
openpyxl==3.1.2
|
11 |
+
xlrd==2.0.1
|
12 |
+
xlsxwriter==3.1.9
|
13 |
+
pyarrow==14.0.1
|
14 |
+
|
15 |
+
# تحليل المستندات
|
16 |
+
PyPDF2==3.0.1
|
17 |
+
python-docx==1.1.0
|
18 |
+
pdf2image==1.17.0
|
19 |
+
pytesseract==0.3.10
|
20 |
+
pymupdf==1.23.7
|
21 |
+
pdfplumber==0.10.3
|
22 |
+
opencv-python-headless==4.8.1.78
|
23 |
+
# poppler-utils ← يُثبت من apt
|
24 |
+
|
25 |
+
# معالجة اللغة العربية
|
26 |
+
arabic-reshaper==3.0.0
|
27 |
+
python-bidi==0.4.2
|
28 |
+
langdetect==1.0.9
|
29 |
+
farasapy==0.0.14
|
30 |
+
# cameltools==1.1.0
|
31 |
+
|
32 |
+
# الذكاء الاصطناعي والتعلم الآلي
|
33 |
+
scikit-learn==1.4.0
|
34 |
+
transformers==4.39.3
|
35 |
+
torch==2.1.2
|
36 |
+
nltk==3.8.1
|
37 |
+
gensim==4.3.2
|
38 |
+
openai==1.69.0
|
39 |
+
anthropic==0.5.0
|
40 |
+
pydantic==2.3.0
|
41 |
+
joblib==1.3.2
|
42 |
+
|
43 |
+
# قواعد البيانات
|
44 |
+
SQLAlchemy==2.0.25
|
45 |
+
SQLAlchemy-Utils==0.41.1
|
46 |
+
alembic==1.13.1
|
47 |
+
sqlite-utils==3.35.1
|
48 |
+
|
49 |
+
# مكونات واجهة المستخدم
|
50 |
+
streamlit-option-menu==0.3.2
|
51 |
+
streamlit-elements==0.1.0
|
52 |
+
streamlit-aggrid==0.3.4.post3
|
53 |
+
streamlit-authenticator==0.2.3
|
54 |
+
streamlit-extras==0.3.5
|
55 |
+
streamlit-echarts==0.4.0
|
56 |
+
streamlit-image-coordinates==0.1.6
|
57 |
+
|
58 |
+
# أدوات وتبعيات إضافية
|
59 |
+
pycountry==23.12.11
|
60 |
+
watchdog==3.0
|
styling/charts.py
ADDED
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
مولد الرسوم البيانية لنظام إدارة المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import numpy as np
|
7 |
+
import matplotlib.pyplot as plt
|
8 |
+
import matplotlib as mpl
|
9 |
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
10 |
+
import tkinter as tk
|
11 |
+
import customtkinter as ctk
|
12 |
+
|
13 |
+
class ChartGenerator:
|
14 |
+
"""فئة مولد الرسوم البيانية"""
|
15 |
+
|
16 |
+
def __init__(self, theme):
|
17 |
+
"""تهيئة مولد الرسوم البيانية"""
|
18 |
+
self.theme = theme
|
19 |
+
|
20 |
+
# تحديد مسار مجلد الرسوم البيانية
|
21 |
+
self.charts_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "charts")
|
22 |
+
|
23 |
+
# إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
|
24 |
+
os.makedirs(self.charts_dir, exist_ok=True)
|
25 |
+
|
26 |
+
# تهيئة نمط الرسوم البيانية
|
27 |
+
self._setup_chart_style()
|
28 |
+
|
29 |
+
def _setup_chart_style(self):
|
30 |
+
"""إعداد نمط الرسوم البيانية"""
|
31 |
+
# تعيين نمط الرسوم البيانية
|
32 |
+
plt.style.use('ggplot')
|
33 |
+
|
34 |
+
# تعيين الخط
|
35 |
+
plt.rcParams['font.family'] = 'sans-serif'
|
36 |
+
plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans', 'Liberation Sans', 'Bitstream Vera Sans', 'sans-serif']
|
37 |
+
|
38 |
+
# تعيين حجم الخط
|
39 |
+
plt.rcParams['font.size'] = 10
|
40 |
+
plt.rcParams['axes.titlesize'] = 14
|
41 |
+
plt.rcParams['axes.labelsize'] = 12
|
42 |
+
plt.rcParams['xtick.labelsize'] = 10
|
43 |
+
plt.rcParams['ytick.labelsize'] = 10
|
44 |
+
plt.rcParams['legend.fontsize'] = 10
|
45 |
+
|
46 |
+
# تعيين الألوان
|
47 |
+
if self.theme.current_theme == "light":
|
48 |
+
plt.rcParams['figure.facecolor'] = self.theme.LIGHT_CARD_BG_COLOR
|
49 |
+
plt.rcParams['axes.facecolor'] = self.theme.LIGHT_BG_COLOR
|
50 |
+
plt.rcParams['axes.edgecolor'] = self.theme.LIGHT_BORDER_COLOR
|
51 |
+
plt.rcParams['axes.labelcolor'] = self.theme.LIGHT_FG_COLOR
|
52 |
+
plt.rcParams['xtick.color'] = self.theme.LIGHT_FG_COLOR
|
53 |
+
plt.rcParams['ytick.color'] = self.theme.LIGHT_FG_COLOR
|
54 |
+
plt.rcParams['text.color'] = self.theme.LIGHT_FG_COLOR
|
55 |
+
plt.rcParams['grid.color'] = self.theme.LIGHT_BORDER_COLOR
|
56 |
+
else:
|
57 |
+
plt.rcParams['figure.facecolor'] = self.theme.DARK_CARD_BG_COLOR
|
58 |
+
plt.rcParams['axes.facecolor'] = self.theme.DARK_BG_COLOR
|
59 |
+
plt.rcParams['axes.edgecolor'] = self.theme.DARK_BORDER_COLOR
|
60 |
+
plt.rcParams['axes.labelcolor'] = self.theme.DARK_FG_COLOR
|
61 |
+
plt.rcParams['xtick.color'] = self.theme.DARK_FG_COLOR
|
62 |
+
plt.rcParams['ytick.color'] = self.theme.DARK_FG_COLOR
|
63 |
+
plt.rcParams['text.color'] = self.theme.DARK_FG_COLOR
|
64 |
+
plt.rcParams['grid.color'] = self.theme.DARK_BORDER_COLOR
|
65 |
+
|
66 |
+
def create_bar_chart(self, data, title, xlabel, ylabel):
|
67 |
+
"""إنشاء رسم بياني شريطي"""
|
68 |
+
# إنشاء الشكل والمحاور
|
69 |
+
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
|
70 |
+
|
71 |
+
# رسم الرسم البياني الشريطي
|
72 |
+
bars = ax.bar(data['labels'], data['values'], color=self.theme.PRIMARY_COLOR[self.theme.current_theme])
|
73 |
+
|
74 |
+
# إضافة القيم فوق الأشرطة
|
75 |
+
for bar in bars:
|
76 |
+
height = bar.get_height()
|
77 |
+
ax.text(bar.get_x() + bar.get_width() / 2., height + 0.1 * max(data['values']),
|
78 |
+
f'{height:,.0f}', ha='center', va='bottom')
|
79 |
+
|
80 |
+
# تعيين العنوان والتسميات
|
81 |
+
ax.set_title(title)
|
82 |
+
ax.set_xlabel(xlabel)
|
83 |
+
ax.set_ylabel(ylabel)
|
84 |
+
|
85 |
+
# تعيين حدود المحور y
|
86 |
+
ax.set_ylim(0, max(data['values']) * 1.2)
|
87 |
+
|
88 |
+
# إضافة الشبكة
|
89 |
+
ax.grid(True, linestyle='--', alpha=0.7)
|
90 |
+
|
91 |
+
# تضييق الشكل
|
92 |
+
fig.tight_layout()
|
93 |
+
|
94 |
+
return fig
|
95 |
+
|
96 |
+
def create_line_chart(self, data, title, xlabel, ylabel):
|
97 |
+
"""إنشاء رسم بياني خطي"""
|
98 |
+
# إنشاء الشكل والمحاور
|
99 |
+
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
|
100 |
+
|
101 |
+
# رسم الرسم البياني الخطي
|
102 |
+
line = ax.plot(data['labels'], data['values'], marker='o', linestyle='-', linewidth=2,
|
103 |
+
color=self.theme.PRIMARY_COLOR[self.theme.current_theme],
|
104 |
+
markersize=8, markerfacecolor=self.theme.SECONDARY_COLOR[self.theme.current_theme])
|
105 |
+
|
106 |
+
# إضافة القيم فوق النقاط
|
107 |
+
for i, value in enumerate(data['values']):
|
108 |
+
ax.text(i, value + 0.05 * max(data['values']), f'{value:,.0f}', ha='center', va='bottom')
|
109 |
+
|
110 |
+
# تعيين العنوان والتسميات
|
111 |
+
ax.set_title(title)
|
112 |
+
ax.set_xlabel(xlabel)
|
113 |
+
ax.set_ylabel(ylabel)
|
114 |
+
|
115 |
+
# تعيي�� حدود المحور y
|
116 |
+
ax.set_ylim(0, max(data['values']) * 1.2)
|
117 |
+
|
118 |
+
# إضافة الشبكة
|
119 |
+
ax.grid(True, linestyle='--', alpha=0.7)
|
120 |
+
|
121 |
+
# تضييق الشكل
|
122 |
+
fig.tight_layout()
|
123 |
+
|
124 |
+
return fig
|
125 |
+
|
126 |
+
def create_pie_chart(self, data, title):
|
127 |
+
"""إنشاء رسم بياني دائري"""
|
128 |
+
# إنشاء الشكل والمحاور
|
129 |
+
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
|
130 |
+
|
131 |
+
# تعيين الألوان
|
132 |
+
colors = [
|
133 |
+
self.theme.PRIMARY_COLOR[self.theme.current_theme],
|
134 |
+
self.theme.SECONDARY_COLOR[self.theme.current_theme],
|
135 |
+
self.theme.ACCENT_COLOR[self.theme.current_theme],
|
136 |
+
self.theme.WARNING_COLOR[self.theme.current_theme],
|
137 |
+
self.theme.SUCCESS_COLOR[self.theme.current_theme]
|
138 |
+
]
|
139 |
+
|
140 |
+
# رسم الرسم البياني الدائري
|
141 |
+
wedges, texts, autotexts = ax.pie(
|
142 |
+
data['values'],
|
143 |
+
labels=data['labels'],
|
144 |
+
autopct='%1.1f%%',
|
145 |
+
startangle=90,
|
146 |
+
colors=colors,
|
147 |
+
wedgeprops={'edgecolor': 'white', 'linewidth': 1},
|
148 |
+
textprops={'color': self.theme.get_color('fg_color')}
|
149 |
+
)
|
150 |
+
|
151 |
+
# تعيين خصائص النص
|
152 |
+
for autotext in autotexts:
|
153 |
+
autotext.set_color('white')
|
154 |
+
autotext.set_fontweight('bold')
|
155 |
+
|
156 |
+
# تعيين العنوان
|
157 |
+
ax.set_title(title)
|
158 |
+
|
159 |
+
# جعل الرسم البياني دائريًا
|
160 |
+
ax.axis('equal')
|
161 |
+
|
162 |
+
# تضييق الشكل
|
163 |
+
fig.tight_layout()
|
164 |
+
|
165 |
+
return fig
|
166 |
+
|
167 |
+
def create_stacked_bar_chart(self, data, title, xlabel, ylabel):
|
168 |
+
"""إنشاء رسم بياني شريطي متراكم"""
|
169 |
+
# إنشاء الشكل والمحاور
|
170 |
+
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
|
171 |
+
|
172 |
+
# تعيين الألوان
|
173 |
+
colors = [
|
174 |
+
self.theme.PRIMARY_COLOR[self.theme.current_theme],
|
175 |
+
self.theme.SECONDARY_COLOR[self.theme.current_theme],
|
176 |
+
self.theme.ACCENT_COLOR[self.theme.current_theme],
|
177 |
+
self.theme.WARNING_COLOR[self.theme.current_theme],
|
178 |
+
self.theme.SUCCESS_COLOR[self.theme.current_theme]
|
179 |
+
]
|
180 |
+
|
181 |
+
# رسم الرسم البياني الشريطي المتراكم
|
182 |
+
bottom = np.zeros(len(data['labels']))
|
183 |
+
for i, category in enumerate(data['categories']):
|
184 |
+
values = data['values'][i]
|
185 |
+
bars = ax.bar(data['labels'], values, bottom=bottom, label=category, color=colors[i % len(colors)])
|
186 |
+
bottom += values
|
187 |
+
|
188 |
+
# تعيين العنوان والتسميات
|
189 |
+
ax.set_title(title)
|
190 |
+
ax.set_xlabel(xlabel)
|
191 |
+
ax.set_ylabel(ylabel)
|
192 |
+
|
193 |
+
# إضافة وسيلة إيضاح
|
194 |
+
ax.legend()
|
195 |
+
|
196 |
+
# إضافة الشبكة
|
197 |
+
ax.grid(True, linestyle='--', alpha=0.7)
|
198 |
+
|
199 |
+
# تضييق الشكل
|
200 |
+
fig.tight_layout()
|
201 |
+
|
202 |
+
return fig
|
203 |
+
|
204 |
+
def create_risk_matrix(self, data, title):
|
205 |
+
"""إنشاء مصفوفة المخاطر"""
|
206 |
+
# إنشاء الشكل والمحاور
|
207 |
+
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
|
208 |
+
|
209 |
+
# تعيين الألوان
|
210 |
+
colors = {
|
211 |
+
'منخفض': self.theme.SUCCESS_COLOR[self.theme.current_theme],
|
212 |
+
'متوسط': self.theme.WARNING_COLOR[self.theme.current_theme],
|
213 |
+
'عالي': self.theme.ERROR_COLOR[self.theme.current_theme]
|
214 |
+
}
|
215 |
+
|
216 |
+
# تعيين قيم المحاور
|
217 |
+
probability_values = {'منخفض': 1, 'متوسط': 2, 'عالي': 3}
|
218 |
+
impact_values = {'منخفض': 1, 'متوسط': 2, 'عالي': 3}
|
219 |
+
|
220 |
+
# رسم المصفوفة
|
221 |
+
for risk in data['risks']:
|
222 |
+
prob = probability_values[risk['probability']]
|
223 |
+
impact = impact_values[risk['impact']]
|
224 |
+
color = colors[risk['probability']] if prob > impact else colors[risk['impact']]
|
225 |
+
ax.scatter(impact, prob, color=color, s=100, alpha=0.7)
|
226 |
+
ax.annotate(risk['name'], (impact, prob), xytext=(5, 5), textcoords='offset points')
|
227 |
+
|
228 |
+
# تعيين حدود المحاور
|
229 |
+
ax.set_xlim(0.5, 3.5)
|
230 |
+
ax.set_ylim(0.5, 3.5)
|
231 |
+
|
232 |
+
# تعيين تسميات المحاور
|
233 |
+
ax.set_xticks([1, 2, 3])
|
234 |
+
ax.set_xticklabels(['منخفض', 'متوسط', 'عالي'])
|
235 |
+
ax.set_yticks([1, 2, 3])
|
236 |
+
ax.set_yticklabels(['منخفض', 'متوسط', 'عالي'])
|
237 |
+
|
238 |
+
# تعيين العنوان والتسميات
|
239 |
+
ax.set_title(title)
|
240 |
+
ax.set_xlabel('التأثير')
|
241 |
+
ax.set_ylabel('الاحتمالية')
|
242 |
+
|
243 |
+
# إضافة الشبكة
|
244 |
+
ax.grid(True, linestyle='--', alpha=0.7)
|
245 |
+
|
246 |
+
# إضافة مناطق المخاطر
|
247 |
+
# منطقة المخاطر المنخفضة (أخضر)
|
248 |
+
ax.add_patch(plt.Rectangle((0.5, 0.5), 1, 1, fill=True, color=self.theme.SUCCESS_COLOR[self.theme.current_theme], alpha=0.1))
|
249 |
+
# منطقة المخاطر المتوسطة (أصفر)
|
250 |
+
ax.add_patch(plt.Rectangle((1.5, 0.5), 1, 1, fill=True, color=self.theme.WARNING_COLOR[self.theme.current_theme], alpha=0.1))
|
251 |
+
ax.add_patch(plt.Rectangle((0.5, 1.5), 1, 1, fill=True, color=self.theme.WARNING_COLOR[self.theme.current_theme], alpha=0.1))
|
252 |
+
# منطقة المخاطر العالية (أحمر)
|
253 |
+
ax.add_patch(plt.Rectangle((2.5, 0.5), 1, 3, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
|
254 |
+
ax.add_patch(plt.Rectangle((0.5, 2.5), 2, 1, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
|
255 |
+
ax.add_patch(plt.Rectangle((1.5, 1.5), 1, 1, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
|
256 |
+
|
257 |
+
# تضييق الشكل
|
258 |
+
fig.tight_layout()
|
259 |
+
|
260 |
+
return fig
|
261 |
+
|
262 |
+
def embed_chart_in_frame(self, parent, fig):
|
263 |
+
"""تضمين الرسم البياني في إطار"""
|
264 |
+
# إنشاء إطار للرسم البياني
|
265 |
+
chart_frame = ctk.CTkFrame(parent, fg_color="transparent")
|
266 |
+
|
267 |
+
# تضمين الرسم البياني في الإطار
|
268 |
+
canvas = FigureCanvasTkAgg(fig, master=chart_frame)
|
269 |
+
canvas.draw()
|
270 |
+
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
271 |
+
|
272 |
+
return chart_frame
|
273 |
+
|
274 |
+
def save_chart(self, fig, name):
|
275 |
+
"""حفظ الرسم البياني"""
|
276 |
+
# تحديد مسار الملف
|
277 |
+
file_path = os.path.join(self.charts_dir, f"{name}.png")
|
278 |
+
|
279 |
+
# حفظ الرسم البياني
|
280 |
+
fig.savefig(file_path, dpi=100, bbox_inches='tight')
|
281 |
+
|
282 |
+
return file_path
|
styling/enhanced_ui.py
ADDED
@@ -0,0 +1,513 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
تحسينات التصميم المرئي وواجهة المستخدم لنظام تحليل المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import streamlit_option_menu as option_menu
|
7 |
+
from streamlit_extras.colored_header import colored_header
|
8 |
+
from streamlit_extras.switch_page_button import switch_page
|
9 |
+
import os
|
10 |
+
import sys
|
11 |
+
from pathlib import Path
|
12 |
+
|
13 |
+
# إضافة مسار المشروع للنظام
|
14 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
15 |
+
|
16 |
+
class UIEnhancer:
|
17 |
+
"""فئة لتحسين واجهة المستخدم وتوحيد التصميم المرئي عبر النظام"""
|
18 |
+
|
19 |
+
# ألوان النظام
|
20 |
+
COLORS = {
|
21 |
+
'primary': '#1E5F74', # أزرق داكن للعناصر الرئيسية
|
22 |
+
'secondary': '#4BA3C3', # أزرق فاتح للعناصر الثانوية
|
23 |
+
'accent': '#F39237', # برتقالي للتأكيد والأزرار المهمة
|
24 |
+
'success': '#4CAF50', # أخضر للنجاح
|
25 |
+
'warning': '#FFC107', # أصفر للتحذيرات
|
26 |
+
'danger': '#E63946', # أحمر للأخطاء
|
27 |
+
'light': '#F5F5F5', # فاتح للخلفيات في الوضع الفاتح
|
28 |
+
'dark': '#1A1A1A', # داكن للخلفيات في الوضع الداكن
|
29 |
+
'text_light': '#FFFFFF', # نص أبيض للوضع الداكن
|
30 |
+
'text_dark': '#333333', # نص داكن للوضع الفاتح
|
31 |
+
'border': '#DDDDDD', # لون الحدود
|
32 |
+
'hover': '#2A7F9E', # لون التحويم
|
33 |
+
}
|
34 |
+
|
35 |
+
# أنماط CSS المخصصة
|
36 |
+
CUSTOM_CSS = """
|
37 |
+
<style>
|
38 |
+
/* تخصيص الشعار والعنوان */
|
39 |
+
.logo-title {
|
40 |
+
display: flex;
|
41 |
+
align-items: center;
|
42 |
+
margin-bottom: 1rem;
|
43 |
+
}
|
44 |
+
|
45 |
+
.logo-img {
|
46 |
+
height: 60px;
|
47 |
+
margin-right: 10px;
|
48 |
+
}
|
49 |
+
|
50 |
+
/* تخصيص البطاقات */
|
51 |
+
.card {
|
52 |
+
border-radius: 10px;
|
53 |
+
padding: 1.5rem;
|
54 |
+
margin-bottom: 1rem;
|
55 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
56 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
57 |
+
}
|
58 |
+
|
59 |
+
.card:hover {
|
60 |
+
transform: translateY(-5px);
|
61 |
+
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
62 |
+
}
|
63 |
+
|
64 |
+
/* تخصيص الأزرار */
|
65 |
+
.custom-button {
|
66 |
+
border-radius: 8px;
|
67 |
+
padding: 0.5rem 1rem;
|
68 |
+
font-weight: 500;
|
69 |
+
transition: all 0.3s ease;
|
70 |
+
}
|
71 |
+
|
72 |
+
.custom-button:hover {
|
73 |
+
opacity: 0.9;
|
74 |
+
transform: translateY(-2px);
|
75 |
+
}
|
76 |
+
|
77 |
+
/* تخصيص القوائم */
|
78 |
+
.nav-link {
|
79 |
+
border-radius: 8px;
|
80 |
+
margin: 0.2rem 0;
|
81 |
+
transition: all 0.3s ease;
|
82 |
+
}
|
83 |
+
|
84 |
+
.nav-link:hover {
|
85 |
+
transform: translateX(5px);
|
86 |
+
}
|
87 |
+
|
88 |
+
/* تخصيص الجداول */
|
89 |
+
.styled-table {
|
90 |
+
width: 100%;
|
91 |
+
border-collapse: separate;
|
92 |
+
border-spacing: 0;
|
93 |
+
border-radius: 10px;
|
94 |
+
overflow: hidden;
|
95 |
+
}
|
96 |
+
|
97 |
+
.styled-table th {
|
98 |
+
background-color: var(--primary-color);
|
99 |
+
color: white;
|
100 |
+
padding: 12px 15px;
|
101 |
+
text-align: right;
|
102 |
+
}
|
103 |
+
|
104 |
+
.styled-table td {
|
105 |
+
padding: 12px 15px;
|
106 |
+
border-bottom: 1px solid var(--border-color);
|
107 |
+
}
|
108 |
+
|
109 |
+
.styled-table tr:last-child td {
|
110 |
+
border-bottom: none;
|
111 |
+
}
|
112 |
+
|
113 |
+
.styled-table tr:nth-child(even) {
|
114 |
+
background-color: rgba(0, 0, 0, 0.05);
|
115 |
+
}
|
116 |
+
|
117 |
+
/* تخصيص لوحات المعلومات */
|
118 |
+
.dashboard-metric {
|
119 |
+
text-align: center;
|
120 |
+
padding: 1rem;
|
121 |
+
border-radius: 10px;
|
122 |
+
background-color: var(--secondary-color);
|
123 |
+
color: white;
|
124 |
+
}
|
125 |
+
|
126 |
+
.dashboard-metric h3 {
|
127 |
+
font-size: 2rem;
|
128 |
+
margin: 0;
|
129 |
+
}
|
130 |
+
|
131 |
+
.dashboard-metric p {
|
132 |
+
margin: 0;
|
133 |
+
opacity: 0.8;
|
134 |
+
}
|
135 |
+
|
136 |
+
/* تخصيص الرسوم البيانية */
|
137 |
+
.chart-container {
|
138 |
+
border-radius: 10px;
|
139 |
+
padding: 1rem;
|
140 |
+
background-color: white;
|
141 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
142 |
+
}
|
143 |
+
|
144 |
+
/* تخصيص الإشعارات */
|
145 |
+
.notification {
|
146 |
+
padding: 1rem;
|
147 |
+
border-radius: 8px;
|
148 |
+
margin-bottom: 0.5rem;
|
149 |
+
display: flex;
|
150 |
+
align-items: center;
|
151 |
+
}
|
152 |
+
|
153 |
+
.notification-icon {
|
154 |
+
margin-left: 1rem;
|
155 |
+
font-size: 1.5rem;
|
156 |
+
}
|
157 |
+
|
158 |
+
/* تخصيص الشريط الجانبي */
|
159 |
+
.sidebar .sidebar-content {
|
160 |
+
background-color: var(--primary-color);
|
161 |
+
color: white;
|
162 |
+
}
|
163 |
+
|
164 |
+
/* تحسين اتجاه النص للغة العربية */
|
165 |
+
body {
|
166 |
+
direction: rtl;
|
167 |
+
text-align: right;
|
168 |
+
}
|
169 |
+
|
170 |
+
/* تخصيص الخط */
|
171 |
+
@font-face {
|
172 |
+
font-family: 'Tajawal';
|
173 |
+
src: url('https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700&display=swap');
|
174 |
+
}
|
175 |
+
|
176 |
+
* {
|
177 |
+
font-family: 'Tajawal', sans-serif;
|
178 |
+
}
|
179 |
+
</style>
|
180 |
+
"""
|
181 |
+
|
182 |
+
def __init__(self, page_title="نظام تحليل المناقصات", page_icon="📊", layout="wide"):
|
183 |
+
"""تهيئة محسن واجهة المستخدم"""
|
184 |
+
self.page_title = page_title
|
185 |
+
self.page_icon = page_icon
|
186 |
+
self.layout = layout
|
187 |
+
self.setup_page()
|
188 |
+
|
189 |
+
def setup_page(self):
|
190 |
+
"""إعداد الصفحة وتطبيق الإعدادات العامة"""
|
191 |
+
st.set_page_config(
|
192 |
+
page_title=self.page_title,
|
193 |
+
page_icon=self.page_icon,
|
194 |
+
layout=self.layout,
|
195 |
+
initial_sidebar_state="expanded"
|
196 |
+
)
|
197 |
+
|
198 |
+
# تطبيق CSS المخصص
|
199 |
+
st.markdown(self.CUSTOM_CSS, unsafe_allow_html=True)
|
200 |
+
|
201 |
+
# إعداد متغيرات الجلسة إذا لم تكن موجودة
|
202 |
+
if 'theme' not in st.session_state:
|
203 |
+
st.session_state.theme = 'light'
|
204 |
+
if 'language' not in st.session_state:
|
205 |
+
st.session_state.language = 'ar'
|
206 |
+
|
207 |
+
def create_sidebar(self, menu_items):
|
208 |
+
"""إنشاء شريط جانبي محسن مع قائمة"""
|
209 |
+
with st.sidebar:
|
210 |
+
# عرض الشعار والعنوان
|
211 |
+
st.markdown(
|
212 |
+
f"""
|
213 |
+
<div class="logo-title">
|
214 |
+
<img src="./assets/images/logo.png" class="logo-img">
|
215 |
+
<h2>{self.page_title}</h2>
|
216 |
+
</div>
|
217 |
+
""",
|
218 |
+
unsafe_allow_html=True
|
219 |
+
)
|
220 |
+
|
221 |
+
# إنشاء قائمة الخيارات
|
222 |
+
selected = option_menu.option_menu(
|
223 |
+
menu_title=None,
|
224 |
+
options=[item["name"] for item in menu_items],
|
225 |
+
icons=[item["icon"] for item in menu_items],
|
226 |
+
menu_icon="cast",
|
227 |
+
default_index=0,
|
228 |
+
styles={
|
229 |
+
"container": {"padding": "0!important", "background-color": f"{self.COLORS['primary']}"},
|
230 |
+
"icon": {"color": "white", "font-size": "18px"},
|
231 |
+
"nav-link": {"color": "white", "font-size": "16px", "text-align": "right", "margin":"0px"},
|
232 |
+
"nav-link-selected": {"background-color": f"{self.COLORS['accent']}"},
|
233 |
+
}
|
234 |
+
)
|
235 |
+
|
236 |
+
# إضافة مفتاح تبديل السمة
|
237 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
238 |
+
col1, col2 = st.columns([1, 3])
|
239 |
+
with col1:
|
240 |
+
st.write("السمة:")
|
241 |
+
with col2:
|
242 |
+
if st.toggle("الوضع الداكن", st.session_state.theme == 'dark'):
|
243 |
+
st.session_state.theme = 'dark'
|
244 |
+
else:
|
245 |
+
st.session_state.theme = 'light'
|
246 |
+
|
247 |
+
# إضافة معلومات المستخدم
|
248 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
249 |
+
st.markdown(
|
250 |
+
f"""
|
251 |
+
<div style="text-align: center;">
|
252 |
+
<p>مرحباً، المستخدم</p>
|
253 |
+
<button class="custom-button" style="background-color: {self.COLORS['danger']}; color: white; border: none;">تسجيل الخروج</button>
|
254 |
+
</div>
|
255 |
+
""",
|
256 |
+
unsafe_allow_html=True
|
257 |
+
)
|
258 |
+
|
259 |
+
return selected
|
260 |
+
|
261 |
+
def create_header(self, title, description=None):
|
262 |
+
"""إنشاء ترويسة محسنة للصفحة"""
|
263 |
+
colored_header(
|
264 |
+
label=title,
|
265 |
+
description=description,
|
266 |
+
color_name=self.COLORS['primary']
|
267 |
+
)
|
268 |
+
|
269 |
+
def create_card(self, title, content, color=None):
|
270 |
+
"""إنشاء بطاقة محسنة"""
|
271 |
+
bg_color = color if color else self.COLORS['light'] if st.session_state.theme == 'light' else self.COLORS['dark']
|
272 |
+
text_color = self.COLORS['text_dark'] if st.session_state.theme == 'light' else self.COLORS['text_light']
|
273 |
+
|
274 |
+
st.markdown(
|
275 |
+
f"""
|
276 |
+
<div class="card" style="background-color: {bg_color}; color: {text_color};">
|
277 |
+
<h3>{title}</h3>
|
278 |
+
<p>{content}</p>
|
279 |
+
</div>
|
280 |
+
""",
|
281 |
+
unsafe_allow_html=True
|
282 |
+
)
|
283 |
+
|
284 |
+
def create_metric_card(self, title, value, delta=None, color=None):
|
285 |
+
"""إنشاء بطاقة مقاييس محسنة"""
|
286 |
+
bg_color = color if color else self.COLORS['primary']
|
287 |
+
|
288 |
+
delta_html = f"<span style='color: {'green' if delta > 0 else 'red'};'>{delta}%</span>" if delta is not None else ""
|
289 |
+
|
290 |
+
st.markdown(
|
291 |
+
f"""
|
292 |
+
<div class="dashboard-metric" style="background-color: {bg_color};">
|
293 |
+
<p>{title}</p>
|
294 |
+
<h3>{value}</h3>
|
295 |
+
{delta_html}
|
296 |
+
</div>
|
297 |
+
""",
|
298 |
+
unsafe_allow_html=True
|
299 |
+
)
|
300 |
+
|
301 |
+
def create_notification(self, message, type_="info"):
|
302 |
+
"""إنشاء إشعار محسن"""
|
303 |
+
colors = {
|
304 |
+
"info": self.COLORS['secondary'],
|
305 |
+
"success": self.COLORS['success'],
|
306 |
+
"warning": self.COLORS['warning'],
|
307 |
+
"error": self.COLORS['danger']
|
308 |
+
}
|
309 |
+
|
310 |
+
icons = {
|
311 |
+
"info": "ℹ️",
|
312 |
+
"success": "✅",
|
313 |
+
"warning": "⚠️",
|
314 |
+
"error": "❌"
|
315 |
+
}
|
316 |
+
|
317 |
+
bg_color = colors.get(type_, colors["info"])
|
318 |
+
icon = icons.get(type_, icons["info"])
|
319 |
+
|
320 |
+
st.markdown(
|
321 |
+
f"""
|
322 |
+
<div class="notification" style="background-color: {bg_color};">
|
323 |
+
<div class="notification-icon">{icon}</div>
|
324 |
+
<div>{message}</div>
|
325 |
+
</div>
|
326 |
+
""",
|
327 |
+
unsafe_allow_html=True
|
328 |
+
)
|
329 |
+
|
330 |
+
def create_button(self, label, type_="primary", on_click=None):
|
331 |
+
"""إنشاء زر محسن"""
|
332 |
+
colors = {
|
333 |
+
"primary": self.COLORS['primary'],
|
334 |
+
"secondary": self.COLORS['secondary'],
|
335 |
+
"accent": self.COLORS['accent'],
|
336 |
+
"success": self.COLORS['success'],
|
337 |
+
"warning": self.COLORS['warning'],
|
338 |
+
"danger": self.COLORS['danger']
|
339 |
+
}
|
340 |
+
|
341 |
+
bg_color = colors.get(type_, colors["primary"])
|
342 |
+
text_color = self.COLORS['text_light']
|
343 |
+
|
344 |
+
return st.button(
|
345 |
+
label,
|
346 |
+
key=f"btn_{label}_{type_}",
|
347 |
+
on_click=on_click,
|
348 |
+
use_container_width=True
|
349 |
+
)
|
350 |
+
|
351 |
+
def create_table(self, data, columns):
|
352 |
+
"""إنشاء جدول محسن"""
|
353 |
+
# تحويل البيانات إلى HTML
|
354 |
+
table_html = f"""
|
355 |
+
<table class="styled-table">
|
356 |
+
<thead>
|
357 |
+
<tr>
|
358 |
+
{"".join([f"<th>{col}</th>" for col in columns])}
|
359 |
+
</tr>
|
360 |
+
</thead>
|
361 |
+
<tbody>
|
362 |
+
"""
|
363 |
+
|
364 |
+
for row in data:
|
365 |
+
table_html += "<tr>"
|
366 |
+
for col in columns:
|
367 |
+
table_html += f"<td>{row.get(col, '')}</td>"
|
368 |
+
table_html += "</tr>"
|
369 |
+
|
370 |
+
table_html += """
|
371 |
+
</tbody>
|
372 |
+
</table>
|
373 |
+
"""
|
374 |
+
|
375 |
+
st.markdown(table_html, unsafe_allow_html=True)
|
376 |
+
|
377 |
+
def apply_theme_colors(self):
|
378 |
+
"""تطبيق ألوان السمة الحالية"""
|
379 |
+
theme_colors = {
|
380 |
+
'light': {
|
381 |
+
'background': self.COLORS['light'],
|
382 |
+
'text': self.COLORS['text_dark'],
|
383 |
+
'border': self.COLORS['border']
|
384 |
+
},
|
385 |
+
'dark': {
|
386 |
+
'background': self.COLORS['dark'],
|
387 |
+
'text': self.COLORS['text_light'],
|
388 |
+
'border': '#444444'
|
389 |
+
}
|
390 |
+
}
|
391 |
+
|
392 |
+
current_theme = theme_colors[st.session_state.theme]
|
393 |
+
|
394 |
+
st.markdown(
|
395 |
+
f"""
|
396 |
+
<style>
|
397 |
+
:root {{
|
398 |
+
--background-color: {current_theme['background']};
|
399 |
+
--text-color: {current_theme['text']};
|
400 |
+
--border-color: {current_theme['border']};
|
401 |
+
--primary-color: {self.COLORS['primary']};
|
402 |
+
--secondary-color: {self.COLORS['secondary']};
|
403 |
+
--accent-color: {self.COLORS['accent']};
|
404 |
+
}}
|
405 |
+
|
406 |
+
.stApp {{
|
407 |
+
background-color: var(--background-color);
|
408 |
+
color: var(--text-color);
|
409 |
+
}}
|
410 |
+
|
411 |
+
.card {{
|
412 |
+
background-color: var(--background-color);
|
413 |
+
color: var(--text-color);
|
414 |
+
border: 1px solid var(--border-color);
|
415 |
+
}}
|
416 |
+
</style>
|
417 |
+
""",
|
418 |
+
unsafe_allow_html=True
|
419 |
+
)
|
420 |
+
|
421 |
+
# استخدام الفئة
|
422 |
+
if __name__ == "__main__":
|
423 |
+
ui = UIEnhancer()
|
424 |
+
ui.apply_theme_colors()
|
425 |
+
|
426 |
+
# إنشاء قائمة العناصر
|
427 |
+
menu_items = [
|
428 |
+
{"name": "لوحة المعلومات", "icon": "house"},
|
429 |
+
{"name": "المناقصات والعقود", "icon": "file-text"},
|
430 |
+
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
431 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
432 |
+
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
433 |
+
{"name": "الموارد والتكاليف", "icon": "people"},
|
434 |
+
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
435 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
436 |
+
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
437 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
438 |
+
{"name": "الإشعارات", "icon": "bell"},
|
439 |
+
{"name": "مقارنة المستندات", "icon": "files"},
|
440 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
441 |
+
{"name": "التقارير", "icon": "bar-chart"},
|
442 |
+
{"name": "الإعدادات", "icon": "gear"}
|
443 |
+
]
|
444 |
+
|
445 |
+
# إنشاء الشريط الجانبي
|
446 |
+
selected = ui.create_sidebar(menu_items)
|
447 |
+
|
448 |
+
# إنشاء ترويسة الصفحة
|
449 |
+
ui.create_header("لوحة المعلومات", "نظرة عامة على المناقصات والمشاريع")
|
450 |
+
|
451 |
+
# إنشاء صفوف للمقاييس
|
452 |
+
col1, col2, col3, col4 = st.columns(4)
|
453 |
+
|
454 |
+
with col1:
|
455 |
+
ui.create_metric_card("المناقصات النشطة", "12", delta=5)
|
456 |
+
|
457 |
+
with col2:
|
458 |
+
ui.create_metric_card("المشاريع الجارية", "8", delta=-2)
|
459 |
+
|
460 |
+
with col3:
|
461 |
+
ui.create_metric_card("المناقصات المربوحة", "65%", delta=10)
|
462 |
+
|
463 |
+
with col4:
|
464 |
+
ui.create_metric_card("قيمة المشاريع", "25M ريال", delta=15)
|
465 |
+
|
466 |
+
# إنشاء صفين للبطاقات والإشعارات
|
467 |
+
col1, col2 = st.columns([2, 1])
|
468 |
+
|
469 |
+
with col1:
|
470 |
+
ui.create_header("المناقصات الحالية", "آخر تحديث: اليوم")
|
471 |
+
|
472 |
+
# بيانات المناقصات
|
473 |
+
tenders_data = [
|
474 |
+
{"رقم المناقصة": "T-2025-001", "العنوان": "إنشاء مبنى إداري", "الحالة": "قيد الدراسة", "تاريخ التقديم": "2025-04-15"},
|
475 |
+
{"رقم المناقصة": "T-2025-002", "العنوان": "صيانة طرق", "الحالة": "تم التقديم", "تاريخ التقديم": "2025-03-20"},
|
476 |
+
{"رقم المناقصة": "T-2025-003", "العنوان": "توريد معدات", "الحالة": "فائز", "تاريخ التقديم": "2025-02-10"}
|
477 |
+
]
|
478 |
+
|
479 |
+
ui.create_table(tenders_data, ["رقم المناقصة", "العنوان", "الحالة", "تاريخ التقديم"])
|
480 |
+
|
481 |
+
with col2:
|
482 |
+
ui.create_header("الإشعارات", "آخر التنبيهات")
|
483 |
+
|
484 |
+
ui.create_notification("موعد تسليم مناقصة T-2025-001 بعد 5 أيام", "warning")
|
485 |
+
ui.create_notification("تم ترسية مناقصة T-2025-003", "success")
|
486 |
+
ui.create_notification("تم تحديث مستندات مناقصة T-2025-002", "info")
|
487 |
+
|
488 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
489 |
+
ui.create_button("عرض جميع الإشعارات", "secondary")
|
490 |
+
|
491 |
+
# إنشاء صف للبطاقات
|
492 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
493 |
+
ui.create_header("الإجراءات السريعة", "اختر إجراءً للبدء")
|
494 |
+
|
495 |
+
col1, col2, col3 = st.columns(3)
|
496 |
+
|
497 |
+
with col1:
|
498 |
+
ui.create_card(
|
499 |
+
"إضافة مناقصة جديدة",
|
500 |
+
"إنشاء مناقصة جديدة وإدخال البيانات الأساسية"
|
501 |
+
)
|
502 |
+
|
503 |
+
with col2:
|
504 |
+
ui.create_card(
|
505 |
+
"تحليل مستند",
|
506 |
+
"تحميل مستند مناقصة جديد للتحليل التلقائي"
|
507 |
+
)
|
508 |
+
|
509 |
+
with col3:
|
510 |
+
ui.create_card(
|
511 |
+
"إنشاء تقرير",
|
512 |
+
"إنشاء تقارير مخصصة للمناقصات والمشاريع"
|
513 |
+
)
|
styling/icons.py
ADDED
@@ -0,0 +1,609 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
مولد الأيقونات لنظام إدارة المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import math
|
7 |
+
from PIL import Image, ImageDraw, ImageFont
|
8 |
+
|
9 |
+
class IconGenerator:
|
10 |
+
"""فئة مولد الأيقونات"""
|
11 |
+
|
12 |
+
def __init__(self):
|
13 |
+
"""تهيئة مولد الأيقونات"""
|
14 |
+
# تحديد مسار مجلد الأيقونات
|
15 |
+
self.icons_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "icons")
|
16 |
+
|
17 |
+
# إنشاء مجلد الأيقونات إذا لم يكن موجودًا
|
18 |
+
os.makedirs(self.icons_dir, exist_ok=True)
|
19 |
+
|
20 |
+
# تحديد حجم الأيقونة الافتراضي
|
21 |
+
self.icon_size = (64, 64)
|
22 |
+
|
23 |
+
# تحديد الألوان الافتراضية
|
24 |
+
self.colors = {
|
25 |
+
"primary": "#2980B9",
|
26 |
+
"secondary": "#1ABC9C",
|
27 |
+
"accent": "#9B59B6",
|
28 |
+
"warning": "#F39C12",
|
29 |
+
"error": "#E74C3C",
|
30 |
+
"success": "#2ECC71",
|
31 |
+
"white": "#FFFFFF",
|
32 |
+
"black": "#333333",
|
33 |
+
"gray": "#95A5A6"
|
34 |
+
}
|
35 |
+
|
36 |
+
def generate_icon(self, name, color=None, background_color=None, size=None):
|
37 |
+
"""توليد أيقونة"""
|
38 |
+
# تحديد الألوان
|
39 |
+
if color is None:
|
40 |
+
color = self.colors["primary"]
|
41 |
+
|
42 |
+
if background_color is None:
|
43 |
+
background_color = self.colors["white"]
|
44 |
+
|
45 |
+
# تحديد الحجم
|
46 |
+
if size is None:
|
47 |
+
size = self.icon_size
|
48 |
+
|
49 |
+
# إنشاء صورة جديدة
|
50 |
+
icon = Image.new("RGBA", size, background_color)
|
51 |
+
draw = ImageDraw.Draw(icon)
|
52 |
+
|
53 |
+
# رسم الأيقونة بناءً على الاسم
|
54 |
+
if name == "dashboard":
|
55 |
+
self._draw_dashboard_icon(draw, size, color)
|
56 |
+
elif name == "projects":
|
57 |
+
self._draw_projects_icon(draw, size, color)
|
58 |
+
elif name == "documents":
|
59 |
+
self._draw_documents_icon(draw, size, color)
|
60 |
+
elif name == "pricing":
|
61 |
+
self._draw_pricing_icon(draw, size, color)
|
62 |
+
elif name == "resources":
|
63 |
+
self._draw_resources_icon(draw, size, color)
|
64 |
+
elif name == "risk":
|
65 |
+
self._draw_risk_icon(draw, size, color)
|
66 |
+
elif name == "reports":
|
67 |
+
self._draw_reports_icon(draw, size, color)
|
68 |
+
elif name == "ai":
|
69 |
+
self._draw_ai_icon(draw, size, color)
|
70 |
+
elif name == "settings":
|
71 |
+
self._draw_settings_icon(draw, size, color)
|
72 |
+
elif name == "logout":
|
73 |
+
self._draw_logout_icon(draw, size, color)
|
74 |
+
elif name == "search":
|
75 |
+
self._draw_search_icon(draw, size, color)
|
76 |
+
elif name == "add":
|
77 |
+
self._draw_add_icon(draw, size, color)
|
78 |
+
elif name == "upload":
|
79 |
+
self._draw_upload_icon(draw, size, color)
|
80 |
+
elif name == "import":
|
81 |
+
self._draw_import_icon(draw, size, color)
|
82 |
+
elif name == "export":
|
83 |
+
self._draw_export_icon(draw, size, color)
|
84 |
+
elif name == "save":
|
85 |
+
self._draw_save_icon(draw, size, color)
|
86 |
+
else:
|
87 |
+
# أيقونة افتراضية
|
88 |
+
self._draw_default_icon(draw, size, color)
|
89 |
+
|
90 |
+
# حفظ الأيقونة
|
91 |
+
icon_path = os.path.join(self.icons_dir, f"{name}.png")
|
92 |
+
icon.save(icon_path)
|
93 |
+
|
94 |
+
return icon_path
|
95 |
+
|
96 |
+
def _draw_dashboard_icon(self, draw, size, color):
|
97 |
+
"""رسم أيقونة لوحة التحكم"""
|
98 |
+
width, height = size
|
99 |
+
padding = width // 8
|
100 |
+
|
101 |
+
# رسم المربعات الأربعة
|
102 |
+
box_size = (width - 3 * padding) // 2
|
103 |
+
|
104 |
+
# المربع العلوي الأيسر
|
105 |
+
draw.rectangle(
|
106 |
+
[(padding, padding), (padding + box_size, padding + box_size)],
|
107 |
+
fill=color
|
108 |
+
)
|
109 |
+
|
110 |
+
# المربع العلوي الأيمن
|
111 |
+
draw.rectangle(
|
112 |
+
[(2 * padding + box_size, padding), (2 * padding + 2 * box_size, padding + box_size)],
|
113 |
+
fill=color
|
114 |
+
)
|
115 |
+
|
116 |
+
# المربع السفلي الأيسر
|
117 |
+
draw.rectangle(
|
118 |
+
[(padding, 2 * padding + box_size), (padding + box_size, 2 * padding + 2 * box_size)],
|
119 |
+
fill=color
|
120 |
+
)
|
121 |
+
|
122 |
+
# المربع السفلي الأيمن
|
123 |
+
draw.rectangle(
|
124 |
+
[(2 * padding + box_size, 2 * padding + box_size), (2 * padding + 2 * box_size, 2 * padding + 2 * box_size)],
|
125 |
+
fill=color
|
126 |
+
)
|
127 |
+
|
128 |
+
def _draw_projects_icon(self, draw, size, color):
|
129 |
+
"""رسم أيقونة المشاريع"""
|
130 |
+
width, height = size
|
131 |
+
padding = width // 8
|
132 |
+
|
133 |
+
# رسم مجلد
|
134 |
+
folder_points = [
|
135 |
+
(padding, height // 3),
|
136 |
+
(width // 3, height // 3),
|
137 |
+
(width // 2, padding),
|
138 |
+
(width - padding, padding),
|
139 |
+
(width - padding, height - padding),
|
140 |
+
(padding, height - padding)
|
141 |
+
]
|
142 |
+
draw.polygon(folder_points, fill=color)
|
143 |
+
|
144 |
+
def _draw_documents_icon(self, draw, size, color):
|
145 |
+
"""رسم أيقونة المستندات"""
|
146 |
+
width, height = size
|
147 |
+
padding = width // 8
|
148 |
+
|
149 |
+
# رسم ورقة
|
150 |
+
draw.rectangle(
|
151 |
+
[(padding, padding), (width - padding, height - padding)],
|
152 |
+
fill=color
|
153 |
+
)
|
154 |
+
|
155 |
+
# رسم خطوط النص
|
156 |
+
line_padding = height // 8
|
157 |
+
line_height = height // 20
|
158 |
+
for i in range(4):
|
159 |
+
y = padding + line_padding + i * (line_height + line_padding)
|
160 |
+
draw.rectangle(
|
161 |
+
[(padding * 2, y), (width - padding * 2, y + line_height)],
|
162 |
+
fill=self.colors["white"]
|
163 |
+
)
|
164 |
+
|
165 |
+
def _draw_pricing_icon(self, draw, size, color):
|
166 |
+
"""رسم أيقونة التسعير"""
|
167 |
+
width, height = size
|
168 |
+
padding = width // 8
|
169 |
+
|
170 |
+
# رسم علامة الدولار
|
171 |
+
center_x = width // 2
|
172 |
+
center_y = height // 2
|
173 |
+
radius = min(width, height) // 3
|
174 |
+
|
175 |
+
# رسم دائرة
|
176 |
+
draw.ellipse(
|
177 |
+
[(center_x - radius, center_y - radius), (center_x + radius, center_y + radius)],
|
178 |
+
fill=color
|
179 |
+
)
|
180 |
+
|
181 |
+
# رسم علامة الدولار
|
182 |
+
line_width = radius // 4
|
183 |
+
draw.rectangle(
|
184 |
+
[(center_x - line_width // 2, center_y - radius * 2 // 3), (center_x + line_width // 2, center_y + radius * 2 // 3)],
|
185 |
+
fill=self.colors["white"]
|
186 |
+
)
|
187 |
+
draw.rectangle(
|
188 |
+
[(center_x - radius * 2 // 3, center_y - line_width // 2), (center_x + radius * 2 // 3, center_y + line_width // 2)],
|
189 |
+
fill=self.colors["white"]
|
190 |
+
)
|
191 |
+
|
192 |
+
def _draw_resources_icon(self, draw, size, color):
|
193 |
+
"""رسم أيقونة الموارد"""
|
194 |
+
width, height = size
|
195 |
+
padding = width // 8
|
196 |
+
|
197 |
+
# رسم ثلاثة أشخاص
|
198 |
+
center_x = width // 2
|
199 |
+
center_y = height // 2
|
200 |
+
radius = min(width, height) // 10
|
201 |
+
|
202 |
+
# الشخص الأول (في الوسط)
|
203 |
+
head_center_y = center_y - radius * 2
|
204 |
+
draw.ellipse(
|
205 |
+
[(center_x - radius, head_center_y - radius), (center_x + radius, head_center_y + radius)],
|
206 |
+
fill=color
|
207 |
+
)
|
208 |
+
draw.polygon(
|
209 |
+
[
|
210 |
+
(center_x, head_center_y + radius),
|
211 |
+
(center_x - radius * 2, center_y + radius * 3),
|
212 |
+
(center_x + radius * 2, center_y + radius * 3)
|
213 |
+
],
|
214 |
+
fill=color
|
215 |
+
)
|
216 |
+
|
217 |
+
# الشخص الثاني (على اليسار)
|
218 |
+
left_center_x = center_x - radius * 4
|
219 |
+
head_center_y = center_y - radius * 2
|
220 |
+
draw.ellipse(
|
221 |
+
[(left_center_x - radius, head_center_y - radius), (left_center_x + radius, head_center_y + radius)],
|
222 |
+
fill=color
|
223 |
+
)
|
224 |
+
draw.polygon(
|
225 |
+
[
|
226 |
+
(left_center_x, head_center_y + radius),
|
227 |
+
(left_center_x - radius * 2, center_y + radius * 3),
|
228 |
+
(left_center_x + radius * 2, center_y + radius * 3)
|
229 |
+
],
|
230 |
+
fill=color
|
231 |
+
)
|
232 |
+
|
233 |
+
# الشخص الثالث (على اليمين)
|
234 |
+
right_center_x = center_x + radius * 4
|
235 |
+
head_center_y = center_y - radius * 2
|
236 |
+
draw.ellipse(
|
237 |
+
[(right_center_x - radius, head_center_y - radius), (right_center_x + radius, head_center_y + radius)],
|
238 |
+
fill=color
|
239 |
+
)
|
240 |
+
draw.polygon(
|
241 |
+
[
|
242 |
+
(right_center_x, head_center_y + radius),
|
243 |
+
(right_center_x - radius * 2, center_y + radius * 3),
|
244 |
+
(right_center_x + radius * 2, center_y + radius * 3)
|
245 |
+
],
|
246 |
+
fill=color
|
247 |
+
)
|
248 |
+
|
249 |
+
def _draw_risk_icon(self, draw, size, color):
|
250 |
+
"""رسم أيقونة المخاطر"""
|
251 |
+
width, height = size
|
252 |
+
padding = width // 8
|
253 |
+
|
254 |
+
# رسم علامة تحذير (مثلث)
|
255 |
+
draw.polygon(
|
256 |
+
[
|
257 |
+
(width // 2, padding),
|
258 |
+
(padding, height - padding),
|
259 |
+
(width - padding, height - padding)
|
260 |
+
],
|
261 |
+
fill=color
|
262 |
+
)
|
263 |
+
|
264 |
+
# رسم علامة التعجب
|
265 |
+
exclamation_width = width // 10
|
266 |
+
exclamation_height = height // 3
|
267 |
+
center_x = width // 2
|
268 |
+
center_y = height // 2
|
269 |
+
|
270 |
+
# الجزء العلوي من علامة التعجب
|
271 |
+
draw.rectangle(
|
272 |
+
[
|
273 |
+
(center_x - exclamation_width // 2, center_y - exclamation_height),
|
274 |
+
(center_x + exclamation_width // 2, center_y)
|
275 |
+
],
|
276 |
+
fill=self.colors["white"]
|
277 |
+
)
|
278 |
+
|
279 |
+
# النقطة السفلية من علامة التعجب
|
280 |
+
dot_radius = exclamation_width
|
281 |
+
draw.ellipse(
|
282 |
+
[
|
283 |
+
(center_x - dot_radius // 2, center_y + exclamation_height // 4),
|
284 |
+
(center_x + dot_radius // 2, center_y + exclamation_height // 4 + dot_radius)
|
285 |
+
],
|
286 |
+
fill=self.colors["white"]
|
287 |
+
)
|
288 |
+
|
289 |
+
def _draw_reports_icon(self, draw, size, color):
|
290 |
+
"""رسم أيقونة التقارير"""
|
291 |
+
width, height = size
|
292 |
+
padding = width // 8
|
293 |
+
|
294 |
+
# رسم ورقة
|
295 |
+
draw.rectangle(
|
296 |
+
[(padding, padding), (width - padding, height - padding)],
|
297 |
+
fill=color
|
298 |
+
)
|
299 |
+
|
300 |
+
# رسم رسم بياني
|
301 |
+
chart_padding = width // 6
|
302 |
+
chart_width = width - 2 * chart_padding
|
303 |
+
chart_height = height // 2
|
304 |
+
chart_bottom = height - chart_padding
|
305 |
+
|
306 |
+
# رسم الأعمدة
|
307 |
+
bar_width = chart_width // 5
|
308 |
+
bar_spacing = bar_width // 2
|
309 |
+
|
310 |
+
for i in range(4):
|
311 |
+
bar_height = (i + 1) * chart_height // 4
|
312 |
+
bar_x = chart_padding + i * (bar_width + bar_spacing)
|
313 |
+
bar_y = chart_bottom - bar_height
|
314 |
+
|
315 |
+
draw.rectangle(
|
316 |
+
[(bar_x, bar_y), (bar_x + bar_width, chart_bottom)],
|
317 |
+
fill=self.colors["white"]
|
318 |
+
)
|
319 |
+
|
320 |
+
def _draw_ai_icon(self, draw, size, color):
|
321 |
+
"""رسم أيقونة الذكاء الاصطناعي"""
|
322 |
+
width, height = size
|
323 |
+
padding = width // 8
|
324 |
+
|
325 |
+
# رسم دماغ (مجرد تمثيل مبسط)
|
326 |
+
center_x = width // 2
|
327 |
+
center_y = height // 2
|
328 |
+
brain_width = width - 2 * padding
|
329 |
+
brain_height = height - 2 * padding
|
330 |
+
|
331 |
+
# رسم الجزء الخارجي من الدماغ
|
332 |
+
draw.ellipse(
|
333 |
+
[(center_x - brain_width // 2, center_y - brain_height // 2), (center_x + brain_width // 2, center_y + brain_height // 2)],
|
334 |
+
fill=color
|
335 |
+
)
|
336 |
+
|
337 |
+
# رسم خطوط الدماغ
|
338 |
+
line_width = brain_width // 10
|
339 |
+
line_spacing = brain_width // 8
|
340 |
+
|
341 |
+
for i in range(-2, 3):
|
342 |
+
y = center_y + i * line_spacing
|
343 |
+
draw.line(
|
344 |
+
[(center_x - brain_width // 3, y), (center_x + brain_width // 3, y)],
|
345 |
+
fill=self.colors["white"],
|
346 |
+
width=line_width
|
347 |
+
)
|
348 |
+
|
349 |
+
def _draw_settings_icon(self, draw, size, color):
|
350 |
+
"""رسم أيقونة الإعدادات"""
|
351 |
+
width, height = size
|
352 |
+
padding = width // 8
|
353 |
+
|
354 |
+
# رسم ترس
|
355 |
+
center_x = width // 2
|
356 |
+
center_y = height // 2
|
357 |
+
outer_radius = min(width, height) // 2 - padding
|
358 |
+
inner_radius = outer_radius * 2 // 3
|
359 |
+
|
360 |
+
# رسم الدائرة الداخلية
|
361 |
+
draw.ellipse(
|
362 |
+
[(center_x - inner_radius, center_y - inner_radius), (center_x + inner_radius, center_y + inner_radius)],
|
363 |
+
fill=color
|
364 |
+
)
|
365 |
+
|
366 |
+
# رسم الأسنان
|
367 |
+
num_teeth = 8
|
368 |
+
tooth_width = outer_radius - inner_radius
|
369 |
+
|
370 |
+
for i in range(num_teeth):
|
371 |
+
angle = 2 * math.pi * i / num_teeth
|
372 |
+
tooth_center_x = center_x + (inner_radius + tooth_width // 2) * math.cos(angle)
|
373 |
+
tooth_center_y = center_y + (inner_radius + tooth_width // 2) * math.sin(angle)
|
374 |
+
|
375 |
+
draw.ellipse(
|
376 |
+
[
|
377 |
+
(tooth_center_x - tooth_width // 2, tooth_center_y - tooth_width // 2),
|
378 |
+
(tooth_center_x + tooth_width // 2, tooth_center_y + tooth_width // 2)
|
379 |
+
],
|
380 |
+
fill=color
|
381 |
+
)
|
382 |
+
|
383 |
+
def _draw_logout_icon(self, draw, size, color):
|
384 |
+
"""رسم أيقونة تسجيل الخروج"""
|
385 |
+
width, height = size
|
386 |
+
padding = width // 8
|
387 |
+
|
388 |
+
# رسم سهم الخروج
|
389 |
+
arrow_width = width - 2 * padding
|
390 |
+
arrow_height = height - 2 * padding
|
391 |
+
|
392 |
+
# رسم المستطيل الرئيسي
|
393 |
+
draw.rectangle(
|
394 |
+
[(padding, padding), (width // 2, height - padding)],
|
395 |
+
fill=color
|
396 |
+
)
|
397 |
+
|
398 |
+
# رسم السهم
|
399 |
+
arrow_points = [
|
400 |
+
(width // 2, height // 3),
|
401 |
+
(width - padding, height // 2),
|
402 |
+
(width // 2, height * 2 // 3),
|
403 |
+
(width // 2, height // 2 + height // 8),
|
404 |
+
(width // 2 + width // 4, height // 2 + height // 8),
|
405 |
+
(width // 2 + width // 4, height // 2 - height // 8),
|
406 |
+
(width // 2, height // 2 - height // 8)
|
407 |
+
]
|
408 |
+
draw.polygon(arrow_points, fill=color)
|
409 |
+
|
410 |
+
def _draw_search_icon(self, draw, size, color):
|
411 |
+
"""رسم أيقونة البحث"""
|
412 |
+
width, height = size
|
413 |
+
padding = width // 8
|
414 |
+
|
415 |
+
# رسم دائرة البحث
|
416 |
+
center_x = width // 2 - padding
|
417 |
+
center_y = height // 2 - padding
|
418 |
+
radius = min(width, height) // 3
|
419 |
+
|
420 |
+
draw.ellipse(
|
421 |
+
[(center_x - radius, center_y - radius), (center_x + radius, center_y + radius)],
|
422 |
+
outline=color,
|
423 |
+
width=radius // 3
|
424 |
+
)
|
425 |
+
|
426 |
+
# رسم مقبض البحث
|
427 |
+
handle_width = radius // 3
|
428 |
+
handle_length = radius
|
429 |
+
handle_angle = math.pi / 4 # 45 درجة
|
430 |
+
|
431 |
+
handle_start_x = center_x + radius * math.cos(handle_angle)
|
432 |
+
handle_start_y = center_y + radius * math.sin(handle_angle)
|
433 |
+
handle_end_x = handle_start_x + handle_length * math.cos(handle_angle)
|
434 |
+
handle_end_y = handle_start_y + handle_length * math.sin(handle_angle)
|
435 |
+
|
436 |
+
draw.line(
|
437 |
+
[(handle_start_x, handle_start_y), (handle_end_x, handle_end_y)],
|
438 |
+
fill=color,
|
439 |
+
width=handle_width
|
440 |
+
)
|
441 |
+
|
442 |
+
def _draw_add_icon(self, draw, size, color):
|
443 |
+
"""رسم أيقونة الإضافة"""
|
444 |
+
width, height = size
|
445 |
+
padding = width // 8
|
446 |
+
|
447 |
+
# رسم علامة الزائد
|
448 |
+
center_x = width // 2
|
449 |
+
center_y = height // 2
|
450 |
+
line_length = min(width, height) - 2 * padding
|
451 |
+
line_width = line_length // 5
|
452 |
+
|
453 |
+
# الخط الأفقي
|
454 |
+
draw.rectangle(
|
455 |
+
[
|
456 |
+
(center_x - line_length // 2, center_y - line_width // 2),
|
457 |
+
(center_x + line_length // 2, center_y + line_width // 2)
|
458 |
+
],
|
459 |
+
fill=color
|
460 |
+
)
|
461 |
+
|
462 |
+
# الخط الرأسي
|
463 |
+
draw.rectangle(
|
464 |
+
[
|
465 |
+
(center_x - line_width // 2, center_y - line_length // 2),
|
466 |
+
(center_x + line_width // 2, center_y + line_length // 2)
|
467 |
+
],
|
468 |
+
fill=color
|
469 |
+
)
|
470 |
+
|
471 |
+
def _draw_upload_icon(self, draw, size, color):
|
472 |
+
"""رسم أيقونة التحميل"""
|
473 |
+
width, height = size
|
474 |
+
padding = width // 8
|
475 |
+
|
476 |
+
# رسم سهم لأعلى
|
477 |
+
center_x = width // 2
|
478 |
+
arrow_width = width // 3
|
479 |
+
arrow_height = height // 2
|
480 |
+
|
481 |
+
# رسم السهم
|
482 |
+
arrow_points = [
|
483 |
+
(center_x, padding),
|
484 |
+
(center_x + arrow_width, padding + arrow_height),
|
485 |
+
(center_x + arrow_width // 2, padding + arrow_height),
|
486 |
+
(center_x + arrow_width // 2, height - padding),
|
487 |
+
(center_x - arrow_width // 2, height - padding),
|
488 |
+
(center_x - arrow_width // 2, padding + arrow_height),
|
489 |
+
(center_x - arrow_width, padding + arrow_height)
|
490 |
+
]
|
491 |
+
draw.polygon(arrow_points, fill=color)
|
492 |
+
|
493 |
+
def _draw_import_icon(self, draw, size, color):
|
494 |
+
"""رسم أيقونة الاستيراد"""
|
495 |
+
width, height = size
|
496 |
+
padding = width // 8
|
497 |
+
|
498 |
+
# رسم سهم للداخل
|
499 |
+
center_y = height // 2
|
500 |
+
arrow_width = width // 2
|
501 |
+
arrow_height = height // 3
|
502 |
+
|
503 |
+
# رسم المستطيل
|
504 |
+
draw.rectangle(
|
505 |
+
[(width - padding - arrow_width // 2, padding), (width - padding, height - padding)],
|
506 |
+
fill=color
|
507 |
+
)
|
508 |
+
|
509 |
+
# رسم السهم
|
510 |
+
arrow_points = [
|
511 |
+
(padding, center_y),
|
512 |
+
(padding + arrow_width, center_y - arrow_height // 2),
|
513 |
+
(padding + arrow_width, center_y - arrow_height // 4),
|
514 |
+
(width - padding - arrow_width // 2, center_y - arrow_height // 4),
|
515 |
+
(width - padding - arrow_width // 2, center_y + arrow_height // 4),
|
516 |
+
(padding + arrow_width, center_y + arrow_height // 4),
|
517 |
+
(padding + arrow_width, center_y + arrow_height // 2)
|
518 |
+
]
|
519 |
+
draw.polygon(arrow_points, fill=color)
|
520 |
+
|
521 |
+
def _draw_export_icon(self, draw, size, color):
|
522 |
+
"""رسم أيقونة التصدير"""
|
523 |
+
width, height = size
|
524 |
+
padding = width // 8
|
525 |
+
|
526 |
+
# رسم سهم للخارج
|
527 |
+
center_y = height // 2
|
528 |
+
arrow_width = width // 2
|
529 |
+
arrow_height = height // 3
|
530 |
+
|
531 |
+
# رسم المستطيل
|
532 |
+
draw.rectangle(
|
533 |
+
[(padding, padding), (padding + arrow_width // 2, height - padding)],
|
534 |
+
fill=color
|
535 |
+
)
|
536 |
+
|
537 |
+
# رسم السهم
|
538 |
+
arrow_points = [
|
539 |
+
(width - padding, center_y),
|
540 |
+
(width - padding - arrow_width, center_y - arrow_height // 2),
|
541 |
+
(width - padding - arrow_width, center_y - arrow_height // 4),
|
542 |
+
(padding + arrow_width // 2, center_y - arrow_height // 4),
|
543 |
+
(padding + arrow_width // 2, center_y + arrow_height // 4),
|
544 |
+
(width - padding - arrow_width, center_y + arrow_height // 4),
|
545 |
+
(width - padding - arrow_width, center_y + arrow_height // 2)
|
546 |
+
]
|
547 |
+
draw.polygon(arrow_points, fill=color)
|
548 |
+
|
549 |
+
def _draw_save_icon(self, draw, size, color):
|
550 |
+
"""رسم أيقونة الحفظ"""
|
551 |
+
width, height = size
|
552 |
+
padding = width // 8
|
553 |
+
|
554 |
+
# رسم أيقونة القرص
|
555 |
+
draw.rectangle(
|
556 |
+
[(padding, padding), (width - padding, height - padding)],
|
557 |
+
fill=color
|
558 |
+
)
|
559 |
+
|
560 |
+
# رسم الشريط العلوي
|
561 |
+
draw.rectangle(
|
562 |
+
[(padding * 2, padding * 2), (width - padding * 2, padding * 4)],
|
563 |
+
fill=self.colors["white"]
|
564 |
+
)
|
565 |
+
|
566 |
+
# رسم المستطيل الداخلي
|
567 |
+
draw.rectangle(
|
568 |
+
[(width // 3, height // 2), (width * 2 // 3, height - padding * 2)],
|
569 |
+
fill=self.colors["white"]
|
570 |
+
)
|
571 |
+
|
572 |
+
def _draw_default_icon(self, draw, size, color):
|
573 |
+
"""رسم أيقونة افتراضية"""
|
574 |
+
width, height = size
|
575 |
+
padding = width // 8
|
576 |
+
|
577 |
+
# رسم دائرة
|
578 |
+
center_x = width // 2
|
579 |
+
center_y = height // 2
|
580 |
+
radius = min(width, height) // 2 - padding
|
581 |
+
|
582 |
+
draw.ellipse(
|
583 |
+
[(center_x - radius, center_y - radius), (center_x + radius, center_y + radius)],
|
584 |
+
fill=color
|
585 |
+
)
|
586 |
+
|
587 |
+
def generate_default_icons(self):
|
588 |
+
"""توليد الأيقونات الافتراضية"""
|
589 |
+
icons = [
|
590 |
+
"dashboard",
|
591 |
+
"projects",
|
592 |
+
"documents",
|
593 |
+
"pricing",
|
594 |
+
"resources",
|
595 |
+
"risk",
|
596 |
+
"reports",
|
597 |
+
"ai",
|
598 |
+
"settings",
|
599 |
+
"logout",
|
600 |
+
"search",
|
601 |
+
"add",
|
602 |
+
"upload",
|
603 |
+
"import",
|
604 |
+
"export",
|
605 |
+
"save"
|
606 |
+
]
|
607 |
+
|
608 |
+
for icon in icons:
|
609 |
+
self.generate_icon(icon)
|
styling/theme.py
ADDED
@@ -0,0 +1,541 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
ملف النمط لنظام إدارة المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import tkinter as tk
|
7 |
+
import customtkinter as ctk
|
8 |
+
from PIL import Image, ImageDraw
|
9 |
+
import matplotlib.pyplot as plt
|
10 |
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
11 |
+
|
12 |
+
class AppTheme:
|
13 |
+
"""فئة نمط التطبيق"""
|
14 |
+
|
15 |
+
# ألوان النمط الفاتح
|
16 |
+
LIGHT_BG_COLOR = "#F5F5F5"
|
17 |
+
LIGHT_FG_COLOR = "#333333"
|
18 |
+
LIGHT_CARD_BG_COLOR = "#FFFFFF"
|
19 |
+
LIGHT_SIDEBAR_BG_COLOR = "#2C3E50"
|
20 |
+
LIGHT_SIDEBAR_FG_COLOR = "#FFFFFF"
|
21 |
+
LIGHT_SIDEBAR_HOVER_COLOR = "#34495E"
|
22 |
+
LIGHT_SIDEBAR_ACTIVE_COLOR = "#1ABC9C"
|
23 |
+
LIGHT_BUTTON_BG_COLOR = "#2980B9"
|
24 |
+
LIGHT_BUTTON_HOVER_COLOR = "#3498DB"
|
25 |
+
LIGHT_BUTTON_ACTIVE_COLOR = "#1F618D"
|
26 |
+
LIGHT_INPUT_BG_COLOR = "#FFFFFF"
|
27 |
+
LIGHT_INPUT_FG_COLOR = "#333333"
|
28 |
+
LIGHT_BORDER_COLOR = "#E0E0E0"
|
29 |
+
|
30 |
+
# ألوان النمط الداكن
|
31 |
+
DARK_BG_COLOR = "#121212"
|
32 |
+
DARK_FG_COLOR = "#E0E0E0"
|
33 |
+
DARK_CARD_BG_COLOR = "#1E1E1E"
|
34 |
+
DARK_SIDEBAR_BG_COLOR = "#1A1A2E"
|
35 |
+
DARK_SIDEBAR_FG_COLOR = "#E0E0E0"
|
36 |
+
DARK_SIDEBAR_HOVER_COLOR = "#16213E"
|
37 |
+
DARK_SIDEBAR_ACTIVE_COLOR = "#0F3460"
|
38 |
+
DARK_BUTTON_BG_COLOR = "#0F3460"
|
39 |
+
DARK_BUTTON_HOVER_COLOR = "#16213E"
|
40 |
+
DARK_BUTTON_ACTIVE_COLOR = "#1A1A2E"
|
41 |
+
DARK_INPUT_BG_COLOR = "#2C2C2C"
|
42 |
+
DARK_INPUT_FG_COLOR = "#E0E0E0"
|
43 |
+
DARK_BORDER_COLOR = "#333333"
|
44 |
+
|
45 |
+
# ألوان الأساسية
|
46 |
+
PRIMARY_COLOR = {
|
47 |
+
"light": "#2980B9",
|
48 |
+
"dark": "#0F3460"
|
49 |
+
}
|
50 |
+
|
51 |
+
SECONDARY_COLOR = {
|
52 |
+
"light": "#1ABC9C",
|
53 |
+
"dark": "#16213E"
|
54 |
+
}
|
55 |
+
|
56 |
+
ACCENT_COLOR = {
|
57 |
+
"light": "#9B59B6",
|
58 |
+
"dark": "#533483"
|
59 |
+
}
|
60 |
+
|
61 |
+
WARNING_COLOR = {
|
62 |
+
"light": "#F39C12",
|
63 |
+
"dark": "#E58E26"
|
64 |
+
}
|
65 |
+
|
66 |
+
ERROR_COLOR = {
|
67 |
+
"light": "#E74C3C",
|
68 |
+
"dark": "#C0392B"
|
69 |
+
}
|
70 |
+
|
71 |
+
SUCCESS_COLOR = {
|
72 |
+
"light": "#2ECC71",
|
73 |
+
"dark": "#27AE60"
|
74 |
+
}
|
75 |
+
|
76 |
+
def __init__(self, config):
|
77 |
+
"""تهيئة النمط"""
|
78 |
+
self.config = config
|
79 |
+
self.current_theme = self.config.get_theme()
|
80 |
+
self.font_family = self.config.get_font()
|
81 |
+
self.font_size = self.config.get_font_size()
|
82 |
+
|
83 |
+
# تهيئة النمط
|
84 |
+
self._setup_theme()
|
85 |
+
|
86 |
+
def _setup_theme(self):
|
87 |
+
"""إعداد النمط"""
|
88 |
+
# تعيين نمط customtkinter
|
89 |
+
ctk.set_appearance_mode(self.current_theme)
|
90 |
+
ctk.set_default_color_theme("blue")
|
91 |
+
|
92 |
+
# تهيئة الخطوط
|
93 |
+
self.fonts = {
|
94 |
+
"title": (self.font_family, self.font_size + 8, "bold"),
|
95 |
+
"subtitle": (self.font_family, self.font_size + 4, "bold"),
|
96 |
+
"heading": (self.font_family, self.font_size + 2, "bold"),
|
97 |
+
"body": (self.font_family, self.font_size, "normal"),
|
98 |
+
"small": (self.font_family, self.font_size - 2, "normal")
|
99 |
+
}
|
100 |
+
|
101 |
+
def apply_theme_to_app(self, app):
|
102 |
+
"""تطبيق النمط على التطبيق"""
|
103 |
+
app.configure(fg_color=self.get_color("bg_color"))
|
104 |
+
|
105 |
+
def get_color(self, color_name):
|
106 |
+
"""الحصول على لون معين"""
|
107 |
+
if self.current_theme == "light":
|
108 |
+
colors = {
|
109 |
+
"bg_color": self.LIGHT_BG_COLOR,
|
110 |
+
"fg_color": self.LIGHT_FG_COLOR,
|
111 |
+
"card_bg_color": self.LIGHT_CARD_BG_COLOR,
|
112 |
+
"sidebar_bg_color": self.LIGHT_SIDEBAR_BG_COLOR,
|
113 |
+
"sidebar_fg_color": self.LIGHT_SIDEBAR_FG_COLOR,
|
114 |
+
"sidebar_hover_color": self.LIGHT_SIDEBAR_HOVER_COLOR,
|
115 |
+
"sidebar_active_color": self.LIGHT_SIDEBAR_ACTIVE_COLOR,
|
116 |
+
"button_bg_color": self.LIGHT_BUTTON_BG_COLOR,
|
117 |
+
"button_hover_color": self.LIGHT_BUTTON_HOVER_COLOR,
|
118 |
+
"button_active_color": self.LIGHT_BUTTON_ACTIVE_COLOR,
|
119 |
+
"input_bg_color": self.LIGHT_INPUT_BG_COLOR,
|
120 |
+
"input_fg_color": self.LIGHT_INPUT_FG_COLOR,
|
121 |
+
"border_color": self.LIGHT_BORDER_COLOR,
|
122 |
+
"primary_color": self.PRIMARY_COLOR["light"],
|
123 |
+
"secondary_color": self.SECONDARY_COLOR["light"],
|
124 |
+
"accent_color": self.ACCENT_COLOR["light"],
|
125 |
+
"warning_color": self.WARNING_COLOR["light"],
|
126 |
+
"error_color": self.ERROR_COLOR["light"],
|
127 |
+
"success_color": self.SUCCESS_COLOR["light"]
|
128 |
+
}
|
129 |
+
else:
|
130 |
+
colors = {
|
131 |
+
"bg_color": self.DARK_BG_COLOR,
|
132 |
+
"fg_color": self.DARK_FG_COLOR,
|
133 |
+
"card_bg_color": self.DARK_CARD_BG_COLOR,
|
134 |
+
"sidebar_bg_color": self.DARK_SIDEBAR_BG_COLOR,
|
135 |
+
"sidebar_fg_color": self.DARK_SIDEBAR_FG_COLOR,
|
136 |
+
"sidebar_hover_color": self.DARK_SIDEBAR_HOVER_COLOR,
|
137 |
+
"sidebar_active_color": self.DARK_SIDEBAR_ACTIVE_COLOR,
|
138 |
+
"button_bg_color": self.DARK_BUTTON_BG_COLOR,
|
139 |
+
"button_hover_color": self.DARK_BUTTON_HOVER_COLOR,
|
140 |
+
"button_active_color": self.DARK_BUTTON_ACTIVE_COLOR,
|
141 |
+
"input_bg_color": self.DARK_INPUT_BG_COLOR,
|
142 |
+
"input_fg_color": self.DARK_INPUT_FG_COLOR,
|
143 |
+
"border_color": self.DARK_BORDER_COLOR,
|
144 |
+
"primary_color": self.PRIMARY_COLOR["dark"],
|
145 |
+
"secondary_color": self.SECONDARY_COLOR["dark"],
|
146 |
+
"accent_color": self.ACCENT_COLOR["dark"],
|
147 |
+
"warning_color": self.WARNING_COLOR["dark"],
|
148 |
+
"error_color": self.ERROR_COLOR["dark"],
|
149 |
+
"success_color": self.SUCCESS_COLOR["dark"]
|
150 |
+
}
|
151 |
+
|
152 |
+
return colors.get(color_name, self.LIGHT_BG_COLOR)
|
153 |
+
|
154 |
+
def get_font(self, font_type):
|
155 |
+
"""الحصول على خط معين"""
|
156 |
+
return self.fonts.get(font_type, self.fonts["body"])
|
157 |
+
|
158 |
+
def toggle_theme(self):
|
159 |
+
"""تبديل النمط بين الفاتح والداكن"""
|
160 |
+
if self.current_theme == "light":
|
161 |
+
self.current_theme = "dark"
|
162 |
+
else:
|
163 |
+
self.current_theme = "light"
|
164 |
+
|
165 |
+
# تحديث النمط في الإعدادات
|
166 |
+
self.config.set_theme(self.current_theme)
|
167 |
+
|
168 |
+
# تحديث نمط customtkinter
|
169 |
+
ctk.set_appearance_mode(self.current_theme)
|
170 |
+
|
171 |
+
return self.current_theme
|
172 |
+
|
173 |
+
def create_styled_frame(self, parent, **kwargs):
|
174 |
+
"""إنشاء إطار منسق"""
|
175 |
+
default_kwargs = {
|
176 |
+
"fg_color": self.get_color("bg_color"),
|
177 |
+
"corner_radius": 10,
|
178 |
+
"border_width": 0
|
179 |
+
}
|
180 |
+
|
181 |
+
# دمج الخصائص المخصصة مع الخصائص الافتراضية
|
182 |
+
for key, value in kwargs.items():
|
183 |
+
default_kwargs[key] = value
|
184 |
+
|
185 |
+
return ctk.CTkFrame(parent, **default_kwargs)
|
186 |
+
|
187 |
+
def create_styled_scrollable_frame(self, parent, **kwargs):
|
188 |
+
"""إنشاء إطار قابل للتمرير منسق"""
|
189 |
+
default_kwargs = {
|
190 |
+
"fg_color": self.get_color("bg_color"),
|
191 |
+
"corner_radius": 10,
|
192 |
+
"border_width": 0
|
193 |
+
}
|
194 |
+
|
195 |
+
# دمج الخصائص المخصصة مع الخصائص الافتراضية
|
196 |
+
for key, value in kwargs.items():
|
197 |
+
default_kwargs[key] = value
|
198 |
+
|
199 |
+
return ctk.CTkScrollableFrame(parent, **default_kwargs)
|
200 |
+
|
201 |
+
def create_styled_label(self, parent, text, **kwargs):
|
202 |
+
"""إنشاء تسمية منسقة"""
|
203 |
+
default_kwargs = {
|
204 |
+
"text": text,
|
205 |
+
"font": self.get_font("body"),
|
206 |
+
"text_color": self.get_color("fg_color")
|
207 |
+
}
|
208 |
+
|
209 |
+
# دمج الخصائص المخصصة مع الخصائص الافتراضية
|
210 |
+
for key, value in kwargs.items():
|
211 |
+
default_kwargs[key] = value
|
212 |
+
|
213 |
+
return ctk.CTkLabel(parent, **default_kwargs)
|
214 |
+
|
215 |
+
def create_styled_button(self, parent, text, **kwargs):
|
216 |
+
"""إنشاء زر منسق"""
|
217 |
+
default_kwargs = {
|
218 |
+
"text": text,
|
219 |
+
"font": self.get_font("body"),
|
220 |
+
"fg_color": self.get_color("button_bg_color"),
|
221 |
+
"hover_color": self.get_color("button_hover_color"),
|
222 |
+
"text_color": "white",
|
223 |
+
"corner_radius": 8,
|
224 |
+
"border_width": 0,
|
225 |
+
"height": 36
|
226 |
+
}
|
227 |
+
|
228 |
+
# إضافة أيقونة إذا تم تحديدها
|
229 |
+
if "icon" in kwargs:
|
230 |
+
icon_name = kwargs.pop("icon")
|
231 |
+
# هنا يمكن إضافة منطق لتحميل الأيقونة
|
232 |
+
|
233 |
+
# دمج الخصائص المخصصة مع الخصائص الافتراضية
|
234 |
+
for key, value in kwargs.items():
|
235 |
+
default_kwargs[key] = value
|
236 |
+
|
237 |
+
return ctk.CTkButton(parent, **default_kwargs)
|
238 |
+
|
239 |
+
def create_styled_entry(self, parent, placeholder_text, **kwargs):
|
240 |
+
"""إنشاء حقل إدخال منسق"""
|
241 |
+
default_kwargs = {
|
242 |
+
"placeholder_text": placeholder_text,
|
243 |
+
"font": self.get_font("body"),
|
244 |
+
"fg_color": self.get_color("input_bg_color"),
|
245 |
+
"text_color": self.get_color("input_fg_color"),
|
246 |
+
"placeholder_text_color": self.get_color("border_color"),
|
247 |
+
"corner_radius": 8,
|
248 |
+
"border_width": 1,
|
249 |
+
"border_color": self.get_color("border_color"),
|
250 |
+
"height": 36
|
251 |
+
}
|
252 |
+
|
253 |
+
# دمج الخصائص المخصصة مع الخصائص الافتراضية
|
254 |
+
for key, value in kwargs.items():
|
255 |
+
default_kwargs[key] = value
|
256 |
+
|
257 |
+
return ctk.CTkEntry(parent, **default_kwargs)
|
258 |
+
|
259 |
+
def create_styled_textbox(self, parent, **kwargs):
|
260 |
+
"""إنشاء مربع نص منسق"""
|
261 |
+
default_kwargs = {
|
262 |
+
"font": self.get_font("body"),
|
263 |
+
"fg_color": self.get_color("input_bg_color"),
|
264 |
+
"text_color": self.get_color("input_fg_color"),
|
265 |
+
"corner_radius": 8,
|
266 |
+
"border_width": 1,
|
267 |
+
"border_color": self.get_color("border_color")
|
268 |
+
}
|
269 |
+
|
270 |
+
# دمج الخصائص المخصصة مع الخصائص الافتراضية
|
271 |
+
for key, value in kwargs.items():
|
272 |
+
default_kwargs[key] = value
|
273 |
+
|
274 |
+
return ctk.CTkTextbox(parent, **default_kwargs)
|
275 |
+
|
276 |
+
def create_styled_combobox(self, parent, values, **kwargs):
|
277 |
+
"""إنشاء قائمة منسدلة منسقة"""
|
278 |
+
default_kwargs = {
|
279 |
+
"values": values,
|
280 |
+
"font": self.get_font("body"),
|
281 |
+
"fg_color": self.get_color("input_bg_color"),
|
282 |
+
"text_color": self.get_color("input_fg_color"),
|
283 |
+
"border_color": self.get_color("border_color"),
|
284 |
+
"button_color": self.get_color("button_bg_color"),
|
285 |
+
"button_hover_color": self.get_color("button_hover_color"),
|
286 |
+
"dropdown_fg_color": self.get_color("card_bg_color"),
|
287 |
+
"dropdown_text_color": self.get_color("fg_color"),
|
288 |
+
"dropdown_hover_color": self.get_color("sidebar_hover_color"),
|
289 |
+
"corner_radius": 8,
|
290 |
+
"border_width": 1,
|
291 |
+
"dropdown_font": self.get_font("body")
|
292 |
+
}
|
293 |
+
|
294 |
+
# دمج الخصائص المخصصة مع الخصائص الافتراضية
|
295 |
+
for key, value in kwargs.items():
|
296 |
+
default_kwargs[key] = value
|
297 |
+
|
298 |
+
return ctk.CTkComboBox(parent, **default_kwargs)
|
299 |
+
|
300 |
+
def create_styled_switch(self, parent, text, **kwargs):
|
301 |
+
"""إنشاء مفتاح تبديل منسق"""
|
302 |
+
default_kwargs = {
|
303 |
+
"text": text,
|
304 |
+
"font": self.get_font("body"),
|
305 |
+
"fg_color": self.get_color("border_color"),
|
306 |
+
"progress_color": self.get_color("button_bg_color"),
|
307 |
+
"button_color": "white",
|
308 |
+
"button_hover_color": "white",
|
309 |
+
"text_color": self.get_color("fg_color")
|
310 |
+
}
|
311 |
+
|
312 |
+
# دمج الخصائص المخصصة مع الخصائص الافتراضية
|
313 |
+
for key, value in kwargs.items():
|
314 |
+
default_kwargs[key] = value
|
315 |
+
|
316 |
+
return ctk.CTkSwitch(parent, **default_kwargs)
|
317 |
+
|
318 |
+
def create_styled_radio_button(self, parent, text, variable, value, **kwargs):
|
319 |
+
"""إنشاء زر راديو منسق"""
|
320 |
+
default_kwargs = {
|
321 |
+
"text": text,
|
322 |
+
"font": self.get_font("body"),
|
323 |
+
"fg_color": self.get_color("button_bg_color"),
|
324 |
+
"border_color": self.get_color("border_color"),
|
325 |
+
"hover_color": self.get_color("button_hover_color"),
|
326 |
+
"text_color": self.get_color("fg_color"),
|
327 |
+
"variable": variable,
|
328 |
+
"value": value
|
329 |
+
}
|
330 |
+
|
331 |
+
# دمج الخصائص المخصصة مع الخصائص الافتراضية
|
332 |
+
for key, value in kwargs.items():
|
333 |
+
default_kwargs[key] = value
|
334 |
+
|
335 |
+
return ctk.CTkRadioButton(parent, **default_kwargs)
|
336 |
+
|
337 |
+
def create_styled_slider(self, parent, **kwargs):
|
338 |
+
"""إنشاء شريط تمرير منسق"""
|
339 |
+
default_kwargs = {
|
340 |
+
"fg_color": self.get_color("border_color"),
|
341 |
+
"progress_color": self.get_color("button_bg_color"),
|
342 |
+
"button_color": self.get_color("button_bg_color"),
|
343 |
+
"button_hover_color": self.get_color("button_hover_color")
|
344 |
+
}
|
345 |
+
|
346 |
+
# دمج الخصائص المخصصة مع الخصائص الافتراضية
|
347 |
+
for key, value in kwargs.items():
|
348 |
+
default_kwargs[key] = value
|
349 |
+
|
350 |
+
return ctk.CTkSlider(parent, **default_kwargs)
|
351 |
+
|
352 |
+
def create_styled_tabview(self, parent, **kwargs):
|
353 |
+
"""إنشاء عرض تبويب منسق"""
|
354 |
+
default_kwargs = {
|
355 |
+
"fg_color": self.get_color("card_bg_color"),
|
356 |
+
"segmented_button_fg_color": self.get_color("sidebar_bg_color"),
|
357 |
+
"segmented_button_selected_color": self.get_color("button_bg_color"),
|
358 |
+
"segmented_button_unselected_color": self.get_color("sidebar_bg_color"),
|
359 |
+
"segmented_button_selected_hover_color": self.get_color("button_hover_color"),
|
360 |
+
"segmented_button_unselected_hover_color": self.get_color("sidebar_hover_color"),
|
361 |
+
"segmented_button_text_color": self.get_color("sidebar_fg_color"),
|
362 |
+
"segmented_button_selected_text_color": "white",
|
363 |
+
"text_color": self.get_color("fg_color"),
|
364 |
+
"corner_radius": 10
|
365 |
+
}
|
366 |
+
|
367 |
+
# دمج الخصائص المخصصة مع الخصائص الافتراضية
|
368 |
+
for key, value in kwargs.items():
|
369 |
+
default_kwargs[key] = value
|
370 |
+
|
371 |
+
return ctk.CTkTabview(parent, **default_kwargs)
|
372 |
+
|
373 |
+
def create_styled_sidebar_button(self, parent, text, icon, command=None):
|
374 |
+
"""إنشاء زر الشريط الجانبي المنسق"""
|
375 |
+
# إنشاء إطار للزر
|
376 |
+
button_frame = ctk.CTkFrame(
|
377 |
+
parent,
|
378 |
+
fg_color="transparent",
|
379 |
+
corner_radius=0
|
380 |
+
)
|
381 |
+
|
382 |
+
# إنشاء الزر
|
383 |
+
button = ctk.CTkButton(
|
384 |
+
button_frame,
|
385 |
+
text=text,
|
386 |
+
font=self.get_font("body"),
|
387 |
+
fg_color="transparent",
|
388 |
+
hover_color=self.get_color("sidebar_hover_color"),
|
389 |
+
text_color=self.get_color("sidebar_fg_color"),
|
390 |
+
anchor="w",
|
391 |
+
corner_radius=0,
|
392 |
+
border_width=0,
|
393 |
+
height=40,
|
394 |
+
command=command
|
395 |
+
)
|
396 |
+
button.pack(fill="x", padx=0, pady=0)
|
397 |
+
|
398 |
+
return button_frame, button
|
399 |
+
|
400 |
+
def create_styled_card(self, parent, title):
|
401 |
+
"""إنشاء بطاقة منسقة"""
|
402 |
+
# إنشاء إطار البطاقة
|
403 |
+
card = self.create_styled_frame(
|
404 |
+
parent,
|
405 |
+
fg_color=self.get_color("card_bg_color")
|
406 |
+
)
|
407 |
+
|
408 |
+
# إنشاء عنوان البطاقة
|
409 |
+
title_label = self.create_styled_label(
|
410 |
+
card,
|
411 |
+
title,
|
412 |
+
font=self.get_font("heading")
|
413 |
+
)
|
414 |
+
title_label.pack(anchor="w", padx=15, pady=(15, 5))
|
415 |
+
|
416 |
+
# إنشاء خط فاصل
|
417 |
+
separator = ctk.CTkFrame(
|
418 |
+
card,
|
419 |
+
height=1,
|
420 |
+
fg_color=self.get_color("border_color")
|
421 |
+
)
|
422 |
+
separator.pack(fill="x", padx=15, pady=(5, 0))
|
423 |
+
|
424 |
+
# إنشاء إطار المحتوى
|
425 |
+
content_frame = self.create_styled_frame(
|
426 |
+
card,
|
427 |
+
fg_color="transparent"
|
428 |
+
)
|
429 |
+
content_frame.pack(fill="both", expand=True, padx=15, pady=15)
|
430 |
+
|
431 |
+
return card, content_frame
|
432 |
+
|
433 |
+
def create_styled_data_table(self, parent, columns, data):
|
434 |
+
"""إنشاء جدول بيانات منسق"""
|
435 |
+
# إنشاء إطار الجدول
|
436 |
+
table_frame = self.create_styled_frame(
|
437 |
+
parent,
|
438 |
+
fg_color="transparent"
|
439 |
+
)
|
440 |
+
|
441 |
+
# إنشاء إطار العناوين
|
442 |
+
header_frame = self.create_styled_frame(
|
443 |
+
table_frame,
|
444 |
+
fg_color=self.get_color("sidebar_bg_color")
|
445 |
+
)
|
446 |
+
header_frame.pack(fill="x", padx=0, pady=(0, 1))
|
447 |
+
|
448 |
+
# إنشاء عناوين الأعمدة
|
449 |
+
for i, column in enumerate(columns):
|
450 |
+
column_label = self.create_styled_label(
|
451 |
+
header_frame,
|
452 |
+
column,
|
453 |
+
font=self.get_font("heading"),
|
454 |
+
text_color=self.get_color("sidebar_fg_color")
|
455 |
+
)
|
456 |
+
column_label.grid(row=0, column=i, sticky="ew", padx=10, pady=10)
|
457 |
+
header_frame.grid_columnconfigure(i, weight=1)
|
458 |
+
|
459 |
+
# إنشاء إطار البيانات
|
460 |
+
data_frame = self.create_styled_scrollable_frame(
|
461 |
+
table_frame,
|
462 |
+
fg_color="transparent"
|
463 |
+
)
|
464 |
+
data_frame.pack(fill="both", expand=True, padx=0, pady=0)
|
465 |
+
|
466 |
+
# إنشاء صفوف البيانات
|
467 |
+
for i, row in enumerate(data):
|
468 |
+
row_frame = self.create_styled_frame(
|
469 |
+
data_frame,
|
470 |
+
fg_color=self.get_color("card_bg_color") if i % 2 == 0 else self.get_color("bg_color")
|
471 |
+
)
|
472 |
+
row_frame.pack(fill="x", padx=0, pady=(0, 1))
|
473 |
+
|
474 |
+
for j, cell in enumerate(row):
|
475 |
+
cell_label = self.create_styled_label(
|
476 |
+
row_frame,
|
477 |
+
cell,
|
478 |
+
font=self.get_font("body")
|
479 |
+
)
|
480 |
+
cell_label.grid(row=0, column=j, sticky="ew", padx=10, pady=10)
|
481 |
+
row_frame.grid_columnconfigure(j, weight=1)
|
482 |
+
|
483 |
+
return table_frame, data_frame
|
484 |
+
|
485 |
+
def create_styled_message_box(self, title, message, message_type="info"):
|
486 |
+
"""إنشاء مربع رسالة منسق"""
|
487 |
+
# تحديد لون الرسالة بناءً على النوع
|
488 |
+
if message_type == "error":
|
489 |
+
color = self.get_color("error_color")
|
490 |
+
elif message_type == "warning":
|
491 |
+
color = self.get_color("warning_color")
|
492 |
+
elif message_type == "success":
|
493 |
+
color = self.get_color("success_color")
|
494 |
+
else: # info
|
495 |
+
color = self.get_color("primary_color")
|
496 |
+
|
497 |
+
# إنشاء نافذة الرسالة
|
498 |
+
message_window = ctk.CTkToplevel()
|
499 |
+
message_window.title(title)
|
500 |
+
message_window.geometry("400x200")
|
501 |
+
message_window.resizable(False, False)
|
502 |
+
message_window.grab_set() # جعل النافذة المنبثقة مركز الاهتمام
|
503 |
+
|
504 |
+
# تطبيق النمط
|
505 |
+
message_window.configure(fg_color=self.get_color("bg_color"))
|
506 |
+
|
507 |
+
# إنشاء إطار الرسالة
|
508 |
+
message_frame = self.create_styled_frame(
|
509 |
+
message_window,
|
510 |
+
fg_color=self.get_color("card_bg_color")
|
511 |
+
)
|
512 |
+
message_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
513 |
+
|
514 |
+
# إنشاء عنوان الرسالة
|
515 |
+
title_label = self.create_styled_label(
|
516 |
+
message_frame,
|
517 |
+
title,
|
518 |
+
font=self.get_font("heading"),
|
519 |
+
text_color=color
|
520 |
+
)
|
521 |
+
title_label.pack(padx=20, pady=(20, 10))
|
522 |
+
|
523 |
+
# إنشاء نص الرسالة
|
524 |
+
message_label = self.create_styled_label(
|
525 |
+
message_frame,
|
526 |
+
message,
|
527 |
+
font=self.get_font("body")
|
528 |
+
)
|
529 |
+
message_label.pack(padx=20, pady=(0, 20))
|
530 |
+
|
531 |
+
# إنشاء زر موافق
|
532 |
+
ok_button = self.create_styled_button(
|
533 |
+
message_frame,
|
534 |
+
"موافق",
|
535 |
+
fg_color=color,
|
536 |
+
hover_color=color,
|
537 |
+
command=message_window.destroy
|
538 |
+
)
|
539 |
+
ok_button.pack(pady=(0, 20))
|
540 |
+
|
541 |
+
return message_window
|
tests/test_app.py
ADDED
@@ -0,0 +1,601 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة اختبار التطبيق لنظام إدارة المناقصات - Hybrid Face
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import sys
|
7 |
+
import logging
|
8 |
+
import unittest
|
9 |
+
import tkinter as tk
|
10 |
+
from pathlib import Path
|
11 |
+
|
12 |
+
# تهيئة السجل
|
13 |
+
logging.basicConfig(
|
14 |
+
level=logging.INFO,
|
15 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
16 |
+
)
|
17 |
+
logger = logging.getLogger('test_app')
|
18 |
+
|
19 |
+
# إضافة المسار الرئيسي للتطبيق إلى مسار البحث
|
20 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
21 |
+
|
22 |
+
# استيراد الوحدات المطلوبة للاختبار
|
23 |
+
from database.db_connector import DatabaseConnector
|
24 |
+
from database.models import User, Project, Document, ProjectItem, Resource, Risk, Report, SystemLog
|
25 |
+
from modules.document_analysis.analyzer import DocumentAnalyzer
|
26 |
+
from modules.pricing.pricing_engine import PricingEngine
|
27 |
+
from modules.risk_analysis.risk_analyzer import RiskAnalyzer
|
28 |
+
from modules.ai_assistant.assistant import AIAssistant
|
29 |
+
from styling.theme import AppTheme
|
30 |
+
from styling.icons import IconGenerator
|
31 |
+
from styling.charts import ChartGenerator
|
32 |
+
from config import AppConfig
|
33 |
+
|
34 |
+
class TestDatabaseConnector(unittest.TestCase):
|
35 |
+
"""اختبار وحدة اتصال قاعدة البيانات"""
|
36 |
+
|
37 |
+
def setUp(self):
|
38 |
+
"""إعداد بيئة الاختبار"""
|
39 |
+
# استخدام قاعدة بيانات اختبار مؤقتة
|
40 |
+
self.config = AppConfig()
|
41 |
+
self.config.DB_NAME = "test_tender_system.db"
|
42 |
+
self.db = DatabaseConnector(self.config)
|
43 |
+
|
44 |
+
def tearDown(self):
|
45 |
+
"""تنظيف بيئة الاختبار"""
|
46 |
+
self.db.disconnect()
|
47 |
+
# حذف قاعدة البيانات المؤقتة
|
48 |
+
if os.path.exists(self.config.DB_NAME):
|
49 |
+
os.remove(self.config.DB_NAME)
|
50 |
+
|
51 |
+
def test_connection(self):
|
52 |
+
"""اختبار الاتصال بقاعدة البيانات"""
|
53 |
+
self.assertTrue(self.db.is_connected)
|
54 |
+
|
55 |
+
def test_execute_query(self):
|
56 |
+
"""اختبار تنفيذ استعلام"""
|
57 |
+
cursor = self.db.execute("SELECT 1")
|
58 |
+
self.assertIsNotNone(cursor)
|
59 |
+
result = cursor.fetchone()
|
60 |
+
self.assertEqual(result[0], 1)
|
61 |
+
|
62 |
+
def test_insert_and_fetch(self):
|
63 |
+
"""اختبار إدراج واسترجاع البيانات"""
|
64 |
+
# إدراج بيانات
|
65 |
+
user_data = {
|
66 |
+
"username": "test_user",
|
67 |
+
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
|
68 |
+
"full_name": "مستخدم اختبار",
|
69 |
+
"email": "[email protected]",
|
70 |
+
"role": "user"
|
71 |
+
}
|
72 |
+
user_id = self.db.insert("users", user_data)
|
73 |
+
self.assertIsNotNone(user_id)
|
74 |
+
|
75 |
+
# استرجاع البيانات
|
76 |
+
user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
|
77 |
+
self.assertIsNotNone(user)
|
78 |
+
self.assertEqual(user["username"], "test_user")
|
79 |
+
self.assertEqual(user["full_name"], "مستخدم اختبار")
|
80 |
+
|
81 |
+
def test_update(self):
|
82 |
+
"""اختبار تحديث البيانات"""
|
83 |
+
# إدراج بيانات
|
84 |
+
user_data = {
|
85 |
+
"username": "update_user",
|
86 |
+
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
|
87 |
+
"full_name": "مستخدم للتحديث",
|
88 |
+
"email": "[email protected]",
|
89 |
+
"role": "user"
|
90 |
+
}
|
91 |
+
user_id = self.db.insert("users", user_data)
|
92 |
+
|
93 |
+
# تحديث البيانات
|
94 |
+
updated_data = {
|
95 |
+
"full_name": "مستخدم تم تحديثه",
|
96 |
+
"role": "admin"
|
97 |
+
}
|
98 |
+
rows_affected = self.db.update("users", updated_data, "id = ?", (user_id,))
|
99 |
+
self.assertEqual(rows_affected, 1)
|
100 |
+
|
101 |
+
# التحقق من التحديث
|
102 |
+
user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
|
103 |
+
self.assertEqual(user["full_name"], "مستخدم تم تحديثه")
|
104 |
+
self.assertEqual(user["role"], "admin")
|
105 |
+
|
106 |
+
def test_delete(self):
|
107 |
+
"""اختبار حذف البيانات"""
|
108 |
+
# إدراج بيانات
|
109 |
+
user_data = {
|
110 |
+
"username": "delete_user",
|
111 |
+
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
|
112 |
+
"full_name": "مستخدم للحذف",
|
113 |
+
"email": "[email protected]",
|
114 |
+
"role": "user"
|
115 |
+
}
|
116 |
+
user_id = self.db.insert("users", user_data)
|
117 |
+
|
118 |
+
# حذف البيانات
|
119 |
+
rows_affected = self.db.delete("users", "id = ?", (user_id,))
|
120 |
+
self.assertEqual(rows_affected, 1)
|
121 |
+
|
122 |
+
# التحقق من الحذف
|
123 |
+
user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
|
124 |
+
self.assertIsNone(user)
|
125 |
+
|
126 |
+
|
127 |
+
class TestModels(unittest.TestCase):
|
128 |
+
"""اختبار نماذج البيانات"""
|
129 |
+
|
130 |
+
def setUp(self):
|
131 |
+
"""��عداد بيئة الاختبار"""
|
132 |
+
# استخدام قاعدة بيانات اختبار مؤقتة
|
133 |
+
self.config = AppConfig()
|
134 |
+
self.config.DB_NAME = "test_models.db"
|
135 |
+
self.db = DatabaseConnector(self.config)
|
136 |
+
|
137 |
+
def tearDown(self):
|
138 |
+
"""تنظيف بيئة الاختبار"""
|
139 |
+
self.db.disconnect()
|
140 |
+
# حذف قاعدة البيانات المؤقتة
|
141 |
+
if os.path.exists(self.config.DB_NAME):
|
142 |
+
os.remove(self.config.DB_NAME)
|
143 |
+
|
144 |
+
def test_user_model(self):
|
145 |
+
"""اختبار نموذج المستخدم"""
|
146 |
+
# إنشاء مستخدم جديد
|
147 |
+
user = User(self.db)
|
148 |
+
user.data = {
|
149 |
+
"username": "model_user",
|
150 |
+
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
|
151 |
+
"full_name": "مستخدم نموذج",
|
152 |
+
"email": "[email protected]",
|
153 |
+
"role": "user",
|
154 |
+
"is_active": 1
|
155 |
+
}
|
156 |
+
|
157 |
+
# حفظ المستخدم
|
158 |
+
self.assertTrue(user.save())
|
159 |
+
self.assertIsNotNone(user.data.get("id"))
|
160 |
+
|
161 |
+
# استرجاع المستخدم
|
162 |
+
retrieved_user = User.get_by_id(user.data["id"], self.db)
|
163 |
+
self.assertIsNotNone(retrieved_user)
|
164 |
+
self.assertEqual(retrieved_user.data["username"], "model_user")
|
165 |
+
|
166 |
+
# مصادقة المستخدم
|
167 |
+
authenticated_user = User.authenticate("model_user", "admin", self.db)
|
168 |
+
self.assertIsNotNone(authenticated_user)
|
169 |
+
self.assertEqual(authenticated_user.data["username"], "model_user")
|
170 |
+
|
171 |
+
# تعيين كلمة مرور جديدة
|
172 |
+
user.set_password("newpassword")
|
173 |
+
user.save()
|
174 |
+
|
175 |
+
# مصادقة المستخدم بكلمة المرور الجديدة
|
176 |
+
authenticated_user = User.authenticate("model_user", "newpassword", self.db)
|
177 |
+
self.assertIsNotNone(authenticated_user)
|
178 |
+
|
179 |
+
# حذف المستخدم
|
180 |
+
self.assertTrue(user.delete())
|
181 |
+
|
182 |
+
def test_project_model(self):
|
183 |
+
"""اختبار نموذج المشروع"""
|
184 |
+
# إنشاء مستخدم للمشروع
|
185 |
+
user = User(self.db)
|
186 |
+
user.data = {
|
187 |
+
"username": "project_user",
|
188 |
+
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
|
189 |
+
"full_name": "مستخدم المشروع",
|
190 |
+
"email": "[email protected]",
|
191 |
+
"role": "user",
|
192 |
+
"is_active": 1
|
193 |
+
}
|
194 |
+
user.save()
|
195 |
+
|
196 |
+
# إنشاء مشروع جديد
|
197 |
+
project = Project(self.db)
|
198 |
+
project.data = {
|
199 |
+
"name": "مشروع اختبار",
|
200 |
+
"client": "عميل اختبار",
|
201 |
+
"description": "وصف مشروع الاختبار",
|
202 |
+
"start_date": "2025-01-01",
|
203 |
+
"end_date": "2025-12-31",
|
204 |
+
"status": "تخطيط",
|
205 |
+
"created_by": user.data["id"]
|
206 |
+
}
|
207 |
+
|
208 |
+
# حفظ المشروع
|
209 |
+
self.assertTrue(project.save())
|
210 |
+
self.assertIsNotNone(project.data.get("id"))
|
211 |
+
|
212 |
+
# استرجاع المشروع
|
213 |
+
retrieved_project = Project.get_by_id(project.data["id"], self.db)
|
214 |
+
self.assertIsNotNone(retrieved_project)
|
215 |
+
self.assertEqual(retrieved_project.data["name"], "مشروع اختبار")
|
216 |
+
|
217 |
+
# إضافة بند للمشروع
|
218 |
+
item = ProjectItem(self.db)
|
219 |
+
item.data = {
|
220 |
+
"project_id": project.data["id"],
|
221 |
+
"name": "بند اختبار",
|
222 |
+
"description": "وصف بند الاختبار",
|
223 |
+
"unit": "م²",
|
224 |
+
"quantity": 100,
|
225 |
+
"unit_price": 500,
|
226 |
+
"total_price": 50000
|
227 |
+
}
|
228 |
+
item.save()
|
229 |
+
|
230 |
+
# حساب التكلفة الإجمالية للمشروع
|
231 |
+
total_cost = project.calculate_total_cost()
|
232 |
+
self.assertEqual(total_cost, 50000)
|
233 |
+
|
234 |
+
# حذف المشروع
|
235 |
+
self.assertTrue(project.delete())
|
236 |
+
|
237 |
+
# حذف المستخدم
|
238 |
+
self.assertTrue(user.delete())
|
239 |
+
|
240 |
+
|
241 |
+
class TestDocumentAnalyzer(unittest.TestCase):
|
242 |
+
"""اختبار محلل المستندات"""
|
243 |
+
|
244 |
+
def setUp(self):
|
245 |
+
"""إعداد بيئة الاختبار"""
|
246 |
+
self.config = AppConfig()
|
247 |
+
self.analyzer = DocumentAnalyzer(self.config)
|
248 |
+
|
249 |
+
# إنشاء مجلد المستندات للاختبار
|
250 |
+
self.test_docs_dir = Path("test_documents")
|
251 |
+
self.test_docs_dir.mkdir(exist_ok=True)
|
252 |
+
|
253 |
+
# إنشاء ملف مستند اختبار
|
254 |
+
self.test_doc_path = self.test_docs_dir / "test_document.txt"
|
255 |
+
with open(self.test_doc_path, "w", encoding="utf-8") as f:
|
256 |
+
f.write("هذا مستند اختبار لمحلل المستندات")
|
257 |
+
|
258 |
+
def tearDown(self):
|
259 |
+
"""تنظيف بيئة الاختبار"""
|
260 |
+
# حذف ملف المستند
|
261 |
+
if self.test_doc_path.exists():
|
262 |
+
self.test_doc_path.unlink()
|
263 |
+
|
264 |
+
# حذف مجلد المستندات
|
265 |
+
if self.test_docs_dir.exists():
|
266 |
+
self.test_docs_dir.rmdir()
|
267 |
+
|
268 |
+
def test_analyze_document(self):
|
269 |
+
"""اختبار تحليل المستند"""
|
270 |
+
# تحليل المستند
|
271 |
+
result = self.analyzer.analyze_document(str(self.test_doc_path), "tender")
|
272 |
+
self.assertTrue(result)
|
273 |
+
|
274 |
+
# انتظار اكتمال التحليل
|
275 |
+
import time
|
276 |
+
max_wait = 5 # ثوانٍ
|
277 |
+
waited = 0
|
278 |
+
while self.analyzer.analysis_in_progress and waited < max_wait:
|
279 |
+
time.sleep(0.5)
|
280 |
+
waited += 0.5
|
281 |
+
|
282 |
+
# التحقق من نتائج التحليل
|
283 |
+
self.assertFalse(self.analyzer.analysis_in_progress)
|
284 |
+
results = self.analyzer.get_analysis_results()
|
285 |
+
self.assertEqual(results["status"], "اكتمل التحليل")
|
286 |
+
self.assertEqual(results["document_path"], str(self.test_doc_path))
|
287 |
+
|
288 |
+
def test_export_analysis_results(self):
|
289 |
+
"""اختبار تصدير نتائج التحليل"""
|
290 |
+
# تحليل المستند
|
291 |
+
self.analyzer.analyze_document(str(self.test_doc_path), "tender")
|
292 |
+
|
293 |
+
# انتظار اكتمال التحليل
|
294 |
+
import time
|
295 |
+
max_wait = 5 # ثوانٍ
|
296 |
+
waited = 0
|
297 |
+
while self.analyzer.analysis_in_progress and waited < max_wait:
|
298 |
+
time.sleep(0.5)
|
299 |
+
waited += 0.5
|
300 |
+
|
301 |
+
# تصدير النتائج
|
302 |
+
export_path = self.test_docs_dir / "analysis_results.json"
|
303 |
+
result_path = self.analyzer.export_analysis_results(str(export_path))
|
304 |
+
self.assertIsNotNone(result_path)
|
305 |
+
|
306 |
+
# التحقق من وجود ملف التصدير
|
307 |
+
self.assertTrue(export_path.exists())
|
308 |
+
|
309 |
+
# حذف ملف التصدير
|
310 |
+
if export_path.exists():
|
311 |
+
export_path.unlink()
|
312 |
+
|
313 |
+
|
314 |
+
class TestPricingEngine(unittest.TestCase):
|
315 |
+
"""اختبار محرك التسعير"""
|
316 |
+
|
317 |
+
def setUp(self):
|
318 |
+
"""إعداد بيئة الاختبار"""
|
319 |
+
self.config = AppConfig()
|
320 |
+
self.pricing_engine = PricingEngine(self.config)
|
321 |
+
|
322 |
+
def test_calculate_pricing(self):
|
323 |
+
"""اختبار حساب التسعير"""
|
324 |
+
# حساب التسعير
|
325 |
+
result = self.pricing_engine.calculate_pricing(1, "comprehensive")
|
326 |
+
self.assertTrue(result)
|
327 |
+
|
328 |
+
# انتظار اكتمال التسعير
|
329 |
+
import time
|
330 |
+
max_wait = 5 # ثوانٍ
|
331 |
+
waited = 0
|
332 |
+
while self.pricing_engine.pricing_in_progress and waited < max_wait:
|
333 |
+
time.sleep(0.5)
|
334 |
+
waited += 0.5
|
335 |
+
|
336 |
+
# التحقق من نتائج التسعير
|
337 |
+
self.assertFalse(self.pricing_engine.pricing_in_progress)
|
338 |
+
results = self.pricing_engine.get_pricing_results()
|
339 |
+
self.assertEqual(results["status"], "اكتمل التسعير")
|
340 |
+
self.assertEqual(results["project_id"], 1)
|
341 |
+
self.assertEqual(results["strategy"], "comprehensive")
|
342 |
+
|
343 |
+
# التحقق من وجود التكاليف المباشرة
|
344 |
+
self.assertIn("direct_costs", results)
|
345 |
+
self.assertIn("total_direct_costs", results["direct_costs"])
|
346 |
+
|
347 |
+
# التحقق من وجود التكاليف غير المباشرة
|
348 |
+
self.assertIn("indirect_costs", results)
|
349 |
+
self.assertIn("total_indirect_costs", results["indirect_costs"])
|
350 |
+
|
351 |
+
# التحقق من وجود تكاليف المخاطر
|
352 |
+
self.assertIn("risk_costs", results)
|
353 |
+
self.assertIn("total_risk_cost", results["risk_costs"])
|
354 |
+
|
355 |
+
# التحقق من وجود ملخص التسعير
|
356 |
+
self.assertIn("summary", results)
|
357 |
+
self.assertIn("final_price", results["summary"])
|
358 |
+
|
359 |
+
|
360 |
+
class TestRiskAnalyzer(unittest.TestCase):
|
361 |
+
"""اختبار محلل المخاطر"""
|
362 |
+
|
363 |
+
def setUp(self):
|
364 |
+
"""إعداد بيئة الاختبار"""
|
365 |
+
self.config = AppConfig()
|
366 |
+
self.risk_analyzer = RiskAnalyzer(self.config)
|
367 |
+
|
368 |
+
def test_analyze_risks(self):
|
369 |
+
"""اختبار تحليل المخاطر"""
|
370 |
+
# تحليل المخاطر
|
371 |
+
result = self.risk_analyzer.analyze_risks(1, "comprehensive")
|
372 |
+
self.assertTrue(result)
|
373 |
+
|
374 |
+
# انتظار اكتمال التحليل
|
375 |
+
import time
|
376 |
+
max_wait = 5 # ثوانٍ
|
377 |
+
waited = 0
|
378 |
+
while self.risk_analyzer.analysis_in_progress and waited < max_wait:
|
379 |
+
time.sleep(0.5)
|
380 |
+
waited += 0.5
|
381 |
+
|
382 |
+
# التحقق من نتائج التحليل
|
383 |
+
self.assertFalse(self.risk_analyzer.analysis_in_progress)
|
384 |
+
results = self.risk_analyzer.get_analysis_results()
|
385 |
+
self.assertEqual(results["status"], "اكتمل التحليل")
|
386 |
+
self.assertEqual(results["project_id"], 1)
|
387 |
+
self.assertEqual(results["method"], "comprehensive")
|
388 |
+
|
389 |
+
# التحقق من وجود المخاطر المحددة
|
390 |
+
self.assertIn("identified_risks", results)
|
391 |
+
self.assertTrue(len(results["identified_risks"]) > 0)
|
392 |
+
|
393 |
+
# التحقق من وجود فئات المخاطر
|
394 |
+
self.assertIn("risk_categories", results)
|
395 |
+
|
396 |
+
# التحقق من وجود مصفوفة المخاطر
|
397 |
+
self.assertIn("risk_matrix", results)
|
398 |
+
|
399 |
+
# التحقق من وجود استراتيجيات التخفيف
|
400 |
+
self.assertIn("mitigation_strategies", results)
|
401 |
+
self.assertTrue(len(results["mitigation_strategies"]) > 0)
|
402 |
+
|
403 |
+
# التحقق من وجود ملخص التحليل
|
404 |
+
self.assertIn("summary", results)
|
405 |
+
self.assertIn("overall_risk_level", results["summary"])
|
406 |
+
|
407 |
+
|
408 |
+
class TestAIAssistant(unittest.TestCase):
|
409 |
+
"""اختبار المساعد الذكي"""
|
410 |
+
|
411 |
+
def setUp(self):
|
412 |
+
"""إعداد بيئة الاختبار"""
|
413 |
+
self.config = AppConfig()
|
414 |
+
self.assistant = AIAssistant(self.config)
|
415 |
+
|
416 |
+
def test_process_query(self):
|
417 |
+
"""اختبار معالجة الاستعلام"""
|
418 |
+
# معالجة استعلام
|
419 |
+
query = "كيف يمكنني تحليل مستند مناقصة؟"
|
420 |
+
result = self.assistant.process_query(query)
|
421 |
+
self.assertTrue(result)
|
422 |
+
|
423 |
+
# انتظار اكتمال المعالجة
|
424 |
+
import time
|
425 |
+
max_wait = 5 # ثوانٍ
|
426 |
+
waited = 0
|
427 |
+
while self.assistant.processing_in_progress and waited < max_wait:
|
428 |
+
time.sleep(0.5)
|
429 |
+
waited += 0.5
|
430 |
+
|
431 |
+
# التحقق من نتائج المعالجة
|
432 |
+
self.assertFalse(self.assistant.processing_in_progress)
|
433 |
+
results = self.assistant.get_processing_results()
|
434 |
+
self.assertEqual(results["status"], "اكتملت المعالجة")
|
435 |
+
self.assertEqual(results["query"], query)
|
436 |
+
|
437 |
+
# التحقق من وجود استجابة
|
438 |
+
self.assertIn("response", results)
|
439 |
+
self.assertTrue(len(results["response"]) > 0)
|
440 |
+
|
441 |
+
# التحقق من وجود اقتراحات
|
442 |
+
self.assertIn("suggestions", results)
|
443 |
+
self.assertTrue(len(results["suggestions"]) > 0)
|
444 |
+
|
445 |
+
def test_conversation_history(self):
|
446 |
+
"""اختبار سجل المحادثة"""
|
447 |
+
# معالجة استعلام
|
448 |
+
query = "ما هي استراتيجيات التسعير المتاحة؟"
|
449 |
+
self.assistant.process_query(query)
|
450 |
+
|
451 |
+
# انتظار اكتمال المعالجة
|
452 |
+
import time
|
453 |
+
max_wait = 5 # ثوانٍ
|
454 |
+
waited = 0
|
455 |
+
while self.assistant.processing_in_progress and waited < max_wait:
|
456 |
+
time.sleep(0.5)
|
457 |
+
waited += 0.5
|
458 |
+
|
459 |
+
# التحقق من سجل المحادثة
|
460 |
+
history = self.assistant.get_conversation_history()
|
461 |
+
self.assertEqual(len(history), 2) # استعلام المستخدم واستجابة المساعد
|
462 |
+
self.assertEqual(history[0]["role"], "user")
|
463 |
+
self.assertEqual(history[0]["content"], query)
|
464 |
+
self.assertEqual(history[1]["role"], "assistant")
|
465 |
+
|
466 |
+
# مسح سجل المحادثة
|
467 |
+
self.assertTrue(self.assistant.clear_conversation_history())
|
468 |
+
history = self.assistant.get_conversation_history()
|
469 |
+
self.assertEqual(len(history), 0)
|
470 |
+
|
471 |
+
|
472 |
+
class TestStyling(unittest.TestCase):
|
473 |
+
"""اختبار وحدات التصميم"""
|
474 |
+
|
475 |
+
def test_app_theme(self):
|
476 |
+
"""اختبار نمط التطبيق"""
|
477 |
+
theme = AppTheme()
|
478 |
+
|
479 |
+
# التحقق من الألوان
|
480 |
+
self.assertIsNotNone(theme.get_color("bg_color"))
|
481 |
+
self.assertIsNotNone(theme.get_color("fg_color"))
|
482 |
+
|
483 |
+
# التحقق من الخطوط
|
484 |
+
self.assertIsNotNone(theme.get_font("body"))
|
485 |
+
self.assertIsNotNone(theme.get_font("title"))
|
486 |
+
|
487 |
+
# التحقق من الأحجام
|
488 |
+
self.assertIsNotNone(theme.get_size("padding_medium"))
|
489 |
+
self.assertIsNotNone(theme.get_size("border_radius"))
|
490 |
+
|
491 |
+
# تغيير النمط
|
492 |
+
self.assertTrue(theme.set_theme("dark"))
|
493 |
+
self.assertEqual(theme.current_theme, "dark")
|
494 |
+
|
495 |
+
# تغيير اللغة
|
496 |
+
self.assertTrue(theme.set_language("en"))
|
497 |
+
self.assertEqual(theme.current_language, "en")
|
498 |
+
|
499 |
+
def test_icon_generator(self):
|
500 |
+
"""اختبار مولد الأيقونات"""
|
501 |
+
icon_generator = IconGenerator()
|
502 |
+
|
503 |
+
# توليد الأيقونات الافتراضية
|
504 |
+
icon_generator.generate_default_icons()
|
505 |
+
|
506 |
+
# التحقق من وجود مجلد الأيقونات
|
507 |
+
self.assertTrue(Path('assets/icons').exists())
|
508 |
+
|
509 |
+
# التحقق من وجود بعض الأيقونات
|
510 |
+
self.assertTrue(Path('assets/icons/dashboard.png').exists())
|
511 |
+
self.assertTrue(Path('assets/icons/projects.png').exists())
|
512 |
+
|
513 |
+
def test_chart_generator(self):
|
514 |
+
"""اختبار مولد الرسوم البيانية"""
|
515 |
+
theme = AppTheme()
|
516 |
+
chart_generator = ChartGenerator(theme)
|
517 |
+
|
518 |
+
# إنشاء بيانات للرسم البياني الشريطي
|
519 |
+
bar_data = {
|
520 |
+
'labels': ['الربع الأول', 'الربع الثاني', 'الربع الثالث', 'الربع الرابع'],
|
521 |
+
'values': [15000, 20000, 18000, 25000]
|
522 |
+
}
|
523 |
+
|
524 |
+
# إنشاء رسم بياني شريطي
|
525 |
+
fig = chart_generator.create_bar_chart(
|
526 |
+
bar_data,
|
527 |
+
'الإيرادات الفصلية',
|
528 |
+
'الفصل',
|
529 |
+
'الإيرادات (ريال)'
|
530 |
+
)
|
531 |
+
|
532 |
+
# التحقق من إنشاء الرسم البياني
|
533 |
+
self.assertIsNotNone(fig)
|
534 |
+
|
535 |
+
# حفظ الرسم البياني
|
536 |
+
save_path = 'test_chart.png'
|
537 |
+
chart_generator.create_bar_chart(
|
538 |
+
bar_data,
|
539 |
+
'الإيرادات الفصلية',
|
540 |
+
'الفصل',
|
541 |
+
'الإيرادات (ريال)',
|
542 |
+
save_path=save_path
|
543 |
+
)
|
544 |
+
|
545 |
+
# التحقق من وجود ملف الرسم البياني
|
546 |
+
self.assertTrue(Path(save_path).exists())
|
547 |
+
|
548 |
+
# حذف ملف الرسم البياني
|
549 |
+
if Path(save_path).exists():
|
550 |
+
Path(save_path).unlink()
|
551 |
+
|
552 |
+
|
553 |
+
def run_tests():
|
554 |
+
"""تشغيل الاختبارات"""
|
555 |
+
# إنشاء مجلد الاختبارات
|
556 |
+
test_dir = Path('test_results')
|
557 |
+
test_dir.mkdir(exist_ok=True)
|
558 |
+
|
559 |
+
# إنشاء ملف لنتائج الاختبارات
|
560 |
+
test_results_file = test_dir / 'test_results.txt'
|
561 |
+
|
562 |
+
# تشغيل الاختبارات وحفظ النتائج
|
563 |
+
with open(test_results_file, 'w', encoding='utf-8') as f:
|
564 |
+
runner = unittest.TextTestRunner(stream=f, verbosity=2)
|
565 |
+
suite = unittest.TestSuite()
|
566 |
+
|
567 |
+
# إضافة اختبارات قاعدة البيانات
|
568 |
+
suite.addTest(unittest.makeSuite(TestDatabaseConnector))
|
569 |
+
suite.addTest(unittest.makeSuite(TestModels))
|
570 |
+
|
571 |
+
# إضافة اختبارات الوحدات
|
572 |
+
suite.addTest(unittest.makeSuite(TestDocumentAnalyzer))
|
573 |
+
suite.addTest(unittest.makeSuite(TestPricingEngine))
|
574 |
+
suite.addTest(unittest.makeSuite(TestRiskAnalyzer))
|
575 |
+
suite.addTest(unittest.makeSuite(TestAIAssistant))
|
576 |
+
|
577 |
+
# إضافة اختبارات التصميم
|
578 |
+
suite.addTest(unittest.makeSuite(TestStyling))
|
579 |
+
|
580 |
+
# تشغيل الاختبارات
|
581 |
+
result = runner.run(suite)
|
582 |
+
|
583 |
+
# كتابة ملخص النتائج
|
584 |
+
f.write("\n\n=== ملخص نتائج الاختبارات ===\n")
|
585 |
+
f.write(f"عدد الاختبارات: {result.testsRun}\n")
|
586 |
+
f.write(f"عدد النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}\n")
|
587 |
+
f.write(f"عدد الإخفاقات: {len(result.failures)}\n")
|
588 |
+
f.write(f"عدد الأخطاء: {len(result.errors)}\n")
|
589 |
+
|
590 |
+
# طباعة ملخص النتائج
|
591 |
+
logger.info(f"تم تشغيل {result.testsRun} اختبار")
|
592 |
+
logger.info(f"النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}")
|
593 |
+
logger.info(f"الإخفاقات: {len(result.failures)}")
|
594 |
+
logger.info(f"الأخطاء: {len(result.errors)}")
|
595 |
+
logger.info(f"تم حفظ نتائج الاختبارات في: {test_results_file}")
|
596 |
+
|
597 |
+
return result
|
598 |
+
|
599 |
+
|
600 |
+
if __name__ == "__main__":
|
601 |
+
run_tests()
|
tests/test_integrated_system.py
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة اختبار النظام المتكامل
|
3 |
+
"""
|
4 |
+
|
5 |
+
import unittest
|
6 |
+
import os
|
7 |
+
import sys
|
8 |
+
from pathlib import Path
|
9 |
+
|
10 |
+
# إضافة المسار الرئيسي للنظام
|
11 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
12 |
+
|
13 |
+
# استيراد الوحدات
|
14 |
+
from modules.pricing.pricing_app import PricingApp
|
15 |
+
from modules.ai_assistant.ai_app import AIAssistantApp
|
16 |
+
from modules.document_analysis.document_app import DocumentAnalysisApp
|
17 |
+
from modules.data_analysis.data_analysis_app import DataAnalysisApp
|
18 |
+
from modules.resources.resources_app import ResourcesApp
|
19 |
+
|
20 |
+
class TestIntegratedSystem(unittest.TestCase):
|
21 |
+
"""اختبارات النظام المتكامل"""
|
22 |
+
|
23 |
+
def setUp(self):
|
24 |
+
"""إعداد بيئة الاختبار"""
|
25 |
+
# التأكد من وجود جميع الملفات الرئيسية
|
26 |
+
self.main_files = [
|
27 |
+
"app.py",
|
28 |
+
"config.py",
|
29 |
+
"requirements.txt"
|
30 |
+
]
|
31 |
+
|
32 |
+
# التأكد من وجود جميع المجلدات الرئيسية
|
33 |
+
self.main_directories = [
|
34 |
+
"modules",
|
35 |
+
"assets",
|
36 |
+
"data",
|
37 |
+
"utils"
|
38 |
+
]
|
39 |
+
|
40 |
+
# التأكد من وجود جميع وحدات النظام
|
41 |
+
self.modules = [
|
42 |
+
"modules/pricing",
|
43 |
+
"modules/ai_assistant",
|
44 |
+
"modules/document_analysis",
|
45 |
+
"modules/data_analysis",
|
46 |
+
"modules/resources",
|
47 |
+
"modules/project_management",
|
48 |
+
"modules/reports",
|
49 |
+
"modules/risk_analysis"
|
50 |
+
]
|
51 |
+
|
52 |
+
def test_main_files_exist(self):
|
53 |
+
"""اختبار وجود الملفات الرئيسية"""
|
54 |
+
for file in self.main_files:
|
55 |
+
file_path = Path(__file__).parent.parent / file
|
56 |
+
self.assertTrue(file_path.exists(), f"الملف {file} غير موجود")
|
57 |
+
|
58 |
+
def test_main_directories_exist(self):
|
59 |
+
"""اختبار وجود المجلدات الرئيسية"""
|
60 |
+
for directory in self.main_directories:
|
61 |
+
dir_path = Path(__file__).parent.parent / directory
|
62 |
+
self.assertTrue(dir_path.exists(), f"المجلد {directory} غير موجود")
|
63 |
+
|
64 |
+
def test_modules_exist(self):
|
65 |
+
"""اختبار وجود وحدات النظام"""
|
66 |
+
for module in self.modules:
|
67 |
+
module_path = Path(__file__).parent.parent / module
|
68 |
+
self.assertTrue(module_path.exists(), f"الوحدة {module} غير موجودة")
|
69 |
+
|
70 |
+
def test_pricing_module(self):
|
71 |
+
"""اختبار وحدة التسعير"""
|
72 |
+
pricing_app = PricingApp()
|
73 |
+
self.assertIsNotNone(pricing_app, "فشل إنشاء وحدة التسعير")
|
74 |
+
|
75 |
+
def test_ai_assistant_module(self):
|
76 |
+
"""اختبار وحدة الذكاء الاصطناعي"""
|
77 |
+
ai_app = AIAssistantApp()
|
78 |
+
self.assertIsNotNone(ai_app, "فشل إنشاء وحدة الذكاء الاصطناعي")
|
79 |
+
|
80 |
+
def test_document_analysis_module(self):
|
81 |
+
"""اختبار وحدة تحليل المستندات"""
|
82 |
+
document_app = DocumentAnalysisApp()
|
83 |
+
self.assertIsNotNone(document_app, "فشل إنشاء وحدة تحليل المستندات")
|
84 |
+
|
85 |
+
def test_data_analysis_module(self):
|
86 |
+
"""اختبار وحدة تحليل البيانات"""
|
87 |
+
data_analysis_app = DataAnalysisApp()
|
88 |
+
self.assertIsNotNone(data_analysis_app, "فشل إنشاء وحدة تحليل البيانات")
|
89 |
+
|
90 |
+
def test_resources_module(self):
|
91 |
+
"""اختبار وحدة الموارد"""
|
92 |
+
resources_app = ResourcesApp()
|
93 |
+
self.assertIsNotNone(resources_app, "فشل إنشاء وحدة الموارد")
|
94 |
+
|
95 |
+
if __name__ == "__main__":
|
96 |
+
unittest.main()
|
tests/test_ui.py
ADDED
@@ -0,0 +1,413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة اختبار واجهة المستخدم لنظام إدارة المناقصات - Hybrid Face
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import sys
|
7 |
+
import logging
|
8 |
+
import unittest
|
9 |
+
import tkinter as tk
|
10 |
+
import customtkinter as ctk
|
11 |
+
from pathlib import Path
|
12 |
+
|
13 |
+
# تهيئة السجل
|
14 |
+
logging.basicConfig(
|
15 |
+
level=logging.INFO,
|
16 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
17 |
+
)
|
18 |
+
logger = logging.getLogger('test_ui')
|
19 |
+
|
20 |
+
# إضافة المسار الرئيسي للتطبيق إلى مسار البحث
|
21 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
22 |
+
|
23 |
+
# استيراد الوحدات المطلوبة للاختبار
|
24 |
+
from styling.theme import AppTheme
|
25 |
+
from styling.icons import IconGenerator
|
26 |
+
from styling.charts import ChartGenerator
|
27 |
+
from config import AppConfig
|
28 |
+
|
29 |
+
class TestUIComponents(unittest.TestCase):
|
30 |
+
"""اختبار مكونات واجهة المستخدم"""
|
31 |
+
|
32 |
+
def setUp(self):
|
33 |
+
"""إعداد بيئة الاختبار"""
|
34 |
+
self.root = ctk.CTk()
|
35 |
+
self.root.withdraw() # إخفاء النافذة أثناء الاختبار
|
36 |
+
self.theme = AppTheme()
|
37 |
+
|
38 |
+
def tearDown(self):
|
39 |
+
"""تنظيف بيئة الاختبار"""
|
40 |
+
self.root.destroy()
|
41 |
+
|
42 |
+
def test_styled_frame(self):
|
43 |
+
"""اختبار الإطار المنسق"""
|
44 |
+
frame = self.theme.create_styled_frame(self.root)
|
45 |
+
self.assertIsNotNone(frame)
|
46 |
+
self.assertEqual(frame.cget("fg_color"), self.theme.get_color("card_bg_color"))
|
47 |
+
self.assertEqual(frame.cget("corner_radius"), self.theme.get_size("border_radius"))
|
48 |
+
|
49 |
+
def test_styled_button(self):
|
50 |
+
"""اختبار الزر المنسق"""
|
51 |
+
button = self.theme.create_styled_button(self.root, "زر اختبار")
|
52 |
+
self.assertIsNotNone(button)
|
53 |
+
self.assertEqual(button.cget("text"), "زر اختبار")
|
54 |
+
self.assertEqual(button.cget("fg_color"), self.theme.get_color("button_bg_color"))
|
55 |
+
self.assertEqual(button.cget("text_color"), self.theme.get_color("button_fg_color"))
|
56 |
+
|
57 |
+
def test_styled_label(self):
|
58 |
+
"""اختبار التسمية المنسقة"""
|
59 |
+
label = self.theme.create_styled_label(self.root, "تسمية اختبار")
|
60 |
+
self.assertIsNotNone(label)
|
61 |
+
self.assertEqual(label.cget("text"), "تسمية اختبار")
|
62 |
+
self.assertEqual(label.cget("text_color"), self.theme.get_color("fg_color"))
|
63 |
+
|
64 |
+
def test_styled_entry(self):
|
65 |
+
"""اختبار حقل الإدخال المنسق"""
|
66 |
+
entry = self.theme.create_styled_entry(self.root, "نص توضيحي")
|
67 |
+
self.assertIsNotNone(entry)
|
68 |
+
self.assertEqual(entry.cget("placeholder_text"), "نص توضيحي")
|
69 |
+
self.assertEqual(entry.cget("fg_color"), self.theme.get_color("input_bg_color"))
|
70 |
+
self.assertEqual(entry.cget("text_color"), self.theme.get_color("input_fg_color"))
|
71 |
+
|
72 |
+
def test_styled_combobox(self):
|
73 |
+
"""اختبار القائمة المنسدلة المنسقة"""
|
74 |
+
values = ["الخيار الأول", "الخيار الثاني", "الخيار الثالث"]
|
75 |
+
combobox = self.theme.create_styled_combobox(self.root, values)
|
76 |
+
self.assertIsNotNone(combobox)
|
77 |
+
self.assertEqual(combobox.cget("values"), values)
|
78 |
+
self.assertEqual(combobox.cget("fg_color"), self.theme.get_color("input_bg_color"))
|
79 |
+
self.assertEqual(combobox.cget("text_color"), self.theme.get_color("input_fg_color"))
|
80 |
+
|
81 |
+
def test_styled_checkbox(self):
|
82 |
+
"""اختبار خانة الاختيار المنسقة"""
|
83 |
+
checkbox = self.theme.create_styled_checkbox(self.root, "خانة اختبار")
|
84 |
+
self.assertIsNotNone(checkbox)
|
85 |
+
self.assertEqual(checkbox.cget("text"), "خانة اختبار")
|
86 |
+
self.assertEqual(checkbox.cget("fg_color"), self.theme.get_color("button_bg_color"))
|
87 |
+
self.assertEqual(checkbox.cget("text_color"), self.theme.get_color("fg_color"))
|
88 |
+
|
89 |
+
def test_styled_radio_button(self):
|
90 |
+
"""اختبار زر الراديو المنسق"""
|
91 |
+
var = ctk.StringVar(value="1")
|
92 |
+
radio_button = self.theme.create_styled_radio_button(self.root, "زر راديو اختبار", var, "1")
|
93 |
+
self.assertIsNotNone(radio_button)
|
94 |
+
self.assertEqual(radio_button.cget("text"), "زر راديو اختبار")
|
95 |
+
self.assertEqual(radio_button.cget("fg_color"), self.theme.get_color("button_bg_color"))
|
96 |
+
self.assertEqual(radio_button.cget("text_color"), self.theme.get_color("fg_color"))
|
97 |
+
|
98 |
+
def test_styled_switch(self):
|
99 |
+
"""اختبار مفتاح التبديل المنسق"""
|
100 |
+
switch = self.theme.create_styled_switch(self.root, "مفتاح اختبار")
|
101 |
+
self.assertIsNotNone(switch)
|
102 |
+
self.assertEqual(switch.cget("text"), "مفتاح اختبار")
|
103 |
+
self.assertEqual(switch.cget("progress_color"), self.theme.get_color("button_bg_color"))
|
104 |
+
self.assertEqual(switch.cget("text_color"), self.theme.get_color("fg_color"))
|
105 |
+
|
106 |
+
def test_styled_slider(self):
|
107 |
+
"""اختبار شريط التمرير المنسق"""
|
108 |
+
slider = self.theme.create_styled_slider(self.root)
|
109 |
+
self.assertIsNotNone(slider)
|
110 |
+
self.assertEqual(slider.cget("fg_color"), self.theme.get_color("input_border_color"))
|
111 |
+
self.assertEqual(slider.cget("progress_color"), self.theme.get_color("button_bg_color"))
|
112 |
+
|
113 |
+
def test_styled_progressbar(self):
|
114 |
+
"""اختبار شريط التقدم المنسق"""
|
115 |
+
progressbar = self.theme.create_styled_progressbar(self.root)
|
116 |
+
self.assertIsNotNone(progressbar)
|
117 |
+
self.assertEqual(progressbar.cget("fg_color"), self.theme.get_color("input_border_color"))
|
118 |
+
self.assertEqual(progressbar.cget("progress_color"), self.theme.get_color("button_bg_color"))
|
119 |
+
|
120 |
+
def test_styled_tabview(self):
|
121 |
+
"""اختبار عرض التبويب المنسق"""
|
122 |
+
tabview = self.theme.create_styled_tabview(self.root)
|
123 |
+
self.assertIsNotNone(tabview)
|
124 |
+
self.assertEqual(tabview.cget("fg_color"), self.theme.get_color("card_bg_color"))
|
125 |
+
|
126 |
+
def test_styled_scrollable_frame(self):
|
127 |
+
"""اختبار الإطار القابل للتمرير المنسق"""
|
128 |
+
scrollable_frame = self.theme.create_styled_scrollable_frame(self.root)
|
129 |
+
self.assertIsNotNone(scrollable_frame)
|
130 |
+
self.assertEqual(scrollable_frame.cget("fg_color"), "transparent")
|
131 |
+
|
132 |
+
def test_styled_textbox(self):
|
133 |
+
"""اختبار مربع النص المنسق"""
|
134 |
+
textbox = self.theme.create_styled_textbox(self.root)
|
135 |
+
self.assertIsNotNone(textbox)
|
136 |
+
self.assertEqual(textbox.cget("fg_color"), self.theme.get_color("input_bg_color"))
|
137 |
+
self.assertEqual(textbox.cget("text_color"), self.theme.get_color("input_fg_color"))
|
138 |
+
|
139 |
+
def test_styled_card(self):
|
140 |
+
"""اختبار البطاقة المنسقة"""
|
141 |
+
card, content_frame = self.theme.create_styled_card(self.root, "بطاقة اختبار")
|
142 |
+
self.assertIsNotNone(card)
|
143 |
+
self.assertIsNotNone(content_frame)
|
144 |
+
self.assertEqual(card.cget("fg_color"), self.theme.get_color("card_bg_color"))
|
145 |
+
|
146 |
+
def test_styled_data_table(self):
|
147 |
+
"""اختبار جدول البيانات المنسق"""
|
148 |
+
columns = ["العمود الأول", "العمود الثاني", "العمود الثالث"]
|
149 |
+
data = [
|
150 |
+
["بيانات 1-1", "بيانات 1-2", "بيانات 1-3"],
|
151 |
+
["بيانات 2-1", "بيانات 2-2", "بيانات 2-3"]
|
152 |
+
]
|
153 |
+
table_frame, data_frame = self.theme.create_styled_data_table(self.root, columns, data)
|
154 |
+
self.assertIsNotNone(table_frame)
|
155 |
+
self.assertIsNotNone(data_frame)
|
156 |
+
self.assertEqual(table_frame.cget("fg_color"), self.theme.get_color("card_bg_color"))
|
157 |
+
|
158 |
+
def test_theme_switching(self):
|
159 |
+
"""اختبار تبديل النمط"""
|
160 |
+
# تعيين النمط الفاتح
|
161 |
+
self.theme.set_theme("light")
|
162 |
+
light_bg_color = self.theme.get_color("bg_color")
|
163 |
+
|
164 |
+
# تعيين النمط الداكن
|
165 |
+
self.theme.set_theme("dark")
|
166 |
+
dark_bg_color = self.theme.get_color("bg_color")
|
167 |
+
|
168 |
+
# التحقق من اختلاف الألوان
|
169 |
+
self.assertNotEqual(light_bg_color, dark_bg_color)
|
170 |
+
|
171 |
+
def test_language_switching(self):
|
172 |
+
"""اختبار تبديل اللغة"""
|
173 |
+
# تعيين اللغة العربية
|
174 |
+
self.theme.set_language("ar")
|
175 |
+
ar_font = self.theme.get_font("body")
|
176 |
+
|
177 |
+
# تعيين اللغة الإنجليزية
|
178 |
+
self.theme.set_language("en")
|
179 |
+
en_font = self.theme.get_font("body")
|
180 |
+
|
181 |
+
# التحقق من اختلاف الخطوط
|
182 |
+
self.assertNotEqual(ar_font[0], en_font[0])
|
183 |
+
|
184 |
+
|
185 |
+
class TestUILayout(unittest.TestCase):
|
186 |
+
"""اختبار تخطيط واجهة المستخدم"""
|
187 |
+
|
188 |
+
def setUp(self):
|
189 |
+
"""إعداد بيئة الاختبار"""
|
190 |
+
self.root = ctk.CTk()
|
191 |
+
self.root.withdraw() # إخفاء النافذة أثناء الاختبار
|
192 |
+
self.theme = AppTheme()
|
193 |
+
|
194 |
+
# إنشاء الإطار الرئيسي
|
195 |
+
self.main_frame = self.theme.create_styled_frame(self.root)
|
196 |
+
self.main_frame.pack(fill="both", expand=True)
|
197 |
+
|
198 |
+
# إنشاء الشريط الجانبي
|
199 |
+
self.sidebar_frame = self.theme.create_styled_frame(
|
200 |
+
self.main_frame,
|
201 |
+
fg_color=self.theme.get_color("sidebar_bg_color")
|
202 |
+
)
|
203 |
+
self.sidebar_frame.pack(side="left", fill="y", padx=0, pady=0)
|
204 |
+
|
205 |
+
# إنشاء إطار المحتوى
|
206 |
+
self.content_frame = self.theme.create_styled_frame(
|
207 |
+
self.main_frame,
|
208 |
+
fg_color=self.theme.get_color("bg_color")
|
209 |
+
)
|
210 |
+
self.content_frame.pack(side="right", fill="both", expand=True, padx=0, pady=0)
|
211 |
+
|
212 |
+
def tearDown(self):
|
213 |
+
"""تنظيف بيئة الاخ��بار"""
|
214 |
+
self.root.destroy()
|
215 |
+
|
216 |
+
def test_sidebar_layout(self):
|
217 |
+
"""اختبار تخطيط الشريط الجانبي"""
|
218 |
+
# إنشاء شعار التطبيق
|
219 |
+
logo_label = self.theme.create_styled_label(
|
220 |
+
self.sidebar_frame,
|
221 |
+
"نظام إدارة المناقصات",
|
222 |
+
font=self.theme.get_font("title"),
|
223 |
+
text_color=self.theme.get_color("sidebar_fg_color")
|
224 |
+
)
|
225 |
+
logo_label.pack(padx=20, pady=20)
|
226 |
+
|
227 |
+
# إنشاء أزرار الشريط الجانبي
|
228 |
+
sidebar_buttons = []
|
229 |
+
button_texts = [
|
230 |
+
"لوحة التحكم", "المشاريع", "المستندات", "التسعير",
|
231 |
+
"الموارد", "المخاطر", "التقارير", "الذكاء الاصطناعي"
|
232 |
+
]
|
233 |
+
|
234 |
+
for text in button_texts:
|
235 |
+
button_frame, button = self.theme.create_styled_sidebar_button(
|
236 |
+
self.sidebar_frame,
|
237 |
+
text
|
238 |
+
)
|
239 |
+
button_frame.pack(fill="x", padx=0, pady=2)
|
240 |
+
sidebar_buttons.append(button)
|
241 |
+
|
242 |
+
# التحقق من إنشاء الأزرار
|
243 |
+
self.assertEqual(len(sidebar_buttons), len(button_texts))
|
244 |
+
for i, button in enumerate(sidebar_buttons):
|
245 |
+
self.assertEqual(button.cget("text"), button_texts[i])
|
246 |
+
|
247 |
+
def test_content_layout(self):
|
248 |
+
"""اختبار تخطيط المحتوى"""
|
249 |
+
# إنشاء شريط العنوان
|
250 |
+
header_frame = self.theme.create_styled_frame(
|
251 |
+
self.content_frame,
|
252 |
+
fg_color=self.theme.get_color("card_bg_color")
|
253 |
+
)
|
254 |
+
header_frame.pack(fill="x", padx=20, pady=20)
|
255 |
+
|
256 |
+
# إنشاء عنوان الصفحة
|
257 |
+
page_title = self.theme.create_styled_label(
|
258 |
+
header_frame,
|
259 |
+
"لوحة التحكم",
|
260 |
+
font=self.theme.get_font("title")
|
261 |
+
)
|
262 |
+
page_title.pack(side="left", padx=20, pady=20)
|
263 |
+
|
264 |
+
# إنشاء زر البحث
|
265 |
+
search_button = self.theme.create_styled_button(
|
266 |
+
header_frame,
|
267 |
+
"بحث"
|
268 |
+
)
|
269 |
+
search_button.pack(side="right", padx=20, pady=20)
|
270 |
+
|
271 |
+
# إنشاء إطار البطاقات
|
272 |
+
cards_frame = self.theme.create_styled_frame(
|
273 |
+
self.content_frame,
|
274 |
+
fg_color="transparent"
|
275 |
+
)
|
276 |
+
cards_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
277 |
+
|
278 |
+
# إنشاء بطاقات
|
279 |
+
cards = []
|
280 |
+
card_titles = [
|
281 |
+
"المشاريع النشطة", "المناقصات الجديدة", "المخاطر العالية", "التقارير المعلقة"
|
282 |
+
]
|
283 |
+
|
284 |
+
for i, title in enumerate(card_titles):
|
285 |
+
card, card_content = self.theme.create_styled_card(
|
286 |
+
cards_frame,
|
287 |
+
title
|
288 |
+
)
|
289 |
+
card.grid(row=i//2, column=i%2, padx=10, pady=10, sticky="nsew")
|
290 |
+
cards.append(card)
|
291 |
+
|
292 |
+
# التحقق من إنشاء البطاقات
|
293 |
+
self.assertEqual(len(cards), len(card_titles))
|
294 |
+
|
295 |
+
# تهيئة أوزان الصفوف والأعمدة
|
296 |
+
cards_frame.grid_columnconfigure(0, weight=1)
|
297 |
+
cards_frame.grid_columnconfigure(1, weight=1)
|
298 |
+
cards_frame.grid_rowconfigure(0, weight=1)
|
299 |
+
cards_frame.grid_rowconfigure(1, weight=1)
|
300 |
+
|
301 |
+
def test_responsive_layout(self):
|
302 |
+
"""اختبار التخطيط المتجاوب"""
|
303 |
+
# تغيير حجم النافذة
|
304 |
+
self.root.geometry("800x600")
|
305 |
+
self.root.update()
|
306 |
+
|
307 |
+
# التحقق من أن الإطار الرئيسي يملأ النافذة
|
308 |
+
self.assertEqual(self.main_frame.winfo_width(), 800)
|
309 |
+
self.assertEqual(self.main_frame.winfo_height(), 600)
|
310 |
+
|
311 |
+
# تغيير حجم النافذة مرة أخرى
|
312 |
+
self.root.geometry("1024x768")
|
313 |
+
self.root.update()
|
314 |
+
|
315 |
+
# التحقق من أن الإطار الرئيسي يملأ النافذة
|
316 |
+
self.assertEqual(self.main_frame.winfo_width(), 1024)
|
317 |
+
self.assertEqual(self.main_frame.winfo_height(), 768)
|
318 |
+
|
319 |
+
|
320 |
+
class TestUIArabicSupport(unittest.TestCase):
|
321 |
+
"""اختبار دعم اللغة العربية في واجهة المستخدم"""
|
322 |
+
|
323 |
+
def setUp(self):
|
324 |
+
"""إعداد بيئة الاختبار"""
|
325 |
+
self.root = ctk.CTk()
|
326 |
+
self.root.withdraw() # إخفاء النافذة أثناء الاختبار
|
327 |
+
self.theme = AppTheme()
|
328 |
+
self.theme.set_language("ar") # تعيين اللغة العربية
|
329 |
+
|
330 |
+
def tearDown(self):
|
331 |
+
"""تنظيف بيئة الاختبار"""
|
332 |
+
self.root.destroy()
|
333 |
+
|
334 |
+
def test_arabic_text_display(self):
|
335 |
+
"""اختبار عرض النص العربي"""
|
336 |
+
# إنشاء تسمية بنص عربي
|
337 |
+
arabic_text = "هذا نص عربي للاختبار"
|
338 |
+
label = self.theme.create_styled_label(self.root, arabic_text)
|
339 |
+
self.assertEqual(label.cget("text"), arabic_text)
|
340 |
+
|
341 |
+
# إنشاء زر بنص عربي
|
342 |
+
button = self.theme.create_styled_button(self.root, "زر باللغة العربية")
|
343 |
+
self.assertEqual(button.cget("text"), "زر باللغة العربية")
|
344 |
+
|
345 |
+
# إنشاء حقل إدخال بنص توضيحي عربي
|
346 |
+
entry = self.theme.create_styled_entry(self.root, "أدخل النص هنا")
|
347 |
+
self.assertEqual(entry.cget("placeholder_text"), "أدخل النص هنا")
|
348 |
+
|
349 |
+
def test_arabic_font(self):
|
350 |
+
"""اختبار الخط العربي"""
|
351 |
+
# التحقق من استخدام خط يدعم العربية
|
352 |
+
ar_font = self.theme.get_font("body")
|
353 |
+
self.assertEqual(ar_font[0], "Cairo")
|
354 |
+
|
355 |
+
def test_rtl_support(self):
|
356 |
+
"""اختبار دعم الكتابة من اليمين إلى اليسار"""
|
357 |
+
# إنشاء إطار
|
358 |
+
frame = self.theme.create_styled_frame(self.root)
|
359 |
+
frame.pack(fill="both", expand=True)
|
360 |
+
|
361 |
+
# إنشاء تسمية بنص عربي
|
362 |
+
label = self.theme.create_styled_label(frame, "نص عربي من اليمين إلى اليسار")
|
363 |
+
label.pack(anchor="e", padx=20, pady=20) # محاذاة إلى اليمين
|
364 |
+
|
365 |
+
# التحقق من المحاذاة
|
366 |
+
self.assertEqual(label.cget("anchor"), "w") # w تعني غرب (يسار)، لكن النص سيظهر من اليمين إلى اليسار
|
367 |
+
|
368 |
+
|
369 |
+
def run_ui_tests():
|
370 |
+
"""تشغيل اختبارات واجهة المستخدم"""
|
371 |
+
# إنشاء مجلد الاختبارات
|
372 |
+
test_dir = Path('test_results')
|
373 |
+
test_dir.mkdir(exist_ok=True)
|
374 |
+
|
375 |
+
# إنشاء ملف لنتائج الاختبارات
|
376 |
+
test_results_file = test_dir / 'ui_test_results.txt'
|
377 |
+
|
378 |
+
# تشغيل الاختبارات وحفظ النتائج
|
379 |
+
with open(test_results_file, 'w', encoding='utf-8') as f:
|
380 |
+
runner = unittest.TextTestRunner(stream=f, verbosity=2)
|
381 |
+
suite = unittest.TestSuite()
|
382 |
+
|
383 |
+
# إضافة اختبارات مكونات واجهة المستخدم
|
384 |
+
suite.addTest(unittest.makeSuite(TestUIComponents))
|
385 |
+
|
386 |
+
# إضافة اختبارات تخطيط واجهة المستخدم
|
387 |
+
suite.addTest(unittest.makeSuite(TestUILayout))
|
388 |
+
|
389 |
+
# إضافة اختبارات دعم اللغة العربية
|
390 |
+
suite.addTest(unittest.makeSuite(TestUIArabicSupport))
|
391 |
+
|
392 |
+
# تشغيل الاختبارات
|
393 |
+
result = runner.run(suite)
|
394 |
+
|
395 |
+
# كتابة ملخص النتائج
|
396 |
+
f.write("\n\n=== ملخص نتائج اختبارات واجهة المستخدم ===\n")
|
397 |
+
f.write(f"عدد الاختبارات: {result.testsRun}\n")
|
398 |
+
f.write(f"عدد النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}\n")
|
399 |
+
f.write(f"عدد الإخفاقات: {len(result.failures)}\n")
|
400 |
+
f.write(f"عدد الأخطاء: {len(result.errors)}\n")
|
401 |
+
|
402 |
+
# طباعة ملخص النتائج
|
403 |
+
logger.info(f"تم تشغيل {result.testsRun} اختبار لواجهة المستخدم")
|
404 |
+
logger.info(f"النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}")
|
405 |
+
logger.info(f"الإخفاقات: {len(result.failures)}")
|
406 |
+
logger.info(f"الأخطاء: {len(result.errors)}")
|
407 |
+
logger.info(f"تم حفظ نتائج الاختبارات في: {test_results_file}")
|
408 |
+
|
409 |
+
return result
|
410 |
+
|
411 |
+
|
412 |
+
if __name__ == "__main__":
|
413 |
+
run_ui_tests()
|