EGYADMIN commited on
Commit
f268115
·
verified ·
1 Parent(s): 007ac13

Delete modules

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. modules/achievements/__init__.py +0 -4
  2. modules/achievements/achievement_system.py +0 -1033
  3. modules/achievements/achievements_app.py +0 -49
  4. modules/ai_assistant/__init__.py +0 -5
  5. modules/ai_assistant/ai_app.py +0 -1067
  6. modules/ai_assistant/ai_assistant.py +0 -773
  7. modules/ai_assistant/ai_assistant_app.py +0 -0
  8. modules/ai_assistant/assistant.py +0 -444
  9. modules/ai_assistant/assistant_app.py +0 -44
  10. modules/ai_finetuning/__init__.py +0 -1
  11. modules/ai_finetuning/finetuning_app.py +0 -53
  12. modules/ai_finetuning/model_finetuning.py +0 -0
  13. modules/data_analysis/data_analysis_app.py +0 -1022
  14. modules/document_analysis/analyzer.py +0 -281
  15. modules/document_analysis/document_analysis_app.py +0 -1114
  16. modules/document_analysis/document_app.py +0 -887
  17. modules/document_analysis/services/__init__.py +0 -22
  18. modules/document_analysis/services/document_parser.py +0 -219
  19. modules/document_analysis/services/item_extractor.py +0 -131
  20. modules/document_analysis/services/text_extractor.py +0 -105
  21. modules/document_comparison/__init__.py +0 -4
  22. modules/document_comparison/comparison_app.py +0 -43
  23. modules/document_comparison/document_comparator.py +0 -1503
  24. modules/document_comparison/document_comparison_app.py +0 -1003
  25. modules/maps/README.md +0 -45
  26. modules/maps/__init__.py +0 -1
  27. modules/maps/interactive_map.py +0 -1671
  28. modules/maps/interactive_map.py.bak +0 -1647
  29. modules/maps/maps_app.py +0 -53
  30. modules/notifications/__init__.py +0 -1
  31. modules/notifications/notifications_app.py +0 -53
  32. modules/notifications/smart_notifications.py +0 -1237
  33. modules/pricing/constants.py +0 -113
  34. modules/pricing/construction_calculator.py +0 -787
  35. modules/pricing/exceptions.py +0 -42
  36. modules/pricing/price_analysis_component.py +0 -932
  37. modules/pricing/price_analyzer.py +0 -1695
  38. modules/pricing/pricing_app.py +0 -0
  39. modules/pricing/pricing_app.py.backup +0 -1242
  40. modules/pricing/pricing_engine.py +0 -430
  41. modules/pricing/services/construction_cost_calculator.py +0 -1006
  42. modules/pricing/services/construction_templates.py +0 -748
  43. modules/pricing/services/local_content_calculator.py +0 -577
  44. modules/pricing/services/price_prediction.py +0 -444
  45. modules/pricing/services/standard_pricing.py +0 -232
  46. modules/pricing/services/templates_catalog/__init__.py +0 -3
  47. modules/pricing/services/templates_catalog/templates_catalog.py +0 -949
  48. modules/pricing/services/unbalanced_pricing.py +0 -213
  49. modules/pricing/specs_analyzer.py +0 -527
  50. modules/project_management/project_management_app.py +0 -666
modules/achievements/__init__.py DELETED
@@ -1,4 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- وحدة نظام الإنجازات المحفز لمراحل المشروع
4
- """
 
 
 
 
 
modules/achievements/achievement_system.py DELETED
@@ -1,1033 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- نظام الإنجازات المحفز لمراحل المشروع
6
- """
7
-
8
- import os
9
- import sys
10
- import json
11
- import streamlit as st
12
- import pandas as pd
13
- import numpy as np
14
- import time
15
- from datetime import datetime, timedelta
16
- import random
17
-
18
- # إضافة مسار النظام للوصول للملفات المشتركة
19
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
20
-
21
- # استيراد مكونات قاعدة البيانات
22
- try:
23
- from database.db_connector import get_connection
24
- except ImportError:
25
- from utils.helpers import get_connection
26
-
27
- from utils.helpers import format_time, get_user_info, load_icons
28
-
29
-
30
- class AchievementSystem:
31
- """نظام الإنجازات المحفز لمراحل المشروع"""
32
-
33
- def __init__(self, user_id=None):
34
- """تهيئة نظام الإنجازات المحفز"""
35
- self.user_id = user_id or 1 # استخدام المستخدم الافتراضي إذا لم يتم توفير معرف المستخدم
36
- self.conn = get_connection()
37
- self.achievements_path = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'achievements')
38
- os.makedirs(self.achievements_path, exist_ok=True)
39
- self.user_data_file = os.path.join(self.achievements_path, f'user_{self.user_id}_achievements.json')
40
- self.icons = load_icons()
41
-
42
- # تحميل بيانات المستخدم
43
- self.load_user_data()
44
-
45
- # تعريف قائمة الإنجازات
46
- self.define_achievements()
47
-
48
- def load_user_data(self):
49
- """تحميل بيانات إنجازات المستخدم"""
50
- try:
51
- if os.path.exists(self.user_data_file):
52
- with open(self.user_data_file, 'r', encoding='utf-8') as f:
53
- self.user_data = json.load(f)
54
- else:
55
- # بيانات افتراضية عند عدم وجود ملف
56
- self.user_data = {
57
- 'user_id': self.user_id,
58
- 'total_points': 0,
59
- 'level': 1,
60
- 'unlocked_achievements': [],
61
- 'in_progress_achievements': [],
62
- 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
63
- }
64
- self.save_user_data()
65
- except Exception as e:
66
- st.error(f"خطأ في تحميل بيانات المستخدم: {e}")
67
- self.user_data = {
68
- 'user_id': self.user_id,
69
- 'total_points': 0,
70
- 'level': 1,
71
- 'unlocked_achievements': [],
72
- 'in_progress_achievements': [],
73
- 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
74
- }
75
-
76
- def save_user_data(self):
77
- """حفظ بيانات إنجازات المستخدم"""
78
- try:
79
- with open(self.user_data_file, 'w', encoding='utf-8') as f:
80
- json.dump(self.user_data, f, ensure_ascii=False, indent=2)
81
- except Exception as e:
82
- st.error(f"خطأ في حفظ بيانات المستخدم: {e}")
83
-
84
- def define_achievements(self):
85
- """تعريف قائمة الإنجازات المتاحة"""
86
- self.achievements = [
87
- {
88
- 'id': 'first_project',
89
- 'name': 'بداية الرحلة',
90
- 'description': 'قم بإنشاء مشروعك الأول',
91
- 'icon': '🏆',
92
- 'points': 100,
93
- 'category': 'مشاريع',
94
- 'difficulty': 'سهل'
95
- },
96
- {
97
- 'id': 'five_projects',
98
- 'name': 'محترف المشاريع',
99
- 'description': 'قم بإنشاء خمسة مشاريع',
100
- 'icon': '🏅',
101
- 'points': 500,
102
- 'category': 'مشاريع',
103
- 'difficulty': 'متوسط'
104
- },
105
- {
106
- 'id': 'ten_projects',
107
- 'name': 'خبير المشاريع',
108
- 'description': 'قم بإنشاء عشرة مشاريع',
109
- 'icon': '🎖️',
110
- 'points': 1000,
111
- 'category': 'مشاريع',
112
- 'difficulty': 'صعب'
113
- },
114
- {
115
- 'id': 'first_document_analysis',
116
- 'name': 'المحلل الأول',
117
- 'description': 'قم بتحليل مستند للمرة الأولى',
118
- 'icon': '📊',
119
- 'points': 150,
120
- 'category': 'تحليل',
121
- 'difficulty': 'سهل'
122
- },
123
- {
124
- 'id': 'five_document_analysis',
125
- 'name': 'محلل متمرس',
126
- 'description': 'قم بتحليل خمسة مستندات',
127
- 'icon': '📈',
128
- 'points': 600,
129
- 'category': 'تحليل',
130
- 'difficulty': 'متوسط'
131
- },
132
- {
133
- 'id': 'complete_boq',
134
- 'name': 'خبير جداول الكميات',
135
- 'description': 'أكمل تحليل جدول كميات كامل',
136
- 'icon': '📋',
137
- 'points': 300,
138
- 'category': 'تحليل',
139
- 'difficulty': 'متوسط'
140
- },
141
- {
142
- 'id': 'risk_analysis',
143
- 'name': 'محلل المخاطر',
144
- 'description': 'أكمل تحليل مخاطر متقدم',
145
- 'icon': '⚠️',
146
- 'points': 400,
147
- 'category': 'مخاطر',
148
- 'difficulty': 'متوسط'
149
- },
150
- {
151
- 'id': 'ten_risk_identified',
152
- 'name': 'متنبئ المخاطر',
153
- 'description': 'تعرف على عشرة مخاطر في المشاريع',
154
- 'icon': '🔍',
155
- 'points': 700,
156
- 'category': 'مخاطر',
157
- 'difficulty': 'صعب'
158
- },
159
- {
160
- 'id': 'first_terms_analysis',
161
- 'name': 'محلل الشروط',
162
- 'description': 'قم بتحليل بنود الشروط والأحكام',
163
- 'icon': '📝',
164
- 'points': 250,
165
- 'category': 'تحليل',
166
- 'difficulty': 'متوسط'
167
- },
168
- {
169
- 'id': 'quick_analysis',
170
- 'name': 'محلل سريع',
171
- 'description': 'أكمل تحليل مستند في أقل من 5 دقائق',
172
- 'icon': '⚡',
173
- 'points': 500,
174
- 'category': 'كفاءة',
175
- 'difficulty': 'صعب'
176
- },
177
- {
178
- 'id': 'voice_narration',
179
- 'name': 'مترجم صوتي',
180
- 'description': 'استخدم ميزة الترجمة الصوتية لأول مرة',
181
- 'icon': '🎙️',
182
- 'points': 200,
183
- 'category': 'ترجمة',
184
- 'difficulty': 'سهل'
185
- },
186
- {
187
- 'id': 'multilingual_expert',
188
- 'name': 'خبير متعدد اللغات',
189
- 'description': 'استخدم الترجمة الصوتية بخمس لغات مختلفة',
190
- 'icon': '🌍',
191
- 'points': 800,
192
- 'category': 'ترجمة',
193
- 'difficulty': 'صعب'
194
- },
195
- {
196
- 'id': 'first_map',
197
- 'name': 'مستكشف الخرائط',
198
- 'description': 'استخدم ميزة الخريطة التفاعلية لأول مرة',
199
- 'icon': '🗺️',
200
- 'points': 200,
201
- 'category': 'خرائط',
202
- 'difficulty': 'سهل'
203
- },
204
- {
205
- 'id': 'ai_fine_tuning',
206
- 'name': 'مدرب الذكاء',
207
- 'description': 'قم بتدريب نموذج ذكاء اصطناعي مخصص',
208
- 'icon': '🧠',
209
- 'points': 1000,
210
- 'category': 'ذكاء اصطناعي',
211
- 'difficulty': 'خبير'
212
- },
213
- {
214
- 'id': 'pricing_master',
215
- 'name': 'سيد التسعير',
216
- 'description': 'أكمل حساب تكلفة مشروع بالكامل',
217
- 'icon': '💰',
218
- 'points': 500,
219
- 'category': 'تسعير',
220
- 'difficulty': 'متوسط'
221
- }
222
- ]
223
-
224
- def calculate_level(self, points):
225
- """حساب مستوى المستخدم بناءً على النقاط"""
226
- # صيغة بسيطة لحساب المستوى: كل 1000 نقطة = مستوى واحد
227
- level = 1 + int(points / 1000)
228
- return level
229
-
230
- def unlock_achievement(self, achievement_id):
231
- """إلغاء قفل إنجاز جديد"""
232
- # التحقق من وجود الإنجاز في القائمة
233
- achievement = next((a for a in self.achievements if a['id'] == achievement_id), None)
234
- if not achievement:
235
- return False
236
-
237
- # التحقق من عدم وجود الإنجاز مسبقاً
238
- if achievement_id in [a['id'] for a in self.user_data['unlocked_achievements']]:
239
- return False
240
-
241
- # إزالة الإنجاز من قائمة "قيد التقدم" إذا كان موجوداً
242
- self.user_data['in_progress_achievements'] = [
243
- a for a in self.user_data['in_progress_achievements']
244
- if a['id'] != achievement_id
245
- ]
246
-
247
- # إضافة الإنجاز إلى القائمة
248
- achievement['unlocked_date'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
249
- self.user_data['unlocked_achievements'].append(achievement)
250
-
251
- # تحديث النقاط والمستوى
252
- self.user_data['total_points'] += achievement['points']
253
- self.user_data['level'] = self.calculate_level(self.user_data['total_points'])
254
-
255
- # حفظ البيانات
256
- self.save_user_data()
257
-
258
- return achievement
259
-
260
- def update_achievement_progress(self, achievement_id, progress, total):
261
- """تحديث تقدم إنجاز معين"""
262
- # التحقق من وجود الإنجاز في القائمة
263
- achievement = next((a for a in self.achievements if a['id'] == achievement_id), None)
264
- if not achievement:
265
- return False
266
-
267
- # التحقق من عدم وجود الإنجاز في قائمة "تم إلغاء قفله"
268
- if achievement_id in [a['id'] for a in self.user_data['unlocked_achievements']]:
269
- return False
270
-
271
- # البحث عن الإنجاز في قائمة "قيد التقدم"
272
- in_progress_achievement = next(
273
- (a for a in self.user_data['in_progress_achievements'] if a['id'] == achievement_id),
274
- None
275
- )
276
-
277
- if in_progress_achievement:
278
- # تحديث التقدم
279
- in_progress_achievement['progress'] = progress
280
- in_progress_achievement['total'] = total
281
- in_progress_achievement['percentage'] = min(100, int((progress / total) * 100))
282
- in_progress_achievement['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
283
- else:
284
- # إضافة الإنجاز إلى قائمة "قيد التقدم"
285
- progress_data = achievement.copy()
286
- progress_data['progress'] = progress
287
- progress_data['total'] = total
288
- progress_data['percentage'] = min(100, int((progress / total) * 100))
289
- progress_data['start_date'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
290
- progress_data['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
291
- self.user_data['in_progress_achievements'].append(progress_data)
292
-
293
- # إذا اكتمل التقدم، قم بإلغاء قفل الإنجاز
294
- if progress >= total:
295
- return self.unlock_achievement(achievement_id)
296
-
297
- # حفظ البيانات
298
- self.save_user_data()
299
-
300
- return True
301
-
302
- def check_and_award_achievements(self, action_type, data=None):
303
- """التحقق من ومنح الإنجازات بناءً على إجراءات المستخدم"""
304
- try:
305
- if action_type == 'create_project':
306
- # حساب عدد المشاريع
307
- projects_count = self._get_projects_count()
308
-
309
- # منح إنجازات المشاريع
310
- if projects_count == 1:
311
- self.unlock_achievement('first_project')
312
- elif projects_count == 5:
313
- self.unlock_achievement('five_projects')
314
- elif projects_count == 10:
315
- self.unlock_achievement('ten_projects')
316
-
317
- # تحديث تقدم الإنجاز
318
- self.update_achievement_progress('five_projects', min(projects_count, 5), 5)
319
- self.update_achievement_progress('ten_projects', min(projects_count, 10), 10)
320
-
321
- elif action_type == 'analyze_document':
322
- # حساب عدد تحليلات المستندات
323
- analysis_count = self._get_document_analysis_count()
324
-
325
- # منح إنجازات تحليل المستندات
326
- if analysis_count == 1:
327
- self.unlock_achievement('first_document_analysis')
328
- elif analysis_count == 5:
329
- self.unlock_achievement('five_document_analysis')
330
-
331
- # تحديث تقدم الإنجاز
332
- self.update_achievement_progress('five_document_analysis', min(analysis_count, 5), 5)
333
-
334
- # التحقق من الوقت المستغرق للتحليل
335
- if data and 'duration_seconds' in data and data['duration_seconds'] < 300: # أقل من 5 دقائق
336
- self.unlock_achievement('quick_analysis')
337
-
338
- elif action_type == 'analyze_boq':
339
- self.unlock_achievement('complete_boq')
340
-
341
- elif action_type == 'analyze_terms':
342
- self.unlock_achievement('first_terms_analysis')
343
-
344
- elif action_type == 'analyze_risks':
345
- self.unlock_achievement('risk_analysis')
346
-
347
- # حساب عدد المخاطر المحددة
348
- if data and 'risks_count' in data:
349
- risks_count = data['risks_count']
350
- risk_total = self._get_total_risks_identified()
351
- new_total = risk_total + risks_count
352
-
353
- # تحديث تقدم إنجاز "متنبئ المخاطر"
354
- self.update_achievement_progress('ten_risk_identified', min(new_total, 10), 10)
355
-
356
- if new_total >= 10 and risk_total < 10:
357
- self.unlock_achievement('ten_risk_identified')
358
-
359
- elif action_type == 'use_voice_narration':
360
- self.unlock_achievement('voice_narration')
361
-
362
- # حساب عدد اللغات المستخدمة
363
- if data and 'language' in data:
364
- languages_used = self._get_languages_used()
365
- if data['language'] not in languages_used:
366
- languages_used.append(data['language'])
367
- self._save_languages_used(languages_used)
368
-
369
- # تحديث تقدم إنجاز "خبير متعدد اللغات"
370
- self.update_achievement_progress('multilingual_expert', len(languages_used), 5)
371
-
372
- if len(languages_used) >= 5:
373
- self.unlock_achievement('multilingual_expert')
374
-
375
- elif action_type == 'use_map':
376
- self.unlock_achievement('first_map')
377
-
378
- elif action_type == 'train_ai_model':
379
- self.unlock_achievement('ai_fine_tuning')
380
-
381
- elif action_type == 'complete_pricing':
382
- self.unlock_achievement('pricing_master')
383
-
384
- except Exception as e:
385
- st.error(f"خطأ في التحقق من الإنجازات: {e}")
386
-
387
- def _get_projects_count(self):
388
- """الحصول على عدد المشاريع"""
389
- try:
390
- cursor = self.conn.cursor()
391
- cursor.execute("SELECT COUNT(*) FROM documents WHERE user_id = %s AND type = 'project'", (self.user_id,))
392
- count = cursor.fetchone()[0]
393
- cursor.close()
394
- return count
395
- except Exception:
396
- # في حالة عدم وجود جدول أو خطأ في الاتصال، استخدم بيانات تقديرية
397
- projects_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'projects')
398
- if os.path.exists(projects_dir):
399
- return len([f for f in os.listdir(projects_dir) if os.path.isdir(os.path.join(projects_dir, f))])
400
- return 0
401
-
402
- def _get_document_analysis_count(self):
403
- """الحصول على عدد تحليلات المستندات"""
404
- try:
405
- cursor = self.conn.cursor()
406
- cursor.execute("SELECT COUNT(*) FROM document_analysis WHERE document_id IN (SELECT id FROM documents WHERE user_id = %s)", (self.user_id,))
407
- count = cursor.fetchone()[0]
408
- cursor.close()
409
- return count
410
- except Exception:
411
- # في حالة عدم وجود جدول أو خطأ في الاتصال، استخدم بيانات تقديرية
412
- return len(self.user_data['unlocked_achievements'])
413
-
414
- def _get_total_risks_identified(self):
415
- """الحصول على إجمالي عدد المخاطر المحددة"""
416
- risks_file = os.path.join(self.achievements_path, f'user_{self.user_id}_risks.json')
417
- if os.path.exists(risks_file):
418
- try:
419
- with open(risks_file, 'r', encoding='utf-8') as f:
420
- risks_data = json.load(f)
421
- return risks_data.get('total_risks', 0)
422
- except Exception:
423
- return 0
424
- return 0
425
-
426
- def _save_total_risks_identified(self, total):
427
- """حفظ إجمالي عدد المخاطر المحددة"""
428
- risks_file = os.path.join(self.achievements_path, f'user_{self.user_id}_risks.json')
429
- try:
430
- with open(risks_file, 'w', encoding='utf-8') as f:
431
- json.dump({'total_risks': total}, f, ensure_ascii=False, indent=2)
432
- except Exception:
433
- pass
434
-
435
- def _get_languages_used(self):
436
- """الحصول على قائمة اللغات المستخدمة"""
437
- languages_file = os.path.join(self.achievements_path, f'user_{self.user_id}_languages.json')
438
- if os.path.exists(languages_file):
439
- try:
440
- with open(languages_file, 'r', encoding='utf-8') as f:
441
- languages_data = json.load(f)
442
- return languages_data.get('languages', [])
443
- except Exception:
444
- return []
445
- return []
446
-
447
- def _save_languages_used(self, languages):
448
- """حفظ قائمة اللغات المستخدمة"""
449
- languages_file = os.path.join(self.achievements_path, f'user_{self.user_id}_languages.json')
450
- try:
451
- with open(languages_file, 'w', encoding='utf-8') as f:
452
- json.dump({'languages': languages}, f, ensure_ascii=False, indent=2)
453
- except Exception:
454
- pass
455
-
456
- def render_achievements_tab(self):
457
- """عرض علامة تبويب الإنجازات"""
458
- st.markdown("<h3 class='achievement-title'>إنجازاتك</h3>", unsafe_allow_html=True)
459
-
460
- # عرض مستوى المستخدم والنقاط
461
- col1, col2 = st.columns([1, 3])
462
- with col1:
463
- st.markdown(f"<div class='level-badge'>المستوى {self.user_data['level']}</div>", unsafe_allow_html=True)
464
- with col2:
465
- # حساب النقاط المطلوبة للمستوى التالي
466
- next_level_points = (self.user_data['level']) * 1000
467
- current_level_points = (self.user_data['level'] - 1) * 1000
468
- progress = (self.user_data['total_points'] - current_level_points) / (next_level_points - current_level_points)
469
-
470
- st.markdown(f"<div class='points-text'>{self.user_data['total_points']} نقطة</div>", unsafe_allow_html=True)
471
- st.progress(progress, text=f"المستوى التالي: {next_level_points} نقطة")
472
-
473
- # تقسيم الإنجازات إلى مجموعات
474
- st.markdown("<h4 class='achievement-subtitle'>الإنجازات المفتوحة</h4>", unsafe_allow_html=True)
475
-
476
- if not self.user_data['unlocked_achievements']:
477
- st.info("لم تقم بفتح أي إنجازات حتى الآن. أكمل المهام للحصول على الإنجازات!")
478
- else:
479
- # عرض الإنجازات المفتوحة بتنسيق الشبكة
480
- cols = st.columns(3)
481
- for i, achievement in enumerate(self.user_data['unlocked_achievements']):
482
- with cols[i % 3]:
483
- self._render_achievement_card(achievement, is_unlocked=True)
484
-
485
- # عرض الإنجازات قيد التقدم
486
- st.markdown("<h4 class='achievement-subtitle'>الإنجازات قيد التقدم</h4>", unsafe_allow_html=True)
487
-
488
- if not self.user_data['in_progress_achievements']:
489
- st.info("ليس لديك أي إنجازات قيد التقدم حالياً.")
490
- else:
491
- # عرض الإنجازات قيد التقدم
492
- for achievement in self.user_data['in_progress_achievements']:
493
- self._render_progress_achievement(achievement)
494
-
495
- # عرض الإنجازات المتاحة
496
- st.markdown("<h4 class='achievement-subtitle'>الإنجازات المتاحة</h4>", unsafe_allow_html=True)
497
-
498
- # فلترة الإنجازات غير المفتوحة وغير قيد التقدم
499
- unlocked_ids = [a['id'] for a in self.user_data['unlocked_achievements']]
500
- in_progress_ids = [a['id'] for a in self.user_data['in_progress_achievements']]
501
- available_achievements = [a for a in self.achievements if a['id'] not in unlocked_ids and a['id'] not in in_progress_ids]
502
-
503
- if not available_achievements:
504
- st.success("رائع! لقد حققت جميع الإنجازات المتاحة.")
505
- else:
506
- # تقسيم الإنجازات المتاحة حسب الفئات
507
- categories = sorted(set(a['category'] for a in available_achievements))
508
- for category in categories:
509
- st.markdown(f"<h5 class='achievement-category'>{category}</h5>", unsafe_allow_html=True)
510
-
511
- category_achievements = [a for a in available_achievements if a['category'] == category]
512
- cols = st.columns(3)
513
- for i, achievement in enumerate(category_achievements):
514
- with cols[i % 3]:
515
- self._render_achievement_card(achievement, is_unlocked=False)
516
-
517
- def _render_achievement_card(self, achievement, is_unlocked):
518
- """عرض بطاقة إنجاز"""
519
- if is_unlocked:
520
- card_class = "achievement-card unlocked"
521
- icon_class = "achievement-icon unlocked"
522
- title_class = "achievement-name unlocked"
523
- points_display = f"{achievement['points']} نقطة"
524
- date_display = f"تم الفتح: {achievement.get('unlocked_date', 'غير معروف')}"
525
- else:
526
- card_class = "achievement-card locked"
527
- icon_class = "achievement-icon locked"
528
- title_class = "achievement-name locked"
529
- points_display = f"{achievement['points']} نقطة"
530
- date_display = f"صعوبة: {achievement['difficulty']}"
531
-
532
- html = f"""
533
- <div class="{card_class}">
534
- <div class="{icon_class}">{achievement['icon']}</div>
535
- <div class="{title_class}">{achievement['name']}</div>
536
- <div class="achievement-description">{achievement['description']}</div>
537
- <div class="achievement-footer">
538
- <span class="achievement-points">{points_display}</span>
539
- <span class="achievement-date">{date_display}</span>
540
- </div>
541
- </div>
542
- """
543
- st.markdown(html, unsafe_allow_html=True)
544
-
545
- def _render_progress_achievement(self, achievement):
546
- """عرض إنجاز قيد التقدم"""
547
- progress = achievement.get('percentage', 0)
548
-
549
- html = f"""
550
- <div class="progress-achievement">
551
- <div class="progress-achievement-header">
552
- <div class="progress-achievement-icon">{achievement['icon']}</div>
553
- <div class="progress-achievement-info">
554
- <div class="progress-achievement-name">{achievement['name']}</div>
555
- <div class="progress-achievement-description">{achievement['description']}</div>
556
- </div>
557
- <div class="progress-achievement-points">{achievement['points']} نقطة</div>
558
- </div>
559
- </div>
560
- """
561
- st.markdown(html, unsafe_allow_html=True)
562
-
563
- st.progress(progress / 100, text=f"{progress}% ({achievement.get('progress', 0)}/{achievement.get('total', 1)})")
564
-
565
- def render_achievements_summary(self):
566
- """عرض ملخص الإنجازات في لوحة التحكم"""
567
- # حساب الإحصائيات
568
- total_achievements = len(self.achievements)
569
- unlocked_count = len(self.user_data['unlocked_achievements'])
570
- in_progress_count = len(self.user_data['in_progress_achievements'])
571
-
572
- st.markdown(f"""
573
- <div class="achievements-summary">
574
- <div class="achievements-summary-header">
575
- <div class="achievements-summary-title">الإنجازات</div>
576
- <div class="achievements-summary-level">المستوى {self.user_data['level']}</div>
577
- </div>
578
- <div class="achievements-summary-progress">
579
- <div class="achievements-summary-percentage">{int((unlocked_count / total_achievements) * 100)}%</div>
580
- <div class="achievements-summary-counts">{unlocked_count} / {total_achievements}</div>
581
- </div>
582
- <div class="achievements-summary-footer">
583
- <div class="achievements-summary-stat">
584
- <div class="achievements-summary-stat-value">{unlocked_count}</div>
585
- <div class="achievements-summary-stat-label">مفتوحة</div>
586
- </div>
587
- <div class="achievements-summary-stat">
588
- <div class="achievements-summary-stat-value">{in_progress_count}</div>
589
- <div class="achievements-summary-stat-label">قيد التقدم</div>
590
- </div>
591
- <div class="achievements-summary-stat">
592
- <div class="achievements-summary-stat-value">{self.user_data['total_points']}</div>
593
- <div class="achievements-summary-stat-label">نقطة</div>
594
- </div>
595
- </div>
596
- </div>
597
- """, unsafe_allow_html=True)
598
-
599
- # عرض آخر 3 إنجازات تم فتحها
600
- if self.user_data['unlocked_achievements']:
601
- st.markdown("<div class='achievements-recent-title'>آخر الإنجازات</div>", unsafe_allow_html=True)
602
-
603
- recent_achievements = sorted(
604
- self.user_data['unlocked_achievements'],
605
- key=lambda x: x.get('unlocked_date', ''),
606
- reverse=True
607
- )[:3]
608
-
609
- for achievement in recent_achievements:
610
- st.markdown(f"""
611
- <div class="achievement-recent-item">
612
- <div class="achievement-recent-icon">{achievement['icon']}</div>
613
- <div class="achievement-recent-info">
614
- <div class="achievement-recent-name">{achievement['name']}</div>
615
- <div class="achievement-recent-date">{achievement.get('unlocked_date', '')}</div>
616
- </div>
617
- <div class="achievement-recent-points">+{achievement['points']}</div>
618
- </div>
619
- """, unsafe_allow_html=True)
620
-
621
- def render(self):
622
- """عرض واجهة نظام الإنجازات"""
623
- st.markdown("<h2 class='module-title'>نظام الإنجازات المحفز لمراحل المشروع</h2>", unsafe_allow_html=True)
624
-
625
- st.markdown("""
626
- <div class="module-description">
627
- نظام الإنجازات يحفزك على إكمال المهام وتحقيق أهداف المشروع من خلال مكافآت
628
- وإنجازات قابلة للفتح. اكتسب النقاط وارتقِ بمستواك وافتح إنجازات جديدة كلما تقدمت في استخدام نظام تحليل المناقصات.
629
- </div>
630
- """, unsafe_allow_html=True)
631
-
632
- # عرض صندوق معلومات عند تشغيل الوحدة لأول مرة
633
- if not self.user_data['unlocked_achievements'] and not self.user_data['in_progress_achievements']:
634
- st.info("""
635
- 👋 مرحباً بك في نظام الإنجازات!
636
-
637
- استكشف الإنجازات المتاحة وابدأ في تحقيقها عن طريق إكمال المهام في أنحاء النظام المختلفة.
638
- كلما حققت المزيد من الإنجازات، حصلت على نقاط أكثر وارتقيت في المستويات.
639
-
640
- ابدأ الآن بإنشاء مشروع جديد أو تحليل مستند!
641
- """)
642
-
643
- # إنشاء علامات تبويب لعرض محتوى مختلف
644
- tab1, tab2, tab3 = st.tabs(["الإنجازات", "المستويات والمكافآت", "الإحصائيات"])
645
-
646
- with tab1:
647
- self.render_achievements_tab()
648
-
649
- with tab2:
650
- st.markdown("<h3 class='achievement-title'>المستويات والمكافآت</h3>", unsafe_allow_html=True)
651
-
652
- # عرض معلومات عن نظام المستويات
653
- st.markdown("""
654
- <div class="levels-info">
655
- <p>نظام المستويات يعتمد على النقاط التي تكتسبها من إنجاز المهام وفتح الإنجازات:</p>
656
- <ul>
657
- <li>المستوى 1: 0 - 999 نقطة</li>
658
- <li>المستوى 2: 1000 - 1999 نقطة</li>
659
- <li>المستوى 3: 2000 - 2999 نقطة</li>
660
- <li>وهكذا...</li>
661
- </ul>
662
- <p>كلما ارتقيت في المستويات، تفتح مكافآت وميزات جديدة في النظام!</p>
663
- </div>
664
- """, unsafe_allow_html=True)
665
-
666
- # عرض قائمة المكافآت
667
- st.markdown("<h4 class='achievement-subtitle'>المكافآت المتاحة</h4>", unsafe_allow_html=True)
668
-
669
- rewards = [
670
- {"level": 2, "name": "قوالب مخصصة", "description": "الوصول إلى قوالب مخصصة للتقارير والتحليلات"},
671
- {"level": 3, "name": "تنبيهات متقدمة", "description": "إعدادات إشعارات متقدمة للمشاريع والمواعيد النهائية"},
672
- {"level": 5, "name": "تحليل معزز", "description": "خيارات إضافية لتحليل المستندات والعقود"},
673
- {"level": 7, "name": "تخصيص متقدم", "description": "خيارات إضافية لتخصيص واجهة النظام والتقارير"},
674
- {"level": 10, "name": "وضع الخبراء", "description": "وضع متقدم مع ميزات خاصة متاحة فقط للمستخدمين المخضرمين"}
675
- ]
676
-
677
- for reward in rewards:
678
- status = "متاح" if self.user_data['level'] >= reward['level'] else "مقفل"
679
- status_class = "available" if self.user_data['level'] >= reward['level'] else "locked"
680
-
681
- st.markdown(f"""
682
- <div class="reward-item">
683
- <div class="reward-level">المستوى {reward['level']}</div>
684
- <div class="reward-info">
685
- <div class="reward-name">{reward['name']}</div>
686
- <div class="reward-description">{reward['description']}</div>
687
- </div>
688
- <div class="reward-status {status_class}">{status}</div>
689
- </div>
690
- """, unsafe_allow_html=True)
691
-
692
- with tab3:
693
- st.markdown("<h3 class='achievement-title'>إحصائيات الإنجازات</h3>", unsafe_allow_html=True)
694
-
695
- # إعداد بيانات للرسم البياني
696
- categories = {}
697
- for achievement in self.achievements:
698
- category = achievement['category']
699
- if category not in categories:
700
- categories[category] = {"total": 0, "unlocked": 0}
701
- categories[category]["total"] += 1
702
-
703
- # حساب الإنجازات المفتوحة لكل فئة
704
- for achievement in self.user_data['unlocked_achievements']:
705
- category = achievement['category']
706
- if category in categories:
707
- categories[category]["unlocked"] += 1
708
-
709
- # تحويل البيانات إلى DataFrame
710
- df = pd.DataFrame([
711
- {
712
- "الفئة": category,
713
- "المفتوحة": data["unlocked"],
714
- "الإجمالي": data["total"],
715
- "النسبة": round((data["unlocked"] / data["total"]) * 100 if data["total"] > 0 else 0)
716
- }
717
- for category, data in categories.items()
718
- ])
719
-
720
- # عرض البيانات في جدول
721
- st.dataframe(
722
- df,
723
- column_config={
724
- "النسبة": st.column_config.ProgressColumn(
725
- "نسبة الإنجاز",
726
- format="%d%%",
727
- min_value=0,
728
- max_value=100
729
- )
730
- },
731
- hide_index=True
732
- )
733
-
734
- # عرض معلومات إضافية
735
- col1, col2, col3 = st.columns(3)
736
- with col1:
737
- total_points_possible = sum(a['points'] for a in self.achievements)
738
- st.metric(
739
- "إجمالي النقاط المحتملة",
740
- f"{total_points_possible}",
741
- f"{int((self.user_data['total_points'] / total_points_possible) * 100)}%"
742
- )
743
-
744
- with col2:
745
- days_since_first = 0
746
- if self.user_data['unlocked_achievements']:
747
- first_date = min([
748
- datetime.strptime(a.get('unlocked_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')), '%Y-%m-%d %H:%M:%S')
749
- for a in self.user_data['unlocked_achievements']
750
- ])
751
- days_since_first = (datetime.now() - first_date).days
752
-
753
- st.metric("أيام النشاط", f"{days_since_first}")
754
-
755
- with col3:
756
- if self.user_data['unlocked_achievements']:
757
- achievements_per_day = round(len(self.user_data['unlocked_achievements']) / max(1, days_since_first), 2)
758
- st.metric("معدل الإنجازات اليومي", f"{achievements_per_day}")
759
- else:
760
- st.metric("معدل الإنجازات اليومي", "0")
761
-
762
- # إضافة CSS مخصص للصفحة
763
- st.markdown("""
764
- <style>
765
- .achievement-title {
766
- color: #1E88E5;
767
- font-size: 1.5rem;
768
- margin-bottom: 1rem;
769
- text-align: right;
770
- }
771
- .achievement-subtitle {
772
- color: #424242;
773
- font-size: 1.2rem;
774
- margin: 1.5rem 0 1rem 0;
775
- text-align: right;
776
- }
777
- .achievement-category {
778
- color: #616161;
779
- font-size: 1rem;
780
- margin: 1rem 0 0.5rem 0;
781
- text-align: right;
782
- border-bottom: 1px solid #e0e0e0;
783
- padding-bottom: 0.3rem;
784
- }
785
- .level-badge {
786
- background-color: #1E88E5;
787
- color: white;
788
- padding: 0.5rem 1rem;
789
- border-radius: 1rem;
790
- font-weight: bold;
791
- text-align: center;
792
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
793
- }
794
- .points-text {
795
- font-size: 1.2rem;
796
- font-weight: bold;
797
- color: #424242;
798
- margin-bottom: 0.5rem;
799
- text-align: right;
800
- }
801
- .achievement-card {
802
- border-radius: 10px;
803
- padding: 1rem;
804
- margin-bottom: 1rem;
805
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
806
- text-align: center;
807
- transition: transform 0.2s;
808
- }
809
- .achievement-card:hover {
810
- transform: translateY(-5px);
811
- }
812
- .achievement-card.unlocked {
813
- background-color: #E3F2FD;
814
- border: 1px solid #BBDEFB;
815
- }
816
- .achievement-card.locked {
817
- background-color: #F5F5F5;
818
- border: 1px solid #E0E0E0;
819
- opacity: 0.7;
820
- }
821
- .achievement-icon {
822
- font-size: 2rem;
823
- margin-bottom: 0.5rem;
824
- }
825
- .achievement-icon.unlocked {
826
- color: #1E88E5;
827
- }
828
- .achievement-icon.locked {
829
- color: #9E9E9E;
830
- }
831
- .achievement-name {
832
- font-weight: bold;
833
- margin-bottom: 0.5rem;
834
- }
835
- .achievement-name.unlocked {
836
- color: #1565C0;
837
- }
838
- .achievement-name.locked {
839
- color: #616161;
840
- }
841
- .achievement-description {
842
- font-size: 0.85rem;
843
- color: #757575;
844
- margin-bottom: 0.7rem;
845
- min-height: 2.5rem;
846
- }
847
- .achievement-footer {
848
- display: flex;
849
- justify-content: space-between;
850
- font-size: 0.8rem;
851
- color: #9E9E9E;
852
- border-top: 1px solid #E0E0E0;
853
- padding-top: 0.5rem;
854
- }
855
- .progress-achievement {
856
- background-color: #F5F5F5;
857
- border-radius: 10px;
858
- padding: 1rem;
859
- margin-bottom: 0.5rem;
860
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
861
- }
862
- .progress-achievement-header {
863
- display: flex;
864
- align-items: center;
865
- margin-bottom: 0.5rem;
866
- }
867
- .progress-achievement-icon {
868
- font-size: 1.5rem;
869
- color: #1E88E5;
870
- margin-left: 1rem;
871
- }
872
- .progress-achievement-info {
873
- flex: 1;
874
- }
875
- .progress-achievement-name {
876
- font-weight: bold;
877
- color: #424242;
878
- }
879
- .progress-achievement-description {
880
- font-size: 0.85rem;
881
- color: #757575;
882
- }
883
- .progress-achievement-points {
884
- color: #1E88E5;
885
- font-weight: bold;
886
- }
887
- .levels-info {
888
- background-color: #F5F5F5;
889
- border-radius: 10px;
890
- padding: 1rem;
891
- margin-bottom: 1.5rem;
892
- text-align: right;
893
- }
894
- .levels-info ul {
895
- list-style-position: inside;
896
- margin: 0.5rem 1rem;
897
- padding: 0;
898
- }
899
- .reward-item {
900
- display: flex;
901
- align-items: center;
902
- background-color: #F5F5F5;
903
- border-radius: 10px;
904
- padding: 1rem;
905
- margin-bottom: 0.5rem;
906
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
907
- }
908
- .reward-level {
909
- background-color: #1E88E5;
910
- color: white;
911
- padding: 0.3rem 0.7rem;
912
- border-radius: 1rem;
913
- font-size: 0.8rem;
914
- font-weight: bold;
915
- margin-left: 1rem;
916
- white-space: nowrap;
917
- }
918
- .reward-info {
919
- flex: 1;
920
- }
921
- .reward-name {
922
- font-weight: bold;
923
- color: #424242;
924
- }
925
- .reward-description {
926
- font-size: 0.85rem;
927
- color: #757575;
928
- }
929
- .reward-status {
930
- font-weight: bold;
931
- padding: 0.3rem 0.7rem;
932
- border-radius: 1rem;
933
- font-size: 0.8rem;
934
- white-space: nowrap;
935
- }
936
- .reward-status.available {
937
- background-color: #C8E6C9;
938
- color: #2E7D32;
939
- }
940
- .reward-status.locked {
941
- background-color: #FFCDD2;
942
- color: #C62828;
943
- }
944
- .achievements-summary {
945
- background-color: #F5F5F5;
946
- border-radius: 10px;
947
- padding: 1rem;
948
- margin-bottom: 1rem;
949
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
950
- }
951
- .achievements-summary-header {
952
- display: flex;
953
- justify-content: space-between;
954
- align-items: center;
955
- margin-bottom: 0.5rem;
956
- }
957
- .achievements-summary-title {
958
- font-weight: bold;
959
- color: #424242;
960
- }
961
- .achievements-summary-level {
962
- background-color: #1E88E5;
963
- color: white;
964
- padding: 0.3rem 0.7rem;
965
- border-radius: 1rem;
966
- font-size: 0.8rem;
967
- font-weight: bold;
968
- }
969
- .achievements-summary-progress {
970
- display: flex;
971
- justify-content: space-between;
972
- align-items: center;
973
- margin-bottom: 1rem;
974
- }
975
- .achievements-summary-percentage {
976
- font-size: 1.2rem;
977
- font-weight: bold;
978
- color: #1E88E5;
979
- }
980
- .achievements-summary-counts {
981
- color: #757575;
982
- }
983
- .achievements-summary-footer {
984
- display: flex;
985
- justify-content: space-between;
986
- text-align: center;
987
- }
988
- .achievements-summary-stat-value {
989
- font-weight: bold;
990
- color: #424242;
991
- font-size: 1.1rem;
992
- }
993
- .achievements-summary-stat-label {
994
- color: #757575;
995
- font-size: 0.8rem;
996
- }
997
- .achievements-recent-title {
998
- font-weight: bold;
999
- color: #424242;
1000
- margin: 1rem 0 0.5rem 0;
1001
- }
1002
- .achievement-recent-item {
1003
- display: flex;
1004
- align-items: center;
1005
- background-color: #E3F2FD;
1006
- border-radius: 10px;
1007
- padding: 0.7rem;
1008
- margin-bottom: 0.5rem;
1009
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1010
- }
1011
- .achievement-recent-icon {
1012
- font-size: 1.2rem;
1013
- color: #1E88E5;
1014
- margin-left: 0.7rem;
1015
- }
1016
- .achievement-recent-info {
1017
- flex: 1;
1018
- }
1019
- .achievement-recent-name {
1020
- font-weight: bold;
1021
- color: #424242;
1022
- font-size: 0.9rem;
1023
- }
1024
- .achievement-recent-date {
1025
- font-size: 0.75rem;
1026
- color: #757575;
1027
- }
1028
- .achievement-recent-points {
1029
- color: #1E88E5;
1030
- font-weight: bold;
1031
- }
1032
- </style>
1033
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/achievements/achievements_app.py DELETED
@@ -1,49 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- وحدة تطبيق نظام الإنجازات المحفز لمراحل المشروع
6
- """
7
-
8
- import os
9
- import sys
10
- import streamlit as st
11
- import pandas as pd
12
- import numpy as np
13
- import time
14
- from datetime import datetime, timedelta
15
-
16
- # إضافة مسار النظام للوصول للملفات المشتركة
17
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
18
-
19
- # استيراد مكونات نظام الإنجازات
20
- from modules.achievements.achievement_system import AchievementSystem
21
-
22
-
23
- class AchievementsApp:
24
- """وحدة تطبيق نظام الإنجازات المحفز لمراحل المشروع"""
25
-
26
- def __init__(self, user_id=None):
27
- """تهيئة وحدة تطبيق نظام الإنجازات المحفز"""
28
- self.achievement_system = AchievementSystem(user_id)
29
-
30
- def render(self):
31
- """عرض واجهة وحدة تطبيق نظام الإنجازات المحفز"""
32
- self.achievement_system.render()
33
-
34
- def render_dashboard_summary(self):
35
- """عرض ملخص الإنجازات في لوحة التحكم"""
36
- self.achievement_system.render_achievements_summary()
37
-
38
-
39
- # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
40
- if __name__ == "__main__":
41
- st.set_page_config(
42
- page_title="نظام الإنجازات المحفز | WAHBi AI",
43
- page_icon="🏆",
44
- layout="wide",
45
- initial_sidebar_state="expanded"
46
- )
47
-
48
- app = AchievementsApp()
49
- app.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/ai_assistant/__init__.py DELETED
@@ -1,5 +0,0 @@
1
- """
2
- وحدة المساعد الذكي
3
- """
4
-
5
- __version__ = '1.0.0'
 
 
 
 
 
 
modules/ai_assistant/ai_app.py DELETED
@@ -1,1067 +0,0 @@
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/ai_assistant.py DELETED
@@ -1,773 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- وحدة المساعد الذكي التفاعلية
6
- تتيح للمستخدمين التفاعل مع نماذج الذكاء الاصطناعي المتقدمة للحصول على مساعدة في تحليل العقود والمناقصات
7
- """
8
-
9
- import os
10
- import sys
11
- import json
12
- import re
13
- import time
14
- import base64
15
- import tempfile
16
- import logging
17
- from datetime import datetime
18
- import streamlit as st
19
- import pandas as pd
20
- import numpy as np
21
- import requests
22
- from io import BytesIO
23
- from PIL import Image
24
- import openai
25
- import plotly.express as px
26
- import plotly.graph_objects as go
27
-
28
- # إضافة مسار النظام للوصول للملفات المشتركة
29
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
30
-
31
- # استيراد المكونات المساعدة
32
- from utils.helpers import create_directory_if_not_exists, format_time, get_user_info, render_credits, load_css
33
-
34
-
35
- class AIAssistant:
36
- """فئة المساعد الذكي التفاعلية"""
37
-
38
- def __init__(self):
39
- """تهيئة المساعد الذكي"""
40
- self.conversations_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'assistant_conversations')
41
- create_directory_if_not_exists(self.conversations_dir)
42
-
43
- # تهيئة مفتاح OpenAI API
44
- self.openai_api_key = os.environ.get("OPENAI_API_KEY")
45
- if self.openai_api_key:
46
- openai.api_key = self.openai_api_key
47
- self.is_api_available = True
48
- else:
49
- self.is_api_available = False
50
-
51
- # نموذج OpenAI المستخدم
52
- self.model = "gpt-4o" # النموذج الأحدث من OpenAI
53
-
54
- # تهيئة حالة المحادثة في الجلسة
55
- if "assistant_messages" not in st.session_state:
56
- st.session_state.assistant_messages = []
57
-
58
- if "assistant_mode" not in st.session_state:
59
- st.session_state.assistant_mode = "general"
60
-
61
- if "document_context" not in st.session_state:
62
- st.session_state.document_context = None
63
-
64
- # الأنماط المتاحة للمساعد
65
- self.assistant_modes = {
66
- "general": "مساعد عام",
67
- "contract_analysis": "تحليل العقود",
68
- "cost_estimation": "تقدير التكاليف",
69
- "risk_assessment": "تقييم المخاطر",
70
- "project_planning": "تخطيط المشاريع"
71
- }
72
-
73
- # توجيهات النظام للمساعد
74
- self.system_prompts = {
75
- "general": """
76
- أنت مساعد ذكي متخصص في شركة شبه الجزيرة للمقاولات، وتعمل ضمن نظام WAHBi لتحليل العقود والمناقصات.
77
- دورك هو مساعدة المستخدمين في:
78
- 1. تحليل المستندات والعقود، وتوضيح بنود العقود وفهم الالتزامات والشروط.
79
- 2. المساعدة في تسعير المشاريع وحساب التكاليف والموارد.
80
- 3. تقييم مخاطر العقود والمشاريع والمساعدة في اتخاذ القرارات.
81
- 4. المساعدة في إدارة المشاريع ومتابعة الإنجاز.
82
-
83
- استخدم لغة مهنية واضحة ومباشرة. قدم إجابات دقيقة ومختصرة.
84
- عند قيام المستخدم بسؤال عن كيفية استخدام النظام، قم بإرشاده إلى الوحدة المناسبة في النظام.
85
-
86
- معلومات هامة عن وحدات النظام:
87
- - وحدة تحليل المستندات: لتحليل العقود والمناقصات باستخدام الذكاء الاصطناعي.
88
- - وحدة مقارنة المستندات: لمقارنة نسخ مختلفة من المستندات وتحديد التغييرات.
89
- - وحدة التسعير المتكاملة: لحساب تكاليف المشاريع بناءً على الموارد والمواد والعمالة.
90
- - وحدة تقييم مخاطر العقود: لتحليل وتقييم المخاطر المحتملة في العقود والمشاريع.
91
- - وحدة متتبع حالة المشروع: لمتابعة تقدم المشاريع وعرض مؤشرات الأداء.
92
- - وحدة خريطة المشاريع: لعرض مواقع المشاريع على الخريطة بشكل تفاعلي.
93
- - وحدة الإشعارات الذكية: لإرسال تنبيهات وإشعارات للمستخدمين حول المشاريع.
94
-
95
- تذكر أن تكون مفيداً ودقيقاً ومهنياً في جميع إجاباتك.
96
- """,
97
-
98
- "contract_analysis": """
99
- أنت محلل عقود متخ��ص في تحليل العقود والمناقصات لشركات المقاولات.
100
- مهمتك هي تحليل العقود وتحديد:
101
- - الالتزامات الرئيسية
102
- - المواعيد النهائية والتسليمات
103
- - الشروط الجزائية والغرامات
104
- - آلية الدفع والمستحقات المالية
105
- - الشروط الخاصة والاستثناءات
106
- - المخاطر المحتملة وكيفية التخفيف منها
107
-
108
- عند تحليل عقد، قم بتوضيح البنود غير المواتية التي قد تسبب مشاكل مستقبلية.
109
- استخدم لغة قانونية دقيقة مع شرح المصطلحات القانونية بلغة مبسطة.
110
- قدم توصيات عملية لكيفية التعامل مع بنود العقد وتجنب المخاطر.
111
- """,
112
-
113
- "cost_estimation": """
114
- أنت خبير في تقدير تكاليف مشاريع البناء والمقاولات.
115
- مهمتك هي مساعدة المستخدم في:
116
- - تقدير تكاليف المشاريع بناءً على وصف المشروع ومتطلباته
117
- - حساب تكاليف المواد والعمالة والمعدات والنفقات العامة
118
- - توضيح كيفية تخصيص الميزانية بين مختلف عناصر المشروع
119
- - تحديد التكاليف غير المباشرة التي قد يغفل عنها المستخدم
120
- - اقتراح طرق لتقليل التكاليف دون التأثير على جودة المشروع
121
-
122
- استخدم أسلوب منهجي في تقدير التكاليف واشرح افتراضاتك بوضوح.
123
- قدم نطاقات تقديرية بدلاً من أرقام دقيقة للتكاليف حيثما كان ذلك مناسباً.
124
- عند الإشارة إلى تكاليف، وضح ما إذا كانت التكاليف تشمل ضريبة القيمة المضافة أم لا.
125
- """,
126
-
127
- "risk_assessment": """
128
- أنت خبير في تقييم مخاطر مشاريع البناء والمقاولات.
129
- مهمتك هي مساعدة المستخدم في:
130
- - تحديد المخاطر المحتملة في المشاريع والعقود
131
- - تقييم احتمالية وتأثير كل خطر
132
- - اقتراح استراتيجيات للتخفيف من المخاطر
133
- - تحليل السيناريوهات المحتملة وخطط الطوارئ
134
- - تقديم أفضل الممارسات لإدارة المخاطر في مشاريع المقاولات
135
-
136
- صنف المخاطر إلى فئات (عالية، متوسطة، منخفضة) بناءً على احتماليتها وتأثيرها.
137
- اشرح كيف يمكن للشركة أن تحول بعض المخاطر إلى فرص.
138
- قدم أمثلة عملية من مشاريع مماثلة لتوضيح كيفية إدارة المخاطر المحددة.
139
- """,
140
-
141
- "project_planning": """
142
- أنت خبير في تخطيط وإدارة مشاريع البناء والمقاولات.
143
- مهمتك هي مساعدة المستخدم في:
144
- - تخطيط المشاريع وتقسيمها إلى مراحل ومهام
145
- - تحديد الموارد اللازمة والجداول الزمنية
146
- - إنشاء مخطط جانت وتحديد المسار الحرج
147
- - التخطيط للموارد البشرية والمعدات والمواد
148
- - متابعة تقدم المشروع ومؤشرات الأداء
149
-
150
- قدم نصائح عملية لإدارة المشاريع بكفاءة وتجنب التأخيرات.
151
- اشرح كيفية التعامل مع التغييرات والمطالبات خلال تنفيذ المشروع.
152
- قدم أفضل الممارسات للتواصل مع أصحاب المصلحة وإدارة التوقعات.
153
- """
154
- }
155
-
156
- def _call_openai_api(self, messages, model=None, max_tokens=2000):
157
- """استدعاء OpenAI API للحصول على استجابة"""
158
- if not self.is_api_available:
159
- return {
160
- "choices": [{"message": {"content": "عذراً، مفتاح OpenAI API غير متوفر. يرجى التواصل مع مسؤول النظام."}}]
161
- }
162
-
163
- try:
164
- if model is None:
165
- model = self.model
166
-
167
- response = openai.ChatCompletion.create(
168
- model=model,
169
- messages=messages,
170
- max_tokens=max_tokens,
171
- temperature=0.7,
172
- top_p=0.9,
173
- frequency_penalty=0,
174
- presence_penalty=0
175
- )
176
-
177
- return response
178
- except Exception as e:
179
- logging.error(f"خطأ في استدعاء OpenAI API: {e}")
180
- return {
181
- "choices": [{"message": {"content": f"عذراً، حدث خطأ في الاتصال بـ OpenAI API: {str(e)}"}}]
182
- }
183
-
184
- def _call_backend_api(self, endpoint, data):
185
- """استدعاء واجهة API الخلفية للنظام"""
186
- try:
187
- response = requests.post(
188
- f"http://localhost:5000/api/{endpoint}",
189
- json=data,
190
- timeout=60
191
- )
192
-
193
- if response.status_code == 200:
194
- return response.json()
195
- else:
196
- logging.error(f"خطأ في استدعاء واجهة API الخلفية: {response.status_code} - {response.text}")
197
- return {"error": f"خطأ في استدعاء واجهة API الخلفية: {response.status_code}"}
198
- except Exception as e:
199
- logging.error(f"خطأ في الاتصال بواجهة API الخلفية: {e}")
200
- return {"error": f"خطأ في الاتصال بواجهة API الخلفية: {str(e)}"}
201
-
202
- def _process_user_message(self, user_message, mode=None):
203
- """معالجة رسالة المستخدم والحصول على رد من المساعد الذكي"""
204
- if mode is None:
205
- mode = st.session_state.assistant_mode
206
-
207
- # إنشاء قائمة الرسائل للمحادثة
208
- messages = [
209
- {"role": "system", "content": self.system_prompts[mode]}
210
- ]
211
-
212
- # إضافة سياق المستند إذا كان متاحاً
213
- if st.session_state.document_context:
214
- messages.append({
215
- "role": "system",
216
- "content": f"معلومات سياقية عن المستند: {st.session_state.document_context}"
217
- })
218
-
219
- # إضافة المحادثة السابقة
220
- for msg in st.session_state.assistant_messages:
221
- messages.append({
222
- "role": msg["role"],
223
- "content": msg["content"]
224
- })
225
-
226
- # إضافة رسالة المستخدم الحالية
227
- messages.append({
228
- "role": "user",
229
- "content": user_message
230
- })
231
-
232
- # استدعاء API
233
- response = self._call_openai_api(messages)
234
-
235
- # استخراج الرد
236
- assistant_response = response["choices"][0]["message"]["content"]
237
-
238
- # تحديث سجل المحادثة
239
- st.session_state.assistant_messages.append({"role": "user", "content": user_message})
240
- st.session_state.assistant_messages.append({"role": "assistant", "content": assistant_response})
241
-
242
- return assistant_response
243
-
244
- def _clear_chat(self):
245
- """مسح المحادثة الحالية"""
246
- st.session_state.assistant_messages = []
247
- st.session_state.document_context = None
248
-
249
- def _save_conversation(self):
250
- """حفظ المحادثة الحالية"""
251
- if not st.session_state.assistant_messages:
252
- st.warning("لا توجد محادثة لحفظها.")
253
- return False
254
-
255
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
256
- user_info = get_user_info()
257
-
258
- conversation_data = {
259
- "timestamp": timestamp,
260
- "user": user_info["username"],
261
- "mode": st.session_state.assistant_mode,
262
- "messages": st.session_state.assistant_messages,
263
- "document_context": st.session_state.document_context
264
- }
265
-
266
- filename = f"conversation_{user_info['username']}_{timestamp}.json"
267
- file_path = os.path.join(self.conversations_dir, filename)
268
-
269
- try:
270
- with open(file_path, 'w', encoding='utf-8') as f:
271
- json.dump(conversation_data, f, ensure_ascii=False, indent=2)
272
-
273
- return True
274
- except Exception as e:
275
- logging.error(f"خطأ في حفظ المحادثة: {e}")
276
- return False
277
-
278
- def _load_conversation(self, filename):
279
- """تحميل محادثة محفوظة"""
280
- file_path = os.path.join(self.conversations_dir, filename)
281
-
282
- try:
283
- with open(file_path, 'r', encoding='utf-8') as f:
284
- conversation_data = json.load(f)
285
-
286
- st.session_state.assistant_messages = conversation_data["messages"]
287
- st.session_state.assistant_mode = conversation_data["mode"]
288
- st.session_state.document_context = conversation_data.get("document_context")
289
-
290
- return True
291
- except Exception as e:
292
- logging.error(f"خطأ في تحميل المحادثة: {e}")
293
- return False
294
-
295
- def _get_saved_conversations(self):
296
- """الحصول على قائمة المحادثات المحفوظة"""
297
- conversations = []
298
-
299
- try:
300
- for filename in os.listdir(self.conversations_dir):
301
- if filename.endswith(".json") and filename.startswith("conversation_"):
302
- file_path = os.path.join(self.conversations_dir, filename)
303
-
304
- with open(file_path, 'r', encoding='utf-8') as f:
305
- data = json.load(f)
306
-
307
- conversations.append({
308
- "filename": filename,
309
- "timestamp": data.get("timestamp", ""),
310
- "user": data.get("user", ""),
311
- "mode": data.get("mode", "general"),
312
- "message_count": len(data.get("messages", []))
313
- })
314
- except Exception as e:
315
- logging.error(f"خطأ في قراءة المحادثات المحفوظة: {e}")
316
-
317
- # ترتيب المحادثات حسب التاريخ (الأحدث أولاً)
318
- conversations.sort(key=lambda x: x["timestamp"], reverse=True)
319
-
320
- return conversations
321
-
322
- def render_chat_interface(self):
323
- """عرض واجهة المحادثة الرئيسية"""
324
- st.markdown("<h2 class='module-title'>المساعد الذكي</h2>", unsafe_allow_html=True)
325
-
326
- # التحقق من توفر OpenAI API
327
- if not self.is_api_available:
328
- st.warning("⚠️ مفتاح OpenAI API غير متوفر. لن يكون المساعد الذكي قادراً على الرد. يرجى التواصل مع مسؤول النظام.")
329
-
330
- # إضافة CSS
331
- st.markdown("""
332
- <style>
333
- .chat-container {
334
- background-color: #f8f9fa;
335
- border-radius: 10px;
336
- padding: 20px;
337
- margin-bottom: 20px;
338
- max-height: 500px;
339
- overflow-y: auto;
340
- }
341
-
342
- .chat-message {
343
- margin-bottom: 15px;
344
- display: flex;
345
- flex-direction: row;
346
- }
347
-
348
- .user-message {
349
- justify-content: flex-end;
350
- }
351
-
352
- .assistant-message {
353
- justify-content: flex-start;
354
- }
355
-
356
- .message-bubble {
357
- padding: 10px 15px;
358
- border-radius: 15px;
359
- max-width: 75%;
360
- }
361
-
362
- .user-bubble {
363
- background-color: #1E88E5;
364
- color: white;
365
- border-top-left-radius: 15px;
366
- border-top-right-radius: 15px;
367
- border-bottom-left-radius: 15px;
368
- border-bottom-right-radius: 0;
369
- }
370
-
371
- .assistant-bubble {
372
- background-color: #f0f0f0;
373
- color: #333;
374
- border-top-left-radius: 15px;
375
- border-top-right-radius: 15px;
376
- border-bottom-left-radius: 0;
377
- border-bottom-right-radius: 15px;
378
- }
379
-
380
- .message-avatar {
381
- width: 40px;
382
- height: 40px;
383
- border-radius: 50%;
384
- background-color: #ccc;
385
- display: flex;
386
- align-items: center;
387
- justify-content: center;
388
- margin: 0 10px;
389
- font-weight: bold;
390
- color: white;
391
- }
392
-
393
- .user-avatar {
394
- background-color: #78909C;
395
- }
396
-
397
- .assistant-avatar {
398
- background-color: #1E88E5;
399
- }
400
-
401
- .message-content {
402
- white-space: pre-wrap;
403
- }
404
-
405
- .message-time {
406
- font-size: 0.8em;
407
- color: #888;
408
- margin-top: 5px;
409
- text-align: right;
410
- }
411
-
412
- .chat-input {
413
- background-color: #f8f9fa;
414
- border-radius: 10px;
415
- padding: 20px;
416
- }
417
-
418
- .suggestions-container {
419
- display: flex;
420
- flex-wrap: wrap;
421
- gap: 10px;
422
- margin-top: 10px;
423
- }
424
-
425
- .suggestion-chip {
426
- background-color: #e9ecef;
427
- border-radius: 20px;
428
- padding: 5px 15px;
429
- cursor: pointer;
430
- text-align: center;
431
- transition: background-color 0.3s;
432
- }
433
-
434
- .suggestion-chip:hover {
435
- background-color: #dee2e6;
436
- }
437
- </style>
438
- """, unsafe_allow_html=True)
439
-
440
- # عرض أوضاع المساعد
441
- st.markdown("#### اختر وضع المساعد الذكي")
442
-
443
- col1, col2, col3, col4, col5 = st.columns(5)
444
-
445
- with col1:
446
- if st.button("مساعد عام", key="mode_general",
447
- help="مساعد عام للإجابة على الأسئلة المتعلقة بالعقود والمناقصات"):
448
- st.session_state.assistant_mode = "general"
449
- st.rerun()
450
-
451
- with col2:
452
- if st.button("تحليل العقود", key="mode_contract_analysis",
453
- help="متخصص في تحليل العقود وتحديد البنود والشروط والمخاطر"):
454
- st.session_state.assistant_mode = "contract_analysis"
455
- st.rerun()
456
-
457
- with col3:
458
- if st.button("تقدير التكاليف", key="mode_cost_estimation",
459
- help="متخصص في تقدير تكاليف المشاريع والبنود"):
460
- st.session_state.assistant_mode = "cost_estimation"
461
- st.rerun()
462
-
463
- with col4:
464
- if st.button("تقييم المخاطر", key="mode_risk_assessment",
465
- help="متخصص في تحديد وتقييم المخاطر المحتملة في المشاريع والعقود"):
466
- st.session_state.assistant_mode = "risk_assessment"
467
- st.rerun()
468
-
469
- with col5:
470
- if st.button("تخطيط المشاريع", key="mode_project_planning",
471
- help="متخصص في تخطيط وإدارة المشاريع وتحديد المراحل والموارد"):
472
- st.session_state.assistant_mode = "project_planning"
473
- st.rerun()
474
-
475
- st.markdown(f"**الوضع الحالي:** {self.assistant_modes[st.session_state.assistant_mode]}")
476
-
477
- # تحميل سياق من مستند (اختياري)
478
- st.markdown("---")
479
- with st.expander("إضافة سياق من مستند", expanded=False):
480
- context_text = st.text_area(
481
- "نص المستند (اختياري)",
482
- value=st.session_state.document_context if st.session_state.document_context else "",
483
- height=150,
484
- help="أضف نص المستند هنا ليتم استخدامه كسياق للمحادثة"
485
- )
486
-
487
- uploaded_file = st.file_uploader(
488
- "أو قم بتحميل ملف نصي أو PDF",
489
- type=["txt", "pdf"],
490
- help="يمكنك تحميل ملف نصي أو PDF ليتم استخدامه كسياق للمحادثة"
491
- )
492
-
493
- doc_col1, doc_col2 = st.columns(2)
494
-
495
- with doc_col1:
496
- if st.button("إضافة السياق", disabled=not context_text and not uploaded_file):
497
- if uploaded_file:
498
- try:
499
- # قراءة الملف المرفوع
500
- if uploaded_file.name.endswith(".pdf"):
501
- import PyPDF2
502
- reader = PyPDF2.PdfReader(uploaded_file)
503
- context = ""
504
- for page in reader.pages:
505
- context += page.extract_text() + "\n"
506
- else:
507
- context = uploaded_file.read().decode("utf-8")
508
-
509
- st.session_state.document_context = context
510
- st.success("تم إضافة نص المستند كسياق للمحادثة بنجاح.")
511
- except Exception as e:
512
- st.error(f"حدث خطأ أثناء قراءة الملف: {str(e)}")
513
- elif context_text:
514
- st.session_state.document_context = context_text
515
- st.success("تم إضافة نص المستند كسياق للمحادثة بنجاح.")
516
-
517
- with doc_col2:
518
- if st.button("مسح السياق", disabled=not st.session_state.document_context):
519
- st.session_state.document_context = None
520
- st.success("تم مسح سياق المستند بنجاح.")
521
-
522
- # عرض المحادثة
523
- st.markdown("---")
524
- st.markdown("#### المحادثة مع المساعد الذكي")
525
-
526
- # عرض رسائل المحادثة
527
- chat_container = st.container()
528
-
529
- with chat_container:
530
- with st.container():
531
- if not st.session_state.assistant_messages:
532
- st.markdown("""
533
- <div style="text-align: center; padding: 30px; color: #666;">
534
- <p>مرحباً بك في المساعد الذكي!</p>
535
- <p>يمكنك البدء بطرح سؤال أو طلب مساعدة.</p>
536
- </div>
537
- """, unsafe_allow_html=True)
538
- else:
539
- message_html = ""
540
-
541
- for msg in st.session_state.assistant_messages:
542
- if msg["role"] == "user":
543
- message_html += f"""
544
- <div class="chat-message user-message">
545
- <div class="message-bubble user-bubble">
546
- <div class="message-content">{msg["content"]}</div>
547
- </div>
548
- <div class="message-avatar user-avatar">أ</div>
549
- </div>
550
- """
551
- else:
552
- message_html += f"""
553
- <div class="chat-message assistant-message">
554
- <div class="message-avatar assistant-avatar">W</div>
555
- <div class="message-bubble assistant-bubble">
556
- <div class="message-content">{msg["content"]}</div>
557
- </div>
558
- </div>
559
- """
560
-
561
- st.markdown(f"""
562
- <div class="chat-container">
563
- {message_html}
564
- </div>
565
- """, unsafe_allow_html=True)
566
-
567
- # ادخال الرسالة
568
- st.markdown("#### أدخل رسالتك")
569
-
570
- with st.container():
571
- with st.form(key="chat_form"):
572
- user_message = st.text_area("رسالتك", height=100, placeholder="اكتب سؤالك أو طلبك هنا...")
573
-
574
- col1, col2, col3 = st.columns([2, 2, 1])
575
-
576
- with col1:
577
- send_button = st.form_submit_button(
578
- "إرسال",
579
- help="إرسال الرسالة إلى المساعد الذكي"
580
- )
581
-
582
- with col2:
583
- suggested_questions = [
584
- "كيف يمكنني تحليل بنود الدفع في العقد؟",
585
- "ما هي أفضل طريقة لتقدير تكاليف مشروع بناء؟",
586
- "كيف أحدد المخاطر المحتملة في مشروع جديد؟",
587
- "كيف يمكنني إنشاء جدول زمني فعال للمشروع؟",
588
- "ما هي أهم البنود التي يجب الانتباه إليها في عقود المقاولات؟"
589
- ]
590
-
591
- if st.session_state.assistant_mode == "contract_analysis":
592
- suggested_questions = [
593
- "كيف أحدد البنود غير المواتية في العقد؟",
594
- "ما هي العناصر الأساسية التي يجب أن يتضمنها عقد المقاولة؟",
595
- "كيف أتعامل مع بنود الغرامات والتعويضات؟",
596
- "كيف يمكنني التفاوض على تحسين شروط الدفع؟",
597
- "ما هي الفروق الرئيسية بين عقد الثمن الثابت وعقد التكلفة زائد أتعاب؟"
598
- ]
599
- elif st.session_state.assistant_mode == "cost_estimation":
600
- suggested_questions = [
601
- "كيف أقدر تكلفة المواد في مشروع بناء؟",
602
- "ما هي نسبة النفقات العامة المعقولة لمشروع مقاولات؟",
603
- "كيف أحسب تكلفة العمالة بدقة؟",
604
- "ما هي العوامل التي تؤثر على تكلفة المعدات؟",
605
- "كيف أقدر هامش الربح المناسب للمشروع؟"
606
- ]
607
-
608
- selected_question = st.selectbox(
609
- "أو اختر سؤال مقترح",
610
- [""] + suggested_questions,
611
- index=0
612
- )
613
-
614
- with col3:
615
- clear_button = st.form_submit_button(
616
- "مسح المحادثة",
617
- help="مسح جميع الرسائل في المحادثة الحالية"
618
- )
619
-
620
- if send_button and user_message:
621
- # معالجة رسالة المستخدم
622
- with st.spinner("جاري معالجة الرسالة..."):
623
- self._process_user_message(user_message)
624
- st.rerun()
625
-
626
- if send_button and selected_question and not user_message:
627
- # استخدام السؤال المقترح
628
- with st.spinner("جاري معالجة الرسالة..."):
629
- self._process_user_message(selected_question)
630
- st.rerun()
631
-
632
- if clear_button:
633
- self._clear_chat()
634
- st.rerun()
635
-
636
- # زر لحفظ المحادثة
637
- col1, col2, col3 = st.columns([1, 1, 2])
638
-
639
- with col1:
640
- if st.button("حفظ المحادثة", key="save_conversation", disabled=not st.session_state.assistant_messages):
641
- if self._save_conversation():
642
- st.success("تم حفظ المحادثة بنجاح.")
643
- else:
644
- st.error("حدث خطأ أثناء حفظ المحادثة.")
645
-
646
- with col2:
647
- if st.button("تحميل محادثة سابقة", key="show_load_conversation"):
648
- st.session_state.show_conversations = True
649
- st.rerun()
650
-
651
- # عرض المحادثات المحفوظة
652
- if "show_conversations" in st.session_state and st.session_state.show_conversations:
653
- st.markdown("---")
654
- st.markdown("#### المحادثات المحفوظة")
655
-
656
- conversations = self._get_saved_conversations()
657
-
658
- if not conversations:
659
- st.info("لا توجد محادثات محفوظة.")
660
- else:
661
- # عرض المحادثات في جدول
662
- conversation_data = []
663
- for conv in conversations:
664
- timestamp = datetime.strptime(conv["timestamp"], "%Y%m%d%H%M%S") if conv["timestamp"] else ""
665
- formatted_time = timestamp.strftime("%Y-%m-%d %H:%M:%S") if timestamp else ""
666
-
667
- conversation_data.append({
668
- "التاريخ": formatted_time,
669
- "المستخدم": conv["user"],
670
- "وضع المساعد": self.assistant_modes.get(conv["mode"], "غير معروف"),
671
- "عدد الرسائل": conv["message_count"],
672
- "الملف": conv["filename"]
673
- })
674
-
675
- df = pd.DataFrame(conversation_data)
676
- st.dataframe(df, height=300)
677
-
678
- # اختيار محادثة لتحميلها
679
- selected_filename = st.selectbox(
680
- "اختر محادثة لتحميلها",
681
- options=[""] + [conv["filename"] for conv in conversations],
682
- format_func=lambda x: next((f"{c['user']} - {datetime.strptime(c['timestamp'], '%Y%m%d%H%M%S').strftime('%Y-%m-%d %H:%M:%S')}" for c in conversations if c["filename"] == x), x),
683
- index=0
684
- )
685
-
686
- col1, col2 = st.columns(2)
687
-
688
- with col1:
689
- if st.button("تحميل المحادثة المختارة", disabled=not selected_filename):
690
- if self._load_conversation(selected_filename):
691
- st.success("تم تحميل المحادثة بنجاح.")
692
- st.session_state.show_conversations = False
693
- st.rerun()
694
- else:
695
- st.error("حدث خطأ أثناء تحميل المحادثة.")
696
-
697
- with col2:
698
- if st.button("إلغاء", key="cancel_load_conversation"):
699
- st.session_state.show_conversations = False
700
- st.rerun()
701
-
702
- # عرض المعلومات عن وضع المساعد الحالي
703
- st.markdown("---")
704
- st.markdown(f"#### معلومات عن وضع المساعد: {self.assistant_modes[st.session_state.assistant_mode]}")
705
-
706
- if st.session_state.assistant_mode == "general":
707
- st.markdown("""
708
- المساعد العام يمكنه مساعدتك في مجموعة متنوعة من المهام المتعلقة بالعقود والمناقصات وإدارة المشاريع. يمكنه:
709
- - الإجابة على الأسئلة العامة حول العقود والمناقصات
710
- - توجيهك إلى الوحدات المناسبة في النظام
711
- - تقديم معلومات عامة عن إدارة المشاريع وأفضل الممارسات
712
- - المساعدة في فهم المصطلحات والمفاهيم المتعلقة بمجال المقاولات
713
- """)
714
- elif st.session_state.assistant_mode == "contract_analysis":
715
- st.markdown("""
716
- مساعد تحليل العقود متخصص في:
717
- - تحليل بنود العقود وتوضيح معانيها
718
- - تحديد الالتزامات والحقوق لكل طرف
719
- - تسليط الضوء على البنود غير المواتية أو الغامضة
720
- - تقديم توصيات للتفاوض على تحسين شروط العقد
721
- - مقارنة العقد مع أفضل الممارسات في الق��اع
722
- """)
723
- elif st.session_state.assistant_mode == "cost_estimation":
724
- st.markdown("""
725
- مساعد تقدير التكاليف متخصص في:
726
- - حساب تكاليف المشاريع بناءً على المتطلبات والمواصفات
727
- - تقدير تكاليف المواد والعمالة والمعدات
728
- - تحليل التكاليف المباشرة وغير المباشرة
729
- - تقديم نصائح لتقليل التكاليف وزيادة الكفاءة
730
- - تحديد العوامل التي قد تؤثر على التكلفة الإجمالية
731
- """)
732
- elif st.session_state.assistant_mode == "risk_assessment":
733
- st.markdown("""
734
- مساعد تقييم المخاطر متخصص في:
735
- - تحديد المخاطر المحتملة في المشاريع والعقود
736
- - تقييم احتمالية وتأثير كل خطر
737
- - اقتراح استراتيجيات للتخفيف من المخاطر
738
- - إنشاء خطط للطوارئ والاستجابة للمخاطر
739
- - تحليل تأثير المخاطر على الجدول الزمني والتكلفة
740
- """)
741
- elif st.session_state.assistant_mode == "project_planning":
742
- st.markdown("""
743
- مساعد تخطيط المشاريع متخصص في:
744
- - تقسيم المشروع إلى مراحل ومهام وأنشطة
745
- - تحديد الموارد اللازمة لكل نشاط
746
- - إنشاء الجداول الزمنية والمسار الحرج
747
- - التخطيط للموارد البشرية والمعدات والمواد
748
- - مراقبة تقدم المشروع وإدارة التغييرات
749
- """)
750
-
751
- # عرض معلومات حقوق الملكية
752
- render_credits()
753
-
754
- def render(self):
755
- """عرض واجهة المساعد الذكي الرئيسية"""
756
- # تحميل CSS المخصص
757
- load_css()
758
-
759
- # عرض واجهة المحادثة
760
- self.render_chat_interface()
761
-
762
-
763
- # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
764
- if __name__ == "__main__":
765
- st.set_page_config(
766
- page_title="المساعد الذكي | WAHBi AI",
767
- page_icon="🤖",
768
- layout="wide",
769
- initial_sidebar_state="expanded"
770
- )
771
-
772
- assistant = AIAssistant()
773
- assistant.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/ai_assistant/ai_assistant_app.py DELETED
The diff for this file is too large to render. See raw diff
 
modules/ai_assistant/assistant.py DELETED
@@ -1,444 +0,0 @@
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/ai_assistant/assistant_app.py DELETED
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- تطبيق المساعد الذكي التفاعلي
6
- يتيح للمستخدمين التفاعل مع نماذج الذكاء الاصطناعي المتقدمة للحصول على مساعدة في تحليل العقود والمناقصات
7
- """
8
-
9
- import os
10
- import sys
11
- import streamlit as st
12
- import pandas as pd
13
- import numpy as np
14
-
15
- # إضافة مسار النظام للوصول للملفات المشتركة
16
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
17
-
18
- # استيراد مكونات المساعد الذكي
19
- from modules.ai_assistant.ai_assistant import AIAssistant
20
-
21
-
22
- class AssistantApp:
23
- """تطبيق المساعد الذكي التفاعلي"""
24
-
25
- def __init__(self):
26
- """تهيئة تطبيق المساعد الذكي"""
27
- self.assistant = AIAssistant()
28
-
29
- def render(self):
30
- """عرض واجهة المستخدم الرئيسية للتطبيق"""
31
- self.assistant.render()
32
-
33
-
34
- # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
35
- if __name__ == "__main__":
36
- st.set_page_config(
37
- page_title="المساعد الذكي | WAHBi AI",
38
- page_icon="🤖",
39
- layout="wide",
40
- initial_sidebar_state="expanded"
41
- )
42
-
43
- app = AssistantApp()
44
- app.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/ai_finetuning/__init__.py DELETED
@@ -1 +0,0 @@
1
- # ملف تهيئة حزمة ضبط نماذج الذكاء الاصطناعي
 
 
modules/ai_finetuning/finetuning_app.py DELETED
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي للمصطلحات التعاقدية المتخصصة
6
- """
7
-
8
- import os
9
- import sys
10
- import streamlit as st
11
- import pandas as pd
12
- import numpy as np
13
-
14
- # إضافة مسار النظام للوصول للملفات المشتركة
15
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
16
-
17
- # استيراد مكونات تخصيص وضبط نماذج الذكاء الاصطناعي
18
- from modules.ai_finetuning.model_finetuning import ModelFinetuning
19
-
20
-
21
- class FinetuningApp:
22
- """وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي"""
23
-
24
- def __init__(self):
25
- """تهيئة وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي"""
26
- self.model_finetuning = ModelFinetuning()
27
-
28
- def render(self):
29
- """عرض واجهة وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي"""
30
- st.markdown("<h2 class='module-title'>وحدة تخصيص وضبط نماذج الذكاء الاصطناعي</h2>", unsafe_allow_html=True)
31
-
32
- st.markdown("""
33
- <div class="module-description">
34
- تمكنك هذه الوحدة من تخصيص وضبط نماذج الذكاء الاصطناعي للتعرف بدقة على المصطلحات التعاقدية والهندسية المتخصصة باللغة العربية.
35
- يمكنك إنشاء قاموس للمصطلحات، وإعداد أمثلة التدريب، وتدريب النماذج واختبارها.
36
- </div>
37
- """, unsafe_allow_html=True)
38
-
39
- # عرض نموذج تخصيص وضبط نماذج الذكاء الاصطناعي
40
- self.model_finetuning.render()
41
-
42
-
43
- # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
44
- if __name__ == "__main__":
45
- st.set_page_config(
46
- page_title="تخصيص وضبط نماذج الذكاء الاصطناعي | WAHBi AI",
47
- page_icon="🧠",
48
- layout="wide",
49
- initial_sidebar_state="expanded"
50
- )
51
-
52
- app = FinetuningApp()
53
- app.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/ai_finetuning/model_finetuning.py DELETED
The diff for this file is too large to render. See raw diff
 
modules/data_analysis/data_analysis_app.py DELETED
@@ -1,1022 +0,0 @@
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 DELETED
@@ -1,281 +0,0 @@
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_analysis_app.py DELETED
@@ -1,1114 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- وحدة تطبيق تحليل المستندات
4
-
5
- هذا الملف يحتوي على الفئة الرئيسية لتطبيق تحليل المستندات.
6
- """
7
-
8
- # استيراد المكتبات القياسية
9
- import os
10
- import sys
11
- import logging
12
- import base64
13
- import json
14
- import time
15
- from io import BytesIO
16
- from pathlib import Path
17
- from urllib.parse import urlparse
18
- from tempfile import NamedTemporaryFile
19
-
20
- # استيراد مكتبة Streamlit
21
- import streamlit as st
22
-
23
- # استيراد المكتبات الإضافية
24
- import requests
25
- from PIL import Image
26
-
27
- try:
28
- # استيراد مكتبات Docling و MLX VLM
29
- from docling_core.types.doc import ImageRefMode
30
- from docling_core.types.doc.document import DocTagsDocument, DoclingDocument
31
- from mlx_vlm import load, generate
32
- from mlx_vlm.prompt_utils import apply_chat_template
33
- from mlx_vlm.utils import load_config, stream_generate
34
- docling_available = True
35
- except ImportError:
36
- docling_available = False
37
- logging.warning("لم يتم العثور على مكتبات Docling و MLX VLM. بعض الوظائف قد لا تعمل.")
38
-
39
- try:
40
- # استيراد مكتبة pdf2image للتعامل مع ملفات PDF
41
- from pdf2image import convert_from_path
42
- pdf_conversion_available = True
43
- except ImportError:
44
- pdf_conversion_available = False
45
- logging.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.")
46
-
47
- # إعداد المسار للوحدات النمطية
48
- current_dir = os.path.dirname(os.path.abspath(__file__))
49
- parent_dir = os.path.dirname(os.path.dirname(current_dir))
50
- if parent_dir not in sys.path:
51
- sys.path.append(parent_dir)
52
-
53
- # استيراد الخدمات باستخدام المسار النسبي
54
- try:
55
- # الطريقة 1: استيراد نسبي مباشر
56
- from .services.text_extractor import TextExtractor
57
- from .services.item_extractor import ItemExtractor
58
- from .services.document_parser import DocumentParser
59
- except ImportError:
60
- try:
61
- # الطريقة 2: استيراد مطلق
62
- from modules.document_analysis.services.text_extractor import TextExtractor
63
- from modules.document_analysis.services.item_extractor import ItemExtractor
64
- from modules.document_analysis.services.document_parser import DocumentParser
65
- except ImportError:
66
- # الطريقة 3: تعريف الفئات مباشرة كحل مؤقت
67
- logging.warning("لا يمكن استيراد خدمات تحليل المستندات. استخدام التعريفات المؤقتة.")
68
-
69
- class TextExtractor:
70
- def __init__(self, config=None):
71
- self.config = config or {}
72
-
73
- def extract_from_pdf(self, file_path):
74
- return "نص مستخرج مؤقت من PDF"
75
-
76
- def extract_from_docx(self, file_path):
77
- return "نص مستخرج مؤقت من DOCX"
78
-
79
- def extract_from_image(self, file_path):
80
- return "نص مستخرج مؤقت من صورة"
81
-
82
- def extract(self, file_path):
83
- _, ext = os.path.splitext(file_path)
84
- ext = ext.lower()
85
-
86
- if ext == '.pdf':
87
- return self.extract_from_pdf(file_path)
88
- elif ext in ('.doc', '.docx'):
89
- return self.extract_from_docx(file_path)
90
- elif ext in ('.jpg', '.jpeg', '.png'):
91
- return self.extract_from_image(file_path)
92
- else:
93
- return "نوع ملف غير مدعوم"
94
-
95
- class ItemExtractor:
96
- def __init__(self, config=None):
97
- self.config = config or {}
98
-
99
- def extract_tables(self, document):
100
- return [{"عنوان": "جدول مؤقت", "بيانات": []}]
101
-
102
- def extract(self, file_path):
103
- return [
104
- {"بند": "بند مؤقت 1", "قيمة": 1000},
105
- {"بند": "بند مؤقت 2", "قيمة": 2000},
106
- {"بند": "بند مؤقت 3", "قيمة": 3000}
107
- ]
108
-
109
- class DocumentParser:
110
- def __init__(self, config=None):
111
- self.config = config or {}
112
-
113
- def parse_document(self, file_path):
114
- return {"نوع": "مستند مؤقت", "محتوى": "محتوى مؤقت"}
115
-
116
- def parse(self, file_path):
117
- return {
118
- "نوع المستند": "مستند مؤقت",
119
- "عدد الصفحات": 5,
120
- "تاريخ التحليل": "2025-03-24",
121
- "درجة الثقة": "80%",
122
- "ملاحظات": "تحليل مؤقت للمستند"
123
- }
124
-
125
-
126
- class DoclingAnalyzer:
127
- """
128
- فئة لتحليل المستندات باستخدام نماذج Docling و MLX VLM
129
- """
130
- def __init__(self):
131
- self.model = None
132
- self.processor = None
133
- self.config = None
134
- self.docling_available = False
135
-
136
- try:
137
- # تحميل النموذج
138
- import os
139
- from mlx_vlm import load, generate
140
- from mlx_vlm.utils import load_config
141
-
142
- model_path = "ds4sd/SmolDocling-256M-preview-mlx-bf16"
143
- self.model, self.processor = load(model_path)
144
- self.config = load_config(model_path)
145
- self.docling_available = True
146
- except Exception as e:
147
- print(f"خطأ في تحميل نموذج Docling: {str(e)}")
148
- self.docling_available = False
149
-
150
- def is_available(self):
151
- """التحقق من توفر نماذج Docling"""
152
- return self.docling_available and self.model is not None
153
-
154
- def analyze_image(self, image_path=None, image_url=None, image_bytes=None, prompt="Convert this page to docling."):
155
- """
156
- تحليل صورة باستخدام نموذج Docling
157
-
158
- المعلمات:
159
- image_path (str): مسار الصورة المحلية (اختياري)
160
- image_url (str): رابط الصورة (اختياري)
161
- image_bytes (bytes): بيانات الصورة (اختياري)
162
- prompt (str): التوجيه للنموذج
163
-
164
- العوائد:
165
- dict: نتائج التحليل متضمنة النص والعلامات والمستند
166
- """
167
- if not self.is_available():
168
- return {
169
- "error": "Docling غير متوفر. يرجى تثبيت المكتبات المطلوبة."
170
- }
171
-
172
- try:
173
- from io import BytesIO
174
- from pathlib import Path
175
- from urllib.parse import urlparse
176
- import requests
177
- from PIL import Image
178
- from docling_core.types.doc import ImageRefMode
179
- from docling_core.types.doc.document import DocTagsDocument, DoclingDocument
180
- from mlx_vlm.prompt_utils import apply_chat_template
181
- from mlx_vlm.utils import stream_generate, load_image
182
-
183
- # تحميل الصورة
184
- pil_image = None
185
- image_source = None
186
-
187
- if image_url:
188
- try:
189
- response = requests.get(image_url, stream=True, timeout=10)
190
- response.raise_for_status()
191
- pil_image = Image.open(BytesIO(response.content))
192
- image_source = image_url
193
- except Exception as e:
194
- return {"error": f"فشل في تحميل الصورة من الرابط: {str(e)}"}
195
- elif image_path:
196
- try:
197
- # التأكد من وجود الملف
198
- if not Path(image_path).exists():
199
- return {"error": f"ملف الصورة غير موجود: {image_path}"}
200
- pil_image = Image.open(image_path)
201
- image_source = image_path
202
- except Exception as e:
203
- return {"error": f"فشل في فتح ملف الصورة: {str(e)}"}
204
- elif image_bytes:
205
- try:
206
- pil_image = Image.open(BytesIO(image_bytes))
207
- # حفظ الصورة مؤقتا للتحليل
208
- temp_path = "/tmp/temp_image.jpg"
209
- pil_image.save(temp_path)
210
- image_source = temp_path
211
- except Exception as e:
212
- return {"error": f"فشل في معالجة بيانات الصورة: {str(e)}"}
213
- else:
214
- return {"error": "يجب توفير مصدر للصورة (مسار، رابط، أو بيانات)"}
215
-
216
- # تطبيق قالب المحادثة
217
- formatted_prompt = apply_chat_template(self.processor, self.config, prompt, num_images=1)
218
-
219
- # إنشاء النتيجة
220
- output = ""
221
-
222
- # تمرير مسار الصورة أو عنوان URL الفعلي
223
- try:
224
- for token in stream_generate(
225
- self.model, self.processor, formatted_prompt, [image_source],
226
- max_tokens=4096, verbose=False
227
- ):
228
- output += token.text
229
- if "</doctag>" in token.text:
230
- break
231
- except Exception as e:
232
- return {"error": f"فشل في تحليل الصورة: {str(e)}"}
233
-
234
- # إنشاء مستند Docling
235
- try:
236
- doctags_doc = DocTagsDocument.from_doctags_and_image_pairs([output], [pil_image])
237
- doc = DoclingDocument(name="AnalyzedDocument")
238
- doc.load_from_doctags(doctags_doc)
239
-
240
- # إرجاع النتائج
241
- return {
242
- "doctags": output,
243
- "markdown": doc.export_to_markdown(),
244
- "document": doc,
245
- "image": pil_image
246
- }
247
- except Exception as e:
248
- return {"error": f"فشل في إنشاء مستند Docling: {str(e)}"}
249
-
250
- except Exception as e:
251
- return {"error": f"حدث خطأ غير متوقع: {str(e)}"}
252
-
253
- def export_to_html(self, doc, output_path="./output.html", show_in_browser=False):
254
- """
255
- تصدير المستند إلى HTML
256
-
257
- المعلمات:
258
- doc (DoclingDocument): مستند Docling
259
- output_path (str): مسار ملف الإخراج
260
- show_in_browser (bool): عرض الملف في المتصفح
261
-
262
- العوائد:
263
- str: مسار ملف HTML المولد
264
- """
265
- if not self.is_available():
266
- return None
267
-
268
- try:
269
- from pathlib import Path
270
- from docling_core.types.doc import ImageRefMode
271
-
272
- # إنشاء مسار الإخراج
273
- out_path = Path(output_path)
274
- # التأكد من وجود المجلد
275
- out_path.parent.mkdir(exist_ok=True, parents=True)
276
-
277
- doc.save_as_html(out_path, image_mode=ImageRefMode.EMBEDDED)
278
-
279
- # فتح في المتصفح إذا تم طلب ذلك
280
- if show_in_browser:
281
- import webbrowser
282
- webbrowser.open(f"file:///{str(out_path.resolve())}")
283
-
284
- return str(out_path)
285
- except Exception as e:
286
- print(f"خطأ في تصدير المستند إلى HTML: {str(e)}")
287
- return None
288
-
289
-
290
- class ClaudeAnalyzer:
291
- """
292
- فئة لتحليل المستندات باستخدام Claude.ai API
293
- """
294
- def __init__(self):
295
- """تهيئة محلل Claude"""
296
- self.api_url = "https://api.anthropic.com/v1/messages"
297
-
298
- def get_api_key(self):
299
- """الحصول على مفتاح API من متغيرات البيئة"""
300
- api_key = os.environ.get("anthropic")
301
- if not api_key:
302
- raise ValueError("مفتاح API لـ Claude غير موجود في متغيرات البيئة")
303
- return api_key
304
-
305
- def analyze_document(self, file_path, model_name="claude-3-7-sonnet", prompt=None):
306
- """
307
- تحليل مستند باستخدام Claude AI
308
-
309
- المعلمات:
310
- file_path: مسار الملف المراد تحليله
311
- model_name: اسم نموذج Claude المراد استخدامه
312
- prompt: التوجيه المخصص للتحليل (اختياري)
313
-
314
- العوائد:
315
- dict: نتائج التحليل
316
- """
317
- try:
318
- # الحصول على مفتاح API
319
- api_key = self.get_api_key()
320
-
321
- # تحديد التوجيه المناسب إذا لم يتم توفيره
322
- if prompt is None:
323
- _, ext = os.path.splitext(file_path)
324
- ext = ext.lower()
325
-
326
- if ext == '.pdf':
327
- prompt = "قم بتحليل هذه الصورة المستخرجة من مستند PDF واستخراج المعلومات الرئيسية مثل العناوين، الفقرات، الجداول، والنقاط المهمة."
328
- elif ext in ('.doc', '.docx'):
329
- prompt = "قم بتحليل هذه الصورة المستخرجة من مستند Word واستخراج المعلومات الرئيسية والخلاصة."
330
- elif ext in ('.jpg', '.jpeg', '.png', '.gif', '.webp'):
331
- prompt = "قم بوصف وتحليل محتوى هذه الصورة بالتفصيل، مع ذكر العناصر المهمة والنصوص والبيانات الموجودة فيها."
332
- else:
333
- prompt = "قم بتحليل محتوى هذا الملف واستخراج المعلومات المفيدة منه."
334
-
335
- # التحقق من نوع الملف وتحويله إذا لزم الأمر
336
- _, ext = os.path.splitext(file_path)
337
- ext = ext.lower()
338
-
339
- processed_file_path = file_path
340
- temp_files = [] # قائمة للملفات المؤقتة لحذفها لاحقاً
341
-
342
- # للملفات غير المدعومة مباشرة (مثل PDF)
343
- if ext not in ('.jpg', '.jpeg', '.png', '.gif', '.webp'):
344
- # إذا كان الملف PDF، حاول تحويله إلى صورة
345
- if ext == '.pdf':
346
- if not pdf_conversion_available:
347
- return {"error": "لا يمكن تحويل ملف PDF إلى صورة. يرجى تثبيت مكتبة pdf2image."}
348
-
349
- try:
350
- # تحويل الصفحة الأولى فقط
351
- images = convert_from_path(file_path, first_page=1, last_page=1)
352
- if images:
353
- # حفظ الصورة بشكل مؤقت
354
- temp_image_path = "/tmp/temp_pdf_image.jpg"
355
- images[0].save(temp_image_path, 'JPEG')
356
- processed_file_path = temp_image_path # استخدام مسار الصورة الجديد
357
- temp_files.append(temp_image_path)
358
- else:
359
- return {"error": "فشل في تحويل ملف PDF إلى صورة"}
360
- except Exception as e:
361
- return {"error": f"فشل في تحويل ملف PDF إلى صورة: {str(e)}"}
362
- else:
363
- return {"error": f"نوع الملف {ext} غير مدعوم. Claude API يدعم فقط الصور (JPEG, PNG, GIF, WebP) أو PDF (يتم تحويله تلقائياً)."}
364
-
365
- # ضغط الصورة إذا كان حجمها كبيراً
366
- try:
367
- img = Image.open(processed_file_path)
368
-
369
- # تحقق من حجم الصورة وضغطها إذا كانت كبيرة
370
- img_width, img_height = img.size
371
- if img_width > 1500 or img_height > 1500:
372
- # تحويل الصورة إلى حجم أصغر (1500×1500 بكسل كحد أقصى)
373
- img.thumbnail((1500, 1500))
374
-
375
- # حفظ الصورة المضغوطة في ملف مؤقت
376
- compressed_image_path = "/tmp/compressed_image.jpg"
377
- img.save(compressed_image_path, format="JPEG", quality=85)
378
-
379
- # إضافة الملف المؤقت إلى القائمة
380
- if processed_file_path not in temp_files:
381
- temp_files.append(compressed_image_path)
382
-
383
- processed_file_path = compressed_image_path
384
- except Exception as e:
385
- logging.warning(f"فشل في ضغط الصورة: {str(e)}. سيتم استخدام الصورة الأصلية.")
386
-
387
- # قراءة محتوى الملف المعالج
388
- with open(processed_file_path, 'rb') as f:
389
- file_content = f.read()
390
-
391
- # التحقق من حجم الملف (يجب أن يكون أقل من 20 ميجابايت)
392
- file_size_mb = len(file_content) / (1024 * 1024)
393
- if file_size_mb > 20:
394
- # محاولة ضغط الصورة أكثر إذا كان حجمها أكبر من 20 ميجابايت
395
- try:
396
- img = Image.open(processed_file_path)
397
-
398
- # ضغط أكبر - حجم أصغر وجودة أقل
399
- compressed_image_path = "/tmp/extra_compressed_image.jpg"
400
- img.thumbnail((1000, 1000))
401
- img.save(compressed_image_path, format="JPEG", quality=70)
402
-
403
- # إضافة الملف المؤقت إلى القائمة
404
- temp_files.append(compressed_image_path)
405
- processed_file_path = compressed_image_path
406
-
407
- # قراءة الملف المضغوط
408
- with open(processed_file_path, 'rb') as f:
409
- file_content = f.read()
410
-
411
- # التحقق من الحجم مرة أخرى
412
- file_size_mb = len(file_content) / (1024 * 1024)
413
- if file_size_mb > 20:
414
- # لا يزال الحجم كبيراً
415
- for temp_file in temp_files:
416
- try:
417
- os.unlink(temp_file)
418
- except:
419
- pass
420
- return {"error": f"حجم الملف كبير جدًا ({file_size_mb:.2f} ميجابايت) حتى بعد الضغط. يجب أن يكون أقل من 20 ميجابايت."}
421
- except Exception as e:
422
- for temp_file in temp_files:
423
- try:
424
- os.unlink(temp_file)
425
- except:
426
- pass
427
- return {"error": f"الملف كبير جدًا ({file_size_mb:.2f} ميجابايت) ولا يمكن ضغطه. يجب أن يكون أقل من 20 ميجابايت."}
428
-
429
- # تحديد نوع الملف المعالج (بعد التحويل إذا تم)
430
- file_type = self._get_file_type(processed_file_path)
431
-
432
- # تحويل المحتوى إلى Base64
433
- file_base64 = base64.b64encode(file_content).decode('utf-8')
434
-
435
- # إعداد البيانات للطلب
436
- headers = {
437
- "Content-Type": "application/json",
438
- "x-api-key": api_key,
439
- "anthropic-version": "2023-06-01"
440
- }
441
-
442
- # التحقق من اسم النموذج وتصحيحه إذا لزم الأمر
443
- valid_models = {
444
- "claude-3-7-sonnet": "claude-3-7-sonnet-20250219",
445
- "claude-3-5-haiku": "claude-3-5-haiku-20240307"
446
- }
447
-
448
- if model_name in valid_models:
449
- model_name = valid_models[model_name]
450
-
451
- # طباعة معلومات التصحيح
452
- logging.debug(f"إرسال طلب إلى Claude API: {model_name}, نوع الملف: {file_type}")
453
-
454
- # تحضير payload للـ API
455
- payload = {
456
- "model": model_name,
457
- "max_tokens": 4096,
458
- "messages": [
459
- {
460
- "role": "user",
461
- "content": [
462
- {"type": "text", "text": prompt},
463
- {
464
- "type": "image",
465
- "source": {
466
- "type": "base64",
467
- "media_type": file_type,
468
- "data": file_base64
469
- }
470
- }
471
- ]
472
- }
473
- ]
474
- }
475
-
476
- # إرسال الطلب إلى API مع محاولات إعادة
477
- for attempt in range(3): # ثلاث محاولات كحد أقصى
478
- try:
479
- response = requests.post(
480
- self.api_url,
481
- headers=headers,
482
- json=payload,
483
- timeout=120 # زيادة مهلة الانتظار إلى دقيقتين
484
- )
485
-
486
- # إذا نجح الطلب، نخرج من الحلقة
487
- if response.status_code == 200:
488
- break
489
-
490
- # إذا كان الخطأ 502، ننتظر ونحاول مرة أخرى
491
- if response.status_code == 502:
492
- wait_time = (attempt + 1) * 5 # انتظار 5، 10، 15 ثانية
493
- logging.warning(f"تم استلام خطأ 502. الانتظار {wait_time} ثانية قبل إعادة المحاولة.")
494
- time.sleep(wait_time)
495
- else:
496
- # إذا كان الخطأ ليس 502، نخرج من الحلقة
497
- break
498
-
499
- except requests.exceptions.RequestException as e:
500
- logging.warning(f"فشل الطلب في المحاولة {attempt+1}: {str(e)}")
501
- if attempt == 2: # آخر محاولة
502
- # حذف الملفات المؤقتة
503
- for temp_file in temp_files:
504
- try:
505
- os.unlink(temp_file)
506
- except:
507
- pass
508
- return {"error": f"فشل الاتصال بعد عدة محاولات: {str(e)}"}
509
- time.sleep((attempt + 1) * 5) # انتظار قبل إعادة المحاولة
510
-
511
- # حذف الملفات المؤقتة
512
- for temp_file in temp_files:
513
- try:
514
- os.unlink(temp_file)
515
- except:
516
- pass
517
-
518
- # التحقق من نجاح الطلب
519
- if response.status_code != 200:
520
- error_message = f"فشل طلب API: {response.status_code}"
521
- try:
522
- error_details = response.json()
523
- error_message += f"\nتفاصيل: {error_details}"
524
- except:
525
- error_message += f"\nتفاصيل: {response.text}"
526
-
527
- return {
528
- "error": error_message
529
- }
530
-
531
- # معالجة الاستجابة
532
- result = response.json()
533
-
534
- return {
535
- "success": True,
536
- "content": result["content"][0]["text"],
537
- "model": result["model"],
538
- "usage": result.get("usage", {})
539
- }
540
-
541
- except Exception as e:
542
- # حذف الملفات المؤقتة في حالة حدوث خطأ
543
- for temp_file in temp_files:
544
- try:
545
- os.unlink(temp_file)
546
- except:
547
- pass
548
-
549
- logging.error(f"خطأ أثناء تحليل المستند: {str(e)}")
550
- import traceback
551
- stack_trace = traceback.format_exc()
552
- return {"error": f"فشل في تحليل المستند: {str(e)}\n{stack_trace}"}
553
-
554
- def _get_file_type(self, file_path):
555
- """تحديد نوع الملف من امتداده"""
556
- _, ext = os.path.splitext(file_path)
557
- ext = ext.lower()
558
-
559
- # Claude API يدعم فقط أنواع الصور التالية
560
- if ext in ('.jpg', '.jpeg'):
561
- return "image/jpeg"
562
- elif ext == '.png':
563
- return "image/png"
564
- elif ext == '.gif':
565
- return "image/gif"
566
- elif ext == '.webp':
567
- return "image/webp"
568
- else:
569
- # للملفات الأخرى، نعيد نوع صورة افتراضي
570
- # هذا سيستخدم فقط إذا تم تحويل الملف إلى صورة أولاً
571
- return "image/jpeg"
572
-
573
- def get_available_models(self):
574
- """
575
- الحصول على قائمة بالنماذج المتاحة
576
-
577
- العوائد:
578
- dict: قائمة بالنماذج مع وصفها
579
- """
580
- return {
581
- "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة",
582
- "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية"
583
- }
584
-
585
- def get_model_full_name(self, short_name):
586
- """
587
- تحويل الاسم المختصر للنموذج إلى الاسم الكامل
588
-
589
- المعلمات:
590
- short_name: الاسم المختصر للنموذج
591
-
592
- العوائد:
593
- str: الاسم الكامل للنموذج
594
- """
595
- valid_models = {
596
- "claude-3-7-sonnet": "claude-3-7-sonnet-20250219",
597
- "claude-3-5-haiku": "claude-3-5-haiku-20240307"
598
- }
599
-
600
- return valid_models.get(short_name, short_name)
601
-
602
-
603
- class DocumentAnalysisApp:
604
- def __init__(self):
605
- # إنشاء كائنات الخدمات
606
- self.text_extractor = TextExtractor()
607
- self.item_extractor = ItemExtractor()
608
- self.document_parser = DocumentParser()
609
-
610
- # إنشاء محلل Docling
611
- self.docling_analyzer = DoclingAnalyzer()
612
-
613
- # إنشاء محلل Claude
614
- self.claude_analyzer = ClaudeAnalyzer()
615
-
616
- def render(self):
617
- """العرض الرئيسي للتطبيق"""
618
- st.title("تحليل المستندات")
619
- st.write("اختر ملفًا لتحليله واستخرج البيانات المطلوبة.")
620
-
621
- # إنشاء علامات تبويب للأنواع المختلفة من التحليل
622
- tabs = st.tabs(["تحليل عام", "تحليل Docling", "تحليل Claude AI"])
623
-
624
- with tabs[0]:
625
- self._render_general_analysis()
626
-
627
- with tabs[1]:
628
- self._render_docling_analysis()
629
-
630
- with tabs[2]:
631
- self._render_claude_analysis()
632
-
633
- def _render_general_analysis(self):
634
- """عرض واجهة التحليل العام"""
635
- uploaded_file = st.file_uploader("ارفع ملف PDF أو DOCX", type=["pdf", "docx"], key="general_uploader")
636
-
637
- if uploaded_file:
638
- with st.spinner("جاري تحليل المستند..."):
639
- file_path = f"/tmp/{uploaded_file.name}"
640
- with open(file_path, "wb") as f:
641
- f.write(uploaded_file.read())
642
-
643
- # تحديد نوع الملف من امتداده
644
- _, ext = os.path.splitext(file_path)
645
- ext = ext.lower()
646
-
647
- # استخراج النص حسب نوع الملف
648
- if ext == '.pdf':
649
- extracted_text = self.text_extractor.extract_from_pdf(file_path)
650
- elif ext in ('.doc', '.docx'):
651
- extracted_text = self.text_extractor.extract_from_docx(file_path)
652
- else:
653
- extracted_text = "نوع ملف غير مدعوم للنص"
654
-
655
- # عرض النص المستخرج
656
- st.subheader("النص المستخرج:")
657
- st.text_area("النص", extracted_text, height=300)
658
-
659
- # استخراج البنود
660
- extracted_items = self.item_extractor.extract(file_path)
661
- if extracted_items:
662
- st.subheader("البنود المستخرجة:")
663
- st.dataframe(extracted_items)
664
-
665
- # تحليل المستند
666
- parsed_data = self.document_parser.parse(file_path)
667
- st.subheader("تحليل المستند:")
668
- st.json(parsed_data)
669
-
670
- def _render_docling_analysis(self):
671
- """عرض واجهة تحليل Docling"""
672
- import streamlit as st
673
- from tempfile import NamedTemporaryFile
674
-
675
- if not self.docling_analyzer.is_available():
676
- st.warning("مكتبات Docling و MLX VLM غير متوفرة. يرجى تثبيت الحزم المطلوبة.")
677
- st.code("""
678
- # يرجى تثبيت الحزم التالية:
679
- pip install docling-core mlx-vlm pillow>=10.3.0 transformers>=4.49.0 tqdm>=4.66.2
680
- """)
681
- return
682
-
683
- st.subheader("تحليل الصور والمستندات باستخدام Docling")
684
-
685
- # اختيار مصدر الصورة
686
- source_option = st.radio("اختر مصدر الصورة:", ["رفع صورة", "رابط صورة"])
687
-
688
- image_path = None
689
- image_url = None
690
- image_data = None
691
-
692
- if source_option == "رفع صورة":
693
- uploaded_image = st.file_uploader("ارفع صورة", type=["jpg", "jpeg", "png"], key="docling_uploader")
694
- if uploaded_image:
695
- # حفظ الصورة المرفوعة إلى ملف مؤقت
696
- image_data = uploaded_image.read()
697
-
698
- # عرض الصورة المرفوعة
699
- st.image(image_data, caption="الصورة المرفوعة", width=400)
700
-
701
- # إنشاء ملف مؤقت لحفظ الصورة
702
- with NamedTemporaryFile(delete=False, suffix=f".{uploaded_image.name.split('.')[-1]}") as temp_file:
703
- temp_file.write(image_data)
704
- image_path = temp_file.name
705
- else:
706
- image_url = st.text_input("أدخل رابط الصورة:")
707
- if image_url:
708
- try:
709
- # عرض الصورة من الرابط
710
- st.image(image_url, caption="الصورة من الرابط", width=400)
711
- except Exception as e:
712
- st.error(f"خطأ في تحميل الصورة: {str(e)}")
713
-
714
- # توجيه للنموذج
715
- prompt = st.text_input("توجيه للنموذج:", value="Convert this page to docling.")
716
-
717
- # زر التحليل
718
- if st.button("تحليل الصورة"):
719
- if image_path or image_url:
720
- with st.spinner("جاري تحليل الصورة..."):
721
- # تحليل الصورة
722
- results = self.docling_analyzer.analyze_image(
723
- image_path=image_path,
724
- image_url=image_url,
725
- image_bytes=None, # نستخدم الملف المؤقت بدلاً من البيانات المباشرة
726
- prompt=prompt
727
- )
728
-
729
- if "error" in results:
730
- st.error(results["error"])
731
- else:
732
- # عرض النتائج
733
- with st.expander("علامات DocTags", expanded=True):
734
- st.code(results["doctags"], language="xml")
735
-
736
- with st.expander("Markdown", expanded=True):
737
- st.code(results["markdown"], language="markdown")
738
-
739
- # تصدير إلى HTML
740
- if st.button("تصدير إلى HTML"):
741
- html_path = self.docling_analyzer.export_to_html(
742
- results["document"],
743
- show_in_browser=True
744
- )
745
- if html_path:
746
- st.success(f"تم تصدير المستند إلى: {html_path}")
747
- else:
748
- st.error("فشل تصدير المستند إلى HTML")
749
-
750
- # حذف الملف المؤقت بعد الانتهاء
751
- if image_path and os.path.exists(image_path) and image_data:
752
- try:
753
- os.unlink(image_path)
754
- except:
755
- pass
756
- else:
757
- st.warning("يرجى اختيار صورة للتحليل أولاً.")
758
-
759
- def _render_claude_analysis(self):
760
- """عرض واجهة تحليل Claude AI مع توسعة البيانات المعروضة"""
761
- import time
762
-
763
- st.subheader("تحليل المستندات باستخدام Claude AI")
764
-
765
- col1, col2 = st.columns([2, 1])
766
-
767
- with col1:
768
- # إضافة اختيار النموذج
769
- claude_models = {
770
- "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة",
771
- "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية"
772
- }
773
-
774
- selected_model = st.radio(
775
- "اختر نموذج Claude",
776
- options=list(claude_models.keys()),
777
- format_func=lambda x: claude_models[x],
778
- horizontal=True
779
- )
780
-
781
- with col2:
782
- # إضافة شرح بسيط للنموذج
783
- if selected_model == "claude-3-7-sonnet":
784
- st.info("نموذج Claude 3.7 Sonnet هو أحدث نموذج ذكي يقدم تحليلاً متعمقاً للمستندات مع دقة عالية")
785
- else:
786
- st.info("نموذج Claude 3.5 Haiku أسرع في التحليل ومناسب للمهام البسيطة والاستخدام اليومي")
787
-
788
- # تخصيص التوجيه مع اقتراحات للتوجيهات المخصصة
789
- st.subheader("تخصيص التحليل")
790
-
791
- prompt_templates = {
792
- "تحليل عام": "قم بتحليل هذا المستند واستخراج جميع المعلومات المهمة.",
793
- "استخراج البيانات الأساسية": "استخرج كافة البيانات الأساسية من هذا المستند بما في ذلك الأسماء والتواريخ والأرقام والمبالغ المالية.",
794
- "تلخيص المستند": "قم بتلخيص هذا المستند بشكل مفصل مع التركيز على النقاط الرئيسية.",
795
- "تحليل العقود": "حلل هذا العقد واستخرج الأطراف والالتزامات والشروط والتواريخ المهمة.",
796
- "تحليل فواتير": "استخرج كافة المعلومات من هذه الفاتورة بما في ذلك المورد والعميل وتفاصيل المنتجات والأسعار والمبالغ الإجمالية."
797
- }
798
-
799
- prompt_type = st.selectbox(
800
- "اختر نوع التوجيه",
801
- options=list(prompt_templates.keys()),
802
- index=0
803
- )
804
-
805
- default_prompt = prompt_templates[prompt_type]
806
-
807
- custom_prompt = st.text_area(
808
- "تخصيص التوجيه للتحليل",
809
- value=default_prompt,
810
- height=100
811
- )
812
-
813
- # خيارات متقدمة
814
- with st.expander("خيارات متقدمة"):
815
- extraction_format = st.selectbox(
816
- "تنسيق استخراج البيانات",
817
- ["عام", "جداول", "قائمة", "هيكل منظم"],
818
- index=0
819
- )
820
-
821
- detail_level = st.slider(
822
- "مستوى التفاصيل",
823
- min_value=1,
824
- max_value=5,
825
- value=3,
826
- help="1: ملخص موجز، 5: تحليل تفصيلي كامل"
827
- )
828
-
829
- # تحديث التوجيه بناء على الخيارات المتقدمة
830
- if extraction_format != "عام" or detail_level != 3:
831
- custom_prompt += f"\n\nاستخدم تنسيق {extraction_format} مع مستوى تفاصيل {detail_level}/5."
832
-
833
- # رفع الملف
834
- uploaded_file = st.file_uploader(
835
- "ارفع ملفًا للتحليل",
836
- type=["pdf", "jpg", "jpeg", "png"],
837
- key="claude_uploader",
838
- help="يدعم ملفات PDF والصور. سيتم تحويل PDF إلى صور لمعالجتها."
839
- )
840
-
841
- # التحقق من وجود مفتاح API
842
- api_available = True
843
- try:
844
- self.claude_analyzer.get_api_key()
845
- except ValueError:
846
- api_available = False
847
- st.warning("مفتاح API لـ Claude غير متوفر. يرجى التأكد من تعيين متغير البيئة 'anthropic'.")
848
-
849
- # زر التحليل
850
- analyze_col1, analyze_col2 = st.columns([1, 3])
851
-
852
- with analyze_col1:
853
- analyze_button = st.button(
854
- "تحليل المستند",
855
- key="analyze_claude_btn",
856
- use_container_width=True,
857
- disabled=not (uploaded_file and api_available)
858
- )
859
-
860
- with analyze_col2:
861
- if not uploaded_file:
862
- st.info("يرجى رفع ملف للتحليل")
863
-
864
- # إجراء التحليل
865
- if uploaded_file and api_available and analyze_button:
866
- # عرض شريط التقدم
867
- progress_bar = st.progress(0, text="جاري تجهيز الملف...")
868
-
869
- with st.spinner(f"جاري التحليل باستخدام {claude_models[selected_model].split('-')[0]}..."):
870
- # حفظ الملف المرفوع إلى ملف مؤقت
871
- temp_path = f"/tmp/{uploaded_file.name}"
872
- with open(temp_path, "wb") as f:
873
- f.write(uploaded_file.getbuffer())
874
-
875
- # تحديث شريط التقدم
876
- progress_bar.progress(25, text="جاري معالجة الملف...")
877
-
878
- try:
879
- # تحليل المستند
880
- progress_bar.progress(40, text="جاري إرسال الطلب إلى Claude AI...")
881
-
882
- results = self.claude_analyzer.analyze_document(
883
- temp_path,
884
- model_name=selected_model,
885
- prompt=custom_prompt
886
- )
887
-
888
- progress_bar.progress(90, text="جاري معالجة النتائج...")
889
-
890
- if "error" in results:
891
- st.error(results["error"])
892
- else:
893
- progress_bar.progress(100, text="اكتمل التحليل!")
894
-
895
- # عرض النتائج بشكل منظم
896
- st.success(f"تم التحليل بنجاح باستخدام {results.get('model', selected_model)}!")
897
-
898
- # إضافة علامات تبويب فرعية للنتائج
899
- result_tabs = st.tabs(["التحليل الكامل", "بيانات مستخرجة", "معلومات إضافية"])
900
-
901
- with result_tabs[0]:
902
- # عرض النتائج الكاملة
903
- st.markdown("## نتائج التحليل")
904
- st.markdown(results["content"])
905
-
906
- with result_tabs[1]:
907
- # محاولة استخراج بيانات منظمة من النتائج
908
- st.markdown("## البيانات المستخرجة")
909
-
910
- # تقسيم النتائج إلى أقسام
911
- content_parts = results["content"].split("\n\n")
912
-
913
- # استخراج العناوين والبيانات الهامة
914
- headings = []
915
- key_values = {}
916
-
917
- for part in content_parts:
918
- # تحديد العناوين
919
- if part.startswith("#") or part.startswith("##") or part.startswith("###"):
920
- headings.append(part.strip())
921
- continue
922
-
923
- # محاولة استخراج أزواج المفتاح/القيمة
924
- if ":" in part and len(part.split(":")) == 2:
925
- key, value = part.split(":")
926
- key_values[key.strip()] = value.strip()
927
-
928
- # عرض العناوين
929
- if headings:
930
- st.markdown("### العناوين الرئيسية")
931
- for heading in headings[:5]: # عرض أهم 5 عناوين
932
- st.markdown(f"- {heading}")
933
-
934
- if len(headings) > 5:
935
- with st.expander(f"عرض {len(headings) - 5} عناوين إضافية"):
936
- for heading in headings[5:]:
937
- st.markdown(f"- {heading}")
938
-
939
- # عرض البيانات الهامة
940
- if key_values:
941
- st.markdown("### بيانات هامة")
942
-
943
- # تحويل البيانات إلى DataFrame
944
- import pandas as pd
945
- df = pd.DataFrame([key_values.values()], columns=key_values.keys())
946
- st.dataframe(df.T)
947
-
948
- # البحث عن الجداول في النص
949
- if "| ------ |" in results["content"] or "\n|" in results["content"]:
950
- st.markdown("### جداول مستخرجة")
951
- # استخراج الجداول من النص Markdown
952
- table_parts = []
953
- in_table = False
954
- current_table = []
955
-
956
- for line in results["content"].split("\n"):
957
- if line.startswith("|") and "-|-" in line.replace(" ", ""):
958
- in_table = True
959
- current_table.append(line)
960
- elif in_table and line.startswith("|"):
961
- current_table.append(line)
962
- elif in_table and not line.startswith("|") and line.strip():
963
- in_table = False
964
- table_parts.append("\n".join(current_table))
965
- current_table = []
966
-
967
- # إضافة الجدول الأخير إذا كان هناك
968
- if current_table:
969
- table_parts.append("\n".join(current_table))
970
-
971
- # عرض الجداول
972
- for i, table in enumerate(table_parts):
973
- st.markdown(f"#### جدول {i+1}")
974
- st.markdown(table)
975
-
976
- # إذا لم يتم العثور على أي بيانات منظمة
977
- if not headings and not key_values and not ("| ------ |" in results["content"] or "\n|" in results["content"]):
978
- st.info("لم يتم العثور على بيانات منظمة في النتائج. يمكنك تعديل التوجيه لطلب تنسيق أكثر هيكلية.")
979
-
980
- with result_tabs[2]:
981
- # عرض معلومات إضافية
982
- st.markdown("## معلومات عن التحليل")
983
-
984
- # عرض معلومات الاستخدام
985
- col1, col2 = st.columns(2)
986
-
987
- with col1:
988
- st.markdown("### معلومات النموذج")
989
- st.markdown(f"**النموذج المستخدم**: {results.get('model', selected_model)}")
990
- st.markdown(f"**تاريخ التحليل**: {time.strftime('%Y-%m-%d %H:%M:%S')}")
991
-
992
- with col2:
993
- st.markdown("### إحصائيات الاستخدام")
994
-
995
- if "usage" in results:
996
- usage = results["usage"]
997
- st.markdown(f"**توكنز المدخلات**: {usage.get('input_tokens', 'غير متوفر')}")
998
- st.markdown(f"**توكنز الإخراج**: {usage.get('output_tokens', 'غير متوفر')}")
999
- st.markdown(f"**إجمالي التوكنز**: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0)}")
1000
- else:
1001
- st.info("معلومات الاستخدام غير متوفرة")
1002
-
1003
- # إضافة خيارات التصدير
1004
- st.markdown("### تصدير النتائج")
1005
-
1006
- export_col1, export_col2 = st.columns(2)
1007
-
1008
- with export_col1:
1009
- # تصدير كنص
1010
- st.download_button(
1011
- label="تحميل النتائج كملف نصي",
1012
- data=results["content"],
1013
- file_name=f"claude_analysis_{uploaded_file.name.split('.')[0]}.txt",
1014
- mime="text/plain"
1015
- )
1016
-
1017
- with export_col2:
1018
- # تصدير كـ Markdown
1019
- st.download_button(
1020
- label="تحميل النتائج كملف Markdown",
1021
- data=results["content"],
1022
- file_name=f"claude_analysis_{uploaded_file.name.split('.')[0]}.md",
1023
- mime="text/markdown"
1024
- )
1025
- finally:
1026
- # حذف الملف المؤقت
1027
- try:
1028
- os.unlink(temp_path)
1029
- except:
1030
- pass
1031
-
1032
- def analyze_document(self, file_path):
1033
- """
1034
- تحليل مستند وإرجاع نتائج التحليل
1035
-
1036
- المعلمات:
1037
- file_path (str): مسار المستند المراد تحليله
1038
-
1039
- العوائد:
1040
- dict: نتائج تحليل المستند
1041
- """
1042
- # تحديد نوع المستند من امتداد الملف
1043
- _, ext = os.path.splitext(file_path)
1044
- ext = ext.lower()
1045
-
1046
- # تحليل المستند حسب نوعه
1047
- if ext == '.pdf':
1048
- text = self.text_extractor.extract_from_pdf(file_path)
1049
- elif ext in ('.doc', '.docx'):
1050
- text = self.text_extractor.extract_from_docx(file_path)
1051
- elif ext in ('.jpg', '.jpeg', '.png'):
1052
- # استخدام محلل Docling للصور إذا كان متاحًا
1053
- if self.docling_analyzer.is_available():
1054
- docling_results = self.docling_analyzer.analyze_image(image_path=file_path)
1055
- if "error" not in docling_results:
1056
- return {
1057
- "نص": docling_results["markdown"],
1058
- "doctags": docling_results["doctags"],
1059
- "معلومات": {
1060
- "نوع المستند": "صورة",
1061
- "تحليل": "تم تحليله باستخدام Docling"
1062
- }
1063
- }
1064
-
1065
- # استخدام المحلل العادي إذا كان Docling غير متاح
1066
- text = self.text_extractor.extract_from_image(file_path)
1067
- else:
1068
- raise ValueError(f"نوع المستند غير مدعوم: {ext}")
1069
-
1070
- # تحليل المستند
1071
- document = self.document_parser.parse_document(file_path)
1072
-
1073
- # استخراج العناصر المنظمة
1074
- tables = self.item_extractor.extract_tables(document)
1075
-
1076
- # إرجاع نتائج التحليل
1077
- return {
1078
- "نص": text,
1079
- "جداول": tables,
1080
- "معلومات": document
1081
- }
1082
-
1083
- def analyze_with_claude(self, file_path, model_name="claude-3-7-sonnet", prompt=None):
1084
- """
1085
- تحليل مستند باستخدام Claude AI
1086
-
1087
- المعلمات:
1088
- file_path (str): مسار المستند المراد تحليله
1089
- model_name (str): اسم نموذج Claude المراد استخدامه
1090
- prompt (str): التوجيه المخصص للتحليل (اختياري)
1091
-
1092
- العوائد:
1093
- dict: نتائج التحليل
1094
- """
1095
- # محاولة تحليل المستند باستخدام Claude
1096
- try:
1097
- # التحقق من وجود المفتاح
1098
- self.claude_analyzer.get_api_key()
1099
-
1100
- # تحليل المستند باستخدام Claude
1101
- return self.claude_analyzer.analyze_document(
1102
- file_path,
1103
- model_name=model_name,
1104
- prompt=prompt
1105
- )
1106
- except Exception as e:
1107
- logging.error(f"خطأ في تحليل المستند باستخدام Claude: {str(e)}")
1108
- return {"error": f"فشل في تحليل المستند باستخدام Claude: {str(e)}"}
1109
-
1110
-
1111
- # تشغيل التطبيق
1112
- if __name__ == "__main__":
1113
- app = DocumentAnalysisApp()
1114
- app.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/document_analysis/document_app.py DELETED
@@ -1,887 +0,0 @@
1
- """
2
- وحدة تحليل المستندات - التطبيق الرئيسي
3
- """
4
-
5
- # استيراد المكتبات القياسية
6
- import os
7
- import sys
8
- import logging
9
- import base64
10
- import json
11
- import time
12
- from io import BytesIO
13
- from pathlib import Path
14
- from urllib.parse import urlparse
15
- from tempfile import NamedTemporaryFile
16
-
17
- # استيراد مكتبة Streamlit
18
- import streamlit as st
19
-
20
- # استيراد المكتبات الإضافية
21
- import pandas as pd
22
- import numpy as np
23
- import matplotlib.pyplot as plt
24
- import plotly.express as px
25
- import plotly.graph_objects as go
26
- import requests
27
- from PIL import Image
28
-
29
- # محاولة استيراد خدمات تحليل المستندات
30
- try:
31
- from .services.text_extractor import TextExtractor
32
- from .services.item_extractor import ItemExtractor
33
- from .services.document_parser import DocumentParser
34
- except ImportError:
35
- try:
36
- from modules.document_analysis.services.text_extractor import TextExtractor
37
- from modules.document_analysis.services.item_extractor import ItemExtractor
38
- from modules.document_analysis.services.document_parser import DocumentParser
39
- except ImportError:
40
- # تعريف فئات وهمية في حالة عدم وجود الخدمات
41
- class TextExtractor:
42
- def __init__(self, config=None):
43
- self.config = config or {}
44
-
45
- def extract_from_pdf(self, file_path):
46
- return "نص مستخرج مؤقت من PDF"
47
-
48
- def extract_from_docx(self, file_path):
49
- return "نص مستخرج مؤقت من DOCX"
50
-
51
- def extract_from_image(self, file_path):
52
- return "نص مستخرج مؤقت من صورة"
53
-
54
- def extract(self, file_path):
55
- _, ext = os.path.splitext(file_path)
56
- ext = ext.lower()
57
-
58
- if ext == '.pdf':
59
- return self.extract_from_pdf(file_path)
60
- elif ext in ('.doc', '.docx'):
61
- return self.extract_from_docx(file_path)
62
- elif ext in ('.jpg', '.jpeg', '.png'):
63
- return self.extract_from_image(file_path)
64
- else:
65
- return "نوع ملف غير مدعوم"
66
-
67
- class ItemExtractor:
68
- def __init__(self, config=None):
69
- self.config = config or {}
70
-
71
- def extract_tables(self, document):
72
- return [{"عنوان": "جدول مؤقت", "بيانات": []}]
73
-
74
- def extract_items(self, document):
75
- return [
76
- {"رقم البند": "A1", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", "الوحدة": "م3", "الكمية": 250.0},
77
- {"رقم البند": "A2", "وصف البند": "توريد وتركيب حديد التسليح للأساسات", "الوحدة": "طن", "الكمية": 25.0},
78
- {"رقم البند": "A3", "وصف البند": "أعمال العزل المائي للأساسات", "الوحدة": "م2", "الكمية": 500.0}
79
- ]
80
-
81
- class DocumentParser:
82
- def __init__(self, config=None):
83
- self.config = config or {}
84
-
85
- def parse_document(self, file_path):
86
- return {
87
- "metadata": {
88
- "title": "مستند مؤقت",
89
- "author": "غير معروف",
90
- "date": "2024-01-01",
91
- "pages": 10
92
- },
93
- "content": "محتوى مؤقت للمستند",
94
- "tables": [],
95
- "items": []
96
- }
97
-
98
- def extract_metadata(self, file_path):
99
- return {
100
- "title": "مستند مؤقت",
101
- "author": "غير معروف",
102
- "date": "2024-01-01",
103
- "pages": 10
104
- }
105
-
106
-
107
- class DocumentAnalysisApp:
108
- """وحدة تحليل المستندات"""
109
-
110
- def __init__(self):
111
- """تهيئة وحدة تحليل المستندات"""
112
-
113
- # تهيئة خدمات تحليل المستندات
114
- self.text_extractor = TextExtractor()
115
- self.item_extractor = ItemExtractor()
116
- self.document_parser = DocumentParser()
117
-
118
- # تهيئة حالة الجلسة
119
- if 'analyzed_documents' not in st.session_state:
120
- st.session_state.analyzed_documents = []
121
-
122
- if 'extracted_items' not in st.session_state:
123
- st.session_state.extracted_items = []
124
-
125
- # إنشاء مجلد مؤقت للملفات
126
- self.temp_dir = Path("temp_documents")
127
- self.temp_dir.mkdir(exist_ok=True)
128
-
129
- def render(self):
130
- """عرض واجهة وحدة تحليل المستندات"""
131
-
132
- st.markdown("<h1 class='module-title'>وحدة تحليل المستندات</h1>", unsafe_allow_html=True)
133
-
134
- tabs = st.tabs([
135
- "تحليل المستندات",
136
- "استخراج البنود والكميات",
137
- "تحليل الصور والمخططات",
138
- "مكتبة المستندات",
139
- "الإعدادات"
140
- ])
141
-
142
- with tabs[0]:
143
- self._render_document_analysis_tab()
144
-
145
- with tabs[1]:
146
- self._render_item_extraction_tab()
147
-
148
- with tabs[2]:
149
- self._render_image_analysis_tab()
150
-
151
- with tabs[3]:
152
- self._render_document_library_tab()
153
-
154
- with tabs[4]:
155
- self._render_settings_tab()
156
-
157
- def _render_document_analysis_tab(self):
158
- """عرض تبويب تحليل المستندات"""
159
-
160
- st.markdown("### تحليل المستندات")
161
-
162
- # رفع المستند
163
- uploaded_file = st.file_uploader("رفع مستند للتحليل", type=["pdf", "docx", "txt", "jpg", "jpeg", "png"], key="document_upload")
164
-
165
- if uploaded_file is not None:
166
- # حفظ الملف مؤقتاً
167
- file_path = self._save_uploaded_file(uploaded_file)
168
-
169
- if file_path:
170
- st.success(f"تم رفع الملف: {uploaded_file.name}")
171
-
172
- # عرض معلومات الملف
173
- file_info = self._get_file_info(file_path)
174
-
175
- col1, col2, col3 = st.columns(3)
176
-
177
- with col1:
178
- st.metric("نوع الملف", file_info["type"])
179
-
180
- with col2:
181
- st.metric("حجم الملف", file_info["size"])
182
-
183
- with col3:
184
- if "pages" in file_info:
185
- st.metric("عدد الصفحات", file_info["pages"])
186
-
187
- # خيارات التحليل
188
- analysis_options = st.multiselect(
189
- "اختر خيارات التحليل",
190
- [
191
- "استخراج النص",
192
- "استخراج الجداول",
193
- "استخراج البنود والكميات",
194
- "استخراج المعلومات الرئيسية",
195
- "تحليل هيكل المستند"
196
- ],
197
- default=["استخراج النص", "استخراج البنود والكميات"],
198
- key="analysis_options"
199
- )
200
-
201
- # زر بدء التحليل
202
- if st.button("بدء التحليل", key="start_analysis_button"):
203
- with st.spinner("جاري تحليل المستند..."):
204
- # محاكاة وقت التحليل
205
- time.sleep(2)
206
-
207
- # تنفيذ التحليل المطلوب
208
- analysis_results = {}
209
-
210
- if "استخراج النص" in analysis_options:
211
- analysis_results["text"] = self.text_extractor.extract(file_path)
212
-
213
- if "استخراج الجداول" in analysis_options:
214
- # محاكاة استخراج الجداول
215
- tables = self.item_extractor.extract_tables(file_path)
216
- analysis_results["tables"] = tables
217
-
218
- if "استخراج البنود والكميات" in analysis_options:
219
- # محاكاة استخراج البنود
220
- items = self.item_extractor.extract_items(file_path)
221
- analysis_results["items"] = items
222
-
223
- # حفظ البنود المستخرجة في حالة الجلسة
224
- st.session_state.extracted_items = items
225
-
226
- if "استخراج المعلومات الرئيسية" in analysis_options:
227
- # محاكاة استخراج المعلومات الرئيسية
228
- metadata = self.document_parser.extract_metadata(file_path)
229
- analysis_results["metadata"] = metadata
230
-
231
- if "تحليل هيكل المستند" in analysis_options:
232
- # محاكاة تحليل هيكل المستند
233
- structure = {
234
- "sections": [
235
- {"title": "مقدمة", "level": 1, "page": 1},
236
- {"title": "نطاق العمل", "level": 1, "page": 2},
237
- {"title": "المواصفات الفنية", "level": 1, "page": 3},
238
- {"title": "جدول الكميات", "level": 1, "page": 5},
239
- {"title": "الشروط الخاصة", "level": 1, "page": 7}
240
- ]
241
- }
242
- analysis_results["structure"] = structure
243
-
244
- # حفظ نتائج التحليل في حالة الجلسة
245
- st.session_state.analyzed_documents.append({
246
- "file_name": uploaded_file.name,
247
- "file_path": str(file_path),
248
- "analysis_options": analysis_options,
249
- "results": analysis_results,
250
- "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
251
- })
252
-
253
- st.success("تم الانتهاء من تحليل المستند!")
254
-
255
- # عرض نتائج التحليل
256
- self._display_analysis_results(analysis_results)
257
-
258
- # عرض سجل التحليلات السابقة
259
- if st.session_state.analyzed_documents:
260
- st.markdown("### سجل التحليلات السابقة")
261
-
262
- for i, doc in enumerate(reversed(st.session_state.analyzed_documents)):
263
- with st.expander(f"{doc['file_name']} ({doc['timestamp']})"):
264
- st.markdown(f"**خيارات التحليل:** {', '.join(doc['analysis_options'])}")
265
-
266
- # عرض نتائج التحليل
267
- self._display_analysis_results(doc['results'])
268
-
269
- # أزرار العمليات
270
- col1, col2 = st.columns(2)
271
-
272
- with col1:
273
- if st.button("إرسال إلى وحدة التسعير", key=f"send_to_pricing_{i}"):
274
- st.success("تم إرسال البيانات إلى وحدة التسعير بنجاح!")
275
-
276
- with col2:
277
- if st.button("تصدير النتائج", key=f"export_results_{i}"):
278
- st.success("تم تصدير النتائج بنجاح!")
279
-
280
- def _render_item_extraction_tab(self):
281
- """عرض تبويب استخراج البنود والكميات"""
282
-
283
- st.markdown("### استخراج البنود والكميات")
284
-
285
- # التحقق من وجود بنود مستخرجة
286
- if not st.session_state.extracted_items:
287
- st.warning("لا توجد بنود مستخرجة. يرجى تحليل مستند أولاً.")
288
-
289
- # عرض بيانات افتراضية للتوضيح
290
- st.markdown("### مثال توضيحي")
291
-
292
- # بيانات افتراضية
293
- sample_items = [
294
- {"رقم البند": "A1", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", "الوحدة": "م3", "الكمية": 250.0},
295
- {"رقم البند": "A2", "وصف البند": "توريد وتركيب حديد التسليح للأساسات", "الوحدة": "طن", "الكمية": 25.0},
296
- {"رقم البند": "A3", "وصف البند": "أعمال العزل المائي للأساسات", "الوحدة": "م2", "الكمية": 500.0},
297
- {"رقم البند": "A4", "وصف البند": "أعمال الردم والدك للأساسات", "الوحدة": "م3", "الكمية": 300.0},
298
- {"رقم البند": "A5", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", "الوحدة": "م3", "الكمية": 120.0}
299
- ]
300
-
301
- # عرض البنود كجدول
302
- items_df = pd.DataFrame(sample_items)
303
- st.dataframe(items_df, use_container_width=True, hide_index=True)
304
-
305
- # زر لاستخدام البيانات التوضيحية
306
- if st.button("استخدام البيانات التوضيحية", key="use_sample_data_button"):
307
- st.session_state.extracted_items = sample_items
308
- st.success("تم استخدام البيانات التوضيحية!")
309
- st.rerun()
310
- else:
311
- # عرض البنود المستخرجة
312
- items_df = pd.DataFrame(st.session_state.extracted_items)
313
-
314
- # إضافة عمود سعر الوحدة والإجمالي إذا لم يكن موجوداً
315
- if "سعر الوحدة" not in items_df.columns:
316
- items_df["سعر الوحدة"] = 0.0
317
-
318
- if "الإجمالي" not in items_df.columns:
319
- items_df["الإجمالي"] = 0.0
320
-
321
- # عرض البنود كجدول قابل للتعديل
322
- st.markdown("### البنود المستخرجة")
323
- edited_df = st.data_editor(items_df, use_container_width=True, hide_index=True, key="items_editor")
324
-
325
- # تحديث البنود المستخرجة بعد التعديل
326
- st.session_state.extracted_items = edited_df.to_dict('records')
327
-
328
- # أزرار العمليات
329
- col1, col2, col3 = st.columns(3)
330
-
331
- with col1:
332
- if st.button("إرسال إلى وحدة التسعير", key="send_to_pricing_button"):
333
- # محاكاة إرسال البيانات إلى وحدة التسعير
334
- if 'current_pricing' not in st.session_state:
335
- st.session_state.current_pricing = {
336
- 'name': "مناقصة جديدة",
337
- 'number': "T-" + time.strftime("%Y-%m-%d"),
338
- 'client': "",
339
- 'location': "",
340
- 'method': "التسعير القياسي",
341
- 'submission_date': None,
342
- 'items': edited_df,
343
- 'status': 'جديد',
344
- 'created_at': time.strftime("%Y-%m-%d %H:%M:%S")
345
- }
346
- else:
347
- st.session_state.current_pricing['items'] = edited_df
348
-
349
- st.success("تم إرسال البنود إلى وحدة التسعير بنجاح!")
350
-
351
- with col2:
352
- if st.button("تصدير إلى Excel", key="export_to_excel_button"):
353
- st.success("تم تصدير البنود إلى Excel بنجاح!")
354
-
355
- with col3:
356
- if st.button("مسح البنود", key="clear_items_button"):
357
- st.session_state.extracted_items = []
358
- st.warning("تم مسح البنود!")
359
- st.rerun()
360
-
361
- # عرض إحصائيات البنود
362
- st.markdown("### إحصائيات البنود")
363
-
364
- col1, col2, col3 = st.columns(3)
365
-
366
- with col1:
367
- st.metric("عدد البنود", len(edited_df))
368
-
369
- with col2:
370
- units_count = edited_df['الوحدة'].value_counts()
371
- most_common_unit = units_count.index[0] if not units_count.empty else "غير متوفر"
372
- st.metric("الوحدة الأكثر استخداماً", most_common_unit)
373
-
374
- with col3:
375
- total_quantity = edited_df['الكمية'].sum()
376
- st.metric("إجمالي الكميات", f"{total_quantity:,.2f}")
377
-
378
- # رسم بياني لتوزيع البنود حسب الوحدة
379
- st.markdown("### توزيع البنود حسب الوحدة")
380
-
381
- units_df = pd.DataFrame(units_count).reset_index()
382
- units_df.columns = ['الوحدة', 'العدد']
383
-
384
- fig = px.pie(
385
- units_df,
386
- values='العدد',
387
- names='الوحدة',
388
- title='توزيع البنود حسب الوحدة',
389
- hole=0.4
390
- )
391
-
392
- st.plotly_chart(fig, use_container_width=True)
393
-
394
- def _render_image_analysis_tab(self):
395
- """عرض تبويب تحليل الصور والمخططات"""
396
-
397
- st.markdown("### تحليل الصور والمخططات")
398
-
399
- # رفع الصورة
400
- uploaded_image = st.file_uploader("رفع صورة أو مخطط للتحليل", type=["jpg", "jpeg", "png", "tif", "tiff"], key="image_upload")
401
-
402
- if uploaded_image is not None:
403
- # عرض الصورة
404
- image = Image.open(uploaded_image)
405
- st.image(image, caption=uploaded_image.name, use_column_width=True)
406
-
407
- # خيارات التحليل
408
- analysis_type = st.selectbox(
409
- "نوع التحليل",
410
- [
411
- "استخراج النص من الصورة",
412
- "تحليل المخططات الهندسية",
413
- "قياس المساحات والأبعاد",
414
- "تحليل مخصص"
415
- ],
416
- key="image_analysis_type"
417
- )
418
-
419
- # زر بدء التحليل
420
- if st.button("بدء التحليل", key="start_image_analysis_button"):
421
- with st.spinner("جاري تحليل الصورة..."):
422
- # محاكاة وقت التحليل
423
- time.sleep(2)
424
-
425
- if analysis_type == "استخراج النص من الصورة":
426
- # محاكاة استخراج النص
427
- extracted_text = "نص مستخرج من الصورة (محاكاة):\n\n"
428
- extracted_text += "مواصفات المشروع:\n"
429
- extracted_text += "- مساحة الأرض: 1000 م2\n"
430
- extracted_text += "- عدد الطوابق: 3\n"
431
- extracted_text += "- ارتفاع المبنى: 12 م\n"
432
-
433
- st.markdown("### النص المستخرج من الصورة")
434
- st.text_area("النص المستخرج", extracted_text, height=200)
435
-
436
- elif analysis_type == "تحليل المخططات الهندسية":
437
- # محاكاة تحليل المخططات
438
- st.markdown("### نتائج تحليل المخطط الهندسي")
439
-
440
- # محاكاة رسم تخطيطي للمخطط
441
- fig, ax = plt.subplots(figsize=(10, 6))
442
- ax.imshow(image)
443
-
444
- # إضافة تعليقات توضيحية
445
- ax.annotate('غرفة المعيشة', xy=(100, 100), xytext=(150, 50),
446
- arrowprops=dict(facecolor='red', shrink=0.05))
447
-
448
- ax.annotate('المطبخ', xy=(300, 150), xytext=(350, 100),
449
- arrowprops=dict(facecolor='blue', shrink=0.05))
450
-
451
- ax.annotate('غرفة النوم', xy=(200, 300), xytext=(250, 350),
452
- arrowprops=dict(facecolor='green', shrink=0.05))
453
-
454
- st.pyplot(fig)
455
-
456
- # عرض معلومات المخطط
457
- st.markdown("### معلومات المخطط")
458
-
459
- col1, col2, col3 = st.columns(3)
460
-
461
- with col1:
462
- st.metric("المساحة الإجمالية", "150 م2")
463
-
464
- with col2:
465
- st.metric("عدد الغرف", "3")
466
-
467
- with col3:
468
- st.metric("عدد الحمامات", "2")
469
-
470
- elif analysis_type == "قياس المساحات والأبعاد":
471
- # محاكاة قياس المساحات
472
- st.markdown("### نتائج قياس المساحات والأبعاد")
473
-
474
- # محاكاة رسم تخطيطي للمساحات
475
- fig, ax = plt.subplots(figsize=(10, 6))
476
- ax.imshow(image)
477
-
478
- # إضافة قياسات
479
- ax.plot([50, 250], [50, 50], 'r-', linewidth=2)
480
- ax.text(150, 40, '10 م', color='red', fontsize=12, ha='center')
481
-
482
- ax.plot([50, 50], [50, 250], 'b-', linewidth=2)
483
- ax.text(40, 150, '8 م', color='blue', fontsize=12, va='center', rotation=90)
484
-
485
- st.pyplot(fig)
486
-
487
- # عرض جدول القياسات
488
- measurements = pd.DataFrame({
489
- 'العنصر': ['الطول', 'العرض', 'المساحة', 'المحيط'],
490
- 'القيمة': ['10 م', '8 م', '80 م2', '36 م']
491
- })
492
-
493
- st.dataframe(measurements, use_container_width=True, hide_index=True)
494
-
495
- else: # تحليل مخصص
496
- st.markdown("### نتائج التحليل المخصص")
497
- st.info("تم تحليل الصورة بنجاح. يمكنك تخصيص التحليل حسب احتياجاتك.")
498
-
499
- # خيارات التصدير
500
- col1, col2 = st.columns(2)
501
-
502
- with col1:
503
- if st.button("تصدير نتائج التحليل", key="export_image_analysis_button"):
504
- st.success("تم تصدير نتائج التحليل بنجاح!")
505
-
506
- with col2:
507
- if st.button("إرسال إلى وحدة التسعير", key="send_image_to_pricing_button"):
508
- st.success("تم إرسال نت��ئج التحليل إلى وحدة التسعير بنجاح!")
509
-
510
- def _render_document_library_tab(self):
511
- """عرض تبويب مكتبة المستندات"""
512
-
513
- st.markdown("### مكتبة المستندات")
514
-
515
- # بيانات افتراضية للمستندات
516
- if 'document_library' not in st.session_state:
517
- st.session_state.document_library = [
518
- {
519
- "id": 1,
520
- "name": "كراسة شروط مشروع توسعة مستشفى الملك فهد",
521
- "type": "PDF",
522
- "size": "5.2 MB",
523
- "pages": 120,
524
- "upload_date": "2024-01-15",
525
- "category": "كراسات الشروط",
526
- "tags": ["صحي", "مستشفى", "توسعة"]
527
- },
528
- {
529
- "id": 2,
530
- "name": "جدول كميات صيانة محطات المياه",
531
- "type": "Excel",
532
- "size": "1.8 MB",
533
- "pages": None,
534
- "upload_date": "2024-02-10",
535
- "category": "جداول الكميات",
536
- "tags": ["مياه", "صيانة", "محطات"]
537
- },
538
- {
539
- "id": 3,
540
- "name": "مخططات إنشاء مدرسة ثانوية",
541
- "type": "PDF",
542
- "size": "12.5 MB",
543
- "pages": 45,
544
- "upload_date": "2024-02-25",
545
- "category": "مخططات",
546
- "tags": ["تعليم", "مدرسة", "إنشاء"]
547
- },
548
- {
549
- "id": 4,
550
- "name": "عقد إنشاء طريق دائري",
551
- "type": "Word",
552
- "size": "0.9 MB",
553
- "pages": 28,
554
- "upload_date": "2024-03-05",
555
- "category": "عقود",
556
- "tags": ["طرق", "إنشاء", "دائري"]
557
- },
558
- {
559
- "id": 5,
560
- "name": "تقرير فني لمشروع تطوير شبكة مياه",
561
- "type": "PDF",
562
- "size": "3.7 MB",
563
- "pages": 65,
564
- "upload_date": "2024-03-15",
565
- "category": "تقارير فنية",
566
- "tags": ["مياه", "شبكة", "تطوير"]
567
- }
568
- ]
569
-
570
- # البحث في المكتبة
571
- search_query = st.text_input("البحث في المكتبة", key="library_search")
572
-
573
- col1, col2, col3 = st.columns(3)
574
-
575
- with col1:
576
- category_filter = st.selectbox(
577
- "تصفية حسب الفئة",
578
- ["الكل", "كراسات الشروط", "جداول الكميات", "مخططات", "عقود", "تقارير فنية"],
579
- key="category_filter"
580
- )
581
-
582
- with col2:
583
- type_filter = st.selectbox(
584
- "تصفية حسب النوع",
585
- ["الكل", "PDF", "Word", "Excel", "Image"],
586
- key="type_filter"
587
- )
588
-
589
- with col3:
590
- sort_by = st.selectbox(
591
- "ترتيب حسب",
592
- ["تاريخ الرفع (الأحدث أولاً)", "تاريخ الرفع (الأقدم أولاً)", "الاسم (أ-ي)", "الاسم (ي-أ)", "الحجم (الأكبر أولاً)", "الحجم (الأصغر أولاً)"],
593
- key="sort_by"
594
- )
595
-
596
- # تطبيق التصفية والبحث
597
- filtered_documents = st.session_state.document_library.copy()
598
-
599
- # تطبيق البحث
600
- if search_query:
601
- filtered_documents = [doc for doc in filtered_documents if search_query.lower() in doc["name"].lower() or
602
- any(search_query.lower() in tag.lower() for tag in doc["tags"])]
603
-
604
- # تطبيق تصفية الفئة
605
- if category_filter != "الكل":
606
- filtered_documents = [doc for doc in filtered_documents if doc["category"] == category_filter]
607
-
608
- # تطبيق تصفية النوع
609
- if type_filter != "الكل":
610
- filtered_documents = [doc for doc in filtered_documents if doc["type"] == type_filter]
611
-
612
- # تطبيق الترتيب
613
- if sort_by == "تاريخ الرفع (الأحدث أولاً)":
614
- filtered_documents.sort(key=lambda x: x["upload_date"], reverse=True)
615
- elif sort_by == "تاريخ الرفع (الأقدم أولاً)":
616
- filtered_documents.sort(key=lambda x: x["upload_date"])
617
- elif sort_by == "الاسم (أ-ي)":
618
- filtered_documents.sort(key=lambda x: x["name"])
619
- elif sort_by == "الاسم (ي-أ)":
620
- filtered_documents.sort(key=lambda x: x["name"], reverse=True)
621
- elif sort_by == "الحجم (الأكبر أولاً)":
622
- filtered_documents.sort(key=lambda x: float(x["size"].split()[0]), reverse=True)
623
- elif sort_by == "الحجم (الأصغر أولاً)":
624
- filtered_documents.sort(key=lambda x: float(x["size"].split()[0]))
625
-
626
- # عرض المستندات
627
- st.markdown(f"### المستندات ({len(filtered_documents)})")
628
-
629
- if not filtered_documents:
630
- st.info("لا توجد مستندات تطابق معايير البحث.")
631
- else:
632
- # عرض المستندات كبطاقات
633
- for i, doc in enumerate(filtered_documents):
634
- with st.container():
635
- col1, col2, col3 = st.columns([3, 1, 1])
636
-
637
- with col1:
638
- st.markdown(f"**{doc['name']}**")
639
- st.markdown(f"الفئة: {doc['category']} | النوع: {doc['type']} | الحجم: {doc['size']} | تاريخ الرفع: {doc['upload_date']}")
640
- st.markdown(f"الوسوم: {', '.join(doc['tags'])}")
641
-
642
- with col2:
643
- if st.button("عرض", key=f"view_doc_{i}"):
644
- st.session_state.selected_document = doc
645
- st.success(f"جاري عرض المستند: {doc['name']}")
646
-
647
- with col3:
648
- if st.button("تحليل", key=f"analyze_doc_{i}"):
649
- st.session_state.selected_document = doc
650
- st.success(f"جاري تحليل المستند: {doc['name']}")
651
-
652
- st.markdown("---")
653
-
654
- # رفع مستند جديد
655
- st.markdown("### رفع مستند جديد")
656
-
657
- uploaded_file = st.file_uploader("اختر ملفاً للرفع", type=["pdf", "docx", "xlsx", "jpg", "jpeg", "png"], key="library_upload")
658
-
659
- if uploaded_file is not None:
660
- col1, col2 = st.columns(2)
661
-
662
- with col1:
663
- doc_category = st.selectbox(
664
- "فئة المستند",
665
- ["كراسات الشروط", "جداول الكميات", "مخططات", "عقود", "تقارير فنية", "أخرى"],
666
- key="doc_category"
667
- )
668
-
669
- with col2:
670
- doc_tags = st.text_input("الوسوم (مفصولة بفواصل)", key="doc_tags")
671
-
672
- if st.button("رفع المستند", key="upload_to_library_button"):
673
- # محاكاة رفع المستند
674
- new_doc = {
675
- "id": len(st.session_state.document_library) + 1,
676
- "name": uploaded_file.name,
677
- "type": uploaded_file.name.split(".")[-1].upper(),
678
- "size": f"{uploaded_file.size / (1024 * 1024):.1f} MB",
679
- "pages": None,
680
- "upload_date": time.strftime("%Y-%m-%d"),
681
- "category": doc_category,
682
- "tags": [tag.strip() for tag in doc_tags.split(",") if tag.strip()]
683
- }
684
-
685
- st.session_state.document_library.append(new_doc)
686
- st.success(f"تم رفع المستند: {uploaded_file.name}")
687
- st.rerun()
688
-
689
- def _render_settings_tab(self):
690
- """عرض تبويب الإعدادات"""
691
-
692
- st.markdown("### إعدادات تحليل المستندات")
693
-
694
- # إعدادات استخراج النص
695
- with st.expander("إعدادات استخراج النص", expanded=True):
696
- st.markdown("#### إعدادات استخراج النص")
697
-
698
- ocr_engine = st.selectbox(
699
- "محرك التعرف الضوئي على النصوص",
700
- ["Tesseract OCR", "Google Cloud Vision", "Amazon Textract", "Microsoft Azure OCR"],
701
- index=0,
702
- key="ocr_engine"
703
- )
704
-
705
- language = st.selectbox(
706
- "لغة المستندات",
707
- ["العربية", "الإنجليزية", "العربية والإنجليزية"],
708
- index=0,
709
- key="ocr_language"
710
- )
711
-
712
- dpi = st.slider(
713
- "دقة المسح (DPI)",
714
- min_value=100,
715
- max_value=600,
716
- value=300,
717
- step=50,
718
- key="ocr_dpi"
719
- )
720
-
721
- if st.button("حفظ إعدادات استخراج النص", key="save_ocr_settings"):
722
- st.success("تم حفظ إعدادات استخراج النص بنجاح!")
723
-
724
- # إعدادات استخراج البنود
725
- with st.expander("إعدادات استخراج البنود", expanded=True):
726
- st.markdown("#### إعدادات استخراج البنود")
727
-
728
- extraction_method = st.selectbox(
729
- "طريقة استخراج البنود",
730
- ["تحليل الجداول", "تحليل النص", "الذكاء الاصطناعي", "مزيج"],
731
- index=3,
732
- key="extraction_method"
733
- )
734
-
735
- auto_detect_units = st.checkbox(
736
- "اكتشاف الوحدات تلقائياً",
737
- value=True,
738
- key="auto_detect_units"
739
- )
740
-
741
- normalize_quantities = st.checkbox(
742
- "توحيد صيغة الكميات",
743
- value=True,
744
- key="normalize_quantities"
745
- )
746
-
747
- if st.button("حفظ إعدادات استخراج البنود", key="save_extraction_settings"):
748
- st.success("تم حفظ إعدادات استخراج البنود بنجاح!")
749
-
750
- # إعدادات تحليل الصور
751
- with st.expander("إعدادات تحليل الصور", expanded=True):
752
- st.markdown("#### إعدادات تحليل الصور")
753
-
754
- image_analysis_engine = st.selectbox(
755
- "محرك تحليل الصور",
756
- ["OpenCV", "Google Cloud Vision", "Amazon Rekognition", "Microsoft Azure Computer Vision"],
757
- index=0,
758
- key="image_analysis_engine"
759
- )
760
-
761
- image_resolution = st.slider(
762
- "دقة تحليل الصور",
763
- min_value=1,
764
- max_value=10,
765
- value=5,
766
- key="image_resolution"
767
- )
768
-
769
- if st.button("حفظ إعدادات تحليل الصور", key="save_image_analysis_settings"):
770
- st.success("تم حفظ إعدادات تحليل الصور بنجاح!")
771
-
772
- # إعدادات متقدمة
773
- with st.expander("إعدادات متقدمة", expanded=False):
774
- st.markdown("#### إعدادات متقدمة")
775
-
776
- temp_files_retention = st.slider(
777
- "مدة الاحتفاظ بالملفات المؤقتة (أيام)",
778
- min_value=1,
779
- max_value=30,
780
- value=7,
781
- key="temp_files_retention"
782
- )
783
-
784
- max_file_size = st.slider(
785
- "الحد الأقصى لحجم الملف (ميجابايت)",
786
- min_value=5,
787
- max_value=100,
788
- value=50,
789
- key="max_file_size"
790
- )
791
-
792
- parallel_processing = st.checkbox(
793
- "تفعيل المعالجة المتوازية",
794
- value=True,
795
- key="parallel_processing"
796
- )
797
-
798
- if st.button("حفظ الإعدادات المتقدمة", key="save_advanced_settings"):
799
- st.success("تم حفظ الإعدادات المتقدمة بنجاح!")
800
-
801
- def _save_uploaded_file(self, uploaded_file):
802
- """حفظ الملف المرفوع في مجلد مؤقت"""
803
- try:
804
- file_path = self.temp_dir / uploaded_file.name
805
- with open(file_path, "wb") as f:
806
- f.write(uploaded_file.getbuffer())
807
- return file_path
808
- except Exception as e:
809
- st.error(f"حدث خطأ أثناء حفظ الملف: {str(e)}")
810
- return None
811
-
812
- def _get_file_info(self, file_path):
813
- """الحصول على معلومات الملف"""
814
- file_info = {
815
- "type": file_path.suffix[1:].upper(),
816
- "size": f"{file_path.stat().st_size / (1024 * 1024):.2f} MB"
817
- }
818
-
819
- # محاولة الحصول على عدد الصفحات للملفات المدعومة
820
- if file_path.suffix.lower() == ".pdf":
821
- # محاكاة عدد الصفحات
822
- file_info["pages"] = 10
823
-
824
- return file_info
825
-
826
- def _display_analysis_results(self, results):
827
- """عرض نتائج التحليل"""
828
-
829
- if not results:
830
- st.info("لا توجد نتائج للعرض.")
831
- return
832
-
833
- # عرض النص المستخرج
834
- if "text" in results:
835
- with st.expander("النص المستخرج", expanded=False):
836
- st.text_area("النص", results["text"], height=200)
837
-
838
- # عرض الجداول المستخرجة
839
- if "tables" in results and results["tables"]:
840
- with st.expander("الجداول المستخرجة", expanded=True):
841
- for i, table in enumerate(results["tables"]):
842
- st.markdown(f"**جدول {i+1}: {table.get('عنوان', 'بدون عنوان')}**")
843
-
844
- if "بيانات" in table and table["بيانات"]:
845
- # محاولة عرض البيانات كجدول
846
- try:
847
- df = pd.DataFrame(table["بيانات"])
848
- st.dataframe(df, use_container_width=True, hide_index=True)
849
- except Exception:
850
- st.text(str(table["بيانات"]))
851
- else:
852
- st.info("لا توجد بيانات في هذا الجدول.")
853
-
854
- # عرض البنود المستخرجة
855
- if "items" in results and results["items"]:
856
- with st.expander("البنود المستخرجة", expanded=True):
857
- items_df = pd.DataFrame(results["items"])
858
- st.dataframe(items_df, use_container_width=True, hide_index=True)
859
-
860
- # زر لإرسال البنود إلى وحدة التسعير
861
- if st.button("إرسال البنود إلى وحدة التسعير", key="send_extracted_items_button"):
862
- st.session_state.extracted_items = results["items"]
863
- st.success("تم إرسال البنود المستخرجة إلى وحدة التسعير!")
864
-
865
- # عرض المعلومات الرئيسية
866
- if "metadata" in results:
867
- with st.expander("المعلومات الرئيسية", expanded=True):
868
- metadata = results["metadata"]
869
-
870
- col1, col2 = st.columns(2)
871
-
872
- with col1:
873
- st.markdown(f"**عنوان المستند:** {metadata.get('title', 'غير متوفر')}")
874
- st.markdown(f"**المؤلف:** {metadata.get('author', 'غير متوفر')}")
875
-
876
- with col2:
877
- st.markdown(f"**التاريخ:** {metadata.get('date', 'غير متوفر')}")
878
- st.markdown(f"**عدد الصفحات:** {metadata.get('pages', 'غير متوفر')}")
879
-
880
- # عرض هيكل المستند
881
- if "structure" in results and "sections" in results["structure"]:
882
- with st.expander("هيكل المستند", expanded=False):
883
- sections = results["structure"]["sections"]
884
-
885
- for section in sections:
886
- indent = "&nbsp;" * (section["level"] * 4)
887
- st.markdown(f"{indent}• **{section['title']}** (صفحة {section['page']})", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/document_analysis/services/__init__.py DELETED
@@ -1,22 +0,0 @@
1
- """
2
- حزمة خدمات تحليل المستندات
3
-
4
- توفر هذه الحزمة الأدوات والخدمات اللازمة لتحليل المستندات بمختلف أنواعها
5
- واستخراج النصوص والبيانات المنظمة منها.
6
- """
7
-
8
- # استيراد الفئات الرئيسية
9
- from .text_extractor import TextExtractor
10
- from .item_extractor import ItemExtractor
11
- from .document_parser import DocumentParser
12
-
13
- # تحديد الفئات التي يمكن استيرادها عند استخدام from services import *
14
- __all__ = [
15
- 'TextExtractor',
16
- 'ItemExtractor',
17
- 'DocumentParser',
18
- ]
19
-
20
- # معلومات الإصدار
21
- __version__ = '0.1.0'
22
- __author__ = 'فريق تطوير تحليل المستندات'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/document_analysis/services/document_parser.py DELETED
@@ -1,219 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- خدمة تحليل المستندات
4
-
5
- هذا الملف يحتوي على الفئة المسؤولة عن تحليل المستندات واستخراج المعلومات الهيكلية منها.
6
- """
7
-
8
- import os
9
- import logging
10
- import datetime
11
-
12
- class DocumentParser:
13
- """فئة تحليل المستندات واستخراج المعلومات منها"""
14
-
15
- def __init__(self, config=None):
16
- """
17
- تهيئة محلل المستندات
18
-
19
- المعلمات:
20
- config (dict): إعدادات محلل المستندات
21
- """
22
- self.config = config or {}
23
- self.logger = logging.getLogger(__name__)
24
-
25
- def parse(self, file_path):
26
- """
27
- تحليل المستند واستخراج المعلومات منه
28
-
29
- المعلمات:
30
- file_path (str): مسار الملف
31
-
32
- العوائد:
33
- dict: معلومات المستند المستخرجة
34
- """
35
- self.logger.info(f"جاري تحليل المستند: {file_path}")
36
-
37
- try:
38
- # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
39
- # محاكاة التحليل للعرض
40
- file_name = os.path.basename(file_path)
41
- file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
42
-
43
- # تحديد نوع الملف
44
- _, ext = os.path.splitext(file_path)
45
- ext = ext.lower()
46
-
47
- # تحديد نوع المستند
48
- document_type = self._get_document_type(ext)
49
-
50
- # محاكاة معلومات المستند
51
- current_date = datetime.datetime.now().strftime("%Y-%m-%d")
52
-
53
- result = {
54
- "اسم الملف": file_name,
55
- "حجم الملف": f"{file_size / 1024:.2f} كيلوبايت",
56
- "نوع الملف": document_type,
57
- "تاريخ التحليل": current_date,
58
- "تقدير عدد الصفحات": self._estimate_pages(file_size),
59
- "نتائج التحليل": {
60
- "نوع المستند": self._classify_document(file_name),
61
- "درجة الثقة": "85%",
62
- "الأقسام الرئيسية": self._get_main_sections(),
63
- "الكلمات الرئيسية": self._get_main_keywords(),
64
- "الشروط الهامة": self._get_important_terms()
65
- }
66
- }
67
-
68
- return result
69
- except Exception as e:
70
- self.logger.error(f"خطأ في تحليل المستند: {str(e)}")
71
- return {"خطأ": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
72
-
73
- def parse_document(self, file_path):
74
- """
75
- تحليل المستند واستخراج المعلومات الأساسية منه
76
-
77
- المعلمات:
78
- file_path (str): مسار الملف
79
-
80
- العوائد:
81
- dict: معلومات المستند الأساسية
82
- """
83
- self.logger.info(f"جاري تحليل المستند الأساسي: {file_path}")
84
-
85
- # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
86
- # محاكاة التحليل للعرض
87
- file_name = os.path.basename(file_path)
88
-
89
- return {
90
- "نوع": self._classify_document(file_name),
91
- "محتوى": "محتوى المستند...",
92
- "هيكل": {
93
- "عنوان": "عنوان المستند",
94
- "أقسام": ["قسم 1", "قسم 2", "قسم 3"]
95
- }
96
- }
97
-
98
- def _get_document_type(self, ext):
99
- """
100
- تحديد نوع المستند من امتداد الملف
101
-
102
- المعلمات:
103
- ext (str): امتداد الملف
104
-
105
- العوائد:
106
- str: نوع المستند
107
- """
108
- document_types = {
109
- '.pdf': 'مستند PDF',
110
- '.doc': 'مستند Word',
111
- '.docx': 'مستند Word',
112
- '.jpg': 'صورة JPEG',
113
- '.jpeg': 'صورة JPEG',
114
- '.png': 'صورة PNG',
115
- '.xlsx': 'جدول Excel',
116
- '.xls': 'جدول Excel',
117
- '.txt': 'ملف نصي'
118
- }
119
-
120
- return document_types.get(ext, 'نوع ملف غير معروف')
121
-
122
- def _estimate_pages(self, file_size):
123
- """
124
- تقدير عدد صفحات المستند بناءً على حجمه
125
-
126
- المعلمات:
127
- file_size (int): حجم الملف بالبايت
128
-
129
- العوائد:
130
- int: تقدير عدد الصفحات
131
- """
132
- # تقدير بسيط: كل 50 كيلوبايت تقريباً صفحة واحدة
133
- # هذا تقدير بسيط جداً ويختلف حسب نوع المستند ومحتواه
134
- return max(1, int(file_size / (50 * 1024)))
135
-
136
- def _classify_document(self, file_name):
137
- """
138
- تصنيف نوع المستند بناءً على اسمه
139
-
140
- المعلمات:
141
- file_name (str): اسم الملف
142
-
143
- العوائد:
144
- str: تصنيف المستند
145
- """
146
- file_name_lower = file_name.lower()
147
-
148
- if 'عقد' in file_name_lower or 'contract' in file_name_lower:
149
- return "عقد"
150
- elif 'مناقصة' in file_name_lower or 'tender' in file_name_lower:
151
- return "مستند مناقصة"
152
- elif 'تقرير' in file_name_lower or 'report' in file_name_lower:
153
- return "تقرير"
154
- elif 'فاتورة' in file_name_lower or 'invoice' in file_name_lower:
155
- return "فاتورة"
156
- elif 'عرض' in file_name_lower or 'proposal' in file_name_lower:
157
- return "عرض سعر"
158
- elif 'مواصفات' in file_name_lower or 'spec' in file_name_lower:
159
- return "مواصفات فنية"
160
- elif 'كراسة' in file_name_lower or 'شروط' in file_name_lower:
161
- return "كراسة شروط"
162
- else:
163
- return "مستند عام"
164
-
165
- def _get_main_sections(self):
166
- """
167
- الحصول على قائمة الأقسام الرئيسية التقديرية للمستند
168
-
169
- العوائد:
170
- list: قائمة الأقسام الرئيسية
171
- """
172
- # محاكاة قائمة الأقسام
173
- return [
174
- "مقدمة",
175
- "نطاق العمل",
176
- "المواصفات الفنية",
177
- "جدول الكميات",
178
- "الشروط والأحكام",
179
- "الجدول الزمني",
180
- "المتطلبات الخاصة"
181
- ]
182
-
183
- def _get_main_keywords(self):
184
- """
185
- الحصول على قائمة الكلمات الرئيسية التقديرية للمستند
186
-
187
- العوائد:
188
- list: قائمة الكلمات الرئيسية
189
- """
190
- # محاكاة قائمة الكلمات الرئيسية
191
- return [
192
- "مناقصة",
193
- "بناء",
194
- "تشييد",
195
- "تسليم مفتاح",
196
- "مواصفات فنية",
197
- "جدول كميات",
198
- "ضمان",
199
- "غرامة تأخير",
200
- "دفعة مقدمة",
201
- "محتوى محلي"
202
- ]
203
-
204
- def _get_important_terms(self):
205
- """
206
- الحصول على قائمة الشروط الهامة التقديرية للمستند
207
-
208
- العوائد:
209
- list: قائمة الشروط الهامة
210
- """
211
- # محاكاة قائمة الشروط الهامة
212
- return [
213
- "مدة تنفيذ المشروع: 18 شهر",
214
- "غرامة التأخير: 0.5% أسبوعياً بحد أقصى 10%",
215
- "الدفعة المقدمة: 10%",
216
- "الضمان النهائي: 5% لمدة سنة",
217
- "شروط الدفع: دفعات شهرية حسب نسبة الإنجاز",
218
- "المحتوى المحلي: 70% كحد أدنى"
219
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/document_analysis/services/item_extractor.py DELETED
@@ -1,131 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- خدمة استخراج البنود من المستندات
4
-
5
- هذا الملف يحتوي على الفئة المسؤولة عن استخراج البنود والجداول من المستندات.
6
- """
7
-
8
- import os
9
- import logging
10
-
11
- class ItemExtractor:
12
- """فئة استخراج البنود من المستندات"""
13
-
14
- def __init__(self, config=None):
15
- """
16
- تهيئة مستخرج البنود
17
-
18
- المعلمات:
19
- config (dict): إعدادات مستخرج البنود
20
- """
21
- self.config = config or {}
22
- self.logger = logging.getLogger(__name__)
23
-
24
- def extract(self, file_path):
25
- """
26
- استخراج البنود من ملف
27
-
28
- المعلمات:
29
- file_path (str): مسار الملف
30
-
31
- العوائد:
32
- list: قائمة البنود المستخرجة
33
- """
34
- self.logger.info(f"جاري استخراج البنود من الملف: {file_path}")
35
-
36
- try:
37
- # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
38
- # محاكاة الاستخراج للعرض
39
- file_name = os.path.basename(file_path)
40
-
41
- # تحديد نوع الملف
42
- _, ext = os.path.splitext(file_path)
43
- ext = ext.lower()
44
-
45
- if ext == '.pdf':
46
- return self._extract_items_from_pdf(file_path)
47
- elif ext in ('.doc', '.docx'):
48
- return self._extract_items_from_docx(file_path)
49
- else:
50
- return [{"بند": "نوع الملف غير مدعوم", "قيمة": 0}]
51
- except Exception as e:
52
- self.logger.error(f"خطأ في استخراج البنود: {str(e)}")
53
- return [{"بند": "حدث خطأ أثناء الاستخراج", "قيمة": 0, "خطأ": str(e)}]
54
-
55
- def _extract_items_from_pdf(self, file_path):
56
- """
57
- استخراج البنود من ملف PDF
58
-
59
- المعلمات:
60
- file_path (str): مسار ملف PDF
61
-
62
- العوائد:
63
- list: قائمة البنود المستخرجة
64
- """
65
- # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
66
- # محاكاة الاستخراج للعرض
67
- return [
68
- {"بند": "أعمال الخرسانة", "وحدة": "م3", "كمية": 250, "سعر الوحدة": 1200, "الإجمالي": 300000},
69
- {"بند": "أعمال التشطيبات", "وحدة": "م2", "كمية": 1500, "سعر الوحدة": 350, "الإجمالي": 525000},
70
- {"بند": "أعمال الكهرباء", "وحدة": "نقطة", "كمية": 120, "سعر الوحدة": 200, "الإجمالي": 24000},
71
- {"بند": "أعمال السباكة", "وحدة": "نقطة", "كمية": 75, "سعر الوحدة": 300, "الإجمالي": 22500},
72
- {"بند": "أعمال الألمنيوم", "وحدة": "م2", "كمية": 85, "سعر الوحدة": 1500, "الإجمالي": 127500}
73
- ]
74
-
75
- def _extract_items_from_docx(self, file_path):
76
- """
77
- استخراج البنود من ملف Word
78
-
79
- المعلمات:
80
- file_path (str): مسار ملف Word
81
-
82
- العوائد:
83
- list: قائمة البنود المستخرجة
84
- """
85
- # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
86
- # محاكاة الاستخراج للعرض
87
- return [
88
- {"بند": "استشارات هندسية", "وحدة": "ساعة", "كمية": 120, "سعر الوحدة": 500, "الإجمالي": 60000},
89
- {"بند": "تصميم معماري", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 100, "الإجمالي": 180000},
90
- {"بند": "تصميم إنشائي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 80, "الإجمالي": 144000},
91
- {"بند": "تصميم كهربائي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 40, "الإجمالي": 72000},
92
- {"بند": "تصميم ميكانيكي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 40, "الإجمالي": 72000}
93
- ]
94
-
95
- def extract_tables(self, document):
96
- """
97
- استخراج الجداول من مستند
98
-
99
- المعلمات:
100
- document (dict): المستند المحلل
101
-
102
- العوائد:
103
- list: قائمة الجداول المستخرجة
104
- """
105
- self.logger.info("جاري استخراج الجداول من المستند")
106
-
107
- try:
108
- # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
109
- # محاكاة الاستخراج للعرض
110
- return [
111
- {
112
- "عنوان": "جدول البنود والتكاليف",
113
- "بيانات": [
114
- {"بند": "أعمال الخرسانة", "وحدة": "م3", "كمية": 250, "سعر الوحدة": 1200, "الإجمالي": 300000},
115
- {"بند": "أعمال التشطيبات", "وحدة": "م2", "كمية": 1500, "سعر الوحدة": 350, "الإجمالي": 525000},
116
- {"بند": "أعمال الكهرباء", "وحدة": "نقطة", "كمية": 120, "سعر الوحدة": 200, "الإجمالي": 24000},
117
- {"بند": "أعمال السباكة", "وحدة": "نقطة", "كمية": 75, "سعر الوحدة": 300, "الإجمالي": 22500},
118
- {"بند": "أعمال الألمنيوم", "وحدة": "م2", "كمية": 85, "سعر الوحدة": 1500, "الإجمالي": 127500}
119
- ]
120
- },
121
- {
122
- "عنوان": "جدول المعلومات العامة",
123
- "بيانات": [
124
- {"اسم المشروع": "مبنى سكني", "المالك": "شركة الإسكان", "الموقع": "الرياض", "المساحة": "2500 م2"},
125
- {"اسم المشروع": "مبنى تجاري", "المالك": "شركة التطوير", "الموقع": "جدة", "المساحة": "3500 م2"}
126
- ]
127
- }
128
- ]
129
- except Exception as e:
130
- self.logger.error(f"خطأ في استخراج الجداول: {str(e)}")
131
- return [{"عنوان": "حدث خطأ أثناء الاستخراج", "بيانات": []}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/document_analysis/services/text_extractor.py DELETED
@@ -1,105 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- خدمة استخراج النص من المستندات
4
-
5
- هذا الملف يحتوي على الفئة المسؤولة عن استخراج النص من أنواع مختلفة من المستندات.
6
- """
7
-
8
- import os
9
- import logging
10
-
11
- class TextExtractor:
12
- """فئة استخراج النص من المستندات"""
13
-
14
- def __init__(self, config=None):
15
- """
16
- تهيئة مستخرج النص
17
-
18
- المعلمات:
19
- config (dict): إعدادات مستخرج النص
20
- """
21
- self.config = config or {}
22
- self.logger = logging.getLogger(__name__)
23
-
24
- def extract(self, file_path):
25
- """
26
- استخراج النص من ملف بناءً على نوع الملف
27
-
28
- المعلمات:
29
- file_path (str): مسار الملف
30
-
31
- العوائد:
32
- str: النص المستخرج
33
- """
34
- _, ext = os.path.splitext(file_path)
35
- ext = ext.lower()
36
-
37
- if ext == '.pdf':
38
- return self.extract_from_pdf(file_path)
39
- elif ext in ('.doc', '.docx'):
40
- return self.extract_from_docx(file_path)
41
- elif ext in ('.jpg', '.jpeg', '.png'):
42
- return self.extract_from_image(file_path)
43
- else:
44
- self.logger.warning(f"نوع ملف غير مدعوم: {ext}")
45
- return f"نوع ملف غير مدعوم: {ext}"
46
-
47
- def extract_from_pdf(self, file_path):
48
- """
49
- استخراج النص من ملف PDF
50
-
51
- المعلمات:
52
- file_path (str): مسار ملف PDF
53
-
54
- العوائد:
55
- str: النص المستخرج
56
- """
57
- self.logger.info(f"جاري استخراج النص من ملف PDF: {file_path}")
58
-
59
- try:
60
- # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل PyPDF2 أو pdfplumber
61
- # محاكاة الاستخراج للعرض
62
- return f"هذا نص مستخرج من ملف PDF: {os.path.basename(file_path)}\n\nيتم استخراج النص من الملف باستخدام مكتبة مناسبة في البيئة الحقيقية."
63
- except Exception as e:
64
- self.logger.error(f"خطأ في استخراج النص من PDF: {str(e)}")
65
- return f"حدث خطأ أثناء استخراج النص: {str(e)}"
66
-
67
- def extract_from_docx(self, file_path):
68
- """
69
- استخراج النص من ملف Word
70
-
71
- المعلمات:
72
- file_path (str): مسار ملف Word
73
-
74
- العوائد:
75
- str: النص المستخرج
76
- """
77
- self.logger.info(f"جاري استخراج النص من ملف Word: {file_path}")
78
-
79
- try:
80
- # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل python-docx
81
- # محاكاة الاستخراج للعرض
82
- return f"هذا نص مستخرج من ملف Word: {os.path.basename(file_path)}\n\nيتم استخراج النص من الملف باستخدام مكتبة مناسبة في البيئة الحقيقية."
83
- except Exception as e:
84
- self.logger.error(f"خطأ في استخراج النص من Word: {str(e)}")
85
- return f"حدث خطأ أثناء استخراج النص: {str(e)}"
86
-
87
- def extract_from_image(self, file_path):
88
- """
89
- استخراج النص من ملف صورة باستخدام OCR
90
-
91
- المعلمات:
92
- file_path (str): مسار ملف الصورة
93
-
94
- العوائد:
95
- str: النص المستخرج
96
- """
97
- self.logger.info(f"جاري استخراج النص من ملف صورة: {file_path}")
98
-
99
- try:
100
- # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل pytesseract
101
- # محاكاة الاستخراج للعرض
102
- return f"هذا نص مستخرج من ملف صورة: {os.path.basename(file_path)}\n\nيتم استخراج النص من الصورة باستخدام تقنية OCR في البيئة الحقيقية."
103
- except Exception as e:
104
- self.logger.error(f"خطأ في استخراج النص من الصورة: {str(e)}")
105
- return f"حدث خطأ أثناء استخراج النص: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/document_comparison/__init__.py DELETED
@@ -1,4 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- وحدة مقارنة المستندات المتقدمة
4
- """
 
 
 
 
 
modules/document_comparison/comparison_app.py DELETED
@@ -1,43 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- تطبيق مقارنة المستندات المتقدمة
6
- """
7
-
8
- import os
9
- import sys
10
- import streamlit as st
11
- import pandas as pd
12
- import numpy as np
13
-
14
- # إضافة مسار النظام للوصول للملفات المشتركة
15
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
16
-
17
- # استيراد مكونات مقارنة المستندات
18
- from modules.document_comparison.document_comparator import DocumentComparator
19
-
20
-
21
- class DocumentComparisonApp:
22
- """تطبيق مقارنة المستندات المتقدمة"""
23
-
24
- def __init__(self):
25
- """تهيئة تطبيق مقارنة المستندات"""
26
- self.comparator = DocumentComparator()
27
-
28
- def render(self):
29
- """عرض واجهة المستخدم الرئيسية للتطبيق"""
30
- self.comparator.render()
31
-
32
-
33
- # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
34
- if __name__ == "__main__":
35
- st.set_page_config(
36
- page_title="مقارنة المستندات المتقدمة | WAHBi AI",
37
- page_icon="📄",
38
- layout="wide",
39
- initial_sidebar_state="expanded"
40
- )
41
-
42
- app = DocumentComparisonApp()
43
- app.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/document_comparison/document_comparator.py DELETED
@@ -1,1503 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- وحدة مقارنة المستندات المتقدمة لتحليل الفروقات بين نسخ المستندات
6
- """
7
-
8
- import os
9
- import sys
10
- import json
11
- import re
12
- import difflib
13
- import Levenshtein
14
- from datetime import datetime
15
- import numpy as np
16
- import pandas as pd
17
- import streamlit as st
18
- import plotly.express as px
19
- import plotly.graph_objects as go
20
- from collections import Counter
21
- from nltk.tokenize import sent_tokenize, word_tokenize
22
- from rouge_score import rouge_scorer
23
- from PyPDF2 import PdfReader
24
- import io
25
-
26
- # إضافة مسار النظام للوصول للملفات المشتركة
27
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
28
-
29
- # استيراد المكونات المساعدة
30
- from utils.helpers import create_directory_if_not_exists, format_time, get_user_info
31
-
32
-
33
- class DocumentComparator:
34
- """فئة مقارنة المستندات المتقدمة"""
35
-
36
- def __init__(self):
37
- """تهيئة مقارن المستندات"""
38
- self.comparison_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'document_comparison')
39
- create_directory_if_not_exists(self.comparison_dir)
40
-
41
- # تهيئة NLTK وتنزيل حزمة punkt إذا لم تكن موجودة
42
- self._initialize_nltk()
43
-
44
- # إعداد مقيم ROUGE لمقارنة النصوص
45
- self.rouge_scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=False)
46
-
47
- def _initialize_nltk(self):
48
- """تهيئة مكتبة NLTK وتنزيل الحزم المطلوبة"""
49
- try:
50
- # استيراد nltk
51
- import nltk
52
-
53
- # قائمة بالحزم المطلوبة
54
- required_packages = ['punkt', 'stopwords', 'wordnet']
55
- for package in required_packages:
56
- try:
57
- # محاولة استخدام الحزمة أولاً، وإذا فشلت يتم تنزيلها
58
- nltk.data.find(f'tokenizers/{package}')
59
- except LookupError:
60
- print(f"تنزيل حزمة NLTK: {package}")
61
- nltk.download(package, quiet=True)
62
-
63
- # محاولة استخدام sent_tokenize للتحقق من وجود حزمة punkt
64
- from nltk.tokenize import sent_tokenize
65
- sent_tokenize("This is a test sentence.")
66
- except LookupError:
67
- # تنزيل حزمة punkt تلقائيًا إذا لم تكن موجودة
68
- import nltk
69
- nltk.download('punkt', quiet=True)
70
- # طباعة رسالة تأكيد التنزيل
71
- st.info("تم تنزيل حزمة NLTK punkt بنجاح للاستخدام في مقارنة المستندات.")
72
-
73
- def _preprocess_text(self, text):
74
- """معالجة النص قبل التحليل"""
75
- # إزالة الأرقام والرموز الخاصة والمسافات الزائدة
76
- text = re.sub(r'\s+', ' ', text)
77
- text = text.strip()
78
- return text
79
-
80
- def _segment_text(self, text):
81
- """تقسيم النص إلى فقرات وجمل"""
82
- # تقسيم النص إلى فقرات
83
- paragraphs = [p.strip() for p in text.split('\n') if p.strip()]
84
-
85
- # تقسيم كل فقرة إلى جمل
86
- sentences = []
87
- for paragraph in paragraphs:
88
- paragraph_sentences = sent_tokenize(paragraph)
89
- sentences.extend(paragraph_sentences)
90
-
91
- return paragraphs, sentences
92
-
93
- def _calculate_similarity(self, text1, text2):
94
- """حساب نسبة التشابه بين نصين"""
95
- # حساب نسبة التشابه باستخدام مقياس Levenshtein
96
- ratio = Levenshtein.ratio(text1, text2)
97
-
98
- # حساب درجات ROUGE
99
- rouge_scores = self.rouge_scorer.score(text1, text2)
100
-
101
- # حساب متوسط نقاط Rouge
102
- rouge1_f1 = rouge_scores['rouge1'].fmeasure
103
- rouge2_f1 = rouge_scores['rouge2'].fmeasure
104
- rougeL_f1 = rouge_scores['rougeL'].fmeasure
105
- avg_rouge = (rouge1_f1 + rouge2_f1 + rougeL_f1) / 3
106
-
107
- # دمج النقاط للحصول على نتيجة نهائية
108
- combined_score = (ratio + avg_rouge) / 2
109
-
110
- return {
111
- 'levenshtein_ratio': ratio,
112
- 'rouge1_f1': rouge1_f1,
113
- 'rouge2_f1': rouge2_f1,
114
- 'rougeL_f1': rougeL_f1,
115
- 'avg_rouge': avg_rouge,
116
- 'combined_score': combined_score
117
- }
118
-
119
- def _extract_text_from_pdf(self, pdf_file):
120
- """استخراج النص من ملف PDF"""
121
- text = ""
122
- try:
123
- # قراءة ملف PDF
124
- pdf_reader = PdfReader(pdf_file)
125
-
126
- # استخراج النص من كل صفحة
127
- for page in pdf_reader.pages:
128
- text += page.extract_text() + "\n"
129
- except Exception as e:
130
- st.error(f"خطأ في قراءة ملف PDF: {e}")
131
-
132
- return text
133
-
134
- def get_document_diff(self, text1, text2, title1="المستند الأول", title2="المستند الثاني"):
135
- """حساب الفروقات بين نصين"""
136
- if not text1 or not text2:
137
- return {
138
- "title1": title1,
139
- "title2": title2,
140
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
141
- "similarity": 0,
142
- "similarity_score": 0,
143
- "text_diffs": [],
144
- "summary": "أحد المستندات فارغ، لا يمكن إجراء المقارنة."
145
- }
146
-
147
- # معالجة النصوص
148
- preprocessed_text1 = self._preprocess_text(text1)
149
- preprocessed_text2 = self._preprocess_text(text2)
150
-
151
- # حساب نسبة التشابه الإجمالية
152
- similarity_metrics = self._calculate_similarity(preprocessed_text1, preprocessed_text2)
153
- similarity_score = similarity_metrics['combined_score']
154
- similarity_percentage = int(similarity_score * 100)
155
-
156
- # تقسيم النصوص إلى فقرات وجمل
157
- paragraphs1, sentences1 = self._segment_text(text1)
158
- paragraphs2, sentences2 = self._segment_text(text2)
159
-
160
- # تحديد الفروقات بين الجمل باستخدام difflib
161
- differ = difflib.Differ()
162
- sentence_diffs = []
163
-
164
- # مصفوفة التشابه بين الجمل
165
- similarity_matrix = np.zeros((len(sentences1), len(sentences2)))
166
- for i, s1 in enumerate(sentences1):
167
- for j, s2 in enumerate(sentences2):
168
- similarity_matrix[i, j] = Levenshtein.ratio(s1, s2)
169
-
170
- # تحديد أفضل مطابقة لكل جملة
171
- matched_sentences2 = set() # تتبع الجمل المطابقة في المستند الثاني
172
-
173
- for i, s1 in enumerate(sentences1):
174
- if len(s1.split()) < 3: # تجاهل الجمل القصيرة جداً
175
- continue
176
-
177
- best_match_idx = -1
178
- best_match_score = 0.7 # عتبة التشابه
179
-
180
- for j, s2 in enumerate(sentences2):
181
- if j in matched_sentences2:
182
- continue # تجاهل الجمل التي تم مطابقتها بالفعل
183
-
184
- if len(s2.split()) < 3: # تجاهل الجمل القصيرة جداً
185
- continue
186
-
187
- score = similarity_matrix[i, j]
188
- if score > best_match_score and score > 0.7:
189
- best_match_score = score
190
- best_match_idx = j
191
-
192
- if best_match_idx != -1:
193
- # وجدنا تطابق، تحديد الفروقات باستخدام difflib
194
- s2 = sentences2[best_match_idx]
195
- diff = list(differ.compare(s1.split(), s2.split()))
196
-
197
- # تحويل مخرجات difflib إلى تنسيق أسهل للاستخدام
198
- formatted_diff = []
199
- for token in diff:
200
- if token.startswith(' '): # متطابق
201
- formatted_diff.append({'text': token[2:], 'status': 'same'})
202
- elif token.startswith('- '): # حذف
203
- formatted_diff.append({'text': token[2:], 'status': 'removed'})
204
- elif token.startswith('+ '): # إضافة
205
- formatted_diff.append({'text': token[2:], 'status': 'added'})
206
-
207
- sentence_diffs.append({
208
- 'doc1_idx': i,
209
- 'doc2_idx': best_match_idx,
210
- 'doc1_text': s1,
211
- 'doc2_text': s2,
212
- 'similarity': best_match_score,
213
- 'diff': formatted_diff
214
- })
215
-
216
- matched_sentences2.add(best_match_idx)
217
- else:
218
- # لم نجد تطابق، هذه الجملة غير موجودة في المستند الثاني
219
- sentence_diffs.append({
220
- 'doc1_idx': i,
221
- 'doc2_idx': -1,
222
- 'doc1_text': s1,
223
- 'doc2_text': "",
224
- 'similarity': 0,
225
- 'diff': [{'text': word, 'status': 'removed'} for word in s1.split()]
226
- })
227
-
228
- # تحديد الجمل الجديدة في المستند الثاني
229
- for j, s2 in enumerate(sentences2):
230
- if j not in matched_sentences2 and len(s2.split()) >= 3:
231
- sentence_diffs.append({
232
- 'doc1_idx': -1,
233
- 'doc2_idx': j,
234
- 'doc1_text': "",
235
- 'doc2_text': s2,
236
- 'similarity': 0,
237
- 'diff': [{'text': word, 'status': 'added'} for word in s2.split()]
238
- })
239
-
240
- # ترتيب الفروقات حسب الموقع في المستند الأول
241
- sentence_diffs.sort(key=lambda x: (x['doc1_idx'] if x['doc1_idx'] != -1 else float('inf'), x['doc2_idx'] if x['doc2_idx'] != -1 else float('inf')))
242
-
243
- # تحديد الفقرات المضافة والمحذوفة
244
- paragraph_diffs = []
245
- matched_paragraphs2 = set()
246
-
247
- for i, p1 in enumerate(paragraphs1):
248
- if len(p1.split()) < 5: # تجاهل الفقرات القصيرة جداً
249
- continue
250
-
251
- best_match_idx = -1
252
- best_match_score = 0.6 # عتبة التشابه
253
-
254
- for j, p2 in enumerate(paragraphs2):
255
- if j in matched_paragraphs2:
256
- continue
257
-
258
- if len(p2.split()) < 5:
259
- continue
260
-
261
- score = Levenshtein.ratio(p1, p2)
262
- if score > best_match_score:
263
- best_match_score = score
264
- best_match_idx = j
265
-
266
- if best_match_idx != -1:
267
- # وجدنا تطابق
268
- p2 = paragraphs2[best_match_idx]
269
- paragraph_diffs.append({
270
- 'doc1_idx': i,
271
- 'doc2_idx': best_match_idx,
272
- 'doc1_text': p1,
273
- 'doc2_text': p2,
274
- 'similarity': best_match_score,
275
- 'status': 'modified' if best_match_score < 0.9 else 'same'
276
- })
277
-
278
- matched_paragraphs2.add(best_match_idx)
279
- else:
280
- # لم نجد تطابق، هذه الفقرة غير موجودة في المستند الثاني
281
- paragraph_diffs.append({
282
- 'doc1_idx': i,
283
- 'doc2_idx': -1,
284
- 'doc1_text': p1,
285
- 'doc2_text': "",
286
- 'similarity': 0,
287
- 'status': 'removed'
288
- })
289
-
290
- # تحديد الفقرات الجديدة في المستند الثاني
291
- for j, p2 in enumerate(paragraphs2):
292
- if j not in matched_paragraphs2 and len(p2.split()) >= 5:
293
- paragraph_diffs.append({
294
- 'doc1_idx': -1,
295
- 'doc2_idx': j,
296
- 'doc1_text': "",
297
- 'doc2_text': p2,
298
- 'similarity': 0,
299
- 'status': 'added'
300
- })
301
-
302
- # ترتيب الفروقات حسب الموقع
303
- paragraph_diffs.sort(key=lambda x: (x['doc1_idx'] if x['doc1_idx'] != -1 else float('inf'), x['doc2_idx'] if x['doc2_idx'] != -1 else float('inf')))
304
-
305
- # تحليل الفروقات للحصول على إحصائيات
306
- total_paragraphs = len(paragraphs1) + len(paragraphs2)
307
- removed_paragraphs = sum(1 for p in paragraph_diffs if p['status'] == 'removed')
308
- added_paragraphs = sum(1 for p in paragraph_diffs if p['status'] == 'added')
309
- modified_paragraphs = sum(1 for p in paragraph_diffs if p['status'] == 'modified')
310
-
311
- # تحليل الكلمات المضافة، المحذوفة والمتغيرة
312
- added_words = []
313
- removed_words = []
314
- modified_contexts = []
315
-
316
- for diff in sentence_diffs:
317
- for token in diff['diff']:
318
- if token['status'] == 'added':
319
- added_words.append(token['text'])
320
- elif token['status'] == 'removed':
321
- removed_words.append(token['text'])
322
-
323
- # جمع السياقات المتغيرة للتحليل
324
- if diff['doc1_idx'] != -1 and diff['doc2_idx'] != -1 and diff['similarity'] < 0.9:
325
- modified_contexts.append({
326
- 'doc1_text': diff['doc1_text'],
327
- 'doc2_text': diff['doc2_text'],
328
- 'similarity': diff['similarity']
329
- })
330
-
331
- # إنشاء التقرير النهائي
332
- comparison_report = {
333
- "title1": title1,
334
- "title2": title2,
335
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
336
- "similarity": similarity_percentage,
337
- "similarity_metrics": similarity_metrics,
338
- "sentence_diffs": sentence_diffs,
339
- "paragraph_diffs": paragraph_diffs,
340
- "statistics": {
341
- "doc1_paragraphs": len(paragraphs1),
342
- "doc2_paragraphs": len(paragraphs2),
343
- "doc1_sentences": len(sentences1),
344
- "doc2_sentences": len(sentences2),
345
- "removed_paragraphs": removed_paragraphs,
346
- "added_paragraphs": added_paragraphs,
347
- "modified_paragraphs": modified_paragraphs,
348
- "removed_words_count": len(removed_words),
349
- "added_words_count": len(added_words),
350
- "top_removed_words": Counter(removed_words).most_common(10),
351
- "top_added_words": Counter(added_words).most_common(10)
352
- },
353
- "modified_contexts": modified_contexts[:10], # أهم 10 سياقات متغيرة
354
- "summary": self._generate_comparison_summary(
355
- similarity_percentage,
356
- len(paragraphs1),
357
- len(paragraphs2),
358
- removed_paragraphs,
359
- added_paragraphs,
360
- modified_paragraphs,
361
- len(removed_words),
362
- len(added_words)
363
- )
364
- }
365
-
366
- # حفظ تقرير المقارنة
367
- self._save_comparison_report(comparison_report, title1, title2)
368
-
369
- return comparison_report
370
-
371
- def _generate_comparison_summary(self, similarity, p1_count, p2_count, removed_p, added_p, modified_p, removed_w, added_w):
372
- """إنشاء ملخص للمقارنة بين المستندين"""
373
- if similarity >= 90:
374
- similarity_description = "متطابقة بشكل كبير"
375
- elif similarity >= 70:
376
- similarity_description = "متشابهة"
377
- elif similarity >= 50:
378
- similarity_description = "متشابهة جزئياً"
379
- else:
380
- similarity_description = "مختلفة"
381
-
382
- summary = f"المستندان {similarity_description} بنسبة {similarity}%. "
383
-
384
- # وصف التغييرات في الفقرات
385
- if removed_p > 0 or added_p > 0 or modified_p > 0:
386
- changes = []
387
- if removed_p > 0:
388
- changes.append(f"تم حذف {removed_p} فقرة")
389
- if added_p > 0:
390
- changes.append(f"تم إضافة {added_p} فقرة")
391
- if modified_p > 0:
392
- changes.append(f"تم تعديل {modified_p} فقرة")
393
-
394
- summary += "التغييرات تشمل: " + "، ".join(changes) + ". "
395
-
396
- # وصف التغييرات في الكلمات
397
- if removed_w > 0 or added_w > 0:
398
- word_changes = []
399
- if removed_w > 0:
400
- word_changes.append(f"تم حذف {removed_w} كلمة")
401
- if added_w > 0:
402
- word_changes.append(f"تم إضافة {added_w} كلمة")
403
-
404
- summary += "على مستوى الكلمات: " + "، ".join(word_changes) + "."
405
-
406
- return summary
407
-
408
- def _save_comparison_report(self, report, title1, title2):
409
- """حفظ تقرير المقارنة"""
410
- # إنشاء اسم ملف فريد
411
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
412
- filename = f"compare_{title1.replace(' ', '_')}_{title2.replace(' ', '_')}_{timestamp}.json"
413
- file_path = os.path.join(self.comparison_dir, filename)
414
-
415
- try:
416
- with open(file_path, 'w', encoding='utf-8') as f:
417
- json.dump(report, f, ensure_ascii=False, indent=2)
418
- except Exception as e:
419
- print(f"خطأ في حفظ تقرير المقارنة: {e}")
420
-
421
- def load_comparison_report(self, filename):
422
- """تحميل تقرير مقارنة محفوظ"""
423
- file_path = os.path.join(self.comparison_dir, filename)
424
-
425
- if not os.path.exists(file_path):
426
- return None
427
-
428
- try:
429
- with open(file_path, 'r', encoding='utf-8') as f:
430
- report = json.load(f)
431
- return report
432
- except Exception as e:
433
- print(f"خطأ في تحميل تقرير المقارنة: {e}")
434
- return None
435
-
436
- def get_comparison_reports(self):
437
- """الحصول على قائمة تقارير المقارنة المحفوظة"""
438
- reports = []
439
-
440
- for filename in os.listdir(self.comparison_dir):
441
- if filename.startswith("compare_") and filename.endswith(".json"):
442
- file_path = os.path.join(self.comparison_dir, filename)
443
- try:
444
- with open(file_path, 'r', encoding='utf-8') as f:
445
- report = json.load(f)
446
- reports.append({
447
- "filename": filename,
448
- "title1": report.get("title1", "مستند 1"),
449
- "title2": report.get("title2", "مستند 2"),
450
- "timestamp": report.get("timestamp", ""),
451
- "similarity": report.get("similarity", 0)
452
- })
453
- except Exception as e:
454
- print(f"خطأ في قراءة تقرير المقارنة {filename}: {e}")
455
-
456
- # ترتيب التقارير حسب التاريخ (الأحدث أولاً)
457
- reports.sort(key=lambda x: x["timestamp"], reverse=True)
458
-
459
- return reports
460
-
461
- def extract_key_differences(self, comparison_report):
462
- """استخراج الاختلافات الرئيسية من تقرير المقارنة"""
463
- if not comparison_report or "paragraph_diffs" not in comparison_report:
464
- return []
465
-
466
- key_differences = []
467
-
468
- # استخراج الفقرات المضافة
469
- added_paragraphs = [p for p in comparison_report["paragraph_diffs"] if p["status"] == "added"]
470
- if added_paragraphs:
471
- key_differences.append({
472
- "type": "added_paragraphs",
473
- "label": "فقرات مضافة",
474
- "count": len(added_paragraphs),
475
- "items": [p["doc2_text"] for p in added_paragraphs]
476
- })
477
-
478
- # استخراج الفقرات المحذوفة
479
- removed_paragraphs = [p for p in comparison_report["paragraph_diffs"] if p["status"] == "removed"]
480
- if removed_paragraphs:
481
- key_differences.append({
482
- "type": "removed_paragraphs",
483
- "label": "فقرات محذوفة",
484
- "count": len(removed_paragraphs),
485
- "items": [p["doc1_text"] for p in removed_paragraphs]
486
- })
487
-
488
- # استخراج الفقرات المعدلة
489
- modified_paragraphs = [p for p in comparison_report["paragraph_diffs"] if p["status"] == "modified"]
490
- if modified_paragraphs:
491
- modified_items = []
492
- for p in modified_paragraphs:
493
- modified_items.append({
494
- "doc1_text": p["doc1_text"],
495
- "doc2_text": p["doc2_text"],
496
- "similarity": p["similarity"]
497
- })
498
-
499
- key_differences.append({
500
- "type": "modified_paragraphs",
501
- "label": "فقرات معدلة",
502
- "count": len(modified_paragraphs),
503
- "items": modified_items
504
- })
505
-
506
- # استخراج الكلمات الرئيسية المضافة والمحذوفة
507
- if "statistics" in comparison_report:
508
- stats = comparison_report["statistics"]
509
-
510
- if "top_added_words" in stats and stats["top_added_words"]:
511
- key_differences.append({
512
- "type": "added_words",
513
- "label": "الكلمات المضافة الأكثر تكراراً",
514
- "count": stats["added_words_count"],
515
- "items": stats["top_added_words"]
516
- })
517
-
518
- if "top_removed_words" in stats and stats["top_removed_words"]:
519
- key_differences.append({
520
- "type": "removed_words",
521
- "label": "الكلمات المحذوفة الأكثر تكراراً",
522
- "count": stats["removed_words_count"],
523
- "items": stats["top_removed_words"]
524
- })
525
-
526
- return key_differences
527
-
528
- def analyze_legal_changes(self, comparison_report):
529
- """تحليل التغييرات القانونية في المستندات"""
530
- if not comparison_report:
531
- return []
532
-
533
- # قائمة المصطلحات القانونية الهامة للبحث عنها
534
- legal_terms = {
535
- "payment": ["دفع", "سداد", "مستحقات", "مقابل", "رسوم", "تكلفة", "مبلغ", "أتعاب"],
536
- "deadlines": ["ميعاد", "موعد", "تاريخ", "أجل", "مدة", "فترة", "مهلة"],
537
- "liability": ["مسؤولية", "التزام", "تحمل", "تعويض", "ضمان", "كفالة"],
538
- "termination": ["إنهاء", "فسخ", "إلغاء", "إيقاف", "إنهاء العلاقة"],
539
- "dispute": ["نزاع", "خلاف", "منازعة", "اعتراض", "تحكيم", "قضاء", "محكمة"],
540
- "penalties": ["غرامة", "عقوبة", "شرط جزائي", "جزاء", "تعويض"],
541
- "conditions": ["شرط", "بند", "حالة", "اشتراط", "متطلب"],
542
- "rights": ["حق", "صلاحية", "امتياز", "منفعة", "ملكية", "تصرف"],
543
- "obligations": ["التزام", "واجب", "تعهد", "إلزام", "لازم"]
544
- }
545
-
546
- # البحث عن التغييرات المتعلقة بالمصطلحات القانونية
547
- legal_changes = []
548
-
549
- if "sentence_diffs" in comparison_report:
550
- for category, terms in legal_terms.items():
551
- category_changes = []
552
-
553
- for diff in comparison_report["sentence_diffs"]:
554
- # فحص فقط الجمل المعدلة (المتطابقة جزئياً)
555
- if diff["doc1_idx"] != -1 and diff["doc2_idx"] != -1 and diff["similarity"] < 0.9:
556
- # فحص ما إذا كانت الجمل�� تحتوي على أي من المصطلحات القانونية
557
- contains_term = False
558
- for term in terms:
559
- if term in diff["doc1_text"].lower() or term in diff["doc2_text"].lower():
560
- contains_term = True
561
- break
562
-
563
- if contains_term:
564
- category_changes.append({
565
- "doc1_text": diff["doc1_text"],
566
- "doc2_text": diff["doc2_text"],
567
- "similarity": diff["similarity"]
568
- })
569
-
570
- if category_changes:
571
- legal_category_name = {
572
- "payment": "الدفع والمستحقات المالية",
573
- "deadlines": "المواعيد والفترات الزمنية",
574
- "liability": "المسؤولية والالتزامات",
575
- "termination": "إنهاء العقد أو فسخه",
576
- "dispute": "النزاعات والخلافات",
577
- "penalties": "الغرامات والعقوبات",
578
- "conditions": "الشروط والبنود",
579
- "rights": "الحقوق والصلاحيات",
580
- "obligations": "الالتزامات والواجبات"
581
- }
582
-
583
- legal_changes.append({
584
- "category": category,
585
- "label": legal_category_name.get(category, category),
586
- "count": len(category_changes),
587
- "changes": category_changes
588
- })
589
-
590
- # ترتيب التغييرات حسب الأهمية (عدد التغييرات)
591
- legal_changes.sort(key=lambda x: x["count"], reverse=True)
592
-
593
- return legal_changes
594
-
595
- def analyze_price_changes(self, text1, text2):
596
- """تحليل التغييرات في الأسعار بين نسختي المستند"""
597
- # البحث عن الأرقام متبوعة بعملة أو تعبيرات تدل على المبالغ
598
- price_pattern = r'(\d{1,3}(?:,\d{3})*(?:\.\d+)?)\s*(?:ريال|دولار|يورو|جنيه|درهم|دينار|SAR|USD|EUR|SR|$|€|£)'
599
- amount_pattern = r'مبلغ[\s\w]*?(\d{1,3}(?:,\d{3})*(?:\.\d+)?)'
600
-
601
- # استخراج الأسعار من كل نص
602
- prices1 = re.findall(price_pattern, text1)
603
- prices1.extend(re.findall(amount_pattern, text1))
604
- prices1 = [p.replace(',', '') for p in prices1]
605
- prices1 = [float(p) for p in prices1 if p]
606
-
607
- prices2 = re.findall(price_pattern, text2)
608
- prices2.extend(re.findall(amount_pattern, text2))
609
- prices2 = [p.replace(',', '') for p in prices2]
610
- prices2 = [float(p) for p in prices2 if p]
611
-
612
- # تحليل التغييرات
613
- price_diff = {
614
- "doc1_prices_count": len(prices1),
615
- "doc2_prices_count": len(prices2),
616
- "doc1_total": sum(prices1) if prices1 else 0,
617
- "doc2_total": sum(prices2) if prices2 else 0,
618
- "doc1_average": sum(prices1) / len(prices1) if prices1 else 0,
619
- "doc2_average": sum(prices2) / len(prices2) if prices2 else 0,
620
- "doc1_min": min(prices1) if prices1 else 0,
621
- "doc2_min": min(prices2) if prices2 else 0,
622
- "doc1_max": max(prices1) if prices1 else 0,
623
- "doc2_max": max(prices2) if prices2 else 0
624
- }
625
-
626
- # حساب التغيير في إجمالي الأسعار
627
- if price_diff["doc1_total"] > 0:
628
- price_diff["total_change_percentage"] = ((price_diff["doc2_total"] - price_diff["doc1_total"]) / price_diff["doc1_total"]) * 100
629
- else:
630
- price_diff["total_change_percentage"] = 0
631
-
632
- return price_diff
633
-
634
- def analyze_date_changes(self, text1, text2):
635
- """تحليل التغييرات في التواريخ بين نسختي المستند"""
636
- # البحث عن التواريخ بالصيغ المختلفة
637
- date_patterns = [
638
- r'\d{1,2}/\d{1,2}/\d{2,4}', # DD/MM/YYYY or MM/DD/YYYY
639
- r'\d{1,2}-\d{1,2}-\d{2,4}', # DD-MM-YYYY or MM-DD-YYYY
640
- r'\d{2,4}/\d{1,2}/\d{1,2}', # YYYY/MM/DD
641
- r'\d{2,4}-\d{1,2}-\d{1,2}', # YYYY-MM-DD
642
- r'\d{1,2}\s+(?:يناير|فبراير|مارس|أبريل|مايو|يونيو|يوليو|أغسطس|سبتمبر|أكتوبر|نوفمبر|ديسمبر)\s+\d{2,4}' # DD شهر YYYY
643
- ]
644
-
645
- dates1 = []
646
- dates2 = []
647
-
648
- for pattern in date_patterns:
649
- dates1.extend(re.findall(pattern, text1))
650
- dates2.extend(re.findall(pattern, text2))
651
-
652
- # إنشاء تقرير التغييرات في التواريخ
653
- date_changes = {
654
- "doc1_dates_count": len(dates1),
655
- "doc2_dates_count": len(dates2),
656
- "doc1_dates": dates1[:10], # أول 10 تواريخ فقط
657
- "doc2_dates": dates2[:10],
658
- "common_dates": list(set(dates1).intersection(set(dates2))),
659
- "removed_dates": list(set(dates1) - set(dates2)),
660
- "added_dates": list(set(dates2) - set(dates1))
661
- }
662
-
663
- return date_changes
664
-
665
- def render_document_comparison(self, text1, text2, title1="المستند الأول", title2="المستند الثاني"):
666
- """عرض مقارنة المستندات بالواجهة التفاعلية"""
667
- st.markdown("<h2 class='module-title'>مقارنة المستندات المتقدمة</h2>", unsafe_allow_html=True)
668
-
669
- if not text1 or not text2:
670
- st.warning("يرجى توفير نصوص المستندين للمقارنة")
671
- return
672
-
673
- with st.spinner("جاري تحليل ومقارنة المستندين..."):
674
- # إجراء المقارنة
675
- comparison_report = self.get_document_diff(text1, text2, title1, title2)
676
-
677
- # تحليل التغييرات القانونية
678
- legal_changes = self.analyze_legal_changes(comparison_report)
679
-
680
- # تحليل التغييرات في الأسعار والتواريخ
681
- price_changes = self.analyze_price_changes(text1, text2)
682
- date_changes = self.analyze_date_changes(text1, text2)
683
-
684
- # عرض ملخص المقارنة
685
- st.markdown("<h3>ملخص المقارنة</h3>", unsafe_allow_html=True)
686
-
687
- col1, col2, col3 = st.columns([1, 1, 1])
688
-
689
- with col1:
690
- similarity = comparison_report["similarity"]
691
- color = "#00b894" if similarity >= 80 else "#fdcb6e" if similarity >= 50 else "#d63031"
692
-
693
- st.markdown(f"""
694
- <div class="similarity-card">
695
- <div class="similarity-title">نسبة التشابه الإجمالية</div>
696
- <div class="similarity-score" style="color: {color};">{similarity}%</div>
697
- <div class="similarity-info">تم تحليل {comparison_report["statistics"]["doc1_paragraphs"]} فقرة في {title1} و {comparison_report["statistics"]["doc2_paragraphs"]} فقرة في {title2}</div>
698
- </div>
699
- """, unsafe_allow_html=True)
700
-
701
- with col2:
702
- st.markdown(f"""
703
- <div class="changes-card">
704
- <div class="changes-title">ملخص التغييرات</div>
705
- <div class="changes-list">
706
- <div class="change-item">
707
- <span class="change-label">فقرات محذوفة:</span>
708
- <span class="change-value">{comparison_report["statistics"]["removed_paragraphs"]}</span>
709
- </div>
710
- <div class="change-item">
711
- <span class="change-label">فقرات مضافة:</span>
712
- <span class="change-value">{comparison_report["statistics"]["added_paragraphs"]}</span>
713
- </div>
714
- <div class="change-item">
715
- <span class="change-label">فقرات معدلة:</span>
716
- <span class="change-value">{comparison_report["statistics"]["modified_paragraphs"]}</span>
717
- </div>
718
- </div>
719
- </div>
720
- """, unsafe_allow_html=True)
721
-
722
- with col3:
723
- st.markdown(f"""
724
- <div class="words-card">
725
- <div class="words-title">تغييرات الكلمات</div>
726
- <div class="words-list">
727
- <div class="words-item">
728
- <span class="words-label">كلمات محذوفة:</span>
729
- <span class="words-value">{comparison_report["statistics"]["removed_words_count"]}</span>
730
- </div>
731
- <div class="words-item">
732
- <span class="words-label">كلمات مضافة:</span>
733
- <span class="words-value">{comparison_report["statistics"]["added_words_count"]}</span>
734
- </div>
735
- </div>
736
- </div>
737
- """, unsafe_allow_html=True)
738
-
739
- # عرض ملخص نصي
740
- st.markdown(f"""
741
- <div class="text-summary">
742
- {comparison_report["summary"]}
743
- </div>
744
- """, unsafe_allow_html=True)
745
-
746
- # عرض تحليل التغييرات القانونية
747
- st.markdown("<h3>تحليل التغييرات القانونية</h3>", unsafe_allow_html=True)
748
-
749
- if legal_changes:
750
- tabs = st.tabs([change["label"] for change in legal_changes])
751
-
752
- for i, tab in enumerate(tabs):
753
- with tab:
754
- st.markdown(f"**عدد التغييرات: {legal_changes[i]['count']}**")
755
-
756
- for j, change in enumerate(legal_changes[i]["changes"]):
757
- col1, col2 = st.columns(2)
758
- with col1:
759
- st.markdown(f"**{title1}:**")
760
- st.markdown(f"<div class='diff-text diff-old'>{change['doc1_text']}</div>", unsafe_allow_html=True)
761
- with col2:
762
- st.markdown(f"**{title2}:**")
763
- st.markdown(f"<div class='diff-text diff-new'>{change['doc2_text']}</div>", unsafe_allow_html=True)
764
-
765
- if j < len(legal_changes[i]["changes"]) - 1:
766
- st.markdown("---")
767
- else:
768
- st.info("لم يتم اكتشاف تغييرات قانونية هامة بين المستندين.")
769
-
770
- # عرض الرسوم البيانية للتغييرات
771
- st.markdown("<h3>رسوم بيانية للتغييرات</h3>", unsafe_allow_html=True)
772
-
773
- col1, col2 = st.columns(2)
774
-
775
- with col1:
776
- # رسم بياني لتوزيع أنواع التغييرات في الفقرات
777
- stats = comparison_report["statistics"]
778
- fig = px.pie(
779
- names=["فقرات متطابقة", "فقرات معدلة", "فقرات محذوفة", "فقرات مضافة"],
780
- values=[
781
- stats["doc1_paragraphs"] - stats["removed_paragraphs"] - stats["modified_paragraphs"],
782
- stats["modified_paragraphs"],
783
- stats["removed_paragraphs"],
784
- stats["added_paragraphs"]
785
- ],
786
- title="توزيع التغييرات في الفقرات",
787
- color_discrete_sequence=["#00b894", "#fdcb6e", "#d63031", "#0984e3"]
788
- )
789
-
790
- fig.update_layout(
791
- font=dict(family="Arial, sans-serif", size=14),
792
- height=350
793
- )
794
-
795
- st.plotly_chart(fig, use_container_width=True)
796
-
797
- with col2:
798
- # رسم بياني للكلمات المضافة والمحذوفة الأكثر تكراراً
799
- words_data = []
800
-
801
- for word, count in comparison_report["statistics"]["top_removed_words"]:
802
- if len(word) > 1: # تجاهل الأحرف المفردة
803
- words_data.append({"word": word, "count": count, "type": "محذوفة"})
804
-
805
- for word, count in comparison_report["statistics"]["top_added_words"]:
806
- if len(word) > 1: # تجاهل الأحرف المفردة
807
- words_data.append({"word": word, "count": count, "type": "مضافة"})
808
-
809
- if words_data:
810
- words_df = pd.DataFrame(words_data)
811
-
812
- fig = px.bar(
813
- words_df,
814
- x="word",
815
- y="count",
816
- color="type",
817
- title="الكلمات المضافة والمحذوفة الأكثر تكراراً",
818
- labels={"word": "الكلمة", "count": "عدد المرات", "type": "النوع"},
819
- color_discrete_map={"محذوفة": "#d63031", "مضافة": "#0984e3"}
820
- )
821
-
822
- fig.update_layout(
823
- font=dict(family="Arial, sans-serif", size=14),
824
- height=350
825
- )
826
-
827
- st.plotly_chart(fig, use_container_width=True)
828
- else:
829
- st.info("لا توجد بيانات كافية للكلمات المضافة والمحذوفة.")
830
-
831
- # عرض تحليل الأسعار والتواريخ
832
- col1, col2 = st.columns(2)
833
-
834
- with col1:
835
- st.markdown("<h3>تحليل التغييرات في الأسعار</h3>", unsafe_allow_html=True)
836
-
837
- if price_changes["doc1_prices_count"] > 0 or price_changes["doc2_prices_count"] > 0:
838
- price_change_direction = "زيادة" if price_changes["total_change_percentage"] > 0 else "نقص"
839
- price_change_color = "#d63031" if price_changes["total_change_percentage"] > 0 else "#00b894"
840
-
841
- st.markdown(f"""
842
- <div class="price-analysis">
843
- <div class="price-summary">تغيير في إجمالي الأسعار بنسبة <span style="color: {price_change_color}; font-weight: bold;">{abs(price_changes['total_change_percentage']):.2f}% ({price_change_direction})</span></div>
844
- <div class="price-details">
845
- <div class="price-row">
846
- <div class="price-label"></div>
847
- <div class="price-value-header">{title1}</div>
848
- <div class="price-value-header">{title2}</div>
849
- </div>
850
- <div class="price-row">
851
- <div class="price-label">عدد الأسعار:</div>
852
- <div class="price-value">{price_changes['doc1_prices_count']}</div>
853
- <div class="price-value">{price_changes['doc2_prices_count']}</div>
854
- </div>
855
- <div class="price-row">
856
- <div class="price-label">الإجمالي:</div>
857
- <div class="price-value">{price_changes['doc1_total']:,.2f}</div>
858
- <div class="price-value">{price_changes['doc2_total']:,.2f}</div>
859
- </div>
860
- <div class="price-row">
861
- <div class="price-label">المتوسط:</div>
862
- <div class="price-value">{price_changes['doc1_average']:,.2f}</div>
863
- <div class="price-value">{price_changes['doc2_average']:,.2f}</div>
864
- </div>
865
- <div class="price-row">
866
- <div class="price-label">الحد الأدنى:</div>
867
- <div class="price-value">{price_changes['doc1_min']:,.2f}</div>
868
- <div class="price-value">{price_changes['doc2_min']:,.2f}</div>
869
- </div>
870
- <div class="price-row">
871
- <div class="price-label">الحد الأقصى:</div>
872
- <div class="price-value">{price_changes['doc1_max']:,.2f}</div>
873
- <div class="price-value">{price_changes['doc2_max']:,.2f}</div>
874
- </div>
875
- </div>
876
- </div>
877
- """, unsafe_allow_html=True)
878
-
879
- # رسم بياني للأسعار
880
- if price_changes["doc1_prices_count"] > 0 and price_changes["doc2_prices_count"] > 0:
881
- price_chart_data = [
882
- {"document": title1, "metric": "الإجمالي", "value": price_changes["doc1_total"]},
883
- {"document": title2, "metric": "الإجمالي", "value": price_changes["doc2_total"]},
884
- {"document": title1, "metric": "المتوسط", "value": price_changes["doc1_average"]},
885
- {"document": title2, "metric": "المتوسط", "value": price_changes["doc2_average"]},
886
- {"document": title1, "metric": "الحد الأقصى", "value": price_changes["doc1_max"]},
887
- {"document": title2, "metric": "الحد الأقصى", "value": price_changes["doc2_max"]}
888
- ]
889
-
890
- price_df = pd.DataFrame(price_chart_data)
891
-
892
- fig = px.bar(
893
- price_df,
894
- x="metric",
895
- y="value",
896
- color="document",
897
- barmode="group",
898
- title="مقارنة الأسعار بين المستندين",
899
- color_discrete_map={title1: "#0984e3", title2: "#00b894"}
900
- )
901
-
902
- fig.update_layout(
903
- font=dict(family="Arial, sans-serif", size=14),
904
- height=350
905
- )
906
-
907
- st.plotly_chart(fig, use_container_width=True)
908
- else:
909
- st.info("لم يتم اكتشاف أي أسعار في المستندين.")
910
-
911
- with col2:
912
- st.markdown("<h3>تحليل التغييرات في التواريخ</h3>", unsafe_allow_html=True)
913
-
914
- if date_changes["doc1_dates_count"] > 0 or date_changes["doc2_dates_count"] > 0:
915
- st.markdown(f"""
916
- <div class="date-analysis">
917
- <div class="date-summary">تم اكتشاف {date_changes['doc1_dates_count']} تاريخ في {title1} و {date_changes['doc2_dates_count']} تاريخ في {title2}</div>
918
- <div class="date-stats">
919
- <div class="date-stat">
920
- <span class="date-label">تواريخ مشتركة:</span>
921
- <span class="date-value">{len(date_changes['common_dates'])}</span>
922
- </div>
923
- <div class="date-stat">
924
- <span class="date-label">تواريخ محذوفة:</span>
925
- <span class="date-value">{len(date_changes['removed_dates'])}</span>
926
- </div>
927
- <div class="date-stat">
928
- <span class="date-label">تواريخ مضافة:</span>
929
- <span class="date-value">{len(date_changes['added_dates'])}</span>
930
- </div>
931
- </div>
932
- </div>
933
- """, unsafe_allow_html=True)
934
-
935
- # عرض التواريخ المحذوفة والمضافة
936
- if date_changes["removed_dates"]:
937
- st.markdown("**التواريخ المحذوفة:**")
938
- for date in date_changes["removed_dates"][:10]: # عرض أول 10 فقط إذا كان هناك الكثير
939
- st.markdown(f"<div class='diff-text diff-old'>{date}</div>", unsafe_allow_html=True)
940
-
941
- if date_changes["added_dates"]:
942
- st.markdown("**التواريخ المضافة:**")
943
- for date in date_changes["added_dates"][:10]: # عرض أول 10 فقط
944
- st.markdown(f"<div class='diff-text diff-new'>{date}</div>", unsafe_allow_html=True)
945
-
946
- # رسم بياني للتواريخ
947
- date_chart_data = [
948
- {"category": "تواريخ مشتركة", "count": len(date_changes["common_dates"])},
949
- {"category": "تواريخ محذوفة", "count": len(date_changes["removed_dates"])},
950
- {"category": "تواريخ مضافة", "count": len(date_changes["added_dates"])}
951
- ]
952
-
953
- date_df = pd.DataFrame(date_chart_data)
954
-
955
- fig = px.bar(
956
- date_df,
957
- x="category",
958
- y="count",
959
- title="توزيع التغييرات في التواريخ",
960
- color="category",
961
- color_discrete_map={
962
- "تواريخ مشتركة": "#00b894",
963
- "تواريخ محذوفة": "#d63031",
964
- "تواريخ مضافة": "#0984e3"
965
- }
966
- )
967
-
968
- fig.update_layout(
969
- font=dict(family="Arial, sans-serif", size=14),
970
- height=350
971
- )
972
-
973
- st.plotly_chart(fig, use_container_width=True)
974
- else:
975
- st.info("لم يتم اكتشاف أي تواريخ في المستندين.")
976
-
977
- # عرض العرض المرئي للتغييرات بين المستندين
978
- st.markdown("<h3>العرض المرئي للتغييرات</h3>", unsafe_allow_html=True)
979
-
980
- # إضافة خيار لتصفية الفروقات
981
- st.markdown("#### تصفية الفروقات حسب النوع")
982
- col1, col2, col3 = st.columns(3)
983
-
984
- with col1:
985
- show_added = st.checkbox("عرض الإضافات", value=True)
986
- with col2:
987
- show_removed = st.checkbox("عرض الحذف", value=True)
988
- with col3:
989
- show_modified = st.checkbox("عرض التعديلات", value=True)
990
-
991
- # تحديد الفروقات للعرض
992
- filtered_diffs = []
993
-
994
- for diff in comparison_report["paragraph_diffs"]:
995
- if diff["status"] == "added" and show_added:
996
- filtered_diffs.append(diff)
997
- elif diff["status"] == "removed" and show_removed:
998
- filtered_diffs.append(diff)
999
- elif diff["status"] == "modified" and show_modified:
1000
- filtered_diffs.append(diff)
1001
-
1002
- # عرض الفروقات
1003
- if filtered_diffs:
1004
- for diff in filtered_diffs:
1005
- if diff["status"] == "added":
1006
- st.markdown(f"""
1007
- <div class="diff-block diff-added">
1008
- <div class="diff-header">
1009
- <div class="diff-title">فقرة مضافة في {title2}</div>
1010
- </div>
1011
- <div class="diff-content">
1012
- {diff["doc2_text"]}
1013
- </div>
1014
- </div>
1015
- """, unsafe_allow_html=True)
1016
-
1017
- elif diff["status"] == "removed":
1018
- st.markdown(f"""
1019
- <div class="diff-block diff-removed">
1020
- <div class="diff-header">
1021
- <div class="diff-title">فقرة محذوفة من {title1}</div>
1022
- </div>
1023
- <div class="diff-content">
1024
- {diff["doc1_text"]}
1025
- </div>
1026
- </div>
1027
- """, unsafe_allow_html=True)
1028
-
1029
- elif diff["status"] == "modified":
1030
- similarity_percentage = int(diff["similarity"] * 100)
1031
-
1032
- st.markdown(f"""
1033
- <div class="diff-block diff-modified">
1034
- <div class="diff-header">
1035
- <div class="diff-title">فقرة معدلة (نسبة التشابه: {similarity_percentage}%)</div>
1036
- </div>
1037
- <div class="diff-content-container">
1038
- <div class="diff-content-old">
1039
- <div class="diff-subtitle">{title1}:</div>
1040
- {diff["doc1_text"]}
1041
- </div>
1042
- <div class="diff-content-new">
1043
- <div class="diff-subtitle">{title2}:</div>
1044
- {diff["doc2_text"]}
1045
- </div>
1046
- </div>
1047
- </div>
1048
- """, unsafe_allow_html=True)
1049
- else:
1050
- st.info("لا توجد فروقات تطابق معايير التصفية المحددة.")
1051
-
1052
- # إضافة CSS للتنسيق
1053
- st.markdown("""
1054
- <style>
1055
- .module-title {
1056
- color: #1E88E5;
1057
- font-size: 1.8rem;
1058
- font-weight: bold;
1059
- margin-bottom: 1rem;
1060
- text-align: center;
1061
- }
1062
-
1063
- .similarity-card, .changes-card, .words-card {
1064
- background-color: #fff;
1065
- border-radius: 8px;
1066
- padding: 1rem;
1067
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
1068
- height: 100%;
1069
- text-align: center;
1070
- }
1071
-
1072
- .similarity-title, .changes-title, .words-title {
1073
- font-weight: bold;
1074
- font-size: 1rem;
1075
- margin-bottom: 0.5rem;
1076
- color: #333;
1077
- }
1078
-
1079
- .similarity-score {
1080
- font-size: 2.5rem;
1081
- font-weight: bold;
1082
- margin-bottom: 0.25rem;
1083
- }
1084
-
1085
- .similarity-info {
1086
- font-size: 0.8rem;
1087
- color: #666;
1088
- }
1089
-
1090
- .changes-list, .words-list {
1091
- text-align: right;
1092
- }
1093
-
1094
- .change-item, .words-item {
1095
- display: flex;
1096
- justify-content: space-between;
1097
- margin-bottom: 0.5rem;
1098
- }
1099
-
1100
- .change-label, .words-label {
1101
- color: #555;
1102
- }
1103
-
1104
- .change-value, .words-value {
1105
- font-weight: bold;
1106
- color: #333;
1107
- }
1108
-
1109
- .text-summary {
1110
- background-color: #f8f9fa;
1111
- border-right: 4px solid #1E88E5;
1112
- padding: 1rem;
1113
- margin: 1rem 0;
1114
- color: #444;
1115
- font-size: 1rem;
1116
- text-align: right;
1117
- }
1118
-
1119
- .diff-text {
1120
- padding: 0.5rem;
1121
- border-radius: 4px;
1122
- margin-bottom: 0.5rem;
1123
- white-space: pre-wrap;
1124
- }
1125
-
1126
- .diff-old {
1127
- background-color: rgba(214, 48, 49, 0.1);
1128
- border-right: 3px solid #d63031;
1129
- }
1130
-
1131
- .diff-new {
1132
- background-color: rgba(9, 132, 227, 0.1);
1133
- border-right: 3px solid #0984e3;
1134
- }
1135
-
1136
- .price-analysis, .date-analysis {
1137
- background-color: #f8f9fa;
1138
- border-radius: 8px;
1139
- padding: 1rem;
1140
- margin-bottom: 1rem;
1141
- }
1142
-
1143
- .price-summary, .date-summary {
1144
- font-size: 1rem;
1145
- margin-bottom: 0.5rem;
1146
- text-align: center;
1147
- }
1148
-
1149
- .price-details {
1150
- margin-top: 1rem;
1151
- }
1152
-
1153
- .price-row {
1154
- display: flex;
1155
- justify-content: space-between;
1156
- margin-bottom: 0.25rem;
1157
- border-bottom: 1px solid #eee;
1158
- padding-bottom: 0.25rem;
1159
- }
1160
-
1161
- .price-label {
1162
- flex: 1;
1163
- text-align: right;
1164
- font-weight: bold;
1165
- color: #555;
1166
- }
1167
-
1168
- .price-value-header {
1169
- flex: 1;
1170
- text-align: center;
1171
- font-weight: bold;
1172
- color: #333;
1173
- }
1174
-
1175
- .price-value {
1176
- flex: 1;
1177
- text-align: center;
1178
- color: #333;
1179
- }
1180
-
1181
- .date-stats {
1182
- display: flex;
1183
- justify-content: space-around;
1184
- margin-top: 0.5rem;
1185
- }
1186
-
1187
- .date-stat {
1188
- text-align: center;
1189
- }
1190
-
1191
- .date-label {
1192
- display: block;
1193
- font-size: 0.9rem;
1194
- color: #555;
1195
- }
1196
-
1197
- .date-value {
1198
- display: block;
1199
- font-size: 1.2rem;
1200
- font-weight: bold;
1201
- color: #333;
1202
- }
1203
-
1204
- .diff-block {
1205
- background-color: #fff;
1206
- border-radius: 8px;
1207
- margin-bottom: 1rem;
1208
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1209
- overflow: hidden;
1210
- }
1211
-
1212
- .diff-header {
1213
- padding: 0.5rem 1rem;
1214
- border-bottom: 1px solid #eee;
1215
- }
1216
-
1217
- .diff-title {
1218
- font-weight: bold;
1219
- color: #333;
1220
- }
1221
-
1222
- .diff-content {
1223
- padding: 1rem;
1224
- white-space: pre-wrap;
1225
- direction: rtl;
1226
- text-align: right;
1227
- }
1228
-
1229
- .diff-content-container {
1230
- display: flex;
1231
- flex-direction: column;
1232
- }
1233
-
1234
- .diff-content-old, .diff-content-new {
1235
- padding: 1rem;
1236
- white-space: pre-wrap;
1237
- direction: rtl;
1238
- text-align: right;
1239
- }
1240
-
1241
- .diff-content-old {
1242
- background-color: rgba(214, 48, 49, 0.05);
1243
- border-bottom: 1px solid #eee;
1244
- }
1245
-
1246
- .diff-content-new {
1247
- background-color: rgba(9, 132, 227, 0.05);
1248
- }
1249
-
1250
- .diff-subtitle {
1251
- font-weight: bold;
1252
- margin-bottom: 0.5rem;
1253
- color: #555;
1254
- }
1255
-
1256
- .diff-added {
1257
- border-right: 4px solid #0984e3;
1258
- }
1259
-
1260
- .diff-removed {
1261
- border-right: 4px solid #d63031;
1262
- }
1263
-
1264
- .diff-modified {
1265
- border-right: 4px solid #fdcb6e;
1266
- }
1267
-
1268
- @media (min-width: 992px) {
1269
- .diff-content-container {
1270
- flex-direction: row;
1271
- }
1272
-
1273
- .diff-content-old, .diff-content-new {
1274
- flex: 1;
1275
- }
1276
-
1277
- .diff-content-old {
1278
- border-bottom: none;
1279
- border-left: 1px solid #eee;
1280
- }
1281
- }
1282
- </style>
1283
- """, unsafe_allow_html=True)
1284
-
1285
- def render_advanced_comparison_tools(self):
1286
- """عرض أدوات المقارنة المتقدمة"""
1287
- st.markdown("<h2 class='module-title'>أدوات مقارنة المستندات المتقدمة</h2>", unsafe_allow_html=True)
1288
-
1289
- st.markdown("""
1290
- <div class="module-description">
1291
- استخدم هذه الأدوات لمقارنة مستندات العقود بشكل متقدم، واكتشاف التغييرات والفروقات بين نسخ المستندات المختلفة،
1292
- مع تحليل التغييرات القانونية والمالية والتواريخ.
1293
- </div>
1294
- """, unsafe_allow_html=True)
1295
-
1296
- # إنشاء علامات التبويب للأدوات المختلفة
1297
- tabs = st.tabs([
1298
- "مقارنة نصية مباشرة",
1299
- "مقارنة ملفات PDF",
1300
- "عرض تقارير المقارنة السابقة"
1301
- ])
1302
-
1303
- with tabs[0]:
1304
- st.markdown("### مقارنة نصية مباشرة")
1305
-
1306
- col1, col2 = st.columns(2)
1307
-
1308
- with col1:
1309
- title1 = st.text_input("عنوان المستند الأول", key="text_title1")
1310
- text1 = st.text_area("نص المستند الأول", height=300, key="text_input1")
1311
-
1312
- with col2:
1313
- title2 = st.text_input("عنوان المستند الثاني", key="text_title2")
1314
- text2 = st.text_area("نص المستند الثاني", height=300, key="text_input2")
1315
-
1316
- if st.button("قارن النصوص", key="compare_text_btn"):
1317
- if text1 and text2:
1318
- self.render_document_comparison(
1319
- text1,
1320
- text2,
1321
- title1 or "المستند الأول",
1322
- title2 or "المستند الثاني"
1323
- )
1324
- else:
1325
- st.warning("يرجى إدخال نص المستندين للمقارنة")
1326
-
1327
- with tabs[1]:
1328
- st.markdown("### مقارنة ملفات PDF")
1329
-
1330
- col1, col2 = st.columns(2)
1331
-
1332
- with col1:
1333
- title1_pdf = st.text_input("عنوان المستند الأول", key="pdf_title1")
1334
- uploaded_file1 = st.file_uploader("تحميل المستند الأول (PDF)", type=["pdf"], key="pdf_upload1")
1335
-
1336
- with col2:
1337
- title2_pdf = st.text_input("عنوان المستند الثاني", key="pdf_title2")
1338
- uploaded_file2 = st.file_uploader("تحميل المستند الثاني (PDF)", type=["pdf"], key="pdf_upload2")
1339
-
1340
- if st.button("قارن ملفات PDF", key="compare_pdf_btn"):
1341
- if uploaded_file1 is not None and uploaded_file2 is not None:
1342
- with st.spinner("جاري استخراج النصوص من ملفات PDF..."):
1343
- text1_pdf = self._extract_text_from_pdf(uploaded_file1)
1344
- text2_pdf = self._extract_text_from_pdf(uploaded_file2)
1345
-
1346
- if text1_pdf and text2_pdf:
1347
- self.render_document_comparison(
1348
- text1_pdf,
1349
- text2_pdf,
1350
- title1_pdf or uploaded_file1.name,
1351
- title2_pdf or uploaded_file2.name
1352
- )
1353
- else:
1354
- st.error("تعذر استخراج النص من ملفات PDF. يرجى التأكد من أن الملفات تحتوي على نصوص قابلة للاستخراج.")
1355
- else:
1356
- st.warning("يرجى تحميل ملفي PDF للمقارنة")
1357
-
1358
- with tabs[2]:
1359
- st.markdown("### تقارير المقارنة السابقة")
1360
-
1361
- # الحصول على تقارير المقارنة المحفوظة
1362
- reports = self.get_comparison_reports()
1363
-
1364
- if reports:
1365
- # عرض التقارير في جدول
1366
- report_data = []
1367
- for report in reports:
1368
- report_data.append({
1369
- "التاريخ": report["timestamp"],
1370
- "المستند الأول": report["title1"],
1371
- "المستند الثاني": report["title2"],
1372
- "نسبة التشابه": f"{report['similarity']}%",
1373
- "الملف": report["filename"]
1374
- })
1375
-
1376
- report_df = pd.DataFrame(report_data)
1377
- st.dataframe(report_df)
1378
-
1379
- # اختيار تقرير لعرضه
1380
- selected_report = st.selectbox(
1381
- "اختر تقريراً لعرضه",
1382
- options=[f"{r['title1']} و {r['title2']} ({r['timestamp']})" for r in reports],
1383
- format_func=lambda x: x
1384
- )
1385
-
1386
- report_index = next((i for i, r in enumerate(reports) if f"{r['title1']} و {r['title2']} ({r['timestamp']})" == selected_report), None)
1387
-
1388
- if report_index is not None and st.button("عرض التقرير المحدد"):
1389
- selected_filename = reports[report_index]["filename"]
1390
- report_data = self.load_comparison_report(selected_filename)
1391
-
1392
- if report_data:
1393
- st.success(f"تم تحميل تقرير المقارنة بنجاح")
1394
-
1395
- # عرض ملخص التقرير
1396
- st.markdown(f"### ملخص تقرير المقارنة")
1397
- st.markdown(f"**نسبة التشابه:** {report_data['similarity']}%")
1398
- st.markdown(f"**تاريخ المقارنة:** {report_data['timestamp']}")
1399
- st.markdown(f"**ملخص التغييرات:** {report_data['summary']}")
1400
-
1401
- # استخراج الاختلافات الرئيسية
1402
- key_differences = self.extract_key_differences(report_data)
1403
-
1404
- if key_differences:
1405
- st.markdown("### الاختلافات الرئيسية")
1406
-
1407
- for diff in key_differences:
1408
- st.markdown(f"#### {diff['label']} ({diff['count']})")
1409
-
1410
- if diff["type"] == "added_paragraphs":
1411
- for item in diff["items"][:5]: # عرض أول 5 فقط
1412
- st.markdown(f"<div class='diff-text diff-new'>{item}</div>", unsafe_allow_html=True)
1413
-
1414
- elif diff["type"] == "removed_paragraphs":
1415
- for item in diff["items"][:5]:
1416
- st.markdown(f"<div class='diff-text diff-old'>{item}</div>", unsafe_allow_html=True)
1417
-
1418
- elif diff["type"] == "modified_paragraphs":
1419
- for item in diff["items"][:3]:
1420
- col1, col2 = st.columns(2)
1421
- with col1:
1422
- st.markdown(f"**{report_data['title1']}:**")
1423
- st.markdown(f"<div class='diff-text diff-old'>{item['doc1_text']}</div>", unsafe_allow_html=True)
1424
- with col2:
1425
- st.markdown(f"**{report_data['title2']}:**")
1426
- st.markdown(f"<div class='diff-text diff-new'>{item['doc2_text']}</div>", unsafe_allow_html=True)
1427
-
1428
- elif diff["type"] in ["added_words", "removed_words"]:
1429
- # عرض الكلمات في شكل جدول
1430
- word_data = []
1431
- for word, count in diff["items"]:
1432
- if len(word) > 1: # تجاهل الأحرف المفردة
1433
- word_data.append({"الكلمة": word, "عدد المرات": count})
1434
-
1435
- if word_data:
1436
- word_df = pd.DataFrame(word_data)
1437
- st.dataframe(word_df)
1438
-
1439
- # تحليل التغييرات القانونية
1440
- legal_changes = self.analyze_legal_changes(report_data)
1441
-
1442
- if legal_changes:
1443
- st.markdown("### تحليل التغييرات القانونية")
1444
-
1445
- for change in legal_changes[:3]: # عرض أهم 3 فئات فقط
1446
- st.markdown(f"#### {change['label']} ({change['count']})")
1447
-
1448
- for item in change["changes"][:2]: # عرض أول مثالين فقط
1449
- col1, col2 = st.columns(2)
1450
- with col1:
1451
- st.markdown(f"**{report_data['title1']}:**")
1452
- st.markdown(f"<div class='diff-text diff-old'>{item['doc1_text']}</div>", unsafe_allow_html=True)
1453
- with col2:
1454
- st.markdown(f"**{report_data['title2']}:**")
1455
- st.markdown(f"<div class='diff-text diff-new'>{item['doc2_text']}</div>", unsafe_allow_html=True)
1456
- else:
1457
- st.error("تعذر تحميل تقرير المقارنة")
1458
- else:
1459
- st.info("لا توجد تقارير مقارنة محفوظة")
1460
-
1461
- # إضافة CSS للتنسيق
1462
- st.markdown("""
1463
- <style>
1464
- .module-title {
1465
- color: #1E88E5;
1466
- font-size: 1.8rem;
1467
- font-weight: bold;
1468
- margin-bottom: 1rem;
1469
- text-align: center;
1470
- }
1471
-
1472
- .module-description {
1473
- background-color: #f8f9fa;
1474
- border-right: 4px solid #1E88E5;
1475
- padding: 1rem;
1476
- margin-bottom: 1.5rem;
1477
- color: #444;
1478
- font-size: 1rem;
1479
- text-align: right;
1480
- }
1481
-
1482
- .diff-text {
1483
- padding: 0.5rem;
1484
- border-radius: 4px;
1485
- margin-bottom: 0.5rem;
1486
- white-space: pre-wrap;
1487
- }
1488
-
1489
- .diff-old {
1490
- background-color: rgba(214, 48, 49, 0.1);
1491
- border-right: 3px solid #d63031;
1492
- }
1493
-
1494
- .diff-new {
1495
- background-color: rgba(9, 132, 227, 0.1);
1496
- border-right: 3px solid #0984e3;
1497
- }
1498
- </style>
1499
- """, unsafe_allow_html=True)
1500
-
1501
- def render(self):
1502
- """عرض واجهة المستخدم الرئيسية للتطبيق"""
1503
- self.render_advanced_comparison_tools()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/document_comparison/document_comparison_app.py DELETED
@@ -1,1003 +0,0 @@
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/README.md DELETED
@@ -1,45 +0,0 @@
1
- # وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
2
-
3
- ## نظرة عامة
4
- تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد، مما يساعد في تقييم طبيعة الموقع بشكل أفضل قبل البدء في العمل.
5
-
6
- ## الميزات الرئيسية
7
-
8
- ### الخريطة التفاعلية
9
- - عرض جميع مواقع المشاريع على خريطة تفاعلية
10
- - إمكانية البحث عن المواقع وتصفيتها
11
- - تجميع المواقع القريبة (Clustering)
12
- - عرض خرائط حرارية لتوزيع المشاريع
13
- - أدوات قياس المسافة والمساحة
14
-
15
- ### عرض التضاريس ثلاثي الأبعاد
16
- - عرض تضاريس موقع المشروع بشكل ثلاثي الأبعاد
17
- - التحكم في نطاق العرض ومقياس الارتفاع
18
- - تحليل الارتفاعات وعرض المقطع الجانبي
19
- - إمكانية تدوير وتكبير العرض للرؤية من زوايا مختلفة
20
-
21
- ### تحليل المواقع
22
- - عرض توزيع المشاريع حسب المدينة والحالة
23
- - تحليل المسافات بين المشاريع
24
- - عرض المشاريع القريبة من مشروع محدد
25
- - رسوم بيانية توضيحية للتوزيع الجغرافي
26
-
27
- ### إدارة المواقع
28
- - إضافة مواقع جديدة
29
- - تحرير وحذف المواقع الموجودة
30
- - استيراد وتصدير بيانات المواقع بصيغ متعددة (CSV, JSON, GeoJSON)
31
-
32
- ## المتطلبات الفنية
33
- - Streamlit
34
- - Folium
35
- - PyDeck
36
- - Pandas
37
- - NumPy
38
- - Plotly
39
- - streamlit-folium
40
-
41
- ## المطورون
42
- فريق تطوير نظام WAHBI AI لتحليل العقود والمناقصات - شركة شبه الجزيرة للمقاولات
43
-
44
- ## تاريخ الإصدار
45
- مارس 2025
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/maps/__init__.py DELETED
@@ -1 +0,0 @@
1
- # ملف تهيئة وحدة الخرائط
 
 
modules/maps/interactive_map.py DELETED
@@ -1,1671 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
6
- تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد
7
- """
8
-
9
- import os
10
- import sys
11
- import streamlit as st
12
- import pandas as pd
13
- import numpy as np
14
- import pydeck as pdk
15
- import folium
16
- from folium.plugins import MarkerCluster, HeatMap, MeasureControl
17
- from streamlit_folium import folium_static
18
- import requests
19
- import json
20
- import random
21
- from typing import List, Dict, Any, Tuple, Optional
22
- import tempfile
23
- import base64
24
- from PIL import Image
25
- from io import BytesIO
26
-
27
- # إضافة مسار النظام للوصول للملفات المشتركة
28
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
29
-
30
- # استيراد مكونات واجهة المستخدم
31
- from utils.components.header import render_header
32
- from utils.components.credits import render_credits
33
- from utils.helpers import format_number, format_currency, styled_button
34
-
35
-
36
- class InteractiveMap:
37
- """فئة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد"""
38
-
39
- def __init__(self):
40
- """تهيئة وحدة الخريطة التفاعلية"""
41
- # تهيئة مجلدات حفظ البيانات
42
- self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/maps"))
43
- os.makedirs(self.data_dir, exist_ok=True)
44
-
45
- # مفاتيح API لخدمات الخرائط
46
- self.mapbox_token = os.environ.get("MAPBOX_TOKEN", "")
47
- self.opentopodata_api = "https://api.opentopodata.org/v1/srtm30m"
48
-
49
- # تهيئة حالة الجلسة
50
- if 'project_locations' not in st.session_state:
51
- st.session_state.project_locations = []
52
-
53
- if 'selected_location' not in st.session_state:
54
- st.session_state.selected_location = None
55
-
56
- if 'terrain_data' not in st.session_state:
57
- st.session_state.terrain_data = None
58
-
59
- # بيانات اختبارية للمشاريع (سيتم استبدالها بالبيانات الفعلية من قاعدة البيانات)
60
- self._initialize_sample_projects()
61
-
62
- def render(self):
63
- """عرض واجهة وحدة الخريطة التفاعلية"""
64
- # عرض الشعار والعنوان الرئيسي
65
- render_header("خريطة مواقع المشاريع التفاعلية")
66
-
67
- # تبويبات الوحدة
68
- tabs = st.tabs([
69
- "الخريطة التفاعلية",
70
- "عرض التضاريس ثلاثي الأبعاد",
71
- "تحليل المواقع",
72
- "إدارة المواقع"
73
- ])
74
-
75
- # تبويب الخريطة التفاعلية
76
- with tabs[0]:
77
- self._render_interactive_map()
78
-
79
- # تبويب عرض التضاريس ثلاثي الأبعاد
80
- with tabs[1]:
81
- self._render_3d_terrain()
82
-
83
- # تبويب تحليل المواقع
84
- with tabs[2]:
85
- self._render_location_analysis()
86
-
87
- # تبويب إدارة المواقع
88
- with tabs[3]:
89
- self._render_location_management()
90
-
91
- # عرض حقوق النشر
92
- render_credits()
93
-
94
- def _render_interactive_map(self):
95
- """عرض الخريطة التفاعلية"""
96
- st.markdown("""
97
- <div class='custom-box info-box'>
98
- <h3>🗺️ الخريطة التفاعلية لمواقع المشاريع</h3>
99
- <p>خريطة تفاعلية تعرض مواقع جميع المشاريع مع إمكانية التكبير والتصغير والتحرك.</p>
100
- <p>يمكنك النقر على أي موقع للحصول على تفاصيل المشروع.</p>
101
- </div>
102
- """, unsafe_allow_html=True)
103
-
104
- # مربع البحث
105
- search_query = st.text_input("البحث عن موقع أو مشروع", key="map_search")
106
-
107
- # أزرار تحكم للخريطة
108
- col1, col2, col3, col4 = st.columns(4)
109
-
110
- with col1:
111
- map_style = st.selectbox(
112
- "نمط الخريطة",
113
- options=["OpenStreetMap", "Stamen Terrain", "Stamen Toner", "CartoDB Positron"],
114
- key="map_style"
115
- )
116
-
117
- with col2:
118
- cluster_markers = st.checkbox("تجميع المواقع القريبة", value=True, key="cluster_markers")
119
-
120
- with col3:
121
- show_heatmap = st.checkbox("عرض خريطة حرارية للمواقع", value=False, key="show_heatmap")
122
-
123
- with col4:
124
- show_measurements = st.checkbox("أدوات القياس", value=False, key="show_measurements")
125
-
126
- # إنشاء الخريطة
127
- if len(st.session_state.project_locations) > 0:
128
- # بيانات النقاط على الخريطة
129
- locations = []
130
-
131
- # تصفية المشاريع حسب البحث
132
- filtered_projects = st.session_state.project_locations
133
- if search_query:
134
- filtered_projects = [
135
- p for p in filtered_projects
136
- if search_query.lower() in p.get("name", "").lower() or
137
- search_query.lower() in p.get("description", "").lower() or
138
- search_query.lower() in p.get("city", "").lower()
139
- ]
140
-
141
- # عرض عدد النتائج
142
- if search_query:
143
- st.markdown(f"عدد النتائج: {len(filtered_projects)}")
144
-
145
- # تحضير البيانات للخريطة
146
- heat_data = []
147
- for project in filtered_projects:
148
- locations.append({
149
- "lat": project.get("latitude"),
150
- "lon": project.get("longitude"),
151
- "name": project.get("name"),
152
- "description": project.get("description"),
153
- "city": project.get("city"),
154
- "status": project.get("status"),
155
- "project_id": project.get("project_id")
156
- })
157
- heat_data.append([project.get("latitude"), project.get("longitude"), 1])
158
-
159
- # تعيين نقطة المركز والتكبير
160
- if filtered_projects:
161
- center_lat = sum(p.get("latitude", 0) for p in filtered_projects) / len(filtered_projects)
162
- center_lon = sum(p.get("longitude", 0) for p in filtered_projects) / len(filtered_projects)
163
- zoom_level = 6 # مستوى التكبير الافتراضي
164
- else:
165
- # مركز المملكة العربية السعودية
166
- center_lat = 24.7136
167
- center_lon = 46.6753
168
- zoom_level = 5
169
-
170
- # تحديد الإسناد (attribution) بناءً على نمط الخريطة
171
- attribution = None
172
- if map_style == "OpenStreetMap":
173
- attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
174
- elif map_style.startswith("Stamen"):
175
- attribution = 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
176
- elif map_style == "CartoDB Positron":
177
- attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="https://cartodb.com/attributions">CartoDB</a>'
178
-
179
- # إنشاء الخريطة
180
- m = folium.Map(
181
- location=[center_lat, center_lon],
182
- zoom_start=zoom_level,
183
- tiles=map_style,
184
- attr=attribution # إضافة سمة الإسناد
185
- )
186
-
187
- # إضافة أدوات القياس إذا تم اختيارها
188
- if show_measurements:
189
- MeasureControl(position='topright', primary_length_unit='kilometers').add_to(m)
190
-
191
- # إضافة النقاط إلى الخريطة
192
- if cluster_markers:
193
- # إنشاء مجموعة تجميع
194
- marker_cluster = MarkerCluster(name="تجميع المشاريع").add_to(m)
195
-
196
- # إضافة النقاط إلى المجموعة
197
- for location in locations:
198
- # إنشاء النافذة المنبثقة
199
- popup_html = f"""
200
- <div style='direction: rtl; text-align: right;'>
201
- <h4>{location['name']}</h4>
202
- <p><strong>الوصف:</strong> {location['description']}</p>
203
- <p><strong>المدينة:</strong> {location['city']}</p>
204
- <p><strong>الحالة:</strong> {location['status']}</p>
205
- <p><strong>الإحداثيات:</strong> {location['lat']:.6f}, {location['lon']:.6f}</p>
206
- <button onclick="window.open('/project/{location['project_id']}', '_self')">عرض تفاصيل المشروع</button>
207
- </div>
208
- """
209
-
210
- # تحديد لون العلامة حسب حالة المشروع
211
- icon_color = 'green'
212
- if location['status'] == 'قيد التنفيذ':
213
- icon_color = 'orange'
214
- elif location['status'] == 'متوقف':
215
- icon_color = 'red'
216
- elif location['status'] == 'مكتمل':
217
- icon_color = 'blue'
218
-
219
- # إضافة العلامة
220
- folium.Marker(
221
- location=[location['lat'], location['lon']],
222
- popup=folium.Popup(popup_html, max_width=300),
223
- tooltip=location['name'],
224
- icon=folium.Icon(color=icon_color, icon='info-sign')
225
- ).add_to(marker_cluster)
226
- else:
227
- # إضافة النقاط مباشرة إلى الخريطة
228
- for location in locations:
229
- # إنشاء النافذة المنبثقة
230
- popup_html = f"""
231
- <div style='direction: rtl; text-align: right;'>
232
- <h4>{location['name']}</h4>
233
- <p><strong>الوصف:</strong> {location['description']}</p>
234
- <p><strong>المدينة:</strong> {location['city']}</p>
235
- <p><strong>الحالة:</strong> {location['status']}</p>
236
- <p><strong>الإحداثيات:</strong> {location['lat']:.6f}, {location['lon']:.6f}</p>
237
- <button onclick="window.open('/project/{location['project_id']}', '_self')">عرض تفاصيل المشروع</button>
238
- </div>
239
- """
240
-
241
- # تحديد لون العلامة حسب حالة المشروع
242
- icon_color = 'green'
243
- if location['status'] == 'قيد التنفيذ':
244
- icon_color = 'orange'
245
- elif location['status'] == 'متوقف':
246
- icon_color = 'red'
247
- elif location['status'] == 'مكتمل':
248
- icon_color = 'blue'
249
-
250
- # إضافة العلامة
251
- folium.Marker(
252
- location=[location['lat'], location['lon']],
253
- popup=folium.Popup(popup_html, max_width=300),
254
- tooltip=location['name'],
255
- icon=folium.Icon(color=icon_color, icon='info-sign')
256
- ).add_to(m)
257
-
258
- # إضافة خريطة حرارية إذا تم اختيارها
259
- if show_heatmap and heat_data:
260
- HeatMap(heat_data, radius=15).add_to(m)
261
-
262
- # إضافة طبقات متنوعة للخريطة
263
- folium.TileLayer('OpenStreetMap').add_to(m)
264
- folium.TileLayer('Stamen Terrain').add_to(m)
265
- folium.TileLayer('Stamen Toner').add_to(m)
266
- folium.TileLayer('CartoDB positron').add_to(m)
267
- folium.TileLayer('CartoDB dark_matter').add_to(m)
268
-
269
- # إضافة أدوات التحكم بالطبقات
270
- folium.LayerControl().add_to(m)
271
-
272
- # عرض الخريطة
273
- st_map = folium_static(m, width=1000, height=600)
274
-
275
- # التفاعل مع النقر على الخريطة (سيتم تنفيذه عندما يتم دعم التفاعل)
276
- # حاليًا لا يوجد دعم مباشر للتفاعل مع الخريطة في Streamlit
277
-
278
- # عرض بيانات المشاريع في جدول
279
- st.markdown("### قائمة المشاريع على الخريطة")
280
-
281
- projects_df = pd.DataFrame(filtered_projects)
282
-
283
- # إعادة تسمية الأعمدة بالعربية
284
- renamed_columns = {
285
- "name": "اسم المشروع",
286
- "city": "المدينة",
287
- "status": "الحالة",
288
- "description": "الوصف",
289
- "project_id": "معرف المشروع",
290
- "latitude": "خط العرض",
291
- "longitude": "خط الطول"
292
- }
293
-
294
- # تحديد الأعمدة للعرض
295
- display_columns = ["name", "city", "status", "project_id"]
296
-
297
- # إنشاء جدول للعرض
298
- display_df = projects_df[display_columns].rename(columns=renamed_columns)
299
-
300
- # عرض الجدول
301
- st.dataframe(display_df, width=1000, height=400)
302
-
303
- # زر لاختيار مشروع لعرض التضاريس
304
- selected_project_id = st.selectbox(
305
- "اختر مشروعًا لعرض التضاريس ثلاثية الأبعاد",
306
- options=projects_df["project_id"].tolist(),
307
- format_func=lambda x: next((p["name"] for p in filtered_projects if p["project_id"] == x), x),
308
- key="select_project_for_terrain"
309
- )
310
-
311
- # زر عرض التضاريس
312
- if styled_button("عرض التضاريس", key="show_terrain_btn", type="primary", icon="🏔️"):
313
- # العثور على المشروع المحدد
314
- selected_project = next((p for p in filtered_projects if p["project_id"] == selected_project_id), None)
315
-
316
- if selected_project:
317
- # تخزين الموقع المحدد في حالة الجلسة
318
- st.session_state.selected_location = {
319
- "latitude": selected_project["latitude"],
320
- "longitude": selected_project["longitude"],
321
- "name": selected_project["name"],
322
- "project_id": selected_project["project_id"]
323
- }
324
-
325
- # جلب بيانات التضاريس
326
- try:
327
- terrain_data = self._fetch_terrain_data(
328
- selected_project["latitude"],
329
- selected_project["longitude"]
330
- )
331
-
332
- # تخزين بيانات التضاريس في حالة الجلسة
333
- st.session_state.terrain_data = terrain_data
334
-
335
- # الانتقال إلى تبويب عرض التضاريس
336
- st.rerun()
337
- except Exception as e:
338
- st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
339
- else:
340
- st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.")
341
-
342
- def _render_3d_terrain(self):
343
- """عرض التضاريس ثلاثي الأبعاد"""
344
- st.markdown("""
345
- <div class='custom-box info-box'>
346
- <h3>🏔️ عرض التضاريس ثلاثي الأبعاد</h3>
347
- <p>عرض تفاعلي ثلاثي الأبعاد لتضاريس موقع المشروع المحدد.</p>
348
- <p>يمكنك تدوير وتكبير العرض للحصول على رؤية أفضل للموقع.</p>
349
- </div>
350
- """, unsafe_allow_html=True)
351
-
352
- # التحقق من وجود موقع محدد
353
- if st.session_state.selected_location is None:
354
- st.info("يرجى اختيار موقع من تبويب 'الخريطة التفاعلية' أولاً.")
355
-
356
- # بديل: السماح بإدخال الإحداثيات يدوياً
357
- st.markdown("### إدخال الإحداثيات يدوياً")
358
-
359
- col1, col2 = st.columns(2)
360
-
361
- with col1:
362
- manual_lat = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="manual_lat")
363
-
364
- with col2:
365
- manual_lon = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="manual_lon")
366
-
367
- if styled_button("عرض التضاريس", key="manual_terrain_btn", type="primary", icon="🏔️"):
368
- try:
369
- # جلب بيانات التضاريس
370
- terrain_data = self._fetch_terrain_data(manual_lat, manual_lon)
371
-
372
- # تخزين بيانات التضاريس والموقع في حالة الجلسة
373
- st.session_state.terrain_data = terrain_data
374
- st.session_state.selected_location = {
375
- "latitude": manual_lat,
376
- "longitude": manual_lon,
377
- "name": f"الموقع المخصص ({manual_lat:.4f}, {manual_lon:.4f})",
378
- "project_id": "custom"
379
- }
380
-
381
- # إعادة تشغيل التطبيق لتحديث العرض
382
- st.rerun()
383
- except Exception as e:
384
- st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
385
-
386
- # عرض خريطة لتحديد الموقع
387
- st.markdown("### حدد موقعًا على الخريطة")
388
- m = folium.Map(
389
- location=[24.7136, 46.6753],
390
- zoom_start=6,
391
- attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
392
- )
393
- folium_static(m, width=1000, height=500)
394
-
395
- st.info("ملاحظة: لا يمكن تحديد موقع على الخريطة مباشرة في هذا الإصدار. يرجى إدخال الإحداثيات يدوياً أو اختيار مشروع من القائمة.")
396
-
397
- return
398
-
399
- # عرض معلومات الموقع المحدد
400
- st.markdown(f"### تضاريس موقع: {st.session_state.selected_location['name']}")
401
- st.markdown(f"الإحداثيات: {st.session_state.selected_location['latitude']:.6f}, {st.session_state.selected_location['longitude']:.6f}")
402
-
403
- # التحقق من وجود بيانات التضاريس
404
- if st.session_state.terrain_data is None:
405
- st.warning("لا توجد بيانات تضاريس متاحة لهذا الموقع. جاري جلب البيانات...")
406
-
407
- try:
408
- # جلب بيانات التضاريس
409
- terrain_data = self._fetch_terrain_data(
410
- st.session_state.selected_location["latitude"],
411
- st.session_state.selected_location["longitude"]
412
- )
413
-
414
- # تخزين بيانات التضاريس في حالة الجلسة
415
- st.session_state.terrain_data = terrain_data
416
-
417
- # إعادة تشغيل التطبيق لتحديث العرض
418
- st.rerun()
419
- except Exception as e:
420
- st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
421
- return
422
-
423
- # عرض الخريطة ثنائية الأبعاد للموقع
424
- st.markdown("### خريطة الموقع")
425
-
426
- # إنشاء خريطة صغيرة للموقع
427
- mini_map = folium.Map(
428
- location=[st.session_state.selected_location["latitude"], st.session_state.selected_location["longitude"]],
429
- zoom_start=10,
430
- attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
431
- )
432
- folium.Marker(location=[st.session_state.selected_location["latitude"], st.session_state.selected_location["longitude"]], tooltip="الموقع المحدد").add_to(mini_map)
433
- folium_static(mini_map, width=700, height=300)
434
-
435
- # عرض بيانات التضاريس
436
- st.markdown("### نموذج التضاريس ثلاثي الأبعاد")
437
-
438
- # تحويل بيانات التضاريس إلى DataFrame
439
- df = pd.DataFrame(st.session_state.terrain_data)
440
-
441
- # اختيار نظام ألوان
442
- color_schemes = {
443
- "Viridis": "Viridis",
444
- "أخضر إلى بني": "Greens",
445
- "أزرق إلى أحمر": "RdBu",
446
- "أرجواني إلى أخضر": "PuGn",
447
- "نظام الارتفاعات": "Terrain"
448
- }
449
-
450
- color_scheme = st.selectbox(
451
- "نظام الألوان",
452
- options=list(color_schemes.keys()),
453
- index=4,
454
- key="3d_color_scheme"
455
- )
456
-
457
- # خيارات العرض
458
- col1, col2, col3 = st.columns(3)
459
-
460
- with col1:
461
- exaggeration = st.slider("تضخيم الارتفاع", 1, 50, 15, key="terrain_exaggeration")
462
-
463
- with col2:
464
- radius = st.slider("نطاق العرض (كم)", 1, 20, 5, key="terrain_radius")
465
-
466
- with col3:
467
- resolution = st.slider("دقة العرض", 10, 100, 50, key="terrain_resolution")
468
-
469
- if not df.empty and len(df) > 1:
470
- # إعادة جلب البيانات إذا تغير النطاق
471
- current_lat = st.session_state.selected_location["latitude"]
472
- current_lon = st.session_state.selected_location["longitude"]
473
- current_radius = radius
474
-
475
- # جلب بيانات جديدة إذا تغير النطاق
476
- if styled_button("تحديث النطاق", key="update_radius_btn"):
477
- try:
478
- # جلب بيانات التضاريس
479
- terrain_data = self._fetch_terrain_data(
480
- current_lat,
481
- current_lon,
482
- radius_km=current_radius
483
- )
484
-
485
- # تخزين بيانات التضاريس في حالة الجلسة
486
- st.session_state.terrain_data = terrain_data
487
-
488
- # إعادة تشغيل التطبيق لتحديث العرض
489
- st.rerun()
490
- except Exception as e:
491
- st.error(f"حدث خطأ أثناء تحديث بيانات التضاريس: {str(e)}")
492
-
493
- # تحويل البيانات إلى تنسيق مناسب لـ PyDeck
494
- x = df["longitude"].values
495
- y = df["latitude"].values
496
- z = df["elevation"].values * exaggeration # تضخيم الارتفاع
497
-
498
- # تطبيع الارتفاعات للحصول على ألوان مناسبة
499
- normalized_elevation = (z - z.min()) / (z.max() - z.min() if z.max() != z.min() else 1)
500
-
501
- # الحصول على نظام الألوان
502
- cmap = self._get_color_map(color_schemes[color_scheme])
503
-
504
- # إنشاء عمود الألوان
505
- df["color"] = [
506
- cmap(ne) if ne <= 1.0 else cmap(1.0)
507
- for ne in normalized_elevation
508
- ]
509
-
510
- # تهيئة عرض PyDeck
511
- view_state = pdk.ViewState(
512
- latitude=current_lat,
513
- longitude=current_lon,
514
- zoom=10,
515
- pitch=45,
516
- bearing=0
517
- )
518
-
519
- # إنشاء طبقة التضاريس
520
- terrain_layer = pdk.Layer(
521
- "ColumnLayer",
522
- data=df,
523
- get_position=["longitude", "latitude"],
524
- get_elevation="elevation * " + str(exaggeration),
525
- get_fill_color="color",
526
- get_radius=resolution,
527
- pickable=True,
528
- auto_highlight=True,
529
- elevation_scale=1,
530
- elevation_range=[0, 1000],
531
- coverage=1,
532
- )
533
-
534
- # إضافة طبقة لعلامة الموقع المحدد
535
- marker_df = pd.DataFrame({
536
- "latitude": [current_lat],
537
- "longitude": [current_lon],
538
- "size": [400]
539
- })
540
-
541
- marker_layer = pdk.Layer(
542
- "ScatterplotLayer",
543
- data=marker_df,
544
- get_position=["longitude", "latitude"],
545
- get_radius="size",
546
- get_fill_color=[255, 0, 0, 200],
547
- pickable=True,
548
- )
549
-
550
- # تهيئة العرض
551
- r = pdk.Deck(
552
- layers=[terrain_layer, marker_layer],
553
- initial_view_state=view_state,
554
- map_style="mapbox://styles/mapbox/satellite-v9",
555
- tooltip={
556
- "html": "<b>ارتفاع:</b> {elevation} متر<br/><b>إحداثيات:</b> {latitude:.6f}, {longitude:.6f}",
557
- "style": {
558
- "backgroundColor": "steelblue",
559
- "color": "white",
560
- "direction": "rtl",
561
- "text-align": "right"
562
- }
563
- }
564
- )
565
-
566
- # عرض نموذج التضاريس
567
- st.pydeck_chart(r)
568
-
569
- # إضافة معلومات إضافية
570
- st.markdown("### معلومات الارتفاع")
571
-
572
- # حساب الإحصاءات
573
- min_elevation = df["elevation"].min()
574
- max_elevation = df["elevation"].max()
575
- avg_elevation = df["elevation"].mean()
576
-
577
- # عرض الإحصاءات
578
- stat_col1, stat_col2, stat_col3 = st.columns(3)
579
-
580
- with stat_col1:
581
- st.metric("أدنى ارتفاع", f"{min_elevation:.1f} متر")
582
-
583
- with stat_col2:
584
- st.metric("متوسط الارتفاع", f"{avg_elevation:.1f} متر")
585
-
586
- with stat_col3:
587
- st.metric("أعلى ارتفاع", f"{max_elevation:.1f} متر")
588
-
589
- # زر لتصدير البيانات
590
- if styled_button("تصدير بيانات التضاريس", key="export_terrain_btn", type="secondary", icon="📊"):
591
- # تحويل البيانات إلى CSV
592
- csv = df.to_csv(index=False)
593
-
594
- # إنشاء رابط تنزيل
595
- b64 = base64.b64encode(csv.encode()).decode()
596
- href = f'<a href="data:file/csv;base64,{b64}" download="terrain_data_{st.session_state.selected_location["project_id"]}.csv" class="btn">تنزيل البيانات (CSV)</a>'
597
- st.markdown(href, unsafe_allow_html=True)
598
- else:
599
- st.error("لا توجد بيانات كافية لعرض نموذج التضاريس. حاول اختيار موقع آخر أو زيادة النطاق.")
600
-
601
- def _render_location_analysis(self):
602
- """عرض تحليل المواقع"""
603
- st.markdown("""
604
- <div class='custom-box info-box'>
605
- <h3>📊 تحليل موقع المشروع</h3>
606
- <p>تحليل متقدم لموقع المشروع وتضاريسه والظروف المحيطة.</p>
607
- <p>يمكنك تحليل الارتفاعات والمسافات وقياس التكاليف المرتبطة بالموقع.</p>
608
- </div>
609
- """, unsafe_allow_html=True)
610
-
611
- # التحقق من وجود مواقع
612
- if len(st.session_state.project_locations) == 0:
613
- st.warning("لا توجد مواقع مشاريع متاحة للتحليل. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.")
614
- return
615
-
616
- # اختيار موقع أو موقعين للتحليل
617
- analysis_type = st.radio(
618
- "نوع التحليل",
619
- options=["تحليل موقع واحد", "مقارنة موقعين"],
620
- key="location_analysis_type",
621
- horizontal=True
622
- )
623
-
624
- # تحويل المواقع إلى DataFrame
625
- projects_df = pd.DataFrame(st.session_state.project_locations)
626
-
627
- if analysis_type == "تحليل موقع واحد":
628
- # اختيار موقع للتحليل
629
- selected_project_id = st.selectbox(
630
- "اختر موقع المشروع للتحليل",
631
- options=projects_df["project_id"].tolist(),
632
- format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
633
- key="analysis_project"
634
- )
635
-
636
- # العثور على المشروع المحدد
637
- selected_project = next((p for p in st.session_state.project_locations if p["project_id"] == selected_project_id), None)
638
-
639
- if selected_project:
640
- # عرض معلومات المشروع
641
- st.markdown(f"### تحليل موقع: {selected_project['name']}")
642
-
643
- # عرض خريطة الموقع
644
- st.markdown("#### موقع المشروع")
645
-
646
- # إنشاء خريطة صغيرة للموقع
647
- m2 = folium.Map(
648
- location=[selected_project["latitude"], selected_project["longitude"]],
649
- zoom_start=10,
650
- attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
651
- )
652
- folium.Marker(location=[selected_project["latitude"], selected_project["longitude"]], tooltip=selected_project["name"]).add_to(m2)
653
-
654
- # إضافة دائرة بنصف قطر محدد
655
- analysis_radius = st.slider("نطاق التحليل (كم)", 1, 50, 10, key="analysis_radius")
656
- folium.Circle(
657
- location=[selected_project["latitude"], selected_project["longitude"]],
658
- radius=analysis_radius * 1000, # تحويل إلى أمتار
659
- color="red",
660
- fill=True,
661
- fill_opacity=0.2
662
- ).add_to(m2)
663
-
664
- folium_static(m2, width=700, height=400)
665
-
666
- # تحليل الموقع
667
- st.markdown("#### عوامل الموقع")
668
-
669
- # تحليل اعتباري للموقع (يمكن استبداله بتحليل حقيقي من خدمات مثل Google Places API)
670
-
671
- # عوامل افتراضية - ستتغير هذه باستخدام بيانات حقيقية
672
- factors = {
673
- "قرب المدينة": random.uniform(0.4, 1.0),
674
- "توفر المياه": random.uniform(0.3, 0.9),
675
- "سهولة الوصول": random.uniform(0.5, 1.0),
676
- "الظروف الجوية": random.uniform(0.6, 1.0),
677
- "التضاريس": random.uniform(0.3, 0.8),
678
- "توفر العمالة": random.uniform(0.5, 0.9),
679
- "البنية التحتية": random.uniform(0.4, 0.9),
680
- "المخاطر البيئية": random.uniform(0.3, 0.7)
681
- }
682
-
683
- # مخطط شريطي للعوامل
684
- factors_df = pd.DataFrame({
685
- "العامل": list(factors.keys()),
686
- "التقييم": list(factors.values())
687
- })
688
-
689
- # الترتيب تنازلياً
690
- factors_df = factors_df.sort_values(by="التقييم", ascending=False)
691
-
692
- # عرض الرسم البياني
693
- st.bar_chart(factors_df.set_index("العامل"))
694
-
695
- # تقييم إجمالي للموقع
696
- overall_score = sum(factors.values()) / len(factors)
697
-
698
- # عرض التقييم الإجمالي
699
- st.markdown(f"#### التقييم الإجمالي للموقع: {overall_score:.2f}/1.0")
700
-
701
- # مؤشر تقدم للتقييم
702
- st.progress(overall_score)
703
-
704
- # تصنيف التقييم
705
- if overall_score >= 0.8:
706
- rating = "ممتاز"
707
- color = "green"
708
- elif overall_score >= 0.6:
709
- rating = "جيد"
710
- color = "blue"
711
- elif overall_score >= 0.4:
712
- rating = "مقبول"
713
- color = "orange"
714
- else:
715
- rating = "ضعيف"
716
- color = "red"
717
-
718
- st.markdown(f"<h4 style='color: {color};'>تصنيف الموقع: {rating}</h4>", unsafe_allow_html=True)
719
-
720
- # توصيات للموقع
721
- st.markdown("#### توصيات الموقع")
722
-
723
- recommendations = [
724
- "تحسين طرق الوصول للموقع لزيادة كفاءة نقل المواد والمعدات.",
725
- "إجراء دراسة جيوتقنية مفصلة للتضاريس قبل البدء في أعمال الحفر.",
726
- "التأكد من توفر مصادر المياه الكافية لاحتياجات المشروع.",
727
- "التنسيق مع السلطات المحلية لتسهيل توصيل الخدمات للموقع.",
728
- "وضع خطة للتعامل مع الظروف الجوية المتقلبة في المنطقة."
729
- ]
730
-
731
- for rec in recommendations:
732
- st.markdown(f"- {rec}")
733
-
734
- # المرافق القريبة
735
- st.markdown("#### المرافق القريبة (تمثيل افتراضي)")
736
-
737
- # بيانات افتراضية للمرافق القريبة
738
- nearby_facilities = {
739
- "مستشفى": random.uniform(5, 30),
740
- "مدرسة": random.uniform(2, 15),
741
- "محطة وقود": random.uniform(2, 20),
742
- "مركز تسوق": random.uniform(3, 25),
743
- "مكتب حكومي": random.uniform(7, 35),
744
- "مطار": random.uniform(15, 100),
745
- "ميناء": random.uniform(20, 150)
746
- }
747
-
748
- # عرض المرافق في جدول
749
- facilities_df = pd.DataFrame({
750
- "المرفق": list(nearby_facilities.keys()),
751
- "المسافة (كم)": list(nearby_facilities.values())
752
- })
753
-
754
- # ترتيب حسب المسافة
755
- facilities_df = facilities_df.sort_values(by="المسافة (كم)")
756
-
757
- # عرض الجدول
758
- st.dataframe(facilities_df, width=700)
759
-
760
- # تقرير تكلفة الموقع
761
- st.markdown("#### تقديرات تكلفة الموقع")
762
-
763
- # بنود التكلفة الافتراضية
764
- cost_items = {
765
- "تكلفة تسوية الأرض": random.uniform(50000, 200000),
766
- "تكلفة البنية التحتية": random.uniform(100000, 500000),
767
- "تكلفة النقل الإضافية": random.uniform(30000, 150000),
768
- "تكلفة الحماية من المخاطر البيئية": random.uniform(20000, 100000),
769
- "تكلفة توصيل الخدمات": random.uniform(40000, 200000)
770
- }
771
-
772
- # عرض بنود التكلفة
773
- st.markdown("##### بنود التكلفة")
774
-
775
- for item, cost in cost_items.items():
776
- st.markdown(f"- {item}: {format_currency(cost)} ريال")
777
-
778
- # إجمالي التكلفة
779
- total_cost = sum(cost_items.values())
780
- st.markdown(f"##### إجمالي تكلفة الموقع: {format_currency(total_cost)} ريال")
781
-
782
- # خيارات تحسين الموقع
783
- st.markdown("#### خيارات تحسين الموقع")
784
-
785
- improvement_options = [
786
- {"name": "تسوية الأرض وإزالة العوائق", "cost": 75000, "impact": 0.15},
787
- {"name": "تحسين طرق الوصول", "cost": 120000, "impact": 0.2},
788
- {"name": "بناء نظام صرف للمياه", "cost": 90000, "impact": 0.18},
789
- {"name": "تعزيز البنية التحتية", "cost": 180000, "impact": 0.25},
790
- {"name": "نظام حماية من العوامل الجوية", "cost": 60000, "impact": 0.12}
791
- ]
792
-
793
- # عرض خيارات التحسين
794
- st.markdown("اختر خيارات التحسين لتقييم التأثير والتكلفة:")
795
-
796
- selected_improvements = []
797
- for i, option in enumerate(improvement_options):
798
- if st.checkbox(f"{option['name']} - {format_currency(option['cost'])} ريال", key=f"imp_{i}"):
799
- selected_improvements.append(option)
800
-
801
- if selected_improvements:
802
- # حساب التأثير والتكلفة الإجمالية
803
- total_impact = sum(imp["impact"] for imp in selected_improvements)
804
- total_improvement_cost = sum(imp["cost"] for imp in selected_improvements)
805
-
806
- # عرض النتائج
807
- st.markdown(f"##### تحسين التقييم المتوقع: +{total_impact:.2f}")
808
- new_score = min(1.0, overall_score + total_impact)
809
- st.markdown(f"##### التقييم الجديد المتوقع: {new_score:.2f}/1.0")
810
- st.progress(new_score)
811
-
812
- # تصنيف التقييم الجديد
813
- if new_score >= 0.8:
814
- new_rating = "ممتاز"
815
- new_color = "green"
816
- elif new_score >= 0.6:
817
- new_rating = "جيد"
818
- new_color = "blue"
819
- elif new_score >= 0.4:
820
- new_rating = "مقبول"
821
- new_color = "orange"
822
- else:
823
- new_rating = "ضعيف"
824
- new_color = "red"
825
-
826
- st.markdown(f"<h5 style='color: {new_color};'>التصنيف الجديد المتوقع: {new_rating}</h5>", unsafe_allow_html=True)
827
-
828
- # عرض التكلفة الإجمالية
829
- st.markdown(f"##### تكلفة التحسينات: {format_currency(total_improvement_cost)} ريال")
830
- else:
831
- st.error("لم يتم العثور على المشروع المحدد.")
832
- else: # مقارنة موقعين
833
- # اختيار موقعين للمقارنة
834
- col1, col2 = st.columns(2)
835
-
836
- with col1:
837
- project_id_1 = st.selectbox(
838
- "الموقع الأول",
839
- options=projects_df["project_id"].tolist(),
840
- format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
841
- key="compare_project_1"
842
- )
843
-
844
- with col2:
845
- # استبعاد الموقع الأول من الخيارات
846
- remaining_options = [pid for pid in projects_df["project_id"].tolist() if pid != project_id_1]
847
-
848
- if remaining_options:
849
- project_id_2 = st.selectbox(
850
- "الموقع الثاني",
851
- options=remaining_options,
852
- format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
853
- key="compare_project_2"
854
- )
855
- else:
856
- st.warning("يجب أن يكون هناك موقعان على الأقل للمقارنة.")
857
- return
858
-
859
- # العثور على المشروعين المحددين
860
- project_1 = next((p for p in st.session_state.project_locations if p["project_id"] == project_id_1), None)
861
- project_2 = next((p for p in st.session_state.project_locations if p["project_id"] == project_id_2), None)
862
-
863
- if project_1 and project_2:
864
- # عرض عنوان المقارنة
865
- st.markdown(f"### مقارنة بين موقعي {project_1['name']} و {project_2['name']}")
866
-
867
- # عرض خريطة توضح الموقعين
868
- st.markdown("#### الموقعان على الخريطة")
869
-
870
- # حساب المركز والزوم المناسب
871
- center_lat = (project_1["latitude"] + project_2["latitude"]) / 2
872
- center_lon = (project_1["longitude"] + project_2["longitude"]) / 2
873
-
874
- # حساب المسافة بين الموقعين
875
- distance = self._calculate_distance(
876
- project_1["latitude"], project_1["longitude"],
877
- project_2["latitude"], project_2["longitude"]
878
- )
879
-
880
- # تحديد مستوى التكبير حسب المسافة
881
- zoom_level = 12 if distance < 10 else (10 if distance < 50 else 8)
882
-
883
- # إنشاء الخريطة
884
- compare_map = folium.Map(
885
- location=[center_lat, center_lon],
886
- zoom_start=zoom_level,
887
- attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
888
- )
889
-
890
- # إضافة العلامات للموقعين
891
- folium.Marker(
892
- location=[project_1["latitude"], project_1["longitude"]],
893
- tooltip=project_1["name"],
894
- icon=folium.Icon(color="blue", icon="info-sign")
895
- ).add_to(compare_map)
896
-
897
- folium.Marker(
898
- location=[project_2["latitude"], project_2["longitude"]],
899
- tooltip=project_2["name"],
900
- icon=folium.Icon(color="red", icon="info-sign")
901
- ).add_to(compare_map)
902
-
903
- # إضافة خط يربط بين الموقعين
904
- folium.PolyLine(
905
- locations=[
906
- [project_1["latitude"], project_1["longitude"]],
907
- [project_2["latitude"], project_2["longitude"]]
908
- ],
909
- color="green",
910
- weight=3,
911
- opacity=0.7,
912
- tooltip=f"المسافة: {distance:.2f} كم"
913
- ).add_to(compare_map)
914
-
915
- # عرض الخريطة
916
- folium_static(compare_map, width=800, height=500)
917
-
918
- # عرض المسافة بين الموقعين
919
- st.markdown(f"#### المسافة بين الموقعين: {distance:.2f} كيلومتر")
920
-
921
- # مقارنة معلومات الموقعين
922
- st.markdown("#### مقارنة المعلومات الأساسية")
923
-
924
- # إنشاء جدول المقارنة
925
- comparison_data = {
926
- "المعلومات": ["المدينة", "الحالة", "خط العرض", "خط الطول", "الوصف"],
927
- project_1["name"]: [
928
- project_1.get("city", ""),
929
- project_1.get("status", ""),
930
- f"{project_1['latitude']:.6f}",
931
- f"{project_1['longitude']:.6f}",
932
- project_1.get("description", "")
933
- ],
934
- project_2["name"]: [
935
- project_2.get("city", ""),
936
- project_2.get("status", ""),
937
- f"{project_2['latitude']:.6f}",
938
- f"{project_2['longitude']:.6f}",
939
- project_2.get("description", "")
940
- ]
941
- }
942
-
943
- comparison_df = pd.DataFrame(comparison_data)
944
- st.dataframe(comparison_df, width=800)
945
-
946
- # مقارنة العوامل البيئية والمكانية
947
- st.markdown("#### مقارنة العوامل")
948
-
949
- # بيانات افتراضية للعوامل - ستتغير هذه باستخدام بيانات حقيقية
950
- factors_comparison = {
951
- "العامل": ["قرب المدينة", "توفر المياه", "سهولة الوصول", "الظروف الجوية", "التضاريس", "توفر العمالة", "البنية التحتية", "المخاطر البيئية"],
952
- project_1["name"]: [random.uniform(0.4, 1.0) for _ in range(8)],
953
- project_2["name"]: [random.uniform(0.4, 1.0) for _ in range(8)]
954
- }
955
-
956
- # تحويل إلى DataFrame
957
- factors_df = pd.DataFrame(factors_comparison)
958
-
959
- # رسم بياني شريطي للمقارنة
960
- st.bar_chart(factors_df.set_index("العامل"))
961
-
962
- # حساب إجمالي التقييم لكل موقع
963
- project_1_score = sum(factors_comparison[project_1["name"]]) / len(factors_comparison[project_1["name"]])
964
- project_2_score = sum(factors_comparison[project_2["name"]]) / len(factors_comparison[project_2["name"]])
965
-
966
- # عرض التقييم الإجمالي
967
- col1, col2 = st.columns(2)
968
-
969
- with col1:
970
- st.markdown(f"##### تقييم {project_1['name']}: {project_1_score:.2f}/1.0")
971
- st.progress(project_1_score)
972
-
973
- with col2:
974
- st.markdown(f"##### تقييم {project_2['name']}: {project_2_score:.2f}/1.0")
975
- st.progress(project_2_score)
976
-
977
- # تحديد الموقع المفضل
978
- preferred_site = project_1["name"] if project_1_score > project_2_score else project_2["name"]
979
- score_diff = abs(project_1_score - project_2_score)
980
-
981
- if score_diff < 0.1:
982
- recommendation = "الموقعان متقاربان في التقييم ويمكن اعتبارهما متكافئين."
983
- color = "blue"
984
- else:
985
- recommendation = f"الموقع الأفضل هو: {preferred_site}"
986
- color = "green"
987
-
988
- st.markdown(f"<h4 style='color: {color};'>{recommendation}</h4>", unsafe_allow_html=True)
989
-
990
- # تحليل التكلفة
991
- st.markdown("#### مقارنة تقديرات التكلفة")
992
-
993
- # بنود الت��لفة الافتراضية
994
- cost_items = ["تسوية الأرض", "البنية التحتية", "النقل", "الحماية من المخاطر", "توصيل الخدمات"]
995
-
996
- site_1_costs = [random.uniform(50000, 200000) for _ in range(len(cost_items))]
997
- site_2_costs = [random.uniform(50000, 200000) for _ in range(len(cost_items))]
998
-
999
- # إنشاء DataFrame للتكاليف
1000
- cost_df = pd.DataFrame({
1001
- "بند التكلفة": cost_items,
1002
- f"{project_1['name']} (ريال)": site_1_costs,
1003
- f"{project_2['name']} (ريال)": site_2_costs
1004
- })
1005
-
1006
- # عرض جدول التكاليف
1007
- st.dataframe(cost_df, width=800)
1008
-
1009
- # حساب إجمالي التكلفة لكل موقع
1010
- total_cost_1 = sum(site_1_costs)
1011
- total_cost_2 = sum(site_2_costs)
1012
-
1013
- # عرض إجمالي التكلفة
1014
- col1, col2 = st.columns(2)
1015
-
1016
- with col1:
1017
- st.metric(
1018
- f"إجمالي تكلفة {project_1['name']}",
1019
- f"{format_currency(total_cost_1)} ريال"
1020
- )
1021
-
1022
- with col2:
1023
- st.metric(
1024
- f"إجمالي تكلفة {project_2['name']}",
1025
- f"{format_currency(total_cost_2)} ريال",
1026
- f"{format_currency(total_cost_2 - total_cost_1)}"
1027
- )
1028
-
1029
- # تحليل إضافي للمقارنة
1030
- st.markdown("#### ملخص المقارنة")
1031
-
1032
- comparison_summary = f"""
1033
- بناءً على التحليل المقدم، يمكن استخلاص الملاحظات التالية:
1034
-
1035
- 1. **المسافة بين الموقعين:** {distance:.2f} كيلومتر.
1036
- 2. **التقييم:** {project_1['name']} بتقييم {project_1_score:.2f}/1.0، و{project_2['name']} بتقييم {project_2_score:.2f}/1.0.
1037
- 3. **التكلفة:** {project_1['name']} بتكلفة {format_currency(total_cost_1)} ريال، و{project_2['name']} بتكلفة {format_currency(total_cost_2)} ريال.
1038
-
1039
- بالنظر إلى العوامل أعلاه، فإن الموقع **{preferred_site}** هو الخيار الأفضل من حيث التوازن بين التقييم والتكلفة.
1040
- """
1041
-
1042
- st.markdown(comparison_summary)
1043
- else:
1044
- st.error("لم يتم العثور على أحد المشروعين المحددين.")
1045
-
1046
- def _render_location_management(self):
1047
- """عرض إدارة المواقع"""
1048
- st.markdown("""
1049
- <div class='custom-box info-box'>
1050
- <h3>📍 إدارة مواقع المشاريع</h3>
1051
- <p>إضافة وتعديل مواقع المشاريع وتصدير واستيراد البيانات.</p>
1052
- <p>يمكنك إدخال مواقع المشاريع الجديدة وتعديل المواقع الموجودة وحذفها.</p>
1053
- </div>
1054
- """, unsafe_allow_html=True)
1055
-
1056
- # تبويبات فرعية للإدارة
1057
- subtabs = st.tabs([
1058
- "إضافة موقع جديد",
1059
- "تحرير المواقع",
1060
- "استيراد/تصدير المواقع"
1061
- ])
1062
-
1063
- # تبويب إضافة موقع جديد
1064
- with subtabs[0]:
1065
- self._render_add_location()
1066
-
1067
- # تبويب تحرير المواقع
1068
- with subtabs[1]:
1069
- self._render_edit_locations()
1070
-
1071
- # تبويب استيراد/تصدير المواقع
1072
- with subtabs[2]:
1073
- self._render_import_export_locations()
1074
-
1075
- def _render_add_location(self):
1076
- """عرض نموذج إضافة موقع جديد"""
1077
- st.markdown("### إضافة موقع مشروع جديد")
1078
-
1079
- # نموذج إضافة موقع جديد
1080
- with st.form(key="add_location_form"):
1081
- # معلومات أساسية
1082
- project_name = st.text_input("اسم المشروع", key="new_project_name")
1083
- project_description = st.text_area("وصف المشروع", key="new_project_description")
1084
-
1085
- # معلومات الموقع
1086
- col1, col2 = st.columns(2)
1087
-
1088
- with col1:
1089
- city = st.text_input("المدينة", key="new_city")
1090
- status = st.selectbox(
1091
- "حالة المشروع",
1092
- options=["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"],
1093
- key="new_status"
1094
- )
1095
-
1096
- with col2:
1097
- latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="new_latitude")
1098
- longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="new_longitude")
1099
-
1100
- # عرض الموقع على خريطة صغيرة
1101
- mini_map = folium.Map(
1102
- location=[latitude, longitude],
1103
- zoom_start=10,
1104
- attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
1105
- )
1106
- folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map)
1107
- folium_static(mini_map, width=700, height=300)
1108
-
1109
- # زر الإضافة
1110
- submit_button = st.form_submit_button("إضافة الموقع")
1111
-
1112
- # معالجة النموذج عند الإرسال
1113
- if submit_button:
1114
- if not project_name:
1115
- st.error("يرجى إدخال اسم المشروع.")
1116
- else:
1117
- # إنشاء معرف فريد للمشروع
1118
- project_id = f"PRJ{len(st.session_state.project_locations) + 1:03d}"
1119
-
1120
- # إضافة المشروع الجديد
1121
- new_project = {
1122
- "project_id": project_id,
1123
- "name": project_name,
1124
- "description": project_description,
1125
- "city": city,
1126
- "status": status,
1127
- "latitude": latitude,
1128
- "longitude": longitude
1129
- }
1130
-
1131
- # إضافة المشروع إلى القائمة
1132
- st.session_state.project_locations.append(new_project)
1133
-
1134
- # حفظ البيانات
1135
- self._save_locations_data()
1136
-
1137
- # عرض رسالة نجاح
1138
- st.success(f"تمت إضافة موقع المشروع '{project_name}' بنجاح.")
1139
-
1140
- # إعادة تحميل الصفحة
1141
- st.rerun()
1142
-
1143
- def _render_edit_locations(self):
1144
- """عرض واجهة تحرير المواقع الموجودة"""
1145
- st.markdown("### تحرير مواقع المشاريع")
1146
-
1147
- if len(st.session_state.project_locations) == 0:
1148
- st.warning("لا توجد مواقع مشاريع للتحرير. يرجى إضافة مواقع أولاً.")
1149
- return
1150
-
1151
- # عرض قائمة المشاريع
1152
- projects_df = pd.DataFrame(st.session_state.project_locations)
1153
-
1154
- # إعادة تسمية الأعمدة بالعربية
1155
- renamed_columns = {
1156
- "name": "اسم المشروع",
1157
- "city": "المدينة",
1158
- "status": "الحالة",
1159
- "description": "الوصف",
1160
- "project_id": "معرف المشروع",
1161
- "latitude": "خط العرض",
1162
- "longitude": "خط الطول"
1163
- }
1164
-
1165
- # تحديد الأعمدة للعرض
1166
- display_columns = ["project_id", "name", "city", "status"]
1167
-
1168
- # إنشاء جدول للعرض
1169
- display_df = projects_df[display_columns].rename(columns=renamed_columns)
1170
-
1171
- # عرض الجدول
1172
- st.dataframe(display_df, width=800, height=300)
1173
-
1174
- # اختيار مشروع للتحرير
1175
- selected_project_id = st.selectbox(
1176
- "اختر مشروعًا للتحرير",
1177
- options=projects_df["project_id"].tolist(),
1178
- format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
1179
- key="edit_project_id"
1180
- )
1181
-
1182
- # العثور على المشروع المحدد
1183
- selected_project_index = next((i for i, p in enumerate(st.session_state.project_locations) if p["project_id"] == selected_project_id), None)
1184
-
1185
- if selected_project_index is not None:
1186
- selected_project = st.session_state.project_locations[selected_project_index]
1187
-
1188
- # نموذج تحرير المشروع
1189
- with st.form(key="edit_location_form"):
1190
- st.markdown(f"### تحرير مشروع: {selected_project['name']}")
1191
-
1192
- # معلومات أساسية
1193
- project_name = st.text_input("اسم المشروع", value=selected_project["name"], key="edit_project_name")
1194
- project_description = st.text_area("وصف المشروع", value=selected_project.get("description", ""), key="edit_project_description")
1195
-
1196
- # معلومات الموقع
1197
- col1, col2 = st.columns(2)
1198
-
1199
- with col1:
1200
- city = st.text_input("المدينة", value=selected_project.get("city", ""), key="edit_city")
1201
- status = st.selectbox(
1202
- "حالة المشروع",
1203
- options=["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"],
1204
- index=["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"].index(selected_project.get("status", "مخطط")),
1205
- key="edit_status"
1206
- )
1207
-
1208
- with col2:
1209
- latitude = st.number_input("خط العرض", value=selected_project["latitude"], step=0.0001, format="%.6f", key="edit_latitude")
1210
- longitude = st.number_input("خط الطول", value=selected_project["longitude"], step=0.0001, format="%.6f", key="edit_longitude")
1211
-
1212
- # عرض الموقع على خريطة صغيرة
1213
- mini_map = folium.Map(
1214
- location=[latitude, longitude],
1215
- zoom_start=10,
1216
- attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
1217
- )
1218
- folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map)
1219
- folium_static(mini_map, width=700, height=300)
1220
-
1221
- # أزرار الإجراءات
1222
- col1, col2 = st.columns(2)
1223
-
1224
- with col1:
1225
- update_button = st.form_submit_button("تحديث المعلومات")
1226
-
1227
- with col2:
1228
- delete_button = st.form_submit_button("حذف المشروع", type="secondary")
1229
-
1230
- # معالجة تحديث المعلومات
1231
- if update_button:
1232
- if not project_name:
1233
- st.error("لا يمكن ترك اسم المشروع فارغًا.")
1234
- else:
1235
- # تحديث معلومات المشروع
1236
- st.session_state.project_locations[selected_project_index] = {
1237
- "project_id": selected_project["project_id"],
1238
- "name": project_name,
1239
- "description": project_description,
1240
- "city": city,
1241
- "status": status,
1242
- "latitude": latitude,
1243
- "longitude": longitude
1244
- }
1245
-
1246
- # حفظ البيانات
1247
- self._save_locations_data()
1248
-
1249
- # عرض رسالة نجاح
1250
- st.success(f"تم تحديث معلومات المشروع '{project_name}' بنجاح.")
1251
-
1252
- # إعادة تحميل الصفحة
1253
- st.rerun()
1254
-
1255
- # معالجة حذف المشروع
1256
- if delete_button:
1257
- # نافذة تأكيد الحذف
1258
- st.warning(f"هل أنت متأكد من رغبتك في حذف المشروع '{selected_project['name']}'؟")
1259
-
1260
- confirm_col1, confirm_col2 = st.columns(2)
1261
-
1262
- with confirm_col1:
1263
- if st.button("نعم، حذف المشروع", key="confirm_delete"):
1264
- # حذف المشروع
1265
- st.session_state.project_locations.pop(selected_project_index)
1266
-
1267
- # حفظ البيانات
1268
- self._save_locations_data()
1269
-
1270
- # عرض رسالة نجاح
1271
- st.success(f"تم حذف المشروع '{selected_project['name']}' بنجاح.")
1272
-
1273
- # إعادة تحميل الصفحة
1274
- st.rerun()
1275
-
1276
- with confirm_col2:
1277
- if st.button("لا، إلغاء الحذف", key="cancel_delete"):
1278
- st.rerun()
1279
- else:
1280
- st.error("لم يتم العثور على المشروع المحدد.")
1281
-
1282
- def _render_import_export_locations(self):
1283
- """عرض واجهة استيراد وتصدير المواقع"""
1284
- st.markdown("### استيراد وتصدير مواقع المشاريع")
1285
-
1286
- # تبويبات فرعية للاستيراد والتصدير
1287
- export_tab, import_tab = st.tabs(["تصدير المواقع", "استيراد المواقع"])
1288
-
1289
- # تبويب تصدير المواقع
1290
- with export_tab:
1291
- st.markdown("#### تصدير مواقع المشاريع")
1292
-
1293
- if len(st.session_state.project_locations) == 0:
1294
- st.warning("لا توجد مواقع مشاريع للتصدير.")
1295
- else:
1296
- # اختيار تنسيق التصدير
1297
- export_format = st.radio(
1298
- "اختر تنسيق التصدير",
1299
- options=["CSV", "Excel", "JSON"],
1300
- horizontal=True,
1301
- key="export_format"
1302
- )
1303
-
1304
- # زر التصدير
1305
- if styled_button("تصدير المواقع", key="export_btn", type="primary", icon="📤"):
1306
- # تصدير البيانات
1307
- exported_data = self._export_locations(export_format.lower())
1308
-
1309
- if exported_data:
1310
- # تحديد نوع الملف ومعلومات التنزيل
1311
- if export_format == "CSV":
1312
- mime_type = "text/csv"
1313
- file_ext = "csv"
1314
- elif export_format == "Excel":
1315
- mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
1316
- file_ext = "xlsx"
1317
- else: # JSON
1318
- mime_type = "application/json"
1319
- file_ext = "json"
1320
-
1321
- # إنشاء رابط التنزيل
1322
- b64 = base64.b64encode(exported_data).decode()
1323
- href = f'<a href="data:{mime_type};base64,{b64}" download="project_locations.{file_ext}" class="btn">تنزيل ملف {export_format}</a>'
1324
- st.markdown(href, unsafe_allow_html=True)
1325
-
1326
- # عرض معاينة البيانات
1327
- if export_format == "CSV":
1328
- st.markdown("#### معاينة البيانات المصدرة")
1329
- st.text(exported_data.decode("utf-8"))
1330
- elif export_format == "JSON":
1331
- st.markdown("#### معاينة البيانات المصدرة")
1332
- st.json(json.loads(exported_data.decode("utf-8")))
1333
-
1334
- # تبويب استيراد المواقع
1335
- with import_tab:
1336
- st.markdown("#### استيراد مواقع المشاريع")
1337
-
1338
- # اختيار تنسيق الاستيراد
1339
- import_format = st.radio(
1340
- "اختر تنسيق الاستيراد",
1341
- options=["CSV", "Excel", "JSON"],
1342
- horizontal=True,
1343
- key="import_format"
1344
- )
1345
-
1346
- # تحميل الملف
1347
- uploaded_file = st.file_uploader(f"تحميل ملف {import_format}", type=[import_format.lower()])
1348
-
1349
- if uploaded_file:
1350
- # معاينة الملف
1351
- st.markdown("#### معاينة الملف المحمل")
1352
-
1353
- if import_format == "CSV":
1354
- df = pd.read_csv(uploaded_file)
1355
- st.dataframe(df)
1356
- elif import_format == "Excel":
1357
- df = pd.read_excel(uploaded_file)
1358
- st.dataframe(df)
1359
- else: # JSON
1360
- json_data = json.load(uploaded_file)
1361
- st.json(json_data)
1362
-
1363
- # خيارات الاستيراد
1364
- import_mode = st.radio(
1365
- "طريقة الاستيراد",
1366
- options=["إضافة إلى المواقع الحالية", "استبدال جميع المواقع"],
1367
- key="import_mode"
1368
- )
1369
-
1370
- # زر الاستيراد
1371
- if styled_button("استيراد المواقع", key="import_btn", type="primary", icon="📥"):
1372
- # إعادة قراءة الملف (قد يكون تم استنفاد التدفق)
1373
- uploaded_file.seek(0)
1374
-
1375
- try:
1376
- # استيراد البيانات
1377
- imported_count = self._import_locations(uploaded_file, import_format.lower())
1378
-
1379
- if import_mode == "استبدال جميع المواقع":
1380
- st.success(f"تم استبدال جميع المواقع بنجاح. عدد المواقع الجديدة: {imported_count}")
1381
- else:
1382
- st.success(f"تمت إضافة {imported_count} مواقع جديدة بنجاح.")
1383
-
1384
- # إعادة تحميل الصفحة
1385
- st.rerun()
1386
- except Exception as e:
1387
- st.error(f"حدث خطأ أثناء استيراد البيانات: {str(e)}")
1388
-
1389
- def _fetch_terrain_data(self, latitude, longitude, radius_km=5):
1390
- """جلب بيانات التضاريس من واجهة برمجة التطبيقات"""
1391
- # حساب نطاق الإحداثيات
1392
- # 1 درجة تقريبًا = 111 كم
1393
- delta = radius_km / 111.0
1394
-
1395
- # إنشاء شبكة نقاط
1396
- lat_min, lat_max = latitude - delta, latitude + delta
1397
- lon_min, lon_max = longitude - delta, longitude + delta
1398
-
1399
- # عدد نقاط الشبكة
1400
- grid_size = 20
1401
-
1402
- # إنشاء شبكة إحداثيات
1403
- lats = np.linspace(lat_min, lat_max, grid_size)
1404
- lons = np.linspace(lon_min, lon_max, grid_size)
1405
-
1406
- # تهيئة مصفوفة النتائج
1407
- results = []
1408
-
1409
- # بناء سلسلة الإحداثيات للطلب
1410
- locations = []
1411
- for lat in lats:
1412
- for lon in lons:
1413
- locations.append(f"{lat:.6f},{lon:.6f}")
1414
-
1415
- # تقسيم الطلبات إلى مجموعات (واجهة البرمجة تقبل 100 نقطة كحد أقصى)
1416
- batch_size = 100
1417
- for i in range(0, len(locations), batch_size):
1418
- batch = locations[i:i+batch_size]
1419
-
1420
- # محاولة استخدام خدمة OpenTopoData
1421
- try:
1422
- url = f"{self.opentopodata_api}?locations={'|'.join(batch)}"
1423
- response = requests.get(url)
1424
-
1425
- if response.status_code == 200:
1426
- data = response.json()
1427
- if "results" in data:
1428
- for result in data["results"]:
1429
- if "elevation" in result:
1430
- results.append({
1431
- "latitude": result["location"]["lat"],
1432
- "longitude": result["location"]["lng"],
1433
- "elevation": result["elevation"]
1434
- })
1435
- else:
1436
- # استخدام بيانات افتراضية في حالة فشل الطلب
1437
- st.warning(f"فشل جلب بيانات التضاريس من الخدمة (رمز الحالة: {response.status_code}). استخدام بيانات افتراضية.")
1438
-
1439
- # إنشاء بيانات افتراضية
1440
- for j, loc in enumerate(batch):
1441
- lat, lon = map(float, loc.split(","))
1442
- # حساب ارتفاع افتراضي بناءً على المسافة من المركز
1443
- dist = self._calculate_distance(latitude, longitude, lat, lon)
1444
- # إضافة ضوضاء عشوائية للحصول على تضاريس أكثر واقعية
1445
- noise = np.sin(lat * 10) * np.cos(lon * 10) * 50
1446
- elevation = 500 - dist * 100 + noise
1447
-
1448
- results.append({
1449
- "latitude": lat,
1450
- "longitude": lon,
1451
- "elevation": max(0, elevation)
1452
- })
1453
- except Exception as e:
1454
- st.warning(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}. استخدام بيانات افتراضية.")
1455
-
1456
- # إنشاء بيانات افتراضية
1457
- for j, loc in enumerate(batch):
1458
- lat, lon = map(float, loc.split(","))
1459
- # حساب ارتفاع افتراضي بناءً على المسافة من المركز
1460
- dist = self._calculate_distance(latitude, longitude, lat, lon)
1461
- # إضافة ضوضاء عشوائية للحصول على تضاريس أكثر واقعية
1462
- noise = np.sin(lat * 10) * np.cos(lon * 10) * 50
1463
- elevation = 500 - dist * 100 + noise
1464
-
1465
- results.append({
1466
- "latitude": lat,
1467
- "longitude": lon,
1468
- "elevation": max(0, elevation)
1469
- })
1470
-
1471
- return results
1472
-
1473
- def _calculate_distance(self, lat1, lon1, lat2, lon2):
1474
- """حساب المسافة بين نقطتين بالكيلومترات باستخدام صيغة هافرساين"""
1475
- from math import radians, sin, cos, sqrt, atan2
1476
-
1477
- # تحويل الإحداثيات إلى راديان
1478
- lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
1479
-
1480
- # صيغة هافرساين
1481
- dlon = lon2 - lon1
1482
- dlat = lat2 - lat1
1483
- a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
1484
- c = 2 * atan2(sqrt(a), sqrt(1-a))
1485
- distance = 6371 * c # نصف قطر الأرض بالكيلومترات
1486
-
1487
- return distance
1488
-
1489
- def _get_color_map(self, scheme):
1490
- """الحصول على خريطة الألوان حسب النظام المختار"""
1491
- import matplotlib.cm as cm
1492
- import matplotlib.colors as colors
1493
-
1494
- # الحصو�� على خريطة الألوان
1495
- colormap = cm.get_cmap(scheme)
1496
-
1497
- # إرجاع دالة لتطبيق خريطة الألوان
1498
- return lambda x: colors.rgb2hex(colormap(x))
1499
-
1500
- def _export_locations(self, format):
1501
- """تصدير مواقع المشاريع إلى ملف"""
1502
- try:
1503
- # تحويل البيانات إلى DataFrame
1504
- df = pd.DataFrame(st.session_state.project_locations)
1505
-
1506
- # تصدير البيانات حسب التنسيق المطلوب
1507
- if format == "csv":
1508
- csv_data = df.to_csv(index=False).encode("utf-8")
1509
- return csv_data
1510
- elif format == "excel":
1511
- # إنشاء ملف إكسل مؤقت
1512
- with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as temp:
1513
- df.to_excel(temp.name, index=False, engine="xlsxwriter")
1514
- temp.flush()
1515
-
1516
- # قراءة الملف كبيانات ثنائية
1517
- with open(temp.name, "rb") as f:
1518
- excel_data = f.read()
1519
-
1520
- # حذف الملف المؤقت
1521
- os.unlink(temp.name)
1522
-
1523
- return excel_data
1524
- elif format == "json":
1525
- json_data = json.dumps(st.session_state.project_locations, ensure_ascii=False, indent=4).encode("utf-8")
1526
- return json_data
1527
- else:
1528
- st.error(f"تنسيق غير مدعوم: {format}")
1529
- return None
1530
- except Exception as e:
1531
- st.error(f"حدث خطأ أثناء تصدير البيانات: {str(e)}")
1532
- return None
1533
-
1534
- def _import_locations(self, uploaded_file, format):
1535
- """استيراد مواقع المشاريع من ملف"""
1536
- try:
1537
- imported_data = []
1538
-
1539
- # تحميل البيانات حسب التنسيق
1540
- if format == "csv":
1541
- df = pd.read_csv(uploaded_file)
1542
- imported_data = df.to_dict("records")
1543
- elif format == "excel":
1544
- df = pd.read_excel(uploaded_file)
1545
- imported_data = df.to_dict("records")
1546
- elif format == "json":
1547
- imported_data = json.load(uploaded_file)
1548
- else:
1549
- raise ValueError(f"تنسيق غير مدعوم: {format}")
1550
-
1551
- # التحقق من صحة البيانات
1552
- required_fields = ["project_id", "name", "latitude", "longitude"]
1553
-
1554
- for item in imported_data:
1555
- missing_fields = [field for field in required_fields if field not in item]
1556
-
1557
- if missing_fields:
1558
- raise ValueError(f"الحقول المطلوبة مفقودة: {', '.join(missing_fields)}")
1559
-
1560
- # تحديث البيانات
1561
- if "import_mode" in st.session_state and st.session_state.import_mode == "استبدال جميع المواقع":
1562
- # استبدال جميع البيانات
1563
- st.session_state.project_locations = imported_data
1564
- else:
1565
- # إضافة البيانات الجديدة فقط
1566
- existing_ids = {p["project_id"] for p in st.session_state.project_locations}
1567
- new_items = [item for item in imported_data if item["project_id"] not in existing_ids]
1568
- st.session_state.project_locations.extend(new_items)
1569
- imported_data = new_items
1570
-
1571
- # حفظ البيانات
1572
- self._save_locations_data()
1573
-
1574
- return len(imported_data)
1575
- except Exception as e:
1576
- raise Exception(f"حدث خطأ أثناء استيراد البيانات: {str(e)}")
1577
-
1578
- def _save_locations_data(self):
1579
- """حفظ بيانات المواقع"""
1580
- try:
1581
- # إنشاء مسار الملف
1582
- file_path = os.path.join(self.data_dir, "project_locations.json")
1583
-
1584
- # حفظ البيانات كملف JSON
1585
- with open(file_path, "w", encoding="utf-8") as f:
1586
- json.dump(st.session_state.project_locations, f, ensure_ascii=False, indent=4)
1587
- except Exception as e:
1588
- st.error(f"حدث خطأ أثناء حفظ بيانات المواقع: {str(e)}")
1589
-
1590
- def _load_locations_data(self):
1591
- """تحميل بيانات المواقع"""
1592
- try:
1593
- # إنشاء مسار الملف
1594
- file_path = os.path.join(self.data_dir, "project_locations.json")
1595
-
1596
- # التحقق من وجود الملف
1597
- if os.path.exists(file_path):
1598
- # تحميل البيانات من ملف JSON
1599
- with open(file_path, "r", encoding="utf-8") as f:
1600
- st.session_state.project_locations = json.load(f)
1601
- else:
1602
- # تهيئة بيانات اختبارية
1603
- self._initialize_sample_projects()
1604
- except Exception as e:
1605
- st.error(f"حدث خطأ أثناء تحميل بيانات المواقع: {str(e)}")
1606
- # تهيئة بيانات اختبارية
1607
- self._initialize_sample_projects()
1608
-
1609
- def _initialize_sample_projects(self):
1610
- """تهيئة بيانات اختبارية للمشاريع"""
1611
- # قائمة بأسماء مدن المملكة العربية السعودية
1612
- saudi_cities = [
1613
- {"name": "الرياض", "lat": 24.7136, "lon": 46.6753},
1614
- {"name": "جدة", "lat": 21.4858, "lon": 39.1925},
1615
- {"name": "مكة المكرمة", "lat": 21.3891, "lon": 39.8579},
1616
- {"name": "المدينة المنورة", "lat": 24.5247, "lon": 39.5692},
1617
- {"name": "الدمام", "lat": 26.4207, "lon": 50.0888},
1618
- {"name": "الطائف", "lat": 21.2704, "lon": 40.4157},
1619
- {"name": "تبوك", "lat": 28.3835, "lon": 36.5662},
1620
- {"name": "بريدة", "lat": 26.3267, "lon": 43.9717},
1621
- {"name": "الخبر", "lat": 26.2172, "lon": 50.1971},
1622
- {"name": "أبها", "lat": 18.2164, "lon": 42.5053}
1623
- ]
1624
-
1625
- # قائمة بأنواع المشاريع
1626
- project_types = [
1627
- "إنشاء مبنى سكني",
1628
- "تطوير طريق سريع",
1629
- "بناء جسر",
1630
- "إنشاء مدرسة",
1631
- "تطوير حديقة عامة",
1632
- "بناء مستشفى",
1633
- "إنشاء محطة تحلية مياه",
1634
- "تطوير مركز تجاري",
1635
- "بناء مصنع",
1636
- "توسعة مطار"
1637
- ]
1638
-
1639
- # قائمة بحالات المشاريع
1640
- project_statuses = ["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"]
1641
-
1642
- # إنشاء مشاريع اختبارية
1643
- sample_projects = []
1644
-
1645
- for i in range(10):
1646
- city = saudi_cities[i]
1647
-
1648
- # إضافة اختلاف عشوائي صغير للإحداثيات
1649
- lat_offset = random.uniform(-0.05, 0.05)
1650
- lon_offset = random.uniform(-0.05, 0.05)
1651
-
1652
- project = {
1653
- "project_id": f"PRJ{i+1:03d}",
1654
- "name": f"{project_types[i]} في {city['name']}",
1655
- "description": f"مشروع {project_types[i]} بمدينة {city['name']}. هذا وصف اختباري للمشروع يوضح تفاصيله وأهدافه ونطاق العمل.",
1656
- "city": city["name"],
1657
- "status": random.choice(project_statuses),
1658
- "latitude": city["lat"] + lat_offset,
1659
- "longitude": city["lon"] + lon_offset
1660
- }
1661
-
1662
- sample_projects.append(project)
1663
-
1664
- # حفظ المشاريع الاختبارية في حالة الجلسة
1665
- st.session_state.project_locations = sample_projects
1666
-
1667
-
1668
- if __name__ == "__main__":
1669
- """تشغيل وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد بشكل مستقل"""
1670
- interactive_map = InteractiveMap()
1671
- interactive_map.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/maps/interactive_map.py.bak DELETED
@@ -1,1647 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
6
- تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد
7
- """
8
-
9
- import os
10
- import sys
11
- import streamlit as st
12
- import pandas as pd
13
- import numpy as np
14
- import pydeck as pdk
15
- import folium
16
- from folium.plugins import MarkerCluster, HeatMap, MeasureControl
17
- from streamlit_folium import folium_static
18
- import requests
19
- import json
20
- import random
21
- from typing import List, Dict, Any, Tuple, Optional
22
- import tempfile
23
- import base64
24
- from PIL import Image
25
- from io import BytesIO
26
-
27
- # إضافة مسار النظام للوصول للملفات المشتركة
28
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
29
-
30
- # استيراد مكونات واجهة المستخدم
31
- from utils.components.header import render_header
32
- from utils.components.credits import render_credits
33
- from utils.helpers import format_number, format_currency, styled_button
34
-
35
-
36
- class InteractiveMap:
37
- """فئة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد"""
38
-
39
- def __init__(self):
40
- """تهيئة وحدة الخريطة التفاعلية"""
41
- # تهيئة مجلدات حفظ البيانات
42
- self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/maps"))
43
- os.makedirs(self.data_dir, exist_ok=True)
44
-
45
- # مفاتيح API لخدمات الخرائط
46
- self.mapbox_token = os.environ.get("MAPBOX_TOKEN", "")
47
- self.opentopodata_api = "https://api.opentopodata.org/v1/srtm30m"
48
-
49
- # تهيئة حالة الجلسة
50
- if 'project_locations' not in st.session_state:
51
- st.session_state.project_locations = []
52
-
53
- if 'selected_location' not in st.session_state:
54
- st.session_state.selected_location = None
55
-
56
- if 'terrain_data' not in st.session_state:
57
- st.session_state.terrain_data = None
58
-
59
- # بيانات اختبارية للمشاريع (سيتم استبدالها بالبيانات الفعلية من قاعدة البيانات)
60
- self._initialize_sample_projects()
61
-
62
- def render(self):
63
- """عرض واجهة وحدة الخريطة التفاعلية"""
64
- # عرض الشعار والعنوان الرئيسي
65
- render_header("خريطة مواقع المشاريع التفاعلية")
66
-
67
- # تبويبات الوحدة
68
- tabs = st.tabs([
69
- "الخريطة التفاعلية",
70
- "عرض التضاريس ثلاثي الأبعاد",
71
- "تحليل المواقع",
72
- "إدارة المواقع"
73
- ])
74
-
75
- # تبويب الخريطة التفاعلية
76
- with tabs[0]:
77
- self._render_interactive_map()
78
-
79
- # تبويب عرض التضاريس ثلاثي الأبعاد
80
- with tabs[1]:
81
- self._render_3d_terrain()
82
-
83
- # تبويب تحليل المواقع
84
- with tabs[2]:
85
- self._render_location_analysis()
86
-
87
- # تبويب إدارة المواقع
88
- with tabs[3]:
89
- self._render_location_management()
90
-
91
- # عرض حقوق النشر
92
- render_credits()
93
-
94
- def _render_interactive_map(self):
95
- """عرض الخريطة التفاعلية"""
96
- st.markdown("""
97
- <div class='custom-box info-box'>
98
- <h3>🗺️ الخريطة التفاعلية لمواقع المشاريع</h3>
99
- <p>خريطة تفاعلية تعرض مواقع جميع المشاريع مع إمكانية التكبير والتصغير والتحرك.</p>
100
- <p>يمكنك النقر على أي موقع للحصول على تفاصيل المشروع.</p>
101
- </div>
102
- """, unsafe_allow_html=True)
103
-
104
- # مربع البحث
105
- search_query = st.text_input("البحث عن موقع أو مشروع", key="map_search")
106
-
107
- # أزرار تحكم للخريطة
108
- col1, col2, col3, col4 = st.columns(4)
109
-
110
- with col1:
111
- map_style = st.selectbox(
112
- "نمط الخريطة",
113
- options=["OpenStreetMap", "Stamen Terrain", "Stamen Toner", "CartoDB Positron"],
114
- key="map_style"
115
- )
116
-
117
- with col2:
118
- cluster_markers = st.checkbox("تجميع المواقع القريبة", value=True, key="cluster_markers")
119
-
120
- with col3:
121
- show_heatmap = st.checkbox("عرض خريطة حرارية للمواقع", value=False, key="show_heatmap")
122
-
123
- with col4:
124
- show_measurements = st.checkbox("أدوات القياس", value=False, key="show_measurements")
125
-
126
- # إنشاء الخريطة
127
- if len(st.session_state.project_locations) > 0:
128
- # بيانات النقاط على الخريطة
129
- locations = []
130
-
131
- # تصفية المشاريع حسب البحث
132
- filtered_projects = st.session_state.project_locations
133
- if search_query:
134
- filtered_projects = [
135
- p for p in filtered_projects
136
- if search_query.lower() in p.get("name", "").lower() or
137
- search_query.lower() in p.get("description", "").lower() or
138
- search_query.lower() in p.get("city", "").lower()
139
- ]
140
-
141
- # عرض عدد النتائج
142
- if search_query:
143
- st.markdown(f"عدد النتائج: {len(filtered_projects)}")
144
-
145
- # تحضير البيانات للخريطة
146
- heat_data = []
147
- for project in filtered_projects:
148
- locations.append({
149
- "lat": project.get("latitude"),
150
- "lon": project.get("longitude"),
151
- "name": project.get("name"),
152
- "description": project.get("description"),
153
- "city": project.get("city"),
154
- "status": project.get("status"),
155
- "project_id": project.get("project_id")
156
- })
157
- heat_data.append([project.get("latitude"), project.get("longitude"), 1])
158
-
159
- # تعيين نقطة المركز والتكبير
160
- if filtered_projects:
161
- center_lat = sum(p.get("latitude", 0) for p in filtered_projects) / len(filtered_projects)
162
- center_lon = sum(p.get("longitude", 0) for p in filtered_projects) / len(filtered_projects)
163
- zoom_level = 6 # مستوى التكبير الافتراضي
164
- else:
165
- # مركز المملكة العربية السعودية
166
- center_lat = 24.7136
167
- center_lon = 46.6753
168
- zoom_level = 5
169
-
170
- # تحديد الإسناد (attribution) بناءً على نمط الخريطة
171
- attribution = None
172
- if map_style == "OpenStreetMap":
173
- attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
174
- elif map_style.startswith("Stamen"):
175
- attribution = 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.'
176
- elif map_style == "CartoDB Positron":
177
- attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="https://cartodb.com/attributions">CartoDB</a>'
178
-
179
- # إنشاء الخريطة
180
- m = folium.Map(
181
- location=[center_lat, center_lon],
182
- zoom_start=zoom_level,
183
- tiles=map_style,
184
- attr=attribution # إضافة سمة الإسناد
185
- )
186
-
187
- # إضافة أدوات القياس إذا تم اختيارها
188
- if show_measurements:
189
- MeasureControl(position='topright', primary_length_unit='kilometers').add_to(m)
190
-
191
- # إضافة النقاط إلى الخريطة
192
- if cluster_markers:
193
- # إنشاء مجموعة تجميع
194
- marker_cluster = MarkerCluster(name="تجميع المشاريع").add_to(m)
195
-
196
- # إضافة النقاط إلى المجموعة
197
- for location in locations:
198
- # إنشاء النافذة المنبثقة
199
- popup_html = f"""
200
- <div style='direction: rtl; text-align: right;'>
201
- <h4>{location['name']}</h4>
202
- <p><strong>الوصف:</strong> {location['description']}</p>
203
- <p><strong>المدينة:</strong> {location['city']}</p>
204
- <p><strong>الحالة:</strong> {location['status']}</p>
205
- <p><strong>الإحداثيات:</strong> {location['lat']:.6f}, {location['lon']:.6f}</p>
206
- <button onclick="window.open('/project/{location['project_id']}', '_self')">عرض تفاصيل المشروع</button>
207
- </div>
208
- """
209
-
210
- # تحديد لون العلامة حسب حالة المشروع
211
- icon_color = 'green'
212
- if location['status'] == 'قيد التنفيذ':
213
- icon_color = 'orange'
214
- elif location['status'] == 'متوقف':
215
- icon_color = 'red'
216
- elif location['status'] == 'مكتمل':
217
- icon_color = 'blue'
218
-
219
- # إضافة العلامة
220
- folium.Marker(
221
- location=[location['lat'], location['lon']],
222
- popup=folium.Popup(popup_html, max_width=300),
223
- tooltip=location['name'],
224
- icon=folium.Icon(color=icon_color, icon='info-sign')
225
- ).add_to(marker_cluster)
226
- else:
227
- # إضافة النقاط مباشرة إلى الخريطة
228
- for location in locations:
229
- # إنشاء النافذة المنبثقة
230
- popup_html = f"""
231
- <div style='direction: rtl; text-align: right;'>
232
- <h4>{location['name']}</h4>
233
- <p><strong>الوصف:</strong> {location['description']}</p>
234
- <p><strong>المدينة:</strong> {location['city']}</p>
235
- <p><strong>الحالة:</strong> {location['status']}</p>
236
- <p><strong>الإحداثيات:</strong> {location['lat']:.6f}, {location['lon']:.6f}</p>
237
- <button onclick="window.open('/project/{location['project_id']}', '_self')">عرض تفاصيل المشروع</button>
238
- </div>
239
- """
240
-
241
- # تحديد لون العلامة حسب حالة المشروع
242
- icon_color = 'green'
243
- if location['status'] == 'قيد التنفيذ':
244
- icon_color = 'orange'
245
- elif location['status'] == 'متوقف':
246
- icon_color = 'red'
247
- elif location['status'] == 'مكتمل':
248
- icon_color = 'blue'
249
-
250
- # إضافة العلامة
251
- folium.Marker(
252
- location=[location['lat'], location['lon']],
253
- popup=folium.Popup(popup_html, max_width=300),
254
- tooltip=location['name'],
255
- icon=folium.Icon(color=icon_color, icon='info-sign')
256
- ).add_to(m)
257
-
258
- # إضافة خريطة حرارية إذا تم اختيارها
259
- if show_heatmap and heat_data:
260
- HeatMap(heat_data, radius=15).add_to(m)
261
-
262
- # إضافة طبقات متنوعة للخريطة
263
- folium.TileLayer('OpenStreetMap').add_to(m)
264
- folium.TileLayer('Stamen Terrain').add_to(m)
265
- folium.TileLayer('Stamen Toner').add_to(m)
266
- folium.TileLayer('CartoDB positron').add_to(m)
267
- folium.TileLayer('CartoDB dark_matter').add_to(m)
268
-
269
- # إضافة أدوات التحكم بالطبقات
270
- folium.LayerControl().add_to(m)
271
-
272
- # عرض الخريطة
273
- st_map = folium_static(m, width=1000, height=600)
274
-
275
- # التفاعل مع النقر على الخريطة (سيتم تنفيذه عندما يتم دعم التفاعل)
276
- # حاليًا لا يوجد دعم مباشر للتفاعل مع الخريطة في Streamlit
277
-
278
- # عرض بيانات المشاريع في جدول
279
- st.markdown("### قائمة المشاريع على الخريطة")
280
-
281
- projects_df = pd.DataFrame(filtered_projects)
282
-
283
- # إعادة تسمية الأعمدة بالعربية
284
- renamed_columns = {
285
- "name": "اسم المشروع",
286
- "city": "المدينة",
287
- "status": "الحالة",
288
- "description": "الوصف",
289
- "project_id": "معرف المشروع",
290
- "latitude": "خط العرض",
291
- "longitude": "خط الطول"
292
- }
293
-
294
- # تحديد الأعمدة للعرض
295
- display_columns = ["name", "city", "status", "project_id"]
296
-
297
- # إنشاء جدول للعرض
298
- display_df = projects_df[display_columns].rename(columns=renamed_columns)
299
-
300
- # عرض الجدول
301
- st.dataframe(display_df, width=1000, height=400)
302
-
303
- # زر لاختيار مشروع لعرض التضاريس
304
- selected_project_id = st.selectbox(
305
- "اختر مشروعًا لعرض التضاريس ثلاثية الأبعاد",
306
- options=projects_df["project_id"].tolist(),
307
- format_func=lambda x: next((p["name"] for p in filtered_projects if p["project_id"] == x), x),
308
- key="select_project_for_terrain"
309
- )
310
-
311
- # زر عرض التضاريس
312
- if styled_button("عرض التضاريس", key="show_terrain_btn", type="primary", icon="🏔️"):
313
- # العثور على المشروع المحدد
314
- selected_project = next((p for p in filtered_projects if p["project_id"] == selected_project_id), None)
315
-
316
- if selected_project:
317
- # تخزين الموقع المحدد في حالة الجلسة
318
- st.session_state.selected_location = {
319
- "latitude": selected_project["latitude"],
320
- "longitude": selected_project["longitude"],
321
- "name": selected_project["name"],
322
- "project_id": selected_project["project_id"]
323
- }
324
-
325
- # جلب بيانات التضاريس
326
- try:
327
- terrain_data = self._fetch_terrain_data(
328
- selected_project["latitude"],
329
- selected_project["longitude"]
330
- )
331
-
332
- # تخزين بيانات التضاريس في حالة الجلسة
333
- st.session_state.terrain_data = terrain_data
334
-
335
- # الانتقال إلى تبويب عرض التضاريس
336
- st.experimental_rerun()
337
- except Exception as e:
338
- st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
339
- else:
340
- st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.")
341
-
342
- def _render_3d_terrain(self):
343
- """عرض التضاريس ثلاثي الأبعاد"""
344
- st.markdown("""
345
- <div class='custom-box info-box'>
346
- <h3>🏔️ عرض التضاريس ثلاثي الأبعاد</h3>
347
- <p>عرض تفاعلي ثلاثي الأبعاد لتضاريس موقع المشروع المحدد.</p>
348
- <p>يمكنك تدوير وتكبير العرض للحصول على رؤية أفضل للموقع.</p>
349
- </div>
350
- """, unsafe_allow_html=True)
351
-
352
- # التحقق من وجود موقع محدد
353
- if st.session_state.selected_location is None:
354
- st.info("يرجى اختيار موقع من تبويب 'الخريطة التفاعلية' أولاً.")
355
-
356
- # بديل: السماح بإدخال الإحداثيات يدوياً
357
- st.markdown("### إدخال الإحداثيات يدوياً")
358
-
359
- col1, col2 = st.columns(2)
360
-
361
- with col1:
362
- manual_lat = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="manual_lat")
363
-
364
- with col2:
365
- manual_lon = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="manual_lon")
366
-
367
- if styled_button("عرض التضاريس", key="manual_terrain_btn", type="primary", icon="🏔️"):
368
- try:
369
- # جلب بيانات التضاريس
370
- terrain_data = self._fetch_terrain_data(manual_lat, manual_lon)
371
-
372
- # تخزين بيانات التضاريس والموقع في حالة الجلسة
373
- st.session_state.terrain_data = terrain_data
374
- st.session_state.selected_location = {
375
- "latitude": manual_lat,
376
- "longitude": manual_lon,
377
- "name": "موقع مخصص",
378
- "project_id": "custom"
379
- }
380
-
381
- st.success("تم جلب بيانات التضاريس بنجاح!")
382
- st.experimental_rerun()
383
- except Exception as e:
384
- st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
385
-
386
- return
387
-
388
- # عرض معلومات الموقع المحدد
389
- location = st.session_state.selected_location
390
- st.markdown(f"### عرض تضاريس موقع: {location['name']}")
391
- st.markdown(f"**الإحداثيات:** {location['latitude']:.6f}, {location['longitude']:.6f}")
392
-
393
- # تجهيز بيانات التضاريس
394
- if st.session_state.terrain_data is None:
395
- # محاولة جلب بيانات التضاريس
396
- try:
397
- terrain_data = self._fetch_terrain_data(
398
- location["latitude"],
399
- location["longitude"]
400
- )
401
-
402
- # تخزين بيانات التضاريس في حالة الجلسة
403
- st.session_state.terrain_data = terrain_data
404
- except Exception as e:
405
- st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
406
- return
407
-
408
- # استخدام بيانات التضاريس المخزنة
409
- terrain_data = st.session_state.terrain_data
410
-
411
- # عرض نطاق التضاريس وإعدادات الارتفاع
412
- col1, col2, col3 = st.columns(3)
413
-
414
- with col1:
415
- elevation_scale = st.slider(
416
- "مقياس الارتفاع",
417
- min_value=1,
418
- max_value=50,
419
- value=15,
420
- key="elevation_scale"
421
- )
422
-
423
- with col2:
424
- radius = st.slider(
425
- "نطاق العرض (كم)",
426
- min_value=1,
427
- max_value=20,
428
- value=5,
429
- key="terrain_radius"
430
- )
431
-
432
- with col3:
433
- color_scheme = st.selectbox(
434
- "نظام الألوان",
435
- options=["terrain", "elevation", "custom"],
436
- key="color_scheme"
437
- )
438
-
439
- # إنشاء نموذج PyDeck للعرض ثلاثي الأبعاد
440
- try:
441
- # تحويل بيانات التضاريس إلى DataFrame
442
- terrain_df = pd.DataFrame(terrain_data)
443
-
444
- # تعيين حجم الخلية بناءً على النطاق
445
- cell_size = radius * 100 # تحويل الكيلومترات إلى أمتار وتقسيمها
446
-
447
- # إنشاء طبقة التضاريس
448
- terrain_layer = pdk.Layer(
449
- "TerrainLayer",
450
- data=None,
451
- elevation_decoder={
452
- "elevations": "elevation",
453
- "bounds": terrain_df["bounds"].iloc[0]
454
- },
455
- texture=None,
456
- elevation_data=terrain_df["terrain"].iloc[0],
457
- elevation_scale=elevation_scale,
458
- color_map=self._get_color_map(color_scheme),
459
- wireframe=True,
460
- pickable=True
461
- )
462
-
463
- # إنشاء طبقة النقطة المركزية
464
- point_layer = pdk.Layer(
465
- "ScatterplotLayer",
466
- data=[{
467
- "position": [location["longitude"], location["latitude"]],
468
- "name": location["name"]
469
- }],
470
- get_position="position",
471
- get_radius=100,
472
- get_fill_color=[255, 0, 0, 200],
473
- pickable=True
474
- )
475
-
476
- # إنشاء عرض PyDeck
477
- INITIAL_VIEW_STATE = pdk.ViewState(
478
- longitude=location["longitude"],
479
- latitude=location["latitude"],
480
- zoom=12,
481
- max_zoom=20,
482
- pitch=45,
483
- bearing=0
484
- )
485
-
486
- deck = pdk.Deck(
487
- map_style="mapbox://styles/mapbox/satellite-v9",
488
- initial_view_state=INITIAL_VIEW_STATE,
489
- api_keys={"mapbox": self.mapbox_token} if self.mapbox_token else None,
490
- layers=[terrain_layer, point_layer],
491
- tooltip={
492
- "html": "<b>{name}</b>",
493
- "style": {
494
- "backgroundColor": "steelblue",
495
- "color": "white"
496
- }
497
- }
498
- )
499
-
500
- # عرض النموذج ثلاثي الأبعاد
501
- st.pydeck_chart(deck)
502
-
503
- # عرض تحليل التضاريس
504
- if "elevation_stats" in terrain_df:
505
- elevation_stats = terrain_df["elevation_stats"].iloc[0]
506
-
507
- st.markdown("### تحليل التضاريس")
508
-
509
- stats_col1, stats_col2, stats_col3, stats_col4 = st.columns(4)
510
-
511
- with stats_col1:
512
- st.metric("أدنى ارتفاع", f"{elevation_stats['min']:.1f} م")
513
-
514
- with stats_col2:
515
- st.metric("أعلى ارتفاع", f"{elevation_stats['max']:.1f} م")
516
-
517
- with stats_col3:
518
- st.metric("متوسط الارتفاع", f"{elevation_stats['mean']:.1f} م")
519
-
520
- with stats_col4:
521
- st.metric("فرق الارتفاع", f"{elevation_stats['range']:.1f} م")
522
-
523
- # عرض رسم بياني للارتفاعات
524
- if "elevation_profile" in terrain_df:
525
- elevation_profile = terrain_df["elevation_profile"].iloc[0]
526
-
527
- # إنشاء DataFrame للرسم البياني
528
- profile_df = pd.DataFrame(elevation_profile)
529
-
530
- # عرض الرسم البياني
531
- st.markdown("### مقطع الارتفاع")
532
-
533
- # استخدام Plotly Express
534
- import plotly.express as px
535
-
536
- fig = px.line(
537
- profile_df,
538
- x="distance",
539
- y="elevation",
540
- title="مقطع الارتفاع عبر الموقع",
541
- labels={"distance": "المسافة (كم)", "elevation": "الارتفاع (م)"}
542
- )
543
-
544
- fig.update_layout(
545
- title_font_size=20,
546
- font_family="Arial",
547
- font_size=14,
548
- height=400
549
- )
550
-
551
- st.plotly_chart(fig, use_container_width=True)
552
-
553
- # أزرار التحكم الإضافية
554
- col1, col2 = st.columns(2)
555
-
556
- with col1:
557
- if styled_button("إعادة تحميل بيانات التضاريس", key="reload_terrain", type="primary", icon="🔄"):
558
- # حذف بيانات التضاريس الحالية
559
- st.session_state.terrain_data = None
560
- st.experimental_rerun()
561
-
562
- with col2:
563
- if styled_button("العودة للخريطة التفاعلية", key="back_to_map", type="secondary", icon="🗺️"):
564
- # إعادة تعيين الموقع المحدد
565
- st.session_state.selected_location = None
566
- st.session_state.terrain_data = None
567
- st.experimental_rerun()
568
-
569
- except Exception as e:
570
- st.error(f"حدث خطأ أثناء عرض التضاريس ثلاثي الأبعاد: {str(e)}")
571
-
572
- def _render_location_analysis(self):
573
- """عرض تحليل المواقع"""
574
- st.markdown("""
575
- <div class='custom-box info-box'>
576
- <h3>📊 تحليل المواقع</h3>
577
- <p>تحليل لمواقع المشاريع وتوزيعها الجغرافي.</p>
578
- <p>يمكنك عرض إحصائيات وتقارير متنوعة حول مواقع المشاريع.</p>
579
- </div>
580
- """, unsafe_allow_html=True)
581
-
582
- # التحقق من وجود مواقع
583
- if not st.session_state.project_locations:
584
- st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.")
585
- return
586
-
587
- # تحويل بيانات المواقع إلى DataFrame
588
- locations_df = pd.DataFrame(st.session_state.project_locations)
589
-
590
- # عرض توزيع المشاريع حسب المدينة
591
- st.markdown("### توزيع المشاريع حسب المدينة")
592
-
593
- city_counts = locations_df["city"].value_counts().reset_index()
594
- city_counts.columns = ["المدينة", "عدد المشاريع"]
595
-
596
- # عرض الرسم البياني
597
- import plotly.express as px
598
-
599
- fig = px.bar(
600
- city_counts,
601
- x="المدينة",
602
- y="عدد المشاريع",
603
- title="توزيع المشاريع حسب المدينة",
604
- color="عدد المشاريع",
605
- color_continuous_scale="Viridis"
606
- )
607
-
608
- fig.update_layout(
609
- title_font_size=20,
610
- font_family="Arial",
611
- font_size=14,
612
- height=400
613
- )
614
-
615
- st.plotly_chart(fig, use_container_width=True)
616
-
617
- # عرض توزيع المشاريع حسب الحالة
618
- st.markdown("### توزيع المشاريع حسب الحالة")
619
-
620
- status_counts = locations_df["status"].value_counts().reset_index()
621
- status_counts.columns = ["الحالة", "عدد المشاريع"]
622
-
623
- # عرض الرسم البياني
624
- fig2 = px.pie(
625
- status_counts,
626
- values="عدد المشاريع",
627
- names="الحالة",
628
- title="توزيع المشاريع حسب الحالة",
629
- color_discrete_sequence=px.colors.qualitative.Set3
630
- )
631
-
632
- fig2.update_layout(
633
- title_font_size=20,
634
- font_family="Arial",
635
- font_size=14,
636
- height=400
637
- )
638
-
639
- st.plotly_chart(fig2, use_container_width=True)
640
-
641
- # عرض تحليل المسافات بين المشاريع
642
- st.markdown("### تحليل المسافات بين المشاريع")
643
-
644
- # حساب مصفوفة المسافات
645
- if len(locations_df) > 1:
646
- # اختيار مشروع كنقطة مرجعية
647
- reference_project = st.selectbox(
648
- "اختر مشروعًا كنقطة مرجعية",
649
- options=locations_df["project_id"].tolist(),
650
- format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
651
- key="reference_project"
652
- )
653
-
654
- # العثور على المشروع المرجعي
655
- ref_project_data = locations_df[locations_df["project_id"] == reference_project].iloc[0]
656
-
657
- # حساب المسافات
658
- distances = []
659
- for _, project in locations_df.iterrows():
660
- if project["project_id"] != reference_project:
661
- distance = self._calculate_distance(
662
- ref_project_data["latitude"], ref_project_data["longitude"],
663
- project["latitude"], project["longitude"]
664
- )
665
-
666
- distances.append({
667
- "project_id": project["project_id"],
668
- "name": project["name"],
669
- "city": project["city"],
670
- "distance": distance
671
- })
672
-
673
- # تحويل البيانات إلى DataFrame
674
- distances_df = pd.DataFrame(distances)
675
-
676
- # ترتيب المشاريع حسب المسافة
677
- distances_df = distances_df.sort_values("distance")
678
-
679
- # عرض المسافات
680
- st.markdown(f"المسافات من مشروع: **{ref_project_data['name']}**")
681
-
682
- # إعادة تسمية الأعمدة
683
- distances_df = distances_df.rename(columns={
684
- "name": "اسم المشروع",
685
- "city": "المدينة",
686
- "distance": "المسافة (كم)"
687
- })
688
-
689
- # تنسيق المسافة
690
- distances_df["المسافة (كم)"] = distances_df["المسافة (كم)"].round(2)
691
-
692
- # عرض الجدول
693
- st.dataframe(distances_df[["اسم المشروع", "المدينة", "المسافة (كم)"]], width=800)
694
-
695
- # عرض رسم بياني للمسافات
696
- fig3 = px.bar(
697
- distances_df,
698
- x="اسم المشروع",
699
- y="المسافة (كم)",
700
- title=f"المسافات من مشروع {ref_project_data['name']}",
701
- color="المسافة (كم)",
702
- color_continuous_scale="Viridis"
703
- )
704
-
705
- fig3.update_layout(
706
- title_font_size=20,
707
- font_family="Arial",
708
- font_size=14,
709
- height=400
710
- )
711
-
712
- st.plotly_chart(fig3, use_container_width=True)
713
-
714
- # عرض المشاريع القريبة على خريطة
715
- st.markdown("### المشاريع القريبة على الخريطة")
716
-
717
- # إنشاء الخريطة
718
- m2 = folium.Map(
719
- location=[ref_project_data["latitude"], ref_project_data["longitude"]],
720
- zoom_start=8,
721
- tiles="OpenStreetMap",
722
- attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
723
- )
724
-
725
- # إضافة المشروع المرجعي
726
- folium.Marker(
727
- location=[ref_project_data["latitude"], ref_project_data["longitude"]],
728
- popup=ref_project_data["name"],
729
- tooltip=ref_project_data["name"],
730
- icon=folium.Icon(color='red', icon='star')
731
- ).add_to(m2)
732
-
733
- # إضافة الدوائر
734
- folium.Circle(
735
- location=[ref_project_data["latitude"], ref_project_data["longitude"]],
736
- radius=50000, # 50 كم
737
- color='red',
738
- fill=True,
739
- fill_opacity=0.1,
740
- popup="50 كم"
741
- ).add_to(m2)
742
-
743
- folium.Circle(
744
- location=[ref_project_data["latitude"], ref_project_data["longitude"]],
745
- radius=100000, # 100 كم
746
- color='orange',
747
- fill=True,
748
- fill_opacity=0.1,
749
- popup="100 كم"
750
- ).add_to(m2)
751
-
752
- folium.Circle(
753
- location=[ref_project_data["latitude"], ref_project_data["longitude"]],
754
- radius=200000, # 200 كم
755
- color='blue',
756
- fill=True,
757
- fill_opacity=0.1,
758
- popup="200 كم"
759
- ).add_to(m2)
760
-
761
- # إضافة المشاريع الأخرى
762
- for _, project in distances_df.iterrows():
763
- project_data = locations_df[locations_df["project_id"] == project["project_id"]].iloc[0]
764
-
765
- folium.Marker(
766
- location=[project_data["latitude"], project_data["longitude"]],
767
- popup=f"{project_data['name']} - {project['المسافة (كم)']} كم",
768
- tooltip=project_data["name"],
769
- icon=folium.Icon(color='green', icon='info-sign')
770
- ).add_to(m2)
771
-
772
- # إضافة خط للربط
773
- folium.PolyLine(
774
- locations=[
775
- [ref_project_data["latitude"], ref_project_data["longitude"]],
776
- [project_data["latitude"], project_data["longitude"]]
777
- ],
778
- color='gray',
779
- weight=2,
780
- opacity=0.5,
781
- popup=f"{project['المسافة (كم)']} كم"
782
- ).add_to(m2)
783
-
784
- # عرض الخريطة
785
- folium_static(m2, width=800, height=500)
786
- else:
787
- st.info("يجب وجود أكثر من مشروع واحد لحساب المسافات.")
788
-
789
- def _render_location_management(self):
790
- """عرض إدارة المواقع"""
791
- st.markdown("""
792
- <div class='custom-box info-box'>
793
- <h3>⚙️ إدارة المواقع</h3>
794
- <p>إضافة وتحرير وحذف مواقع المشاريع.</p>
795
- <p>يمكنك إضافة مواقع جديدة أو تحديث المواقع الموجودة.</p>
796
- </div>
797
- """, unsafe_allow_html=True)
798
-
799
- # تبويبات إدارة المواقع
800
- management_tabs = st.tabs(["إضافة موقع جديد", "تحرير المواقع الموجودة", "استيراد وتصدير المواقع"])
801
-
802
- # تبويب إضافة موقع جديد
803
- with management_tabs[0]:
804
- self._render_add_location()
805
-
806
- # تبويب تحرير المواقع الموجودة
807
- with management_tabs[1]:
808
- self._render_edit_locations()
809
-
810
- # تبويب استيراد وتصدير المواقع
811
- with management_tabs[2]:
812
- self._render_import_export_locations()
813
-
814
- def _render_add_location(self):
815
- """عرض نموذج إضافة موقع جديد"""
816
- st.markdown("### إضافة موقع مشروع جديد")
817
-
818
- # البيانات الأساسية
819
- project_name = st.text_input("اسم المشروع", key="new_project_name")
820
- project_desc = st.text_area("وصف المشروع", key="new_project_desc")
821
-
822
- col1, col2 = st.columns(2)
823
-
824
- with col1:
825
- project_city = st.text_input("المدينة", key="new_project_city")
826
- project_status = st.selectbox(
827
- "حالة المشروع",
828
- options=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"],
829
- key="new_project_status"
830
- )
831
-
832
- with col2:
833
- project_id = st.text_input("معرف المشروع (اختياري)", key="new_project_id", placeholder="سيتم إنشاؤه تلقائيًا إذا تُرك فارغًا")
834
-
835
- # إدخال إحداثيات الموقع
836
- st.markdown("#### إحداثيات الموقع")
837
- location_method = st.radio(
838
- "طريقة تحديد الموقع",
839
- options=["إدخال يدوي", "اختيار من الخريطة"],
840
- key="new_location_method"
841
- )
842
-
843
- # تحديد الموقع
844
- if location_method == "إدخال يدوي":
845
- loc_col1, loc_col2 = st.columns(2)
846
-
847
- with loc_col1:
848
- latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="new_latitude")
849
-
850
- with loc_col2:
851
- longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="new_longitude")
852
-
853
- # عرض الموقع على خريطة صغيرة
854
- mini_map = folium.Map(location=[latitude, longitude], zoom_start=10)
855
- folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map)
856
- folium_static(mini_map, width=700, height=300)
857
- else:
858
- st.markdown("#### اختر الموقع من الخريطة")
859
- st.info("انقر على الخريطة لتحديد الموقع.")
860
-
861
- # إنشاء خريطة
862
- m = folium.Map(location=[24.7136, 46.6753], zoom_start=6)
863
-
864
- # إضافة محدد النقر
865
- m.add_child(folium.ClickForMarker(popup="الموقع المحدد"))
866
-
867
- # عرض الخريطة
868
- map_data = folium_static(m, width=700, height=400)
869
-
870
- # استخراج الإحداثيات المحددة (ليس مدعومًا حاليًا في Streamlit)
871
- st.warning("ملاحظة: خاصية النقر على الخريطة غير مدعومة حاليًا في Streamlit. يرجى استخدام الإدخال اليدوي.")
872
-
873
- latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="map_latitude")
874
- longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="map_longitude")
875
-
876
- # زر إضافة الموقع
877
- if styled_button("إضافة الموقع", key="add_location", type="primary", icon="➕"):
878
- if not project_name or not project_desc or not project_city:
879
- st.error("يرجى تعبئة جميع الحقول المطلوبة.")
880
- else:
881
- # إنشاء معرف فريد للمشروع إذا لم يتم تحديده
882
- if not project_id:
883
- project_id = f"PRJ-{len(st.session_state.project_locations) + 1:04d}"
884
-
885
- # إنشاء كائن الموقع
886
- new_location = {
887
- "name": project_name,
888
- "description": project_desc,
889
- "city": project_city,
890
- "status": project_status,
891
- "latitude": latitude,
892
- "longitude": longitude,
893
- "project_id": project_id,
894
- "created_at": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S"),
895
- "updated_at": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
896
- }
897
-
898
- # إضافة الموقع للقائمة
899
- st.session_state.project_locations.append(new_location)
900
-
901
- # حفظ البيانات
902
- self._save_locations_data()
903
-
904
- st.success(f"تمت إضافة موقع المشروع '{project_name}' بنجاح!")
905
- st.balloons()
906
-
907
- def _render_edit_locations(self):
908
- """عرض واجهة تحرير المواقع الموجودة"""
909
- st.markdown("### تحرير أو حذف مواقع المشاريع")
910
-
911
- # التحقق من وجود مواقع
912
- if not st.session_state.project_locations:
913
- st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع أولاً.")
914
- return
915
-
916
- # اختيار المشروع للتحرير
917
- selected_project_id = st.selectbox(
918
- "اختر مشروعًا للتحرير",
919
- options=[p["project_id"] for p in st.session_state.project_locations],
920
- format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
921
- key="edit_project_select"
922
- )
923
-
924
- # العثور على المشروع المحدد
925
- selected_project = next((p for p in st.session_state.project_locations if p["project_id"] == selected_project_id), None)
926
-
927
- if selected_project:
928
- # عرض نموذج التحرير
929
- st.markdown(f"### تحرير مشروع: {selected_project['name']}")
930
-
931
- # البيانات الأساسية
932
- project_name = st.text_input("اسم المشروع", value=selected_project["name"], key="edit_project_name")
933
- project_desc = st.text_area("وصف المشروع", value=selected_project["description"], key="edit_project_desc")
934
-
935
- col1, col2 = st.columns(2)
936
-
937
- with col1:
938
- project_city = st.text_input("المدينة", value=selected_project["city"], key="edit_project_city")
939
- project_status = st.selectbox(
940
- "حالة المشروع",
941
- options=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"],
942
- index=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"].index(selected_project["status"]),
943
- key="edit_project_status"
944
- )
945
-
946
- with col2:
947
- st.text_input("معرف المشروع", value=selected_project["project_id"], disabled=True, key="edit_project_id")
948
-
949
- # إدخال إحداثيات الموقع
950
- st.markdown("#### إحداثيات الموقع")
951
-
952
- # تحديد الموقع
953
- loc_col1, loc_col2 = st.columns(2)
954
-
955
- with loc_col1:
956
- latitude = st.number_input(
957
- "خط العرض",
958
- value=selected_project["latitude"],
959
- step=0.0001,
960
- format="%.6f",
961
- key="edit_latitude"
962
- )
963
-
964
- with loc_col2:
965
- longitude = st.number_input(
966
- "خط الطول",
967
- value=selected_project["longitude"],
968
- step=0.0001,
969
- format="%.6f",
970
- key="edit_longitude"
971
- )
972
-
973
- # عرض الموقع على خريطة صغيرة
974
- mini_map = folium.Map(location=[latitude, longitude], zoom_start=10)
975
- folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map)
976
- folium_static(mini_map, width=700, height=300)
977
-
978
- # أزرار الإجراءات
979
- col1, col2 = st.columns(2)
980
-
981
- with col1:
982
- if styled_button("حفظ التغييرات", key="save_location_changes", type="primary", icon="💾"):
983
- if not project_name or not project_desc or not project_city:
984
- st.error("يرجى تعبئة جميع الحقول المطلوبة.")
985
- else:
986
- # تحديث بيانات المشروع
987
- selected_project["name"] = project_name
988
- selected_project["description"] = project_desc
989
- selected_project["city"] = project_city
990
- selected_project["status"] = project_status
991
- selected_project["latitude"] = latitude
992
- selected_project["longitude"] = longitude
993
- selected_project["updated_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
994
-
995
- # حفظ البيانات
996
- self._save_locations_data()
997
-
998
- st.success(f"تم تحديث بيانات المشروع '{project_name}' بنجاح!")
999
- st.experimental_rerun()
1000
-
1001
- with col2:
1002
- if styled_button("حذف المشروع", key="delete_location", type="danger", icon="🗑️"):
1003
- # تأكيد الحذف
1004
- st.warning(f"هل أنت متأكد من حذف المشروع '{selected_project['name']}'؟")
1005
-
1006
- confirm_col1, confirm_col2 = st.columns(2)
1007
-
1008
- with confirm_col1:
1009
- if styled_button("تأكيد الحذف", key="confirm_delete", type="danger", icon="✓"):
1010
- # إزالة المشروع من القائمة
1011
- st.session_state.project_locations.remove(selected_project)
1012
-
1013
- # حفظ البيانات
1014
- self._save_locations_data()
1015
-
1016
- st.success(f"تم حذف المشروع '{selected_project['name']}' بنجاح!")
1017
- st.experimental_rerun()
1018
-
1019
- with confirm_col2:
1020
- if styled_button("إلغاء", key="cancel_delete", type="secondary", icon="❌"):
1021
- st.experimental_rerun()
1022
-
1023
- def _render_import_export_locations(self):
1024
- """عرض واجهة استيراد وتصدير المواقع"""
1025
- st.markdown("### استيراد وتصدير مواقع المشاريع")
1026
-
1027
- col1, col2 = st.columns(2)
1028
-
1029
- with col1:
1030
- st.markdown("#### تصدير المواقع")
1031
-
1032
- export_format = st.selectbox(
1033
- "صيغة التصدير",
1034
- options=["CSV", "JSON", "GeoJSON"],
1035
- key="export_format"
1036
- )
1037
-
1038
- if styled_button("تصدير المواقع", key="export_locations", type="primary", icon="📤"):
1039
- self._export_locations(export_format)
1040
-
1041
- with col2:
1042
- st.markdown("#### استيراد المواقع")
1043
-
1044
- import_format = st.selectbox(
1045
- "صيغة الاستيراد",
1046
- options=["CSV", "JSON", "GeoJSON"],
1047
- key="import_format"
1048
- )
1049
-
1050
- uploaded_file = st.file_uploader(
1051
- "اختر ملف للاستيراد",
1052
- type=["csv", "json", "geojson"],
1053
- key="import_locations_file"
1054
- )
1055
-
1056
- if uploaded_file is not None:
1057
- if styled_button("استيراد المواقع", key="import_locations", type="success", icon="📥"):
1058
- self._import_locations(uploaded_file, import_format)
1059
-
1060
- # عرض إحصائيات البيانات
1061
- st.markdown("### إحصائيات البيانات")
1062
-
1063
- stats_col1, stats_col2, stats_col3 = st.columns(3)
1064
-
1065
- with stats_col1:
1066
- st.metric("عدد المشاريع", len(st.session_state.project_locations))
1067
-
1068
- with stats_col2:
1069
- cities = set(p["city"] for p in st.session_state.project_locations)
1070
- st.metric("عدد المدن", len(cities))
1071
-
1072
- with stats_col3:
1073
- statuses = {}
1074
- for p in st.session_state.project_locations:
1075
- statuses[p["status"]] = statuses.get(p["status"], 0) + 1
1076
-
1077
- status_str = ", ".join([f"{k}: {v}" for k, v in statuses.items()])
1078
- st.metric("توزيع الحالات", status_str if statuses else "لا توجد بيانات")
1079
-
1080
- # خيارات متقدمة
1081
- with st.expander("خيارات متقدمة"):
1082
- if styled_button("حذف جميع المواقع", key="clear_locations", type="danger", icon="🗑️"):
1083
- # تأكيد الحذف
1084
- st.warning("هل أنت متأكد من حذف جميع مواقع المشاريع؟ لا يمكن التراجع عن هذا الإجراء.")
1085
-
1086
- confirm_col1, confirm_col2 = st.columns(2)
1087
-
1088
- with confirm_col1:
1089
- if styled_button("تأكيد الحذف", key="confirm_clear", type="danger", icon="✓"):
1090
- # مسح القائمة
1091
- st.session_state.project_locations = []
1092
-
1093
- # حفظ البيانات
1094
- self._save_locations_data()
1095
-
1096
- st.success("تم حذف جميع مواقع المشاريع بنجاح!")
1097
- st.experimental_rerun()
1098
-
1099
- with confirm_col2:
1100
- if styled_button("إلغاء", key="cancel_clear", type="secondary", icon="❌"):
1101
- st.experimental_rerun()
1102
-
1103
- def _fetch_terrain_data(self, latitude, longitude, radius_km=5):
1104
- """جلب بيانات التضاريس من واجهة برمجة التطبيقات"""
1105
- try:
1106
- # تحديث حالة الجلسة
1107
- import plotly.express as px
1108
-
1109
- # تعيين الإحداثيات وحجم المنطقة
1110
- center_lat, center_lon = latitude, longitude
1111
-
1112
- # تحويل نصف القطر من كم إلى درجات (تقريبي)
1113
- radius_deg = radius_km / 111.0 # تقريب: 1 درجة = 111 كم
1114
-
1115
- # تحديد حدود المنطقة
1116
- min_lat = center_lat - radius_deg
1117
- max_lat = center_lat + radius_deg
1118
- min_lon = center_lon - radius_deg
1119
- max_lon = center_lon + radius_deg
1120
-
1121
- # إنشاء شبكة من النقاط
1122
- resolution = 50 # عدد النقاط في كل اتجاه
1123
- lats = np.linspace(min_lat, max_lat, resolution)
1124
- lons = np.linspace(min_lon, max_lon, resolution)
1125
-
1126
- # إنشاء مصفوفة للإحداثيات
1127
- grid_lats, grid_lons = np.meshgrid(lats, lons)
1128
-
1129
- # تحويل الشبكة إلى قائمة من النقاط
1130
- points = []
1131
- for i in range(grid_lats.shape[0]):
1132
- for j in range(grid_lats.shape[1]):
1133
- points.append((grid_lats[i, j], grid_lons[i, j]))
1134
-
1135
- # تقسيم النقاط إلى مجموعات لتقليل عدد الطلبات
1136
- batch_size = 100
1137
- batches = [points[i:i + batch_size] for i in range(0, len(points), batch_size)]
1138
-
1139
- # إنشاء بيانات التضاريس
1140
- elevation_data = np.zeros((len(lats), len(lons)))
1141
-
1142
- # محاكاة بيانات التضاريس (يمكن استبدالها بواجهة برمجة تطبيقات حقيقية)
1143
- for batch_idx, batch in enumerate(batches):
1144
- # في بيئة الإنتاج، سيتم استبدال هذا بطلب API حقيقي
1145
- # هنا نقوم بمحاكاة بيانات التضاريس لأغراض العرض
1146
- for point_idx, (lat, lon) in enumerate(batch):
1147
- # حساب المؤشر في مصفوفة الارتفاع
1148
- lat_idx = np.abs(lats - lat).argmin()
1149
- lon_idx = np.abs(lons - lon).argmin()
1150
-
1151
- # محاكاة الارتفاع (في بيئة الإنتاج سيكون هذا من واجهة برمجة التطبيقات)
1152
- # هنا نصنع تضاريس اصطناعية باستخدام دالة جيبية
1153
- dist_from_center = np.sqrt(
1154
- (lat - center_lat) ** 2 + (lon - center_lon) ** 2
1155
- )
1156
-
1157
- # إنشاء بعض التلال والوديان الاصطناعية
1158
- elevation = 500 + 200 * np.sin(dist_from_center * 100) + 100 * np.cos(lat * 30) + 150 * np.sin(lon * 40)
1159
-
1160
- # إضافة بعض الضوضاء العشوائية
1161
- elevation += np.random.normal(0, 30)
1162
-
1163
- # تخزين الارتفاع
1164
- elevation_data[lat_idx, lon_idx] = elevation
1165
-
1166
- # حساب إحصائيات الارتفاع
1167
- elevation_stats = {
1168
- "min": float(np.min(elevation_data)),
1169
- "max": float(np.max(elevation_data)),
1170
- "mean": float(np.mean(elevation_data)),
1171
- "range": float(np.max(elevation_data) - np.min(elevation_data))
1172
- }
1173
-
1174
- # إنشاء مقطع ارتفاع من الشمال إلى الجنوب عبر المركز
1175
- center_lon_idx = np.abs(lons - center_lon).argmin()
1176
- ns_profile = []
1177
- for i, lat in enumerate(lats):
1178
- ns_profile.append({
1179
- "distance": (lat - min_lat) * 111.0, # تحويل الدرجات إلى كم
1180
- "elevation": float(elevation_data[i, center_lon_idx])
1181
- })
1182
-
1183
- # إنشاء مقطع ارتفاع من الشرق إلى الغرب عبر المركز
1184
- center_lat_idx = np.abs(lats - center_lat).argmin()
1185
- ew_profile = []
1186
- for i, lon in enumerate(lons):
1187
- ew_profile.append({
1188
- "distance": (lon - min_lon) * 111.0 * np.cos(np.radians(center_lat)), # تحويل الدرجات إلى كم مع تصحيح خط العرض
1189
- "elevation": float(elevation_data[center_lat_idx, i])
1190
- })
1191
-
1192
- # دمج المقاطع
1193
- elevation_profile = ns_profile + ew_profile
1194
-
1195
- # تحضير بيانات التضاريس للعرض ثلاثي الأبعاد
1196
- bounds = [min_lon, min_lat, max_lon, max_lat]
1197
-
1198
- # تحويل مصفوفة الارتفاع إلى تنسيق مناسب لـ PyDeck
1199
- terrain_array = elevation_data.astype(np.float32)
1200
-
1201
- # إنشاء كائن للتضاريس
1202
- terrain_data = [{
1203
- "bounds": bounds,
1204
- "terrain": terrain_array.tolist(),
1205
- "elevation_stats": elevation_stats,
1206
- "elevation_profile": elevation_profile
1207
- }]
1208
-
1209
- return terrain_data
1210
-
1211
- except Exception as e:
1212
- st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
1213
- raise e
1214
-
1215
- def _calculate_distance(self, lat1, lon1, lat2, lon2):
1216
- """حساب المسافة بين نقطتين بالكيلومترات باستخدام صيغة هافرساين"""
1217
- import math
1218
-
1219
- # تحويل الإحداثيات إلى راديان
1220
- lat1 = math.radians(lat1)
1221
- lon1 = math.radians(lon1)
1222
- lat2 = math.radians(lat2)
1223
- lon2 = math.radians(lon2)
1224
-
1225
- # صيغة هافرساين
1226
- dlon = lon2 - lon1
1227
- dlat = lat2 - lat1
1228
- a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
1229
- c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
1230
- distance = 6371 * c # نصف قطر الأرض بالكيلومترات
1231
-
1232
- return distance
1233
-
1234
- def _get_color_map(self, scheme):
1235
- """الحصول على خريطة الألوان حسب النظام المختار"""
1236
- if scheme == "terrain":
1237
- return [
1238
- [0, (0, 50, 0)],
1239
- [0.1, (0, 100, 0)],
1240
- [0.25, (0, 150, 0)],
1241
- [0.4, (200, 170, 0)],
1242
- [0.6, (150, 100, 0)],
1243
- [0.8, (100, 50, 0)],
1244
- [1, (200, 200, 200)]
1245
- ]
1246
- elif scheme == "elevation":
1247
- return [
1248
- [0, (0, 0, 100)],
1249
- [0.2, (0, 100, 150)],
1250
- [0.4, (0, 150, 50)],
1251
- [0.6, (150, 150, 0)],
1252
- [0.8, (150, 50, 0)],
1253
- [1, (100, 0, 0)]
1254
- ]
1255
- else: # custom
1256
- return [
1257
- [0, (30, 100, 200)],
1258
- [0.3, (60, 170, 250)],
1259
- [0.5, (200, 220, 150)],
1260
- [0.7, (180, 120, 60)],
1261
- [0.9, (110, 60, 30)],
1262
- [1, (80, 30, 10)]
1263
- ]
1264
-
1265
- def _export_locations(self, format):
1266
- """تصدير مواقع المشاريع إلى ملف"""
1267
- try:
1268
- if not st.session_state.project_locations:
1269
- st.error("لا توجد مواقع مشاريع للتصدير.")
1270
- return
1271
-
1272
- if format == "CSV":
1273
- # تصدير إلى CSV
1274
- df = pd.DataFrame(st.session_state.project_locations)
1275
-
1276
- csv_data = df.to_csv(index=False)
1277
-
1278
- st.download_button(
1279
- label="تنزيل ملف CSV",
1280
- data=csv_data,
1281
- file_name="project_locations.csv",
1282
- mime="text/csv"
1283
- )
1284
-
1285
- elif format == "JSON":
1286
- # تصدير إلى JSON
1287
- json_data = json.dumps(st.session_state.project_locations, ensure_ascii=False, indent=2)
1288
-
1289
- st.download_button(
1290
- label="تنزيل ملف JSON",
1291
- data=json_data,
1292
- file_name="project_locations.json",
1293
- mime="application/json"
1294
- )
1295
-
1296
- elif format == "GeoJSON":
1297
- # تصدير إلى GeoJSON
1298
- features = []
1299
-
1300
- for location in st.session_state.project_locations:
1301
- feature = {
1302
- "type": "Feature",
1303
- "geometry": {
1304
- "type": "Point",
1305
- "coordinates": [location["longitude"], location["latitude"]]
1306
- },
1307
- "properties": {
1308
- "name": location["name"],
1309
- "description": location["description"],
1310
- "city": location["city"],
1311
- "status": location["status"],
1312
- "project_id": location["project_id"],
1313
- "created_at": location.get("created_at", ""),
1314
- "updated_at": location.get("updated_at", "")
1315
- }
1316
- }
1317
-
1318
- features.append(feature)
1319
-
1320
- geojson = {
1321
- "type": "FeatureCollection",
1322
- "features": features
1323
- }
1324
-
1325
- geojson_data = json.dumps(geojson, ensure_ascii=False, indent=2)
1326
-
1327
- st.download_button(
1328
- label="تنزيل ملف GeoJSON",
1329
- data=geojson_data,
1330
- file_name="project_locations.geojson",
1331
- mime="application/geo+json"
1332
- )
1333
-
1334
- st.success(f"تم تصدير {len(st.session_state.project_locations)} موقع بنجاح!")
1335
-
1336
- except Exception as e:
1337
- st.error(f"حدث خطأ أثناء تصدير المواقع: {str(e)}")
1338
-
1339
- def _import_locations(self, uploaded_file, format):
1340
- """استيراد مواقع المشاريع من ملف"""
1341
- try:
1342
- if format == "CSV":
1343
- # استيراد من CSV
1344
- df = pd.read_csv(uploaded_file)
1345
-
1346
- # التحقق من وجود الأعمدة المطلوبة
1347
- required_columns = ["name", "latitude", "longitude"]
1348
- missing_columns = [col for col in required_columns if col not in df.columns]
1349
-
1350
- if missing_columns:
1351
- st.error(f"الملف لا يحتوي على الأعمدة التالية: {', '.join(missing_columns)}")
1352
- return
1353
-
1354
- # تحويل DataFrame إلى قائمة من القواميس
1355
- imported_locations = df.to_dict("records")
1356
-
1357
- elif format == "JSON":
1358
- # استيراد من JSON
1359
- imported_locations = json.loads(uploaded_file.read())
1360
-
1361
- elif format == "GeoJSON":
1362
- # استيراد من GeoJSON
1363
- geojson = json.loads(uploaded_file.read())
1364
-
1365
- # التحقق من صحة التنسيق
1366
- if "type" not in geojson or geojson["type"] != "FeatureCollection" or "features" not in geojson:
1367
- st.error("تنسيق GeoJSON غير صحيح.")
1368
- return
1369
-
1370
- # تحويل المميزات إلى مواقع
1371
- imported_locations = []
1372
-
1373
- for feature in geojson["features"]:
1374
- if feature["type"] == "Feature" and feature["geometry"]["type"] == "Point":
1375
- coords = feature["geometry"]["coordinates"]
1376
- properties = feature["properties"]
1377
-
1378
- location = {
1379
- "name": properties.get("name", ""),
1380
- "description": properties.get("description", ""),
1381
- "city": properties.get("city", ""),
1382
- "status": properties.get("status", "جديد"),
1383
- "longitude": coords[0],
1384
- "latitude": coords[1],
1385
- "project_id": properties.get("project_id", f"PRJ-{len(imported_locations)+1:04d}"),
1386
- "created_at": properties.get("created_at", pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")),
1387
- "updated_at": properties.get("updated_at", pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S"))
1388
- }
1389
-
1390
- imported_locations.append(location)
1391
-
1392
- # التحقق من وجود البيانات المطلوبة في الملف المستورد
1393
- valid_locations = []
1394
- for location in imported_locations:
1395
- # التحقق من وجود الحقول المطلوبة
1396
- if "name" not in location or "latitude" not in location or "longitude" not in location:
1397
- continue
1398
-
1399
- # إضافة القيم الافتراضية إذا لم تكن موجودة
1400
- if "description" not in location:
1401
- location["description"] = ""
1402
-
1403
- if "city" not in location:
1404
- location["city"] = ""
1405
-
1406
- if "status" not in location:
1407
- location["status"] = "جديد"
1408
-
1409
- if "project_id" not in location:
1410
- location["project_id"] = f"PRJ-{len(valid_locations)+1:04d}"
1411
-
1412
- if "created_at" not in location:
1413
- location["created_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
1414
-
1415
- if "updated_at" not in location:
1416
- location["updated_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
1417
-
1418
- valid_locations.append(location)
1419
-
1420
- if not valid_locations:
1421
- st.error("لم يتم العثور على مواقع صالحة في الملف.")
1422
- return
1423
-
1424
- # سؤال المستخدم عن كيفية الاستيراد
1425
- import_mode = st.radio(
1426
- "كيفية الاستيراد",
1427
- options=["إضافة إلى المواقع الموجودة", "استبدال المواقع الموجودة"],
1428
- key="import_mode"
1429
- )
1430
-
1431
- if styled_button("تأكيد الاستيراد", key="confirm_import", type="success", icon="✓"):
1432
- if import_mode == "إضافة إلى المواقع الموجودة":
1433
- # إضافة المواقع المستوردة إلى القائمة الحالية
1434
- st.session_state.project_locations.extend(valid_locations)
1435
- else:
1436
- # استبدال المواقع الموجودة بالمواقع المستوردة
1437
- st.session_state.project_locations = valid_locations
1438
-
1439
- # حفظ البيانات
1440
- self._save_locations_data()
1441
-
1442
- st.success(f"تم استيراد {len(valid_locations)} موقع بنجاح!")
1443
- st.experimental_rerun()
1444
-
1445
- except Exception as e:
1446
- st.error(f"حدث خطأ أثناء استيراد المواقع: {str(e)}")
1447
-
1448
- def _save_locations_data(self):
1449
- """حفظ بيانات المواقع"""
1450
- try:
1451
- # التأكد من وجود المجلد
1452
- os.makedirs(self.data_dir, exist_ok=True)
1453
-
1454
- # حفظ البيانات
1455
- locations_file = os.path.join(self.data_dir, "project_locations.json")
1456
-
1457
- with open(locations_file, 'w', encoding='utf-8') as f:
1458
- json.dump(st.session_state.project_locations, f, ensure_ascii=False, indent=2)
1459
- except Exception as e:
1460
- st.error(f"حدث خطأ أثناء حفظ بيانات المواقع: {str(e)}")
1461
-
1462
- def _load_locations_data(self):
1463
- """تحميل بيانات المواقع"""
1464
- try:
1465
- # التحقق من وجود الملف
1466
- locations_file = os.path.join(self.data_dir, "project_locations.json")
1467
-
1468
- if os.path.exists(locations_file):
1469
- with open(locations_file, 'r', encoding='utf-8') as f:
1470
- locations = json.load(f)
1471
-
1472
- # تحديث حالة الجلسة
1473
- st.session_state.project_locations = locations
1474
- except Exception as e:
1475
- st.error(f"حدث خطأ أثناء تحميل بيانات المواقع: {str(e)}")
1476
-
1477
- def _initialize_sample_projects(self):
1478
- """تهيئة بيانات اختبارية للمشاريع"""
1479
- # التحقق من وجود بيانات محفوظة
1480
- locations_file = os.path.join(self.data_dir, "project_locations.json")
1481
-
1482
- if os.path.exists(locations_file):
1483
- # تحميل البيانات المحفوظة
1484
- self._load_locations_data()
1485
- return
1486
-
1487
- # إنشاء بيانات اختبارية إذا لم تكن هناك بيانات محفوظة
1488
- sample_projects = [
1489
- {
1490
- "name": "تطوير شبكة الطرق في منطقة الرياض",
1491
- "description": "مشروع تطوير وتوسعة شبكة الطرق الرئيسية في منطقة الرياض",
1492
- "city": "الرياض",
1493
- "status": "قيد التنفيذ",
1494
- "latitude": 24.7136,
1495
- "longitude": 46.6753,
1496
- "project_id": "PRJ-0001",
1497
- "created_at": "2025-01-15 10:30:00",
1498
- "updated_at": "2025-01-15 10:30:00"
1499
- },
1500
- {
1501
- "name": "إنشاء سد وادي حنيفة",
1502
- "description": "مشروع إنشاء سد لحجز مياه الأمطار في وادي حنيفة",
1503
- "city": "الرياض",
1504
- "status": "جديد",
1505
- "latitude": 24.6748,
1506
- "longitude": 46.5831,
1507
- "project_id": "PRJ-0002",
1508
- "created_at": "2025-02-01 14:45:00",
1509
- "updated_at": "2025-02-01 14:45:00"
1510
- },
1511
- {
1512
- "name": "تطوير ميناء جدة الإسلامي",
1513
- "description": "مشروع تطوير وتوسعة ميناء جدة الإسلامي لزيادة الطاقة الاستيعابية",
1514
- "city": "جدة",
1515
- "status": "قيد التنفيذ",
1516
- "latitude": 21.4858,
1517
- "longitude": 39.1925,
1518
- "project_id": "PRJ-0003",
1519
- "created_at": "2024-11-20 09:15:00",
1520
- "updated_at": "2024-11-20 09:15:00"
1521
- },
1522
- {
1523
- "name": "إنشاء مطار الدمام الجديد",
1524
- "description": "مشروع إنشاء مطار جديد في مدينة الدمام لتلبية الطلب المتزايد",
1525
- "city": "الدمام",
1526
- "status": "متوقف",
1527
- "latitude": 26.4207,
1528
- "longitude": 50.0888,
1529
- "project_id": "PRJ-0004",
1530
- "created_at": "2024-10-05 11:30:00",
1531
- "updated_at": "2024-10-05 11:30:00"
1532
- },
1533
- {
1534
- "name": "توسعة جامعة الملك فهد للبترول والمعادن",
1535
- "description": "مشروع توسعة مباني ومرافق جامعة الملك فهد للبترول والمعادن",
1536
- "city": "الظهران",
1537
- "status": "قيد التنفيذ",
1538
- "latitude": 26.3927,
1539
- "longitude": 50.1150,
1540
- "project_id": "PRJ-0005",
1541
- "created_at": "2025-01-10 08:00:00",
1542
- "updated_at": "2025-01-10 08:00:00"
1543
- },
1544
- {
1545
- "name": "إنشاء محطة تحلية مياه القنفذة",
1546
- "description": "مشروع إنشاء محطة تحلية مياه جديدة في محافظة القنفذة",
1547
- "city": "القنفذة",
1548
- "status": "جديد",
1549
- "latitude": 19.1299,
1550
- "longitude": 41.0825,
1551
- "project_id": "PRJ-0006",
1552
- "created_at": "2025-02-20 15:20:00",
1553
- "updated_at": "2025-02-20 15:20:00"
1554
- },
1555
- {
1556
- "name": "تطوير مجمع حكومي في حائل",
1557
- "description": "مشروع إنشاء وتطوير مجمع للدوائر الحكومية في مدينة حائل",
1558
- "city": "حائل",
1559
- "status": "مكتمل",
1560
- "latitude": 27.5114,
1561
- "longitude": 41.7208,
1562
- "project_id": "PRJ-0007",
1563
- "created_at": "2024-06-15 10:00:00",
1564
- "updated_at": "2024-12-10 14:30:00"
1565
- },
1566
- {
1567
- "name": "إنشاء مستشفى الإحساء العام",
1568
- "description": "مشروع إنشاء مستشفى عام جديد في محافظة الإحساء بسعة 500 سرير",
1569
- "city": "الإحساء",
1570
- "status": "قيد التنفيذ",
1571
- "latitude": 25.3753,
1572
- "longitude": 49.5873,
1573
- "project_id": "PRJ-0008",
1574
- "created_at": "2024-09-01 09:45:00",
1575
- "updated_at": "2024-09-01 09:45:00"
1576
- },
1577
- {
1578
- "name": "تطوير شبكة الصرف الصحي في أبها",
1579
- "description": "مشروع تطوير وتوسعة شبكة الصرف الصحي في مدينة أبها",
1580
- "city": "أبها",
1581
- "status": "جديد",
1582
- "latitude": 18.2164,
1583
- "longitude": 42.5053,
1584
- "project_id": "PRJ-0009",
1585
- "created_at": "2025-02-25 11:15:00",
1586
- "updated_at": "2025-02-25 11:15:00"
1587
- },
1588
- {
1589
- "name": "إنشاء مدينة صناعية في سكاكا",
1590
- "description": "مشروع إنشاء مدينة صناعية جديدة في منطقة سكاكا",
1591
- "city": "سكاكا",
1592
- "status": "متوقف",
1593
- "latitude": 29.9720,
1594
- "longitude": 40.2006,
1595
- "project_id": "PRJ-0010",
1596
- "created_at": "2024-07-20 13:30:00",
1597
- "updated_at": "2024-07-20 13:30:00"
1598
- }
1599
- ]
1600
-
1601
- # تحديث حالة الجلسة
1602
- st.session_state.project_locations = sample_projects
1603
-
1604
- # حفظ البيانات
1605
- self._save_locations_data()
1606
-
1607
-
1608
- # فئة تحويل Folium إلى Streamlit
1609
- class folium_static:
1610
- """فئة لعرض خرائط Folium في Streamlit"""
1611
-
1612
- def __init__(self, fig, width=700, height=500):
1613
- """عرض خريطة Folium في Streamlit"""
1614
- import streamlit.components.v1 as components
1615
-
1616
- # تحويل خريطة Folium إلى HTML
1617
- fig_html = fig._repr_html_()
1618
-
1619
- # إنشاء مكون HTML مخصص
1620
- components.html(fig_html, width=width, height=height)
1621
-
1622
-
1623
- # تشغيل الوحدة بشكل مستقل
1624
- def main():
1625
- """تشغيل وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد بشكل مستقل"""
1626
- # تهيئة الواجهة
1627
- st.set_page_config(
1628
- page_title="الخريطة التفاعلية | WAHBi AI",
1629
- page_icon="🗺️",
1630
- layout="wide",
1631
- initial_sidebar_state="expanded",
1632
- menu_items={
1633
- 'Get Help': 'mailto:[email protected]',
1634
- 'Report a bug': 'mailto:[email protected]',
1635
- 'About': 'وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد - جزء من نظام WAHBi AI لتحليل المناقصات'
1636
- }
1637
- )
1638
-
1639
- # تهيئة وحدة الخريطة التفاعلية
1640
- interactive_map = InteractiveMap()
1641
-
1642
- # عرض واجهة الوحدة
1643
- interactive_map.render()
1644
-
1645
- # تشغيل الوحدة عند استدعاء الملف مباشرة
1646
- if __name__ == "__main__":
1647
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/maps/maps_app.py DELETED
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- وحدة تطبيق الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
6
- """
7
-
8
- import os
9
- import sys
10
- import streamlit as st
11
- import pandas as pd
12
- import numpy as np
13
-
14
- # إضافة مسار النظام للوصول للملفات المشتركة
15
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
16
-
17
- # استيراد مكونات الخريطة التفاعلية
18
- from modules.maps.interactive_map import InteractiveMap
19
-
20
-
21
- class MapsApp:
22
- """وحدة تطبيق الخريطة التفاعلية"""
23
-
24
- def __init__(self):
25
- """تهيئة وحدة تطبيق الخريطة التفاعلية"""
26
- self.interactive_map = InteractiveMap()
27
-
28
- def render(self):
29
- """عرض واجهة وحدة تطبيق الخريطة التفاعلية"""
30
- st.markdown("<h2 class='module-title'>وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد</h2>", unsafe_allow_html=True)
31
-
32
- st.markdown("""
33
- <div class="module-description">
34
- تمكنك هذه الوحدة من عرض وإدارة مواقع المشاريع على خريطة تفاعلية، مع إمكانية عرض التضاريس بشكل ثلاثي الأبعاد.
35
- يمكنك إضافة وتحرير مواقع المشاريع، وتحليل توزيعها الجغرافي، وعرض المعلومات الطبوغرافية للمواقع.
36
- </div>
37
- """, unsafe_allow_html=True)
38
-
39
- # عرض وحدة الخريطة التفاعلية
40
- self.interactive_map.render()
41
-
42
-
43
- # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
44
- if __name__ == "__main__":
45
- st.set_page_config(
46
- page_title="الخريطة التفاعلية | WAHBi AI",
47
- page_icon="🗺️",
48
- layout="wide",
49
- initial_sidebar_state="expanded"
50
- )
51
-
52
- app = MapsApp()
53
- app.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/notifications/__init__.py DELETED
@@ -1 +0,0 @@
1
- # ملف تهيئة وحدة الإشعارات الذكية
 
 
modules/notifications/notifications_app.py DELETED
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- وحدة تطبيق نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات
6
- """
7
-
8
- import os
9
- import sys
10
- import streamlit as st
11
- import pandas as pd
12
- import numpy as np
13
-
14
- # إضافة مسار النظام للوصول للملفات المشتركة
15
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
16
-
17
- # استيراد مكونات الإشعارات الذكية
18
- from modules.notifications.smart_notifications import SmartNotificationSystem
19
-
20
-
21
- class NotificationsApp:
22
- """وحدة تطبيق نظام الإشعارات الذكي"""
23
-
24
- def __init__(self):
25
- """تهيئة وحدة تطبيق نظام الإشعارات الذكي"""
26
- self.smart_notification_system = SmartNotificationSystem()
27
-
28
- def render(self):
29
- """عرض واجهة وحدة تطبيق نظام الإشعارات الذكي"""
30
- st.markdown("<h2 class='module-title'>نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات</h2>", unsafe_allow_html=True)
31
-
32
- st.markdown("""
33
- <div class="module-description">
34
- يتيح لك نظام الإشعارات الذكي متابعة تحديثات المشاريع وتلقي تنبيهات مخصصة حسب أدوارك واهتماماتك.
35
- يمكنك تخصيص إعدادات الإشعارات وجدولة التذكيرات للمواعيد النهائية والمهام.
36
- </div>
37
- """, unsafe_allow_html=True)
38
-
39
- # عرض نظام الإشعارات الذكي
40
- self.smart_notification_system.render()
41
-
42
-
43
- # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
44
- if __name__ == "__main__":
45
- st.set_page_config(
46
- page_title="نظام الإشعارات الذكي | WAHBi AI",
47
- page_icon="🔔",
48
- layout="wide",
49
- initial_sidebar_state="expanded"
50
- )
51
-
52
- app = NotificationsApp()
53
- app.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/notifications/smart_notifications.py DELETED
@@ -1,1237 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- وحدة نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات
6
- تتيح هذه الوحدة متابعة تحديثات المشاريع وإرسال تنبيهات ذكية مخصصة للمستخدمين بناءً على أدوارهم واهتماماتهم
7
- """
8
-
9
- import os
10
- import sys
11
- import streamlit as st
12
- import pandas as pd
13
- import numpy as np
14
- import json
15
- import datetime
16
- import time
17
- import threading
18
- import logging
19
- from typing import List, Dict, Any, Tuple, Optional, Union
20
-
21
- # إضافة مسار النظام للوصول للملفات المشتركة
22
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
23
-
24
- # استيراد مكونات واجهة المستخدم
25
- from utils.components.header import render_header
26
- from utils.components.credits import render_credits
27
- from utils.helpers import format_number, format_currency, styled_button
28
-
29
-
30
- class SmartNotificationSystem:
31
- """فئة نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات"""
32
-
33
- def __init__(self):
34
- """تهيئة نظام الإشعارات الذكي"""
35
- # تهيئة مجلدات حفظ البيانات
36
- self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/notifications"))
37
- os.makedirs(self.data_dir, exist_ok=True)
38
-
39
- # تهيئة قائمة الإشعارات
40
- if 'notifications' not in st.session_state:
41
- st.session_state.notifications = []
42
-
43
- if 'unread_count' not in st.session_state:
44
- st.session_state.unread_count = 0
45
-
46
- if 'notification_channels' not in st.session_state:
47
- st.session_state.notification_channels = {
48
- "browser": True,
49
- "email": False,
50
- "sms": False,
51
- "mobile_app": False
52
- }
53
-
54
- if 'notification_preferences' not in st.session_state:
55
- st.session_state.notification_preferences = {
56
- "project_updates": True,
57
- "document_analysis": True,
58
- "deadline_reminders": True,
59
- "risk_alerts": True,
60
- "price_changes": True,
61
- "team_mentions": True,
62
- "system_updates": True
63
- }
64
-
65
- # تحميل الإشعارات المحفوظة
66
- self._load_notifications()
67
-
68
- # تسجيل الأحداث
69
- logging.basicConfig(
70
- level=logging.INFO,
71
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
72
- handlers=[
73
- logging.FileHandler(os.path.join(self.data_dir, "notifications.log")),
74
- logging.StreamHandler()
75
- ]
76
- )
77
- self.logger = logging.getLogger("smart_notifications")
78
-
79
- def render(self):
80
- """عرض واجهة نظام الإشعارات الذكي"""
81
- render_header("نظام الإشعارات الذكي")
82
-
83
- # تبويبات الوحدة
84
- tabs = st.tabs([
85
- "جميع الإشعارات",
86
- "إشعارات غير مقروءة",
87
- "إعدادات الإشعارات",
88
- "جدولة الإشعارات",
89
- "تقارير وإحصائيات"
90
- ])
91
-
92
- # تبويب جميع الإشعارات
93
- with tabs[0]:
94
- self._render_all_notifications()
95
-
96
- # تبويب الإشعارات غير المقروءة
97
- with tabs[1]:
98
- self._render_unread_notifications()
99
-
100
- # تبويب إعدادات الإشعارات
101
- with tabs[2]:
102
- self._render_notification_settings()
103
-
104
- # تبويب جدولة الإشعارات
105
- with tabs[3]:
106
- self._render_notification_scheduling()
107
-
108
- # تبويب تقارير وإحصائيات
109
- with tabs[4]:
110
- self._render_notification_analytics()
111
-
112
- # عرض حقوق النشر
113
- render_credits()
114
-
115
- def _render_all_notifications(self):
116
- """عرض جميع الإشعارات"""
117
- st.markdown("""
118
- <div class='custom-box info-box'>
119
- <h3>🔔 جميع الإشعارات</h3>
120
- <p>عرض كافة الإشعارات والتنبيهات الخاصة بالمشاريع والنظام.</p>
121
- </div>
122
- """, unsafe_allow_html=True)
123
-
124
- # أزرار التحكم
125
- col1, col2, col3 = st.columns([1, 1, 1])
126
-
127
- with col1:
128
- if styled_button("تحديث الإشعارات", key="refresh_notifications", type="primary", icon="🔄"):
129
- self._load_notifications()
130
- st.success("تم تحديث الإشعارات بنجاح")
131
-
132
- with col2:
133
- if styled_button("تعليم الكل كمقروء", key="mark_all_read", type="secondary", icon="✓"):
134
- self._mark_all_as_read()
135
- st.success("تم تعليم جميع الإشعارات كمقروءة")
136
-
137
- with col3:
138
- if styled_button("حذف جميع الإشعارات", key="clear_notifications", type="danger", icon="🗑️"):
139
- confirmed = st.text_input("اكتب 'تأكيد' لحذف جميع الإشعارات", key="confirm_clear")
140
- if confirmed == "تأكيد":
141
- self._clear_all_notifications()
142
- st.success("تم حذف جميع الإشعارات بنجاح")
143
-
144
- # فلترة الإشعارات
145
- filter_col1, filter_col2 = st.columns(2)
146
-
147
- with filter_col1:
148
- notification_type = st.multiselect(
149
- "تصفية حسب النوع",
150
- options=[
151
- "تحديث مشروع", "وثيقة جديدة", "تذكير موعد نهائي",
152
- "تنبيه مخاطر", "تغيير سعر", "إشارة فريق العمل", "تحديث النظام"
153
- ],
154
- key="filter_notification_type"
155
- )
156
-
157
- with filter_col2:
158
- date_range = st.date_input(
159
- "نطاق التاريخ",
160
- value=(
161
- datetime.datetime.now() - datetime.timedelta(days=30),
162
- datetime.datetime.now()
163
- ),
164
- key="filter_date_range"
165
- )
166
-
167
- # تصفية الإشعارات
168
- filtered_notifications = self._filter_notifications(
169
- notification_type=notification_type,
170
- date_range=date_range
171
- )
172
-
173
- # عرض الإشعارات المصفاة
174
- if filtered_notifications:
175
- for notification in filtered_notifications:
176
- self._render_notification_card(notification)
177
- else:
178
- st.info("لا توجد إشعارات متاحة")
179
-
180
- def _render_unread_notifications(self):
181
- """عرض الإشعارات غير المقروءة"""
182
- st.markdown("""
183
- <div class='custom-box info-box'>
184
- <h3>🔔 الإشعارات غير المقروءة</h3>
185
- <p>عرض الإشعارات والتنبيهات التي لم تتم قراءتها بعد.</p>
186
- </div>
187
- """, unsafe_allow_html=True)
188
-
189
- # أزرار التحكم
190
- col1, col2 = st.columns(2)
191
-
192
- with col1:
193
- if styled_button("تحديث الإشعارات", key="refresh_unread", type="primary", icon="🔄"):
194
- self._load_notifications()
195
- st.success("تم تحديث الإشعارات بنجاح")
196
-
197
- with col2:
198
- if styled_button("تعليم الكل كمقروء", key="mark_unread_read", type="secondary", icon="✓"):
199
- self._mark_all_as_read()
200
- st.success("تم تعليم جميع الإشعارات كمقروءة")
201
-
202
- # فلترة الإشعارات غير المقروءة
203
- unread_notifications = [n for n in st.session_state.notifications if not n.get("read", False)]
204
-
205
- # عرض الإشعارات غير المقروءة
206
- if unread_notifications:
207
- for notification in unread_notifications:
208
- self._render_notification_card(notification, show_mark_button=True)
209
- else:
210
- st.success("لا توجد إشعارات غير مقروءة")
211
-
212
- def _render_notification_settings(self):
213
- """عرض إعدادات الإشعارات"""
214
- st.markdown("""
215
- <div class='custom-box info-box'>
216
- <h3>⚙️ إعدادات الإشعارات</h3>
217
- <p>تخصيص إعدادات وتفضيلات الإشعارات الخاصة بك.</p>
218
- </div>
219
- """, unsafe_allow_html=True)
220
-
221
- # قسم قنوات الإشعارات
222
- st.markdown("### قنوات الإشعارات")
223
- st.markdown("حدد الطرق التي ترغب في تلقي الإشعارات من خلالها.")
224
-
225
- channels_col1, channels_col2 = st.columns(2)
226
-
227
- with channels_col1:
228
- st.session_state.notification_channels["browser"] = st.checkbox(
229
- "إشعارات المتصفح",
230
- value=st.session_state.notification_channels.get("browser", True),
231
- key="channel_browser"
232
- )
233
-
234
- st.session_state.notification_channels["email"] = st.checkbox(
235
- "البريد الإلكتروني",
236
- value=st.session_state.notification_channels.get("email", False),
237
- key="channel_email"
238
- )
239
-
240
- if st.session_state.notification_channels["email"]:
241
- email = st.text_input(
242
- "البريد الإلكتروني ��لإشعارات",
243
- value=st.session_state.get("notification_email", ""),
244
- key="notification_email"
245
- )
246
- st.session_state.notification_email = email
247
-
248
- with channels_col2:
249
- st.session_state.notification_channels["sms"] = st.checkbox(
250
- "الرسائل النصية (SMS)",
251
- value=st.session_state.notification_channels.get("sms", False),
252
- key="channel_sms"
253
- )
254
-
255
- if st.session_state.notification_channels["sms"]:
256
- phone = st.text_input(
257
- "رقم الهاتف للإشعارات",
258
- value=st.session_state.get("notification_phone", ""),
259
- key="notification_phone"
260
- )
261
- st.session_state.notification_phone = phone
262
-
263
- st.session_state.notification_channels["mobile_app"] = st.checkbox(
264
- "تطبيق الهاتف المحمول",
265
- value=st.session_state.notification_channels.get("mobile_app", False),
266
- key="channel_mobile_app"
267
- )
268
-
269
- # قسم تفضيلات الإشعارات
270
- st.markdown("### أنواع الإشعارات")
271
- st.markdown("حدد أنواع الإشعارات التي ترغب في تلقيها.")
272
-
273
- prefs_col1, prefs_col2 = st.columns(2)
274
-
275
- with prefs_col1:
276
- st.session_state.notification_preferences["project_updates"] = st.checkbox(
277
- "تحديثات المشاريع",
278
- value=st.session_state.notification_preferences.get("project_updates", True),
279
- key="pref_project_updates"
280
- )
281
-
282
- st.session_state.notification_preferences["document_analysis"] = st.checkbox(
283
- "تحليل المستندات",
284
- value=st.session_state.notification_preferences.get("document_analysis", True),
285
- key="pref_document_analysis"
286
- )
287
-
288
- st.session_state.notification_preferences["deadline_reminders"] = st.checkbox(
289
- "تذكيرات المواعيد النهائية",
290
- value=st.session_state.notification_preferences.get("deadline_reminders", True),
291
- key="pref_deadline_reminders"
292
- )
293
-
294
- st.session_state.notification_preferences["risk_alerts"] = st.checkbox(
295
- "تنبيهات المخاطر",
296
- value=st.session_state.notification_preferences.get("risk_alerts", True),
297
- key="pref_risk_alerts"
298
- )
299
-
300
- with prefs_col2:
301
- st.session_state.notification_preferences["price_changes"] = st.checkbox(
302
- "تغييرات الأسعار",
303
- value=st.session_state.notification_preferences.get("price_changes", True),
304
- key="pref_price_changes"
305
- )
306
-
307
- st.session_state.notification_preferences["team_mentions"] = st.checkbox(
308
- "إشارات فريق العمل",
309
- value=st.session_state.notification_preferences.get("team_mentions", True),
310
- key="pref_team_mentions"
311
- )
312
-
313
- st.session_state.notification_preferences["system_updates"] = st.checkbox(
314
- "تحديثات النظام",
315
- value=st.session_state.notification_preferences.get("system_updates", True),
316
- key="pref_system_updates"
317
- )
318
-
319
- # إعدادات التكرار
320
- st.markdown("### إعدادات التكرار")
321
-
322
- frequency = st.radio(
323
- "تكرار الإشعارات المتشابهة",
324
- options=["فوري", "تجميع كل ساعة", "تجميع كل يوم", "مخصص"],
325
- index=0,
326
- key="notification_frequency"
327
- )
328
-
329
- if frequency == "مخصص":
330
- custom_hours = st.number_input(
331
- "التجميع كل (ساعات)",
332
- min_value=1,
333
- max_value=24,
334
- value=4,
335
- key="custom_frequency_hours"
336
- )
337
- st.session_state.custom_frequency_hours = custom_hours
338
-
339
- # إعدادات متقدمة
340
- with st.expander("إعدادات متقدمة"):
341
- st.checkbox(
342
- "عرض الإشعارات عند بدء تشغيل النظام",
343
- value=True,
344
- key="show_on_startup"
345
- )
346
-
347
- st.checkbox(
348
- "الإشعارات الصوتية",
349
- value=False,
350
- key="audio_notifications"
351
- )
352
-
353
- st.checkbox(
354
- "حفظ سجل الإشعارات",
355
- value=True,
356
- key="log_notifications"
357
- )
358
-
359
- # ��ستدعاء القيمة من session_state إذا كانت موجودة أو استخدام القيمة الافتراضية
360
- retention_days = st.slider(
361
- "الاحتفاظ بالإشعارات (أيام)",
362
- min_value=7,
363
- max_value=365,
364
- value=st.session_state.get("retention_days_value", 90),
365
- key="retention_days"
366
- )
367
- # حفظ القيمة في مفتاح آخر بعد تحديثها عن طريق المستخدم
368
- if "retention_days_value" not in st.session_state:
369
- st.session_state.retention_days_value = retention_days
370
-
371
- # زر حفظ الإعدادات
372
- if styled_button("حفظ الإعدادات", key="save_notification_settings", type="primary", icon="💾"):
373
- self._save_notification_settings()
374
- st.success("تم حفظ إعدادات الإشعارات بنجاح")
375
-
376
- def _render_notification_scheduling(self):
377
- """عرض واجهة جدولة الإشعارات"""
378
- st.markdown("""
379
- <div class='custom-box info-box'>
380
- <h3>🕒 جدولة الإشعارات</h3>
381
- <p>إنشاء وإدارة الإشعارات المجدولة والتذكيرات الدورية.</p>
382
- </div>
383
- """, unsafe_allow_html=True)
384
-
385
- # إنشاء تذكير جديد
386
- st.markdown("### إنشاء تذكير جديد")
387
-
388
- col1, col2 = st.columns(2)
389
-
390
- with col1:
391
- reminder_name = st.text_input("عنوان التذكير", key="new_reminder_name")
392
- reminder_desc = st.text_area("وصف التذكير", key="new_reminder_desc")
393
- reminder_date = st.date_input("تاريخ التذكير", key="new_reminder_date")
394
- reminder_time = st.time_input("وقت التذكير", key="new_reminder_time")
395
-
396
- with col2:
397
- reminder_type = st.selectbox(
398
- "نوع التذكير",
399
- options=[
400
- "موعد نهائي للمناقصة",
401
- "اجتماع مشروع",
402
- "زيارة موقع",
403
- "تسليم مستندات",
404
- "دفعة مالية",
405
- "مراجعة أداء",
406
- "أخرى"
407
- ],
408
- key="new_reminder_type"
409
- )
410
-
411
- reminder_priority = st.select_slider(
412
- "الأولوية",
413
- options=["منخفضة", "متوسطة", "عالية", "حرجة"],
414
- value="متوسطة",
415
- key="new_reminder_priority"
416
- )
417
-
418
- reminder_repeat = st.selectbox(
419
- "التكرار",
420
- options=[
421
- "مرة واحدة",
422
- "يومياً",
423
- "أسبوعياً",
424
- "شهرياً",
425
- "سنوياً"
426
- ],
427
- key="new_reminder_repeat"
428
- )
429
-
430
- if reminder_type == "أخرى":
431
- custom_type = st.text_input("حدد نوع التذكير", key="custom_reminder_type")
432
-
433
- # زر إضافة التذكير
434
- if styled_button("إضافة التذكير", key="add_reminder", type="primary", icon="➕"):
435
- if not reminder_name or not reminder_desc:
436
- st.error("يرجى تعبئة حقول العنوان والوصف")
437
- else:
438
- self._add_scheduled_notification(
439
- title=reminder_name,
440
- message=reminder_desc,
441
- notification_date=datetime.datetime.combine(reminder_date, reminder_time),
442
- notification_type=reminder_type if reminder_type != "أخرى" else custom_type,
443
- priority=reminder_priority,
444
- repeat=reminder_repeat
445
- )
446
- st.success("تم إضافة التذكير بنجاح")
447
-
448
- # عرض التذكيرات المجدولة
449
- st.markdown("### التذكيرات المجدولة")
450
-
451
- # التحقق من وجود تذكيرات مجدولة
452
- scheduled_notifications = self._get_scheduled_notifications()
453
-
454
- if scheduled_notifications:
455
- # عرض التذكيرات في جدول
456
- scheduled_df = pd.DataFrame(scheduled_notifications)
457
-
458
- # تنسيق البيانات للعرض
459
- display_df = scheduled_df.copy()
460
- display_df["التاريخ والوقت"] = display_df["notification_date"].apply(lambda x: x.strftime("%Y-%m-%d %H:%M"))
461
- display_df["العنوان"] = display_df["title"]
462
- display_df["النوع"] = display_df["notification_type"]
463
- display_df["الأولوية"] = display_df["priority"]
464
- display_df["التكرار"] = display_df["repeat"]
465
-
466
- # عرض الجدول
467
- st.dataframe(
468
- display_df[["العنوان", "النوع", "التاريخ والوقت", "الأولوية", "التكرار"]],
469
- use_container_width=True
470
- )
471
-
472
- # عرض الإشعارات المجدولة كبطاقات
473
- for notification in scheduled_notifications:
474
- with st.expander(f"{notification['title']} - {notification['notification_date'].strftime('%Y-%m-%d %H:%M')}"):
475
- notification_col1, notification_col2 = st.columns([3, 1])
476
-
477
- with notification_col1:
478
- st.markdown(f"**الوصف:** {notification['message']}")
479
- st.markdown(f"**النوع:** {notification['notification_type']}")
480
- st.markdown(f"**الأولوية:** {notification['priority']}")
481
- st.markdown(f"**التكرار:** {notification['repeat']}")
482
-
483
- with notification_col2:
484
- if styled_button("تعديل", key=f"edit_{notification['id']}", type="secondary", icon="✏️"):
485
- # تنفيذ في المرحلة القادمة
486
- st.info("ميزة التعديل قيد التطوير")
487
-
488
- if styled_button("حذف", key=f"delete_{notification['id']}", type="danger", icon="🗑️"):
489
- self._delete_scheduled_notification(notification['id'])
490
- st.rerun()
491
- else:
492
- st.info("لا توجد تذكيرات مجدولة")
493
-
494
- def _render_notification_analytics(self):
495
- """عرض تقارير وإحصائيات الإشعارات"""
496
- st.markdown("""
497
- <div class='custom-box info-box'>
498
- <h3>📊 تقارير وإحصائيات الإشعارات</h3>
499
- <p>تحليل وعرض إحصائيات الإشعارات والتنبيهات.</p>
500
- </div>
501
- """, unsafe_allow_html=True)
502
-
503
- # إحصائيات عامة
504
- st.markdown("### إحصائيات عامة")
505
-
506
- # التحقق من وجود إشعارات
507
- if st.session_state.notifications:
508
- # إعداد البيانات
509
- total_count = len(st.session_state.notifications)
510
- read_count = len([n for n in st.session_state.notifications if n.get("read", False)])
511
- unread_count = total_count - read_count
512
-
513
- # تصنيف الإشعارات حسب النوع
514
- notification_types = {}
515
- for notification in st.session_state.notifications:
516
- notification_type = notification.get("notification_type", "أخرى")
517
- notification_types[notification_type] = notification_types.get(notification_type, 0) + 1
518
-
519
- # عرض الإحصائيات
520
- metric_col1, metric_col2, metric_col3 = st.columns(3)
521
-
522
- with metric_col1:
523
- st.metric("إجمالي الإشعارات", total_count)
524
-
525
- with metric_col2:
526
- st.metric("الإشعارات المقروءة", read_count, delta=f"{read_count/total_count*100:.1f}%" if total_count > 0 else "0%")
527
-
528
- with metric_col3:
529
- st.metric("الإشعارات غير المقروءة", unread_count, delta=f"{unread_count/total_count*100:.1f}%" if total_count > 0 else "0%")
530
-
531
- # رسم بياني لتوزيع الإشعارات حسب النوع
532
- st.markdown("### توزيع الإشعارات حسب النوع")
533
-
534
- # إنشاء DataFrame للرسم البياني
535
- types_df = pd.DataFrame({
536
- "النوع": list(notification_types.keys()),
537
- "العدد": list(notification_types.values())
538
- })
539
-
540
- # رسم بياني دائري
541
- import plotly.express as px
542
-
543
- fig = px.pie(
544
- types_df,
545
- values="العدد",
546
- names="النوع",
547
- title="توزيع الإشعارات حسب النوع",
548
- color_discrete_sequence=px.colors.sequential.RdBu
549
- )
550
-
551
- fig.update_layout(
552
- title_font_size=20,
553
- font_family="Arial",
554
- font_size=14,
555
- height=400
556
- )
557
-
558
- st.plotly_chart(fig, use_container_width=True)
559
-
560
- # رسم بياني لتوزيع الإشعارات حسب الوقت
561
- st.markdown("### توزيع الإشعارات حسب الوقت")
562
-
563
- # تحويل التواريخ إلى DataFrame
564
- dates = [
565
- n.get("timestamp", datetime.datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0)
566
- for n in st.session_state.notifications
567
- if "timestamp" in n
568
- ]
569
-
570
- if dates:
571
- date_counts = pd.Series(dates).value_counts().sort_index()
572
-
573
- # إنشاء DataFrame للرسم البياني
574
- date_df = pd.DataFrame({
575
- "التاريخ": date_counts.index,
576
- "العدد": date_counts.values
577
- })
578
-
579
- # رسم بياني خطي
580
- fig2 = px.line(
581
- date_df,
582
- x="التاريخ",
583
- y="العدد",
584
- title="توزيع الإشعارات حسب التاريخ",
585
- markers=True
586
- )
587
-
588
- fig2.update_layout(
589
- title_font_size=20,
590
- font_family="Arial",
591
- font_size=14,
592
- height=400
593
- )
594
-
595
- st.plotly_chart(fig2, use_container_width=True)
596
-
597
- # تصدير البيانات
598
- st.markdown("### تصدير بيانات الإشعارات")
599
-
600
- export_col1, export_col2 = st.columns(2)
601
-
602
- with export_col1:
603
- if styled_button("تصدير CSV", key="export_csv", type="primary", icon="📄"):
604
- # تحويل الإشعارات إلى DataFrame
605
- export_df = pd.DataFrame(st.session_state.notifications)
606
-
607
- # تنسيق البيانات
608
- if "timestamp" in export_df.columns:
609
- export_df["timestamp"] = export_df["timestamp"].apply(
610
- lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if isinstance(x, datetime.datetime) else str(x)
611
- )
612
-
613
- # تصدير إلى CSV
614
- csv_data = export_df.to_csv(index=False)
615
-
616
- # تنزيل الملف
617
- st.download_button(
618
- label="تنزيل ملف CSV",
619
- data=csv_data,
620
- file_name=f"notifications_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
621
- mime="text/csv"
622
- )
623
-
624
- with export_col2:
625
- if styled_button("تصدير JSON", key="export_json", type="primary", icon="📄"):
626
- # تنسيق البيانات
627
- export_data = []
628
- for notification in st.session_state.notifications:
629
- export_item = notification.copy()
630
- if "timestamp" in export_item and isinstance(export_item["timestamp"], datetime.datetime):
631
- export_item["timestamp"] = export_item["timestamp"].strftime("%Y-%m-%d %H:%M:%S")
632
- export_data.append(export_item)
633
-
634
- # تحويل إلى JSON
635
- json_data = json.dumps(export_data, ensure_ascii=False, indent=2)
636
-
637
- # تنزيل الملف
638
- st.download_button(
639
- label="تنزيل ملف JSON",
640
- data=json_data,
641
- file_name=f"notifications_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
642
- mime="application/json"
643
- )
644
- else:
645
- st.info("لا توجد بيانات كافية لعرض الرسم البياني")
646
- else:
647
- st.info("لا توجد إشعارات لعرض الإحصائيات")
648
-
649
- def _render_notification_card(self, notification, show_mark_button=False):
650
- """عرض بطاقة إشعار"""
651
- # تعيين نمط البطاقة حسب الأولوية والحالة
652
- card_style = "notification-card"
653
- if not notification.get("read", False):
654
- card_style += " unread-notification"
655
-
656
- priority = notification.get("priority", "متوسطة")
657
- if priority == "عالية" or priority == "حرجة":
658
- card_style += " high-priority-notification"
659
-
660
- # تعيين الأيقونة حسب نوع الإشعار
661
- icon_map = {
662
- "تحديث مشروع": "🔄",
663
- "وثيقة جديدة": "📄",
664
- "تذكير موعد نهائي": "⏰",
665
- "تنبيه مخاطر": "⚠️",
666
- "تغيير سعر": "💰",
667
- "إشارة فريق العمل": "👥",
668
- "تحديث الن��ام": "🖥️"
669
- }
670
-
671
- notification_type = notification.get("notification_type", "تحديث مشروع")
672
- icon = icon_map.get(notification_type, "🔔")
673
-
674
- # تنسيق التاريخ
675
- timestamp = notification.get("timestamp", datetime.datetime.now())
676
- if isinstance(timestamp, datetime.datetime):
677
- time_str = timestamp.strftime("%Y-%m-%d %H:%M")
678
- else:
679
- time_str = str(timestamp)
680
-
681
- # إنشاء HTML للبطاقة
682
- card_html = f"""
683
- <div class="{card_style}">
684
- <div class="notification-header">
685
- <span class="notification-icon">{icon}</span>
686
- <span class="notification-title">{notification.get('title', 'إشعار جديد')}</span>
687
- <span class="notification-time">{time_str}</span>
688
- </div>
689
- <div class="notification-body">
690
- <p>{notification.get('message', '')}</p>
691
- </div>
692
- <div class="notification-footer">
693
- <span class="notification-type">{notification_type}</span>
694
- <span class="notification-priority">{priority}</span>
695
- </div>
696
- </div>
697
- """
698
-
699
- # عرض البطاقة
700
- st.markdown(card_html, unsafe_allow_html=True)
701
-
702
- # إضافة أزرار التحكم
703
- if show_mark_button:
704
- col1, col2 = st.columns([1, 4])
705
-
706
- with col1:
707
- if styled_button("تعليم كمقروء", key=f"mark_read_{notification.get('id', '')}", type="secondary", icon="✓"):
708
- self._mark_notification_as_read(notification.get('id', ''))
709
- st.rerun()
710
-
711
- with col2:
712
- if notification.get("link"):
713
- if styled_button("عرض التفاصيل", key=f"view_details_{notification.get('id', '')}", type="primary", icon="🔍"):
714
- # افتح الرابط المرتبط بالإشعار
715
- # ملاحظة: هذا سيعمل بشكل مختلف حسب بيئة التشغيل
716
- st.markdown(f"[عرض التفاصيل]({notification.get('link')})")
717
-
718
- def add_notification(self, title, message, notification_type="تحديث مشروع", priority="متوسطة", link=None):
719
- """
720
- إضافة إشعار جديد
721
-
722
- المعلمات:
723
- title: عنوان الإشعار
724
- message: نص الإشعار
725
- notification_type: نوع الإشعار
726
- priority: أولوية الإشعار
727
- link: رابط مرتبط بالإشعار (اختياري)
728
-
729
- الإرجاع:
730
- معرف الإشعار الجديد
731
- """
732
- # إنشاء معرف فريد للإشعار
733
- notification_id = f"notif_{int(time.time())}_{len(st.session_state.notifications)}"
734
-
735
- # إنشاء كائن الإشعار
736
- notification = {
737
- "id": notification_id,
738
- "title": title,
739
- "message": message,
740
- "notification_type": notification_type,
741
- "priority": priority,
742
- "read": False,
743
- "timestamp": datetime.datetime.now(),
744
- "link": link
745
- }
746
-
747
- # إضافة الإشعار لقائمة الإشعارات
748
- st.session_state.notifications.append(notification)
749
-
750
- # زيادة عداد الإشعارات غير المقروءة
751
- st.session_state.unread_count += 1
752
-
753
- # حفظ الإشعارات
754
- self._save_notifications()
755
-
756
- # تسجيل الإشعار
757
- self.logger.info(
758
- f"تمت إضافة إشعار جديد: {title} ({notification_type})"
759
- )
760
-
761
- return notification_id
762
-
763
- def _mark_notification_as_read(self, notification_id):
764
- """
765
- تعليم إشعار كمقروء
766
-
767
- المعلمات:
768
- notification_id: معرف الإشعار
769
-
770
- الإرجاع:
771
- قيمة بوليانية تشير إلى نجاح العملية
772
- """
773
- # البحث عن الإشعار
774
- for i, notification in enumerate(st.session_state.notifications):
775
- if notification.get("id") == notification_id and not notification.get("read", False):
776
- # تعليم الإشعار كمقروء
777
- st.session_state.notifications[i]["read"] = True
778
-
779
- # تحديث عداد الإشعارات غير المقروءة
780
- st.session_state.unread_count = max(0, st.session_state.unread_count - 1)
781
-
782
- # حفظ الإشعارات
783
- self._save_notifications()
784
-
785
- return True
786
-
787
- return False
788
-
789
- def _mark_all_as_read(self):
790
- """
791
- تعليم جميع الإشعارات كمقروءة
792
-
793
- الإرجاع:
794
- عدد الإشعارات التي تم تعليمها
795
- """
796
- count = 0
797
-
798
- # تعليم جميع الإشعارات كمقروءة
799
- for i, notification in enumerate(st.session_state.notifications):
800
- if not notification.get("read", False):
801
- st.session_state.notifications[i]["read"] = True
802
- count += 1
803
-
804
- # إعادة تعيين عداد الإشعارات غير المقروءة
805
- st.session_state.unread_count = 0
806
-
807
- # حفظ الإشعارات
808
- self._save_notifications()
809
-
810
- return count
811
-
812
- def _clear_all_notifications(self):
813
- """
814
- حذف جميع الإشعارات
815
-
816
- الإرجاع:
817
- عدد الإشعارات التي تم حذفها
818
- """
819
- count = len(st.session_state.notifications)
820
-
821
- # مسح قائمة الإشعارات
822
- st.session_state.notifications = []
823
-
824
- # إعادة تعيين عداد الإشعارات غير المقروءة
825
- st.session_state.unread_count = 0
826
-
827
- # حفظ الإشعارات
828
- self._save_notifications()
829
-
830
- return count
831
-
832
- def _filter_notifications(self, notification_type=None, date_range=None):
833
- """
834
- تصفية الإشعارات حسب النوع والتاريخ
835
-
836
- المعلمات:
837
- notification_type: قائمة أنواع الإشعارات
838
- date_range: نطاق تاريخ الإشعارات
839
-
840
- الإرجاع:
841
- قائمة الإشعارات المصفاة
842
- """
843
- filtered_notifications = st.session_state.notifications.copy()
844
-
845
- # تصفية حسب النوع
846
- if notification_type and len(notification_type) > 0:
847
- filtered_notifications = [
848
- n for n in filtered_notifications
849
- if n.get("notification_type") in notification_type
850
- ]
851
-
852
- # تصفية حسب نطاق التاريخ
853
- if date_range and len(date_range) == 2:
854
- start_date, end_date = date_range
855
-
856
- # تحويل التواريخ إلى datetime
857
- start_date = datetime.datetime.combine(start_date, datetime.time.min)
858
- end_date = datetime.datetime.combine(end_date, datetime.time.max)
859
-
860
- filtered_notifications = [
861
- n for n in filtered_notifications
862
- if isinstance(n.get("timestamp"), datetime.datetime) and
863
- start_date <= n.get("timestamp") <= end_date
864
- ]
865
-
866
- return filtered_notifications
867
-
868
- def _add_scheduled_notification(self, title, message, notification_date, notification_type="تذكير", priority="متوسطة", repeat="مرة واحدة"):
869
- """
870
- إضافة إشعار مجدول
871
-
872
- المعلمات:
873
- title: عنوان الإشعار
874
- message: نص الإشعار
875
- notification_date: تاريخ ووقت الإشعار
876
- notification_type: نوع الإشعار
877
- priority: أولوية الإشعار
878
- repeat: نمط تكرار الإشعار
879
-
880
- الإرجاع:
881
- معرف الإشعار المجدول
882
- """
883
- # إنشاء معرف فريد للإشعار المجدول
884
- scheduled_id = f"sched_{int(time.time())}_{len(self._get_scheduled_notifications())}"
885
-
886
- # إنشاء كائن الإشعار المجدول
887
- scheduled_notification = {
888
- "id": scheduled_id,
889
- "title": title,
890
- "message": message,
891
- "notification_date": notification_date,
892
- "notification_type": notification_type,
893
- "priority": priority,
894
- "repeat": repeat,
895
- "created_at": datetime.datetime.now(),
896
- "last_triggered": None
897
- }
898
-
899
- # إضافة الإشعار المجدول للقائمة
900
- scheduled_notifications = self._get_scheduled_notifications()
901
- scheduled_notifications.append(scheduled_notification)
902
-
903
- # حفظ الإشعارات المجدولة
904
- self._save_scheduled_notifications(scheduled_notifications)
905
-
906
- # تسجيل الإشعار المجدول
907
- self.logger.info(
908
- f"تمت إضافة إشعار مجدول: {title} ({notification_date.strftime('%Y-%m-%d %H:%M')})"
909
- )
910
-
911
- return scheduled_id
912
-
913
- def _delete_scheduled_notification(self, notification_id):
914
- """
915
- حذف إشعار مجدول
916
-
917
- المعلمات:
918
- notification_id: معرف الإشعار المجدول
919
-
920
- الإرجاع:
921
- قيمة بوليانية تشير إلى نج��ح العملية
922
- """
923
- scheduled_notifications = self._get_scheduled_notifications()
924
-
925
- # البحث عن الإشعار المجدول
926
- for i, notification in enumerate(scheduled_notifications):
927
- if notification.get("id") == notification_id:
928
- # حذف الإشعار المجدول
929
- del scheduled_notifications[i]
930
-
931
- # حفظ الإشعارات المجدولة
932
- self._save_scheduled_notifications(scheduled_notifications)
933
-
934
- # تسجيل الحذف
935
- self.logger.info(
936
- f"تم حذف الإشعار المجدول: {notification_id}"
937
- )
938
-
939
- return True
940
-
941
- return False
942
-
943
- def _get_scheduled_notifications(self):
944
- """
945
- الحصول على قائمة الإشعارات المجدولة
946
-
947
- الإرجاع:
948
- قائمة الإشعارات المجدولة
949
- """
950
- try:
951
- # التحقق من وجود ملف الإشعارات المجدولة
952
- scheduled_file = os.path.join(self.data_dir, "scheduled_notifications.json")
953
-
954
- if os.path.exists(scheduled_file):
955
- with open(scheduled_file, 'r', encoding='utf-8') as f:
956
- scheduled_data = json.load(f)
957
-
958
- # تحويل التواريخ من نصوص إلى كائنات datetime
959
- for notification in scheduled_data:
960
- if "notification_date" in notification:
961
- notification["notification_date"] = datetime.datetime.fromisoformat(notification["notification_date"])
962
-
963
- if "created_at" in notification:
964
- notification["created_at"] = datetime.datetime.fromisoformat(notification["created_at"])
965
-
966
- if "last_triggered" in notification and notification["last_triggered"]:
967
- notification["last_triggered"] = datetime.datetime.fromisoformat(notification["last_triggered"])
968
-
969
- return scheduled_data
970
-
971
- return []
972
-
973
- except Exception as e:
974
- self.logger.error(f"حدث خطأ أثناء قراءة الإشعارات المجدولة: {str(e)}")
975
- return []
976
-
977
- def _save_scheduled_notifications(self, scheduled_notifications):
978
- """
979
- حفظ قائمة الإشعارات المجدولة
980
-
981
- المعلمات:
982
- scheduled_notifications: قائمة الإشعارات المجدولة
983
- """
984
- try:
985
- # التأكد من وجود المجلد
986
- os.makedirs(self.data_dir, exist_ok=True)
987
-
988
- # تحويل كائنات datetime إلى نصوص
989
- scheduled_data = []
990
-
991
- for notification in scheduled_notifications:
992
- notification_copy = notification.copy()
993
-
994
- if "notification_date" in notification_copy and isinstance(notification_copy["notification_date"], datetime.datetime):
995
- notification_copy["notification_date"] = notification_copy["notification_date"].isoformat()
996
-
997
- if "created_at" in notification_copy and isinstance(notification_copy["created_at"], datetime.datetime):
998
- notification_copy["created_at"] = notification_copy["created_at"].isoformat()
999
-
1000
- if "last_triggered" in notification_copy and isinstance(notification_copy["last_triggered"], datetime.datetime):
1001
- notification_copy["last_triggered"] = notification_copy["last_triggered"].isoformat()
1002
-
1003
- scheduled_data.append(notification_copy)
1004
-
1005
- # حفظ البيانات
1006
- scheduled_file = os.path.join(self.data_dir, "scheduled_notifications.json")
1007
-
1008
- with open(scheduled_file, 'w', encoding='utf-8') as f:
1009
- json.dump(scheduled_data, f, ensure_ascii=False, indent=2)
1010
-
1011
- except Exception as e:
1012
- self.logger.error(f"حدث خطأ أثناء حفظ الإشعارات المجدولة: {str(e)}")
1013
-
1014
- def _save_notification_settings(self):
1015
- """حفظ إعدادات الإشعارات"""
1016
- try:
1017
- # التأكد من وجود المجلد
1018
- os.makedirs(self.data_dir, exist_ok=True)
1019
-
1020
- # إعداد البيانات
1021
- settings_data = {
1022
- "notification_channels": st.session_state.notification_channels,
1023
- "notification_preferences": st.session_state.notification_preferences,
1024
- "notification_email": st.session_state.get("notification_email", ""),
1025
- "notification_phone": st.session_state.get("notification_phone", ""),
1026
- "notification_frequency": st.session_state.get("notification_frequency", "فوري"),
1027
- "custom_frequency_hours": st.session_state.get("custom_frequency_hours", 4),
1028
- "show_on_startup": st.session_state.get("show_on_startup", True),
1029
- "audio_notifications": st.session_state.get("audio_notifications", False),
1030
- "log_notifications": st.session_state.get("log_notifications", True),
1031
- "retention_days": st.session_state.get("retention_days", 90)
1032
- }
1033
-
1034
- # حفظ البيانات
1035
- settings_file = os.path.join(self.data_dir, "notification_settings.json")
1036
-
1037
- with open(settings_file, 'w', encoding='utf-8') as f:
1038
- json.dump(settings_data, f, ensure_ascii=False, indent=2)
1039
-
1040
- # تسجيل الحفظ
1041
- self.logger.info("تم حفظ إعدادات الإشعارات بنجاح")
1042
-
1043
- except Exception as e:
1044
- self.logger.error(f"حدث خطأ أثناء حفظ إعدادات الإشعارات: {str(e)}")
1045
-
1046
- def _load_notification_settings(self):
1047
- """تحميل إعدادات الإشعارات"""
1048
- try:
1049
- # التحقق من وجود ملف الإعدادات
1050
- settings_file = os.path.join(self.data_dir, "notification_settings.json")
1051
-
1052
- if os.path.exists(settings_file):
1053
- with open(settings_file, 'r', encoding='utf-8') as f:
1054
- settings_data = json.load(f)
1055
-
1056
- # تحديث حالة الجلسة
1057
- st.session_state.notification_channels = settings_data.get("notification_channels", {})
1058
- st.session_state.notification_preferences = settings_data.get("notification_preferences", {})
1059
- st.session_state.notification_email = settings_data.get("notification_email", "")
1060
- st.session_state.notification_phone = settings_data.get("notification_phone", "")
1061
- st.session_state.notification_frequency = settings_data.get("notification_frequency", "فوري")
1062
- st.session_state.custom_frequency_hours = settings_data.get("custom_frequency_hours", 4)
1063
- st.session_state.show_on_startup = settings_data.get("show_on_startup", True)
1064
- st.session_state.audio_notifications = settings_data.get("audio_notifications", False)
1065
- st.session_state.log_notifications = settings_data.get("log_notifications", True)
1066
- st.session_state.retention_days = settings_data.get("retention_days", 90)
1067
-
1068
- # تسجيل التحميل
1069
- self.logger.info("تم تحميل إعدادات الإشعارات بنجاح")
1070
-
1071
- except Exception as e:
1072
- self.logger.error(f"حدث خطأ أثناء تحميل إعدادات الإشعارات: {str(e)}")
1073
-
1074
- def _save_notifications(self):
1075
- """حفظ الإشعارات"""
1076
- try:
1077
- # التأكد من وجود المجلد
1078
- os.makedirs(self.data_dir, exist_ok=True)
1079
-
1080
- # تحويل كائنات datetime إلى نصوص
1081
- notifications_data = []
1082
-
1083
- for notification in st.session_state.notifications:
1084
- notification_copy = notification.copy()
1085
-
1086
- if "timestamp" in notification_copy and isinstance(notification_copy["timestamp"], datetime.datetime):
1087
- notification_copy["timestamp"] = notification_copy["timestamp"].isoformat()
1088
-
1089
- notifications_data.append(notification_copy)
1090
-
1091
- # حفظ البيانات
1092
- notifications_file = os.path.join(self.data_dir, "notifications.json")
1093
-
1094
- with open(notifications_file, 'w', encoding='utf-8') as f:
1095
- json.dump(notifications_data, f, ensure_ascii=False, indent=2)
1096
-
1097
- except Exception as e:
1098
- self.logger.error(f"حدث خطأ أثناء حفظ الإشعارات: {str(e)}")
1099
-
1100
- def _load_notifications(self):
1101
- """تحميل الإشعارات"""
1102
- try:
1103
- # التحقق من وجود ملف الإشعارات
1104
- notifications_file = os.path.join(self.data_dir, "notifications.json")
1105
-
1106
- if os.path.exists(notifications_file):
1107
- with open(notifications_file, 'r', encoding='utf-8') as f:
1108
- notifications_data = json.load(f)
1109
-
1110
- # تحويل النصوص إلى كائنات datetime
1111
- for notification in notifications_data:
1112
- if "timestamp" in notification:
1113
- notification["timestamp"] = datetime.datetime.fromisoformat(notification["timestamp"])
1114
-
1115
- # تحديث حالة الجلسة
1116
- st.session_state.notifications = notifications_data
1117
-
1118
- # حساب عدد الإشعارات غير المقروءة
1119
- st.session_state.unread_count = len([
1120
- n for n in st.session_state.notifications
1121
- if not n.get("read", False)
1122
- ])
1123
-
1124
- # تحميل إعدادات الإشعارات
1125
- self._load_notification_settings()
1126
-
1127
- # تسجيل التحميل
1128
- self.logger.info(f"تم تحميل {len(notifications_data)} إشعار بنجاح")
1129
-
1130
- except Exception as e:
1131
- self.logger.error(f"حدث خطأ أثناء تحميل الإشعارات: {str(e)}")
1132
-
1133
- def check_scheduled_notifications(self):
1134
- """
1135
- التحقق من الإشعارات المجدولة وإطلاقها إذا حان وقتها
1136
-
1137
- الإرجاع:
1138
- عدد الإشعارات التي تم إطلاقها
1139
- """
1140
- count = 0
1141
-
1142
- # الحصول على الإشعارات المجدولة
1143
- scheduled_notifications = self._get_scheduled_notifications()
1144
-
1145
- # الوقت الحالي
1146
- now = datetime.datetime.now()
1147
-
1148
- # التحقق من كل إشعار مجدول
1149
- for notification in scheduled_notifications:
1150
- notification_date = notification.get("notification_date")
1151
-
1152
- if notification_date and notification_date <= now:
1153
- # إنشاء إشعار جديد
1154
- self.add_notification(
1155
- title=notification.get("title"),
1156
- message=notification.get("message"),
1157
- notification_type=notification.get("notification_type"),
1158
- priority=notification.get("priority")
1159
- )
1160
-
1161
- # تحديث آخر مرة تم فيها إطلاق الإشعار
1162
- notification["last_triggered"] = now
1163
-
1164
- # التعامل مع التكرار
1165
- repeat = notification.get("repeat", "مرة واحدة")
1166
-
1167
- if repeat == "مرة واحدة":
1168
- # حذف الإشعار المجدول
1169
- self._delete_scheduled_notification(notification.get("id"))
1170
- else:
1171
- # حساب التاريخ التالي
1172
- if repeat == "يومياً":
1173
- new_date = notification_date + datetime.timedelta(days=1)
1174
- elif repeat == "أسبوعياً":
1175
- new_date = notification_date + datetime.timedelta(weeks=1)
1176
- elif repeat == "شهرياً":
1177
- # إضافة شهر (تقريبي)
1178
- new_month = notification_date.month + 1
1179
- new_year = notification_date.year
1180
-
1181
- if new_month > 12:
1182
- new_month = 1
1183
- new_year += 1
1184
-
1185
- new_date = notification_date.replace(year=new_year, month=new_month)
1186
- elif repeat == "سنوياً":
1187
- new_date = notification_date.replace(year=notification_date.year + 1)
1188
- else:
1189
- # افتراضي: يومياً
1190
- new_date = notification_date + datetime.timedelta(days=1)
1191
-
1192
- # تحديث تاريخ الإشعار المجدول
1193
- notification["notification_date"] = new_date
1194
-
1195
- count += 1
1196
-
1197
- # حفظ الإشعارات المجدولة إذا تم تغييرها
1198
- if count > 0:
1199
- self._save_scheduled_notifications(scheduled_notifications)
1200
-
1201
- return count
1202
-
1203
-
1204
- # تطبيق وحدة نظام الإشعارات الذكي
1205
- class NotificationsApp:
1206
- """وحدة تطبيق نظام الإشعارات الذكي"""
1207
-
1208
- def __init__(self):
1209
- """تهيئة وحدة تطبيق نظام الإشعارات الذكي"""
1210
- self.smart_notification_system = SmartNotificationSystem()
1211
-
1212
- def render(self):
1213
- """عرض واجهة وحدة تطبيق نظام الإشعارات الذكي"""
1214
- st.markdown("<h2 class='module-title'>نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات</h2>", unsafe_allow_html=True)
1215
-
1216
- st.markdown("""
1217
- <div class="module-description">
1218
- يتيح لك نظام الإشعارات الذكي متابعة تحديثات المشاريع وتلقي تنبيهات مخصصة حسب أدوارك واهتماماتك.
1219
- يمكنك تخصيص إعدادات الإشعارات وجدولة التذكيرات للمواعيد النهائية والمهام.
1220
- </div>
1221
- """, unsafe_allow_html=True)
1222
-
1223
- # عرض نظام الإشعارات الذكي
1224
- self.smart_notification_system.render()
1225
-
1226
-
1227
- # تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
1228
- if __name__ == "__main__":
1229
- st.set_page_config(
1230
- page_title="نظام الإشعارات الذكي | WAHBi AI",
1231
- page_icon="🔔",
1232
- layout="wide",
1233
- initial_sidebar_state="expanded"
1234
- )
1235
-
1236
- app = NotificationsApp()
1237
- app.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/constants.py DELETED
@@ -1,113 +0,0 @@
1
- """
2
- ثوابت وحدة التسعير
3
- """
4
-
5
- # أوزان المحتوى المحلي
6
- LOCAL_CONTENT_WEIGHTS = {
7
- 'منتجات_البناء': 1.5, # المنتجات الأساسية في البناء لها وزن أكبر
8
- 'المنتجات_الإنشائية': 1.5, # المنتجات الإنشائية لها وزن أكبر
9
- 'منتجات_التشطيب': 1.0, # منتجات التشطيب لها وزن عادي
10
- 'الخدمات_الهندسية': 1.3, # الخدمات الهندسية لها وزن أكبر
11
- 'الخدمات_الإدارية': 1.0, # الخدمات الإدارية لها وزن عادي
12
- 'القوى_العاملة_الفنية': 1.2, # القوى العاملة الفنية لها وزن أكبر
13
- 'القوى_العاملة_العادية': 1.0, # القوى العاملة العادية لها وزن عادي
14
- 'القوى_العاملة_الإدارية': 0.8 # القوى العاملة الإدارية لها وزن أقل
15
- }
16
-
17
- # فئات التكاليف
18
- COST_CATEGORIES = {
19
- 'مباشرة': [
20
- 'مواد',
21
- 'عمالة',
22
- 'معدات',
23
- 'مقاولين من الباطن'
24
- ],
25
- 'غير_مباشرة': [
26
- 'إدارة المشروع',
27
- 'ضمانات بنكية',
28
- 'تأمينات',
29
- 'مكاتب الموقع',
30
- 'نقل وسكن',
31
- 'مرافق',
32
- 'أمن وسلامة'
33
- ],
34
- 'مصاريف_عامة': [
35
- 'مصاريف إدارية',
36
- 'رواتب إدارية',
37
- 'إيجارات',
38
- 'اتصالات',
39
- 'قرطاسية',
40
- 'تسويق وعلاقات عامة'
41
- ],
42
- 'احتياطيات': [
43
- 'احتياطي مخاطر',
44
- 'احتياطي تضخم',
45
- 'احتياطي تغييرات'
46
- ]
47
- }
48
-
49
- # أنواع التسعير
50
- PRICING_TYPES = {
51
- 'قياسي': 'التسعير المتوازن لجميع البنود',
52
- 'غير_متزن': 'تحميل بعض البنود بسعر أعلى وتخفيض بنود أخرى مع الحفاظ على نفس الإجمالي',
53
- 'تنافسي': 'التسعير بناءً على أسعار المنافسين',
54
- 'ربحية': 'التسعير بناءً على هامش الربح المستهدف'
55
- }
56
-
57
- # أنواع استراتيجيات التسعير غير المتزن
58
- UNBALANCED_PRICING_STRATEGIES = {
59
- 'تحميل_أمامي': 'زيادة أسعار البنود المبكرة في المشروع',
60
- 'تحميل_خلفي': 'زيادة أسعار البنود المتأخرة في المشروع',
61
- 'تحميل_مؤكد': 'زيادة أسعار البنود المؤكدة التنفيذ',
62
- 'تخفيض_متغير': 'تخفيض أسعار البنود المحتمل تغير كمياتها'
63
- }
64
-
65
- # معلمات افتراضية للمشروع
66
- DEFAULT_PROJECT_PARAMS = {
67
- 'نسبة_المصاريف_العامة': 8.0, # 8% من التكاليف المباشرة
68
- 'نسبة_الأرباح': 10.0, # 10% من التكاليف الكلية
69
- 'نسبة_احتياطي_المخاطر': 5.0, # 5% من التكاليف المباشرة
70
- 'نسبة_ضمان_ابتدائي': 2.0, # 2% من قيمة العطاء
71
- 'نسبة_ضمان_نهائي': 5.0, # 5% من قيمة العطاء
72
- 'نسبة_محتجزات': 10.0, # 10% من قيمة المستخلصات
73
- 'نسبة_دفعة_مقدمة': 10.0 # 10% من قيمة العطاء
74
- }
75
-
76
- # وحدات القياس
77
- UNITS_OF_MEASURE = {
78
- 'طولية': ['م.ط', 'متر طولي', 'م'],
79
- 'مسطحة': ['م2', 'متر مربع'],
80
- 'حجمية': ['م3', 'متر مكعب'],
81
- 'وزن': ['كجم', 'طن', 'جم'],
82
- 'عدد': ['عدد', 'وحدة', 'قطعة'],
83
- 'زمن': ['يوم', 'ساعة', 'شهر'],
84
- 'نقطة': ['نقطة', 'مخرج']
85
- }
86
-
87
- # نسب الزيادة في التكاليف
88
- COST_INCREASE_FACTORS = {
89
- 'تعقيد_مرتفع': 1.25, # زيادة 25% للأعمال المعقدة
90
- 'تعقيد_متوسط': 1.15, # زيادة 15% للأعمال متوسطة التعقيد
91
- 'منطقة_نائية': 1.2, # زيادة 20% للمناطق النائية
92
- 'ظروف_جوية_قاسية': 1.15, # زيادة 15% للظروف الجوية القاسية
93
- 'ظروف_الموقع_صعبة': 1.2, # زيادة 20% لظروف الموقع الصعبة
94
- 'عاجل': 1.3 # زيادة 30% للأعمال العاجلة
95
- }
96
-
97
- # أنواع المشاريع
98
- PROJECT_TYPES = [
99
- 'سكني',
100
- 'تجاري',
101
- 'صناعي',
102
- 'تعليمي',
103
- 'صحي',
104
- 'بنية تحتية',
105
- 'طرق',
106
- 'نقل',
107
- 'طاقة',
108
- 'مياه وصرف صحي',
109
- 'اتصالات',
110
- 'عسكري',
111
- 'ترفيهي',
112
- 'متعدد الاستخدام'
113
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/construction_calculator.py DELETED
@@ -1,787 +0,0 @@
1
- """
2
- حاسبة تكاليف البناء المتكاملة
3
- تتضمن العناصر التالية:
4
- - المواد الخام
5
- - المعدات
6
- - العمالة
7
- - المصاريف الإدارية
8
- - هامش الربح
9
- """
10
-
11
- import streamlit as st
12
- import pandas as pd
13
- import numpy as np
14
- import plotly.express as px
15
- import plotly.graph_objects as go
16
-
17
-
18
- def render_construction_calculator():
19
- """
20
- عرض حاسبة تكاليف البناء المتكاملة
21
- """
22
- # التأكد من وجود المتغيرات في حالة الجلسة
23
- if 'materials_cost' not in st.session_state:
24
- st.session_state.materials_cost = 0.0
25
- if 'equipment_cost' not in st.session_state:
26
- st.session_state.equipment_cost = 0.0
27
- if 'labor_cost' not in st.session_state:
28
- st.session_state.labor_cost = 0.0
29
- if 'admin_cost' not in st.session_state:
30
- st.session_state.admin_cost = 0.0
31
- if 'profit_margin' not in st.session_state:
32
- st.session_state.profit_margin = 15.0
33
-
34
- st.markdown("<h2 class='module-title'>حاسبة تكاليف البناء المتكاملة</h2>", unsafe_allow_html=True)
35
-
36
- # معلومات المشروع
37
- st.markdown("<h3>معلومات المشروع</h3>", unsafe_allow_html=True)
38
-
39
- col1, col2 = st.columns(2)
40
-
41
- with col1:
42
- project_name = st.text_input("اسم المشروع", "مشروع سكني")
43
- project_location = st.text_input("موقع المشروع", "الرياض - حي النرجس")
44
-
45
- with col2:
46
- project_area = st.number_input("المساحة الإجمالية (م²)", min_value=1, value=500)
47
- project_type = st.selectbox(
48
- "نوع المشروع",
49
- options=[
50
- "سكني", "تجاري", "صناعي", "إداري", "صحي", "تعليمي",
51
- "بنية تحتية", "طرق", "جسور", "أخرى"
52
- ]
53
- )
54
-
55
- # التبويبات الرئيسية للحاسبة
56
- tabs = st.tabs([
57
- "المواد الخام", "المعدات", "العمالة", "المصاريف الإدارية", "هامش الربح", "التقرير النهائي"
58
- ])
59
-
60
- # تعريف المتغيرات العامة
61
- if "materials_cost" not in st.session_state:
62
- st.session_state.materials_cost = 0.0
63
- if "equipment_cost" not in st.session_state:
64
- st.session_state.equipment_cost = 0.0
65
- if "labor_cost" not in st.session_state:
66
- st.session_state.labor_cost = 0.0
67
- if "admin_cost" not in st.session_state:
68
- st.session_state.admin_cost = 0.0
69
- if "profit_margin" not in st.session_state:
70
- st.session_state.profit_margin = 10.0
71
- if "materials" not in st.session_state:
72
- st.session_state.materials = []
73
- if "equipment" not in st.session_state:
74
- st.session_state.equipment = []
75
- if "labor" not in st.session_state:
76
- st.session_state.labor = []
77
- if "admin_expenses" not in st.session_state:
78
- st.session_state.admin_expenses = []
79
-
80
- # تبويب المواد الخام
81
- with tabs[0]:
82
- render_materials_tab()
83
-
84
- # تبويب المعدات
85
- with tabs[1]:
86
- render_equipment_tab()
87
-
88
- # تبويب العمالة
89
- with tabs[2]:
90
- render_labor_tab()
91
-
92
- # تبويب المصاريف الإدارية
93
- with tabs[3]:
94
- render_admin_tab()
95
-
96
- # تبويب هامش الربح
97
- with tabs[4]:
98
- render_profit_tab()
99
-
100
- # تبويب التقرير النهائي
101
- with tabs[5]:
102
- render_final_report(project_name, project_location, project_area, project_type)
103
-
104
-
105
- def render_materials_tab():
106
- """
107
- عرض تبويب المواد الخام
108
- """
109
- st.markdown("<h3>تكاليف المواد الخام</h3>", unsafe_allow_html=True)
110
-
111
- # إضافة مادة جديدة
112
- st.markdown("<h4>إضافة مادة جديدة</h4>", unsafe_allow_html=True)
113
-
114
- col1, col2, col3, col4 = st.columns(4)
115
-
116
- with col1:
117
- material_name = st.text_input("اسم المادة", key="new_material_name")
118
- with col2:
119
- material_quantity = st.number_input("الكمية", min_value=0.0, step=0.1, key="new_material_quantity")
120
- with col3:
121
- material_unit = st.selectbox(
122
- "الوحدة",
123
- options=["م²", "م³", "طن", "كجم", "لتر", "قطعة", "لفة", "كيس", "أخرى"],
124
- key="new_material_unit"
125
- )
126
- with col4:
127
- material_price = st.number_input("السعر للوحدة (ريال)", min_value=0.0, step=0.01, key="new_material_price")
128
-
129
- if st.button("إضافة مادة", key="add_material_btn"):
130
- total_price = material_quantity * material_price
131
- new_material = {
132
- "name": material_name,
133
- "quantity": material_quantity,
134
- "unit": material_unit,
135
- "price": material_price,
136
- "total": total_price
137
- }
138
- st.session_state.materials.append(new_material)
139
- st.success(f"تمت إضافة {material_name} بنجاح!")
140
-
141
- # عرض قائمة المواد المضافة
142
- if st.session_state.materials:
143
- st.markdown("<h4>قائمة المواد المضافة</h4>", unsafe_allow_html=True)
144
-
145
- materials_df = pd.DataFrame(st.session_state.materials)
146
- materials_df.columns = ["اسم المادة", "الكمية", "الوحدة", "السعر للوحدة", "التكلفة الإجمالية"]
147
- st.dataframe(materials_df)
148
-
149
- total_materials_cost = sum(item["total"] for item in st.session_state.materials)
150
- st.session_state.materials_cost = total_materials_cost
151
-
152
- st.markdown(f"<h4>إجمالي تكلفة المواد: <span style='color:var(--primary-color)'>{total_materials_cost:,.2f} ريال</span></h4>", unsafe_allow_html=True)
153
-
154
- # رسم بياني للمواد حسب التكلفة
155
- if len(st.session_state.materials) > 1:
156
- st.markdown("<h4>توزيع تكاليف المواد</h4>", unsafe_allow_html=True)
157
-
158
- fig = px.pie(
159
- materials_df,
160
- values="التكلفة الإجمالية",
161
- names="اسم المادة",
162
- title="توزيع تكاليف المواد",
163
- color_discrete_sequence=px.colors.sequential.Teal,
164
- hole=0.4
165
- )
166
- fig.update_layout(
167
- font=dict(family="Almarai, Arial", size=14),
168
- margin=dict(t=50, b=50, l=20, r=20)
169
- )
170
- st.plotly_chart(fig, use_container_width=True)
171
-
172
- st.markdown("---")
173
-
174
- # استيراد بيانات المواد من ملف
175
- st.markdown("<h4>استيراد بيانات المواد من ملف</h4>", unsafe_allow_html=True)
176
- uploaded_file = st.file_uploader("اختر ملف Excel أو CSV", type=["xlsx", "csv"], key="materials_upload")
177
-
178
- if uploaded_file is not None:
179
- if uploaded_file.name.endswith('.csv'):
180
- df = pd.read_csv(uploaded_file)
181
- else:
182
- df = pd.read_excel(uploaded_file)
183
-
184
- st.success("تم استيراد البيانات بنجاح!")
185
- st.dataframe(df)
186
-
187
- if st.button("إضافة المواد من الملف"):
188
- try:
189
- # تحويل أسماء الأعمدة للمطابقة مع النظام
190
- column_mapping = {
191
- "المادة": "name",
192
- "اسم المادة": "name",
193
- "الكمية": "quantity",
194
- "الوحدة": "unit",
195
- "السعر": "price",
196
- "سعر الوحدة": "price"
197
- }
198
-
199
- mapped_df = df.rename(columns=column_mapping)
200
-
201
- # حساب التكلفة الإجمالية لكل مادة
202
- for _, row in mapped_df.iterrows():
203
- total_price = row["quantity"] * row["price"]
204
- new_material = {
205
- "name": row["name"],
206
- "quantity": row["quantity"],
207
- "unit": row["unit"],
208
- "price": row["price"],
209
- "total": total_price
210
- }
211
- st.session_state.materials.append(new_material)
212
-
213
- st.success("تمت إضافة جميع المواد من الملف بنجاح!")
214
-
215
- except Exception as e:
216
- st.error(f"حدث خطأ: {str(e)}")
217
- st.error("تأكد من أن الملف يحتوي على الأعمدة المطلوبة: اسم المادة، الكمية، الوحدة، السعر للوحدة")
218
-
219
-
220
- def render_equipment_tab():
221
- """
222
- عرض تبويب المعدات
223
- """
224
- st.markdown("<h3>تكاليف المعدات</h3>", unsafe_allow_html=True)
225
-
226
- # إضافة معدة جديدة
227
- st.markdown("<h4>إضافة معدة جديدة</h4>", unsafe_allow_html=True)
228
-
229
- col1, col2, col3 = st.columns(3)
230
-
231
- with col1:
232
- equipment_name = st.text_input("اسم المعدة", key="new_equipment_name")
233
- with col2:
234
- rental_type = st.selectbox(
235
- "نوع الإيجار",
236
- options=["يومي", "أسبوعي", "شهري", "سنوي", "مملوكة (استهلاك)"],
237
- key="rental_type"
238
- )
239
- with col3:
240
- usage_period = st.number_input(f"مدة الاستخدام ({rental_type})", min_value=1, value=1, key="usage_period")
241
-
242
- col4, col5, col6 = st.columns(3)
243
-
244
- with col4:
245
- equipment_rate = st.number_input(f"سعر الإيجار لكل ({rental_type}) (ريال)", min_value=0.0, step=0.01, key="equipment_rate")
246
- with col5:
247
- fuel_cost = st.number_input("تكلفة الوقود اليومية (ريال)", min_value=0.0, step=0.01, key="fuel_cost")
248
- with col6:
249
- operator_cost = st.number_input("تكلفة المشغل اليومية (ريال)", min_value=0.0, step=0.01, key="operator_cost")
250
-
251
- # حساب إجمالي التكلفة
252
- rental_days = {
253
- "يومي": 1,
254
- "أسبوعي": 7,
255
- "شهري": 30,
256
- "سنوي": 365,
257
- "مملوكة (استهلاك)": 1
258
- }
259
-
260
- total_days = usage_period * rental_days[rental_type]
261
- total_equipment_cost = equipment_rate * usage_period
262
- total_fuel_cost = fuel_cost * total_days
263
- total_operator_cost = operator_cost * total_days
264
- total_cost = total_equipment_cost + total_fuel_cost + total_operator_cost
265
-
266
- if st.button("إضافة معدة", key="add_equipment_btn"):
267
- new_equipment = {
268
- "name": equipment_name,
269
- "rental_type": rental_type,
270
- "usage_period": usage_period,
271
- "equipment_rate": equipment_rate,
272
- "fuel_cost": fuel_cost,
273
- "operator_cost": operator_cost,
274
- "total": total_cost
275
- }
276
- st.session_state.equipment.append(new_equipment)
277
- st.success(f"تمت إضافة {equipment_name} بنجاح!")
278
-
279
- # عرض تفاصيل الحساب
280
- st.markdown("<div class='card' style='margin-top: 10px;'>", unsafe_allow_html=True)
281
- st.markdown(f"<p>عدد أيام الاستخدام الإجمالية: {total_days} يوم</p>", unsafe_allow_html=True)
282
- st.markdown(f"<p>تكلفة إيجار المعدة: {total_equipment_cost:,.2f} ريال</p>", unsafe_allow_html=True)
283
- st.markdown(f"<p>تكلفة الوقود: {total_fuel_cost:,.2f} ريال</p>", unsafe_allow_html=True)
284
- st.markdown(f"<p>تكلفة المشغل: {total_operator_cost:,.2f} ريال</p>", unsafe_allow_html=True)
285
- st.markdown(f"<h4>التكلفة الإجمالية للمعدة: {total_cost:,.2f} ريال</h4>", unsafe_allow_html=True)
286
- st.markdown("</div>", unsafe_allow_html=True)
287
-
288
- # عرض قائمة المعدات المضافة
289
- if st.session_state.equipment:
290
- st.markdown("<h4>قائمة المعدات المضافة</h4>", unsafe_allow_html=True)
291
-
292
- equipment_data = []
293
- for item in st.session_state.equipment:
294
- equipment_data.append({
295
- "اسم المعدة": item["name"],
296
- "نوع الإيجار": item["rental_type"],
297
- "مدة الاستخدام": item["usage_period"],
298
- "إيجار الوحدة": item["equipment_rate"],
299
- "تكلفة الوقود": item["fuel_cost"],
300
- "تكلفة المشغل": item["operator_cost"],
301
- "التكلفة الإجمالية": item["total"]
302
- })
303
-
304
- equipment_df = pd.DataFrame(equipment_data)
305
- st.dataframe(equipment_df)
306
-
307
- total_equipment_cost = sum(item["total"] for item in st.session_state.equipment)
308
- st.session_state.equipment_cost = total_equipment_cost
309
-
310
- st.markdown(f"<h4>إجمالي تكلفة المعدات: <span style='color:var(--primary-color)'>{total_equipment_cost:,.2f} ريال</span></h4>", unsafe_allow_html=True)
311
-
312
- # رسم بياني للمعدات حسب التكلفة
313
- if len(st.session_state.equipment) > 1:
314
- st.markdown("<h4>توزيع تكاليف المعدات</h4>", unsafe_allow_html=True)
315
-
316
- fig = go.Figure()
317
-
318
- fig.add_trace(go.Bar(
319
- x=[item["اسم المعدة"] for item in equipment_data],
320
- y=[item["التكلفة الإجمالية"] for item in equipment_data],
321
- name="التكلفة الإجمالية",
322
- marker_color="teal"
323
- ))
324
-
325
- fig.update_layout(
326
- title="تكاليف المعدات",
327
- xaxis_title="المعدة",
328
- yaxis_title="التكلفة (ريال)",
329
- font=dict(family="Almarai, Arial", size=14),
330
- margin=dict(t=50, b=50, l=20, r=20)
331
- )
332
-
333
- st.plotly_chart(fig, use_container_width=True)
334
-
335
-
336
- def render_labor_tab():
337
- """
338
- عرض تبويب العمالة
339
- """
340
- st.markdown("<h3>تكاليف العمالة</h3>", unsafe_allow_html=True)
341
-
342
- # إضافة عامل أو مجموعة عمال
343
- st.markdown("<h4>إضافة عمالة جديدة</h4>", unsafe_allow_html=True)
344
-
345
- col1, col2, col3 = st.columns(3)
346
-
347
- with col1:
348
- labor_type = st.text_input("نوع العمالة", key="new_labor_type")
349
- with col2:
350
- labor_count = st.number_input("العدد", min_value=1, value=1, key="new_labor_count")
351
- with col3:
352
- payment_type = st.selectbox(
353
- "نوع الدفع",
354
- options=["يومي", "أسبوعي", "شهري", "بالقطعة"],
355
- key="new_payment_type"
356
- )
357
-
358
- col4, col5, col6 = st.columns(3)
359
-
360
- with col4:
361
- wage_rate = st.number_input(f"الأجرة ({payment_type}) (ريال)", min_value=0.0, step=0.01, key="new_wage_rate")
362
- with col5:
363
- work_period = st.number_input(f"مدة العمل ({payment_type})", min_value=1, value=30, key="new_work_period")
364
- with col6:
365
- benefits_percent = st.slider("نسبة البدلات والتأمين (%)", min_value=0, max_value=50, value=15, key="new_benefits_percent")
366
-
367
- # حساب إجمالي التكلفة
368
- days_factor = {
369
- "يومي": 1,
370
- "أسبوعي": 7,
371
- "شهري": 30,
372
- "بالقطعة": 1
373
- }
374
-
375
- monthly_days = work_period * days_factor[payment_type] / 30 # تحويل الأيام إلى شهور
376
-
377
- if payment_type == "بالقطعة":
378
- total_labor_cost = labor_count * wage_rate * work_period
379
- else:
380
- # حساب الراتب الشهري
381
- monthly_wage = wage_rate * 30 / days_factor[payment_type]
382
- # حساب تكلفة البدلات والتأمين
383
- benefits_cost = monthly_wage * (benefits_percent / 100)
384
- # إجمالي التكلفة الشهرية
385
- monthly_total_cost = monthly_wage + benefits_cost
386
- # إجمالي التكلفة
387
- total_labor_cost = labor_count * monthly_total_cost * monthly_days
388
-
389
- if st.button("إضافة عمالة"):
390
- new_labor = {
391
- "type": labor_type,
392
- "count": labor_count,
393
- "payment_type": payment_type,
394
- "wage_rate": wage_rate,
395
- "work_period": work_period,
396
- "benefits_percent": benefits_percent,
397
- "total": total_labor_cost
398
- }
399
- st.session_state.labor.append(new_labor)
400
- st.success(f"تمت إضافة {labor_type} بنجاح!")
401
-
402
- # عرض تفاصيل الحساب
403
- st.markdown("<div class='card' style='margin-top: 10px;'>", unsafe_allow_html=True)
404
- if payment_type != "بالقطعة":
405
- monthly_wage = wage_rate * 30 / days_factor[payment_type]
406
- benefits_cost = monthly_wage * (benefits_percent / 100)
407
- monthly_total_cost = monthly_wage + benefits_cost
408
-
409
- st.markdown(f"<p>الراتب الشهري للعامل: {monthly_wage:,.2f} ريال</p>", unsafe_allow_html=True)
410
- st.markdown(f"<p>تكلفة البدلات والتأمين الشهرية: {benefits_cost:,.2f} ريال</p>", unsafe_allow_html=True)
411
- st.markdown(f"<p>إجمالي التكلفة الشهرية للعامل: {monthly_total_cost:,.2f} ريال</p>", unsafe_allow_html=True)
412
- st.markdown(f"<p>مدة العمل بالشهور: {monthly_days:.2f} شهر</p>", unsafe_allow_html=True)
413
- else:
414
- st.markdown(f"<p>سعر القطعة: {wage_rate:,.2f} ريال</p>", unsafe_allow_html=True)
415
- st.markdown(f"<p>عدد القطع: {work_period}</p>", unsafe_allow_html=True)
416
-
417
- st.markdown(f"<p>عدد العمال: {labor_count}</p>", unsafe_allow_html=True)
418
- st.markdown(f"<h4>التكلفة الإجمالية للعمالة: {total_labor_cost:,.2f} ريال</h4>", unsafe_allow_html=True)
419
- st.markdown("</div>", unsafe_allow_html=True)
420
-
421
- # عرض قائمة العمالة المضافة
422
- if st.session_state.labor:
423
- st.markdown("<h4>قائمة العمالة المضافة</h4>", unsafe_allow_html=True)
424
-
425
- labor_data = []
426
- for item in st.session_state.labor:
427
- labor_data.append({
428
- "نوع العمالة": item["type"],
429
- "العدد": item["count"],
430
- "نوع الدفع": item["payment_type"],
431
- "معدل الأجرة": item["wage_rate"],
432
- "مدة العمل": item["work_period"],
433
- "نسبة البدلات": f"{item['benefits_percent']}%",
434
- "التكلفة الإجمالية": item["total"]
435
- })
436
-
437
- labor_df = pd.DataFrame(labor_data)
438
- st.dataframe(labor_df)
439
-
440
- total_labor_cost = sum(item["total"] for item in st.session_state.labor)
441
- st.session_state.labor_cost = total_labor_cost
442
-
443
- st.markdown(f"<h4>إجمالي تكلفة العمالة: <span style='color:var(--primary-color)'>{total_labor_cost:,.2f} ريال</span></h4>", unsafe_allow_html=True)
444
-
445
- # رسم بياني للعمالة حسب التكلفة
446
- if len(st.session_state.labor) > 1:
447
- st.markdown("<h4>توزيع تكاليف العمالة</h4>", unsafe_allow_html=True)
448
-
449
- fig = px.bar(
450
- labor_df,
451
- x="نوع العمالة",
452
- y="التكلفة الإجمالية",
453
- color="العدد",
454
- title="توزيع تكاليف العمالة",
455
- color_continuous_scale=px.colors.sequential.Teal
456
- )
457
- fig.update_layout(
458
- font=dict(family="Almarai, Arial", size=14),
459
- margin=dict(t=50, b=50, l=20, r=20)
460
- )
461
- st.plotly_chart(fig, use_container_width=True)
462
-
463
-
464
- def render_admin_tab():
465
- """
466
- عرض تبويب المصاريف الإدارية
467
- """
468
- st.markdown("<h3>المصاريف الإدارية والعمومية</h3>", unsafe_allow_html=True)
469
-
470
- # إضافة مصروف جديد
471
- st.markdown("<h4>إضافة مصروف جديد</h4>", unsafe_allow_html=True)
472
-
473
- col1, col2, col3 = st.columns(3)
474
-
475
- with col1:
476
- expense_name = st.text_input("اسم المصروف", key="new_expense_name")
477
- with col2:
478
- expense_type = st.selectbox(
479
- "نوع المصروف",
480
- options=[
481
- "رواتب إدارية", "إيجارات", "مكتبية", "سفر", "تأمين",
482
- "استشارات", "رسوم حكومية", "منافع", "أخرى"
483
- ],
484
- key="new_expense_type"
485
- )
486
- with col3:
487
- expense_amount = st.number_input("المبلغ (ريال)", min_value=0.0, step=100.0, key="new_expense_amount")
488
-
489
- if st.button("إضافة مصروف"):
490
- new_expense = {
491
- "name": expense_name,
492
- "type": expense_type,
493
- "amount": expense_amount
494
- }
495
- st.session_state.admin_expenses.append(new_expense)
496
- st.success(f"تمت إضافة {expense_name} بنجاح!")
497
-
498
- # عرض قائمة المصاريف المضافة
499
- if st.session_state.admin_expenses:
500
- st.markdown("<h4>قائمة المصاريف الإدارية</h4>", unsafe_allow_html=True)
501
-
502
- admin_data = []
503
- for item in st.session_state.admin_expenses:
504
- admin_data.append({
505
- "اسم المصروف": item["name"],
506
- "نوع المصروف": item["type"],
507
- "المبلغ": item["amount"]
508
- })
509
-
510
- admin_df = pd.DataFrame(admin_data)
511
- st.dataframe(admin_df)
512
-
513
- total_admin_cost = sum(item["amount"] for item in st.session_state.admin_expenses)
514
- st.session_state.admin_cost = total_admin_cost
515
-
516
- st.markdown(f"<h4>إجمالي المصاريف الإدارية: <span style='color:var(--primary-color)'>{total_admin_cost:,.2f} ريال</span></h4>", unsafe_allow_html=True)
517
-
518
- # رسم بياني للمصاريف حسب النوع
519
- if len(st.session_state.admin_expenses) > 1:
520
- st.markdown("<h4>توزيع المصاريف الإدارية حسب النوع</h4>", unsafe_allow_html=True)
521
-
522
- # تجميع المصاريف حسب النوع
523
- expense_by_type = admin_df.groupby("نوع المصروف")["المبلغ"].sum().reset_index()
524
-
525
- fig = px.pie(
526
- expense_by_type,
527
- values="المبلغ",
528
- names="نوع المصروف",
529
- title="توزيع المصاريف الإدارية",
530
- color_discrete_sequence=px.colors.sequential.Teal,
531
- hole=0.4
532
- )
533
- fig.update_layout(
534
- font=dict(family="Almarai, Arial", size=14),
535
- margin=dict(t=50, b=50, l=20, r=20)
536
- )
537
- st.plotly_chart(fig, use_container_width=True)
538
-
539
- # نسبة المصاريف الإدارية
540
- st.markdown("<h4>احتساب المصاريف الإدارية بالنسبة المئوية</h4>", unsafe_allow_html=True)
541
-
542
- # حساب التكاليف المباشرة
543
- direct_costs = st.session_state.materials_cost + st.session_state.equipment_cost + st.session_state.labor_cost
544
-
545
- col1, col2 = st.columns(2)
546
-
547
- with col1:
548
- admin_percent = st.slider("نسبة المصاريف الإدارية من التكاليف المباشرة (%)", min_value=0, max_value=30, value=10, key="admin_percent")
549
-
550
- with col2:
551
- calculated_admin_cost = direct_costs * (admin_percent / 100)
552
- st.markdown(f"<div class='card'><h4>المصاريف الإدارية بالنسبة: <span style='color:var(--primary-color)'>{calculated_admin_cost:,.2f} ريال</span></h4></div>", unsafe_allow_html=True)
553
-
554
- if st.button("استخدام النسبة المئوية للمصاريف الإدارية"):
555
- st.session_state.admin_cost = calculated_admin_cost
556
- st.success("تم تحديث إجمالي المصاريف الإدارية بناء على النسبة المئوية!")
557
-
558
-
559
- def render_profit_tab():
560
- """
561
- عرض تبويب هامش الربح
562
- """
563
- st.markdown("<h3>هامش الربح</h3>", unsafe_allow_html=True)
564
-
565
- # حساب التكاليف المباشرة والإجمالية
566
- direct_costs = st.session_state.materials_cost + st.session_state.equipment_cost + st.session_state.labor_cost
567
- total_costs = direct_costs + st.session_state.admin_cost
568
-
569
- # عرض ملخص التكاليف
570
- st.markdown("<div class='card'>", unsafe_allow_html=True)
571
- st.markdown("<h4>ملخص التكاليف</h4>", unsafe_allow_html=True)
572
- st.markdown(f"<p>إجمالي تكلفة المواد: <span style='color:var(--text-medium)'>{st.session_state.materials_cost:,.2f} ريال</span></p>", unsafe_allow_html=True)
573
- st.markdown(f"<p>إجمالي تكلفة المعدات: <span style='color:var(--text-medium)'>{st.session_state.equipment_cost:,.2f} ريال</span></p>", unsafe_allow_html=True)
574
- st.markdown(f"<p>إجمالي تكلفة العمالة: <span style='color:var(--text-medium)'>{st.session_state.labor_cost:,.2f} ريال</span></p>", unsafe_allow_html=True)
575
- st.markdown(f"<p>إجمالي التكاليف المباشرة: <span style='color:var(--primary-color)'>{direct_costs:,.2f} ريال</span></p>", unsafe_allow_html=True)
576
- st.markdown(f"<p>إجمالي المصاريف الإدارية: <span style='color:var(--text-medium)'>{st.session_state.admin_cost:,.2f} ريال</span></p>", unsafe_allow_html=True)
577
- st.markdown(f"<h4>إجمالي التكاليف: <span style='color:var(--primary-color)'>{total_costs:,.2f} ريال</span></h4>", unsafe_allow_html=True)
578
- st.markdown("</div>", unsafe_allow_html=True)
579
-
580
- # تحديد هامش الربح
581
- st.markdown("<h4>تحديد هامش الربح</h4>", unsafe_allow_html=True)
582
-
583
- col1, col2 = st.columns(2)
584
-
585
- with col1:
586
- profit_margin = st.slider("نسبة هامش الربح (%)", min_value=0, max_value=30, value=int(st.session_state.profit_margin), key="profit_margin_slider")
587
- st.session_state.profit_margin = profit_margin
588
-
589
- with col2:
590
- profit_amount = total_costs * (profit_margin / 100)
591
- st.markdown(f"<div class='card'><h4>قيمة هامش الربح: <span style='color:var(--primary-color)'>{profit_amount:,.2f} ريال</span></h4></div>", unsafe_allow_html=True)
592
-
593
- # إجمالي قيمة العرض
594
- total_price = total_costs + profit_amount
595
- st.markdown("<div class='card' style='background: var(--primary-light);'>", unsafe_allow_html=True)
596
- st.markdown(f"<h3>إجمالي قيمة العرض: <span style='color:var(--primary-color)'>{total_price:,.2f} ريال</span></h3>", unsafe_allow_html=True)
597
- st.markdown("</div>", unsafe_allow_html=True)
598
-
599
- # تحليل الحساسية لهامش الربح
600
- st.markdown("<h4>تحليل حساسية هامش الربح</h4>", unsafe_allow_html=True)
601
-
602
- sensitivity_data = []
603
- for margin in range(5, 31, 5):
604
- profit = total_costs * (margin / 100)
605
- total = total_costs + profit
606
- sensitivity_data.append({
607
- "نسبة الربح": f"{margin}%",
608
- "قيمة الربح": profit,
609
- "إجمالي العرض": total
610
- })
611
-
612
- sensitivity_df = pd.DataFrame(sensitivity_data)
613
-
614
- # رسم بياني لتحليل الحساسية
615
- fig = go.Figure()
616
-
617
- fig.add_trace(go.Bar(
618
- x=[item["نسبة الربح"] for item in sensitivity_data],
619
- y=[item["قيمة الربح"] for item in sensitivity_data],
620
- name="قيمة الربح",
621
- marker_color="rgba(14, 165, 165, 0.7)"
622
- ))
623
-
624
- fig.add_trace(go.Scatter(
625
- x=[item["نسبة الربح"] for item in sensitivity_data],
626
- y=[item["إجمالي العرض"] for item in sensitivity_data],
627
- name="إجمالي العرض",
628
- mode="lines+markers",
629
- marker=dict(size=8, color="rgba(255, 154, 60, 1.0)"),
630
- line=dict(width=3, color="rgba(255, 154, 60, 0.7)")
631
- ))
632
-
633
- fig.update_layout(
634
- title="تحليل حساسية هامش الربح",
635
- xaxis_title="نسبة الربح",
636
- yaxis_title="القيمة (ريال)",
637
- font=dict(family="Almarai, Arial", size=14),
638
- margin=dict(t=50, b=50, l=20, r=20),
639
- hovermode="x unified"
640
- )
641
-
642
- st.plotly_chart(fig, use_container_width=True)
643
-
644
- # جدول تحليل الحساسية
645
- st.dataframe(sensitivity_df)
646
-
647
-
648
- def render_final_report(project_name, project_location, project_area, project_type):
649
- """
650
- عرض التقرير النهائي للتكاليف
651
- """
652
- st.markdown("<h3>التقرير النهائي لتكاليف المشروع</h3>", unsafe_allow_html=True)
653
-
654
- # التأكد من وجود المتغيرات المطلوبة في حالة الجلسة وضمان أن لديهم قيم صحيحة
655
- required_fields = {
656
- 'materials_cost': 0.0,
657
- 'equipment_cost': 0.0,
658
- 'labor_cost': 0.0,
659
- 'admin_cost': 0.0,
660
- 'profit_margin': 15.0,
661
- 'materials': [],
662
- 'equipment': [],
663
- 'labor': [],
664
- 'admin_expenses': []
665
- }
666
-
667
- # مرور على كافة الحقول المطلوبة للتأكد من وجودها
668
- for field, default_value in required_fields.items():
669
- if field not in st.session_state:
670
- st.session_state[field] = default_value
671
-
672
- # التحقق من أن القيم العددية صالحة (غير None وليست NaN)
673
- if field in ['materials_cost', 'equipment_cost', 'labor_cost', 'admin_cost', 'profit_margin']:
674
- # إذا كانت القيمة None أو NaN، استخدم القيمة الافتراضية
675
- if st.session_state[field] is None or pd.isna(st.session_state[field]):
676
- st.session_state[field] = default_value
677
-
678
- # حساب التكاليف المباشرة والإجمالية
679
- direct_costs = st.session_state.materials_cost + st.session_state.equipment_cost + st.session_state.labor_cost
680
- total_costs = direct_costs + st.session_state.admin_cost
681
- profit_amount = total_costs * (st.session_state.profit_margin / 100)
682
- total_price = total_costs + profit_amount
683
-
684
- # معلومات المشروع
685
- st.markdown("<div class='card'>", unsafe_allow_html=True)
686
- st.markdown("<h4>معلومات المشروع</h4>", unsafe_allow_html=True)
687
- col1, col2 = st.columns(2)
688
-
689
- with col1:
690
- st.markdown(f"<p><strong>اسم المشروع:</strong> {project_name}</p>", unsafe_allow_html=True)
691
- st.markdown(f"<p><strong>نوع المشروع:</strong> {project_type}</p>", unsafe_allow_html=True)
692
-
693
- with col2:
694
- st.markdown(f"<p><strong>موقع المشروع:</strong> {project_location}</p>", unsafe_allow_html=True)
695
- st.markdown(f"<p><strong>المساحة الإجمالية:</strong> {project_area} م²</p>", unsafe_allow_html=True)
696
-
697
- st.markdown("</div>", unsafe_allow_html=True)
698
-
699
- # ملخص التكاليف
700
- st.markdown("<div class='card'>", unsafe_allow_html=True)
701
- st.markdown("<h4>ملخص التكاليف</h4>", unsafe_allow_html=True)
702
-
703
- col1, col2 = st.columns(2)
704
-
705
- with col1:
706
- st.markdown(f"<p><strong>تكلفة المواد:</strong> {st.session_state.materials_cost:,.2f} ريال</p>", unsafe_allow_html=True)
707
- st.markdown(f"<p><strong>تكلفة المعدات:</strong> {st.session_state.equipment_cost:,.2f} ريال</p>", unsafe_allow_html=True)
708
- st.markdown(f"<p><strong>تكلفة العمالة:</strong> {st.session_state.labor_cost:,.2f} ريال</p>", unsafe_allow_html=True)
709
- st.markdown(f"<p><strong>إجمالي التكاليف المباشرة:</strong> {direct_costs:,.2f} ريال</p>", unsafe_allow_html=True)
710
-
711
- with col2:
712
- st.markdown(f"<p><strong>المصاريف الإدارية:</strong> {st.session_state.admin_cost:,.2f} ريال</p>", unsafe_allow_html=True)
713
- st.markdown(f"<p><strong>إجمالي التكاليف:</strong> {total_costs:,.2f} ريال</p>", unsafe_allow_html=True)
714
- st.markdown(f"<p><strong>هامش الربح ({st.session_state.profit_margin}%):</strong> {profit_amount:,.2f} ريال</p>", unsafe_allow_html=True)
715
- st.markdown(f"<h4>إجمالي قيمة العرض: {total_price:,.2f} ريال</h4>", unsafe_allow_html=True)
716
-
717
- st.markdown("</div>", unsafe_allow_html=True)
718
-
719
- # عرض التفاصيل بالمتر المربع
720
- if project_area > 0:
721
- per_sqm_cost = total_price / project_area
722
- st.markdown("<div class='card'>", unsafe_allow_html=True)
723
- st.markdown("<h4>تكلفة المتر المربع</h4>", unsafe_allow_html=True)
724
- st.markdown(f"<p>تكلفة المتر المربع الإجمالية: <strong>{per_sqm_cost:,.2f} ريال/م²</strong></p>", unsafe_allow_html=True)
725
- st.markdown("</div>", unsafe_allow_html=True)
726
- else:
727
- st.markdown("<div class='card'>", unsafe_allow_html=True)
728
- st.markdown("<h4>تكلفة المتر المربع</h4>", unsafe_allow_html=True)
729
- st.markdown("<p>يرجى إدخال مساحة صحيحة للمشروع لحساب تكلفة المتر المربع</p>", unsafe_allow_html=True)
730
- st.markdown("</div>", unsafe_allow_html=True)
731
-
732
- # رسم بياني لتوزيع التكاليف
733
- st.markdown("<h4>توزيع التكاليف</h4>", unsafe_allow_html=True)
734
-
735
- # تجنب القسمة على صفر
736
- if total_price > 0:
737
- cost_distribution = [
738
- {"النوع": "المواد", "القيمة": st.session_state.materials_cost, "النسبة": st.session_state.materials_cost / total_price * 100},
739
- {"النوع": "المعدات", "القيمة": st.session_state.equipment_cost, "النسبة": st.session_state.equipment_cost / total_price * 100},
740
- {"النوع": "العمالة", "القيمة": st.session_state.labor_cost, "النسبة": st.session_state.labor_cost / total_price * 100},
741
- {"النوع": "المصاريف الإدارية", "القيمة": st.session_state.admin_cost, "النسبة": st.session_state.admin_cost / total_price * 100},
742
- {"النوع": "هامش الربح", "القيمة": profit_amount, "النسبة": profit_amount / total_price * 100}
743
- ]
744
- else:
745
- # إذا كان المجموع صفر، اجعل جميع النسب المئوية صفر
746
- cost_distribution = [
747
- {"النوع": "المواد", "القيمة": st.session_state.materials_cost, "النسبة": 0},
748
- {"النوع": "المعدات", "القيمة": st.session_state.equipment_cost, "النسبة": 0},
749
- {"النوع": "العمالة", "القيمة": st.session_state.labor_cost, "النسبة": 0},
750
- {"النوع": "المصاريف الإدارية", "القيمة": st.session_state.admin_cost, "النسبة": 0},
751
- {"النوع": "هامش الربح", "القيمة": profit_amount, "النسبة": 0}
752
- ]
753
-
754
- cost_df = pd.DataFrame(cost_distribution)
755
-
756
- fig = px.pie(
757
- cost_df,
758
- values="القيمة",
759
- names="النوع",
760
- title="توزيع التكاليف والأرباح",
761
- color_discrete_sequence=px.colors.sequential.Teal,
762
- hole=0.4
763
- )
764
-
765
- fig.update_traces(textposition='inside', textinfo='percent+label')
766
-
767
- fig.update_layout(
768
- annotations=[dict(text=f"{total_price:,.0f} ريال", x=0.5, y=0.5, font_size=14, showarrow=False)],
769
- font=dict(family="Almarai, Arial", size=14),
770
- margin=dict(t=50, b=50, l=20, r=20)
771
- )
772
-
773
- st.plotly_chart(fig, use_container_width=True)
774
-
775
- # جدول توزيع التكاليف
776
- st.dataframe(cost_df)
777
-
778
- # زر لإنشاء تقرير PDF
779
- col1, col2 = st.columns(2)
780
-
781
- with col1:
782
- if st.button("تصدير التقرير إلى PDF"):
783
- st.success("تم تصدير التقرير بنجاح!")
784
-
785
- with col2:
786
- if st.button("حفظ التقرير في قاعدة البيانات"):
787
- st.success("تم حفظ التقرير في قاعدة البيانات بنجاح!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/exceptions.py DELETED
@@ -1,42 +0,0 @@
1
- """
2
- استثناءات وحدة التسعير
3
- """
4
-
5
- class PricingError(Exception):
6
- """استثناء أساسي لأخطاء التسعير"""
7
- pass
8
-
9
-
10
- class LocalContentCalculationError(PricingError):
11
- """استثناء لأخطاء حساب المحتوى المحلي"""
12
- pass
13
-
14
-
15
- class PriceEstimationError(PricingError):
16
- """استثناء لأخطاء تقدير الأسعار"""
17
- pass
18
-
19
-
20
- class ResourceNotFoundError(PricingError):
21
- """استثناء لعدم وجود المورد المطلوب"""
22
- pass
23
-
24
-
25
- class InvalidInputError(PricingError):
26
- """استثناء للمدخلات غير الصالحة"""
27
- pass
28
-
29
-
30
- class ModelLoadingError(PricingError):
31
- """استثناء لأخطاء تحميل النموذج"""
32
- pass
33
-
34
-
35
- class DataProcessingError(PricingError):
36
- """استثناء لأخطاء معالجة البيانات"""
37
- pass
38
-
39
-
40
- class UnbalancedPricingError(PricingError):
41
- """استثناء لأخطاء التسعير غير المتزن"""
42
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/price_analysis_component.py DELETED
@@ -1,932 +0,0 @@
1
- import streamlit as st
2
- import pandas as pd
3
- import numpy as np
4
- from datetime import datetime
5
- import time
6
-
7
- class PriceAnalysisComponent:
8
- """مكون تحليل الأسعار للبنود"""
9
-
10
- def __init__(self):
11
- """تهيئة مكون تحليل الأسعار"""
12
- # تهيئة قائمة الوحدات المتاحة
13
- self.unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
14
-
15
- # تهيئة فئات التكاليف
16
- self.cost_categories = [
17
- "مواد",
18
- "عمالة",
19
- "معدات",
20
- "مقاولي الباطن",
21
- "مصاريف عامة",
22
- "أرباح"
23
- ]
24
-
25
- # تهيئة قائمة البنود وتحليل أسعارها
26
- if 'items_price_analysis' not in st.session_state:
27
- st.session_state.items_price_analysis = {}
28
-
29
- def render(self):
30
- """عرض واجهة تحليل الأسعار"""
31
- st.markdown("<h2 class='module-title'>تحليل أسعار البنود</h2>", unsafe_allow_html=True)
32
-
33
- # التحقق من وجود بنود في التسعير الحالي
34
- if 'current_pricing' not in st.session_state or 'items' not in st.session_state.current_pricing:
35
- st.warning("ليس هناك بنود للتحليل. يرجى إنشاء تسعير أولاً.")
36
- return
37
-
38
- # الحصول على البنود من التسعير الحالي
39
- items = st.session_state.current_pricing['items'].copy()
40
-
41
- # عرض قائمة البنود
42
- st.markdown("### قائمة البنود")
43
- st.dataframe(items[['رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي']],
44
- use_container_width=True, hide_index=True)
45
-
46
- # اختيار البند لتحليل السعر
47
- selected_item_id = st.selectbox(
48
- "اختر البند لتحليل السعر",
49
- options=items['رقم البند'].tolist(),
50
- format_func=lambda x: f"{x}: {items[items['رقم البند'] == x]['وصف البند'].values[0][:50]}..."
51
- )
52
-
53
- if selected_item_id:
54
- # الحصول على البند المحدد
55
- selected_item = items[items['رقم البند'] == selected_item_id].iloc[0]
56
-
57
- # عرض تفاصيل البند المختار
58
- col1, col2, col3 = st.columns(3)
59
-
60
- with col1:
61
- st.metric("رقم البند", selected_item['رقم البند'])
62
-
63
- with col2:
64
- st.metric("الكمية", f"{selected_item['الكمية']} {selected_item['الوحدة']}")
65
-
66
- with col3:
67
- st.metric("سعر الوحدة", f"{selected_item['سعر الوحدة']:,.2f} ريال")
68
-
69
- st.markdown(f"**وصف البند**: {selected_item['وصف البند']}")
70
-
71
- # إنشاء أو تحديث تحليل السعر للبند المحدد
72
- if selected_item_id not in st.session_state.items_price_analysis:
73
- # إنشاء تحليل سعر افتراضي
74
- self._create_default_price_analysis(selected_item_id, selected_item)
75
-
76
- # عرض وتحرير تحليل السعر
77
- self._render_price_analysis_editor(selected_item_id, selected_item)
78
-
79
- def _create_default_price_analysis(self, item_id, item):
80
- """إنشاء تحليل سعر افتراضي للبند"""
81
- # إنشاء قائمة مكونات تحليل السعر
82
- components = pd.DataFrame(columns=[
83
- 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي'
84
- ])
85
-
86
- # إضافة مكونات افتراضية بناءً على نوع البند
87
- is_concrete = 'خرسان' in item['وصف البند']
88
- is_steel = 'حديد' in item['وصف البند'] or 'تسليح' in item['وصف البند']
89
- is_bricks = 'بلوك' in item['وصف البند'] or 'طوب' in item['وصف البند']
90
- is_paint = 'دهان' in item['وصف البند'] or 'طلاء' in item['وصف البند']
91
- is_insulation = 'عزل' in item['وصف البند']
92
-
93
- # إضافة المكونات بناءً على نوع البند
94
- if is_concrete:
95
- # مكونات الخرسانة
96
- default_components = pd.DataFrame({
97
- 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
98
- 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'],
99
- 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1],
100
- 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
101
- 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150],
102
- 'الإجمالي': [175, 40, 96, 400, 500, 100, 150]
103
- })
104
- components = pd.concat([components, default_components], ignore_index=True)
105
-
106
- elif is_steel:
107
- # مكونات الحديد
108
- default_components = pd.DataFrame({
109
- 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
110
- 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'],
111
- 'الكمية': [1000, 10, 1, 1, 1],
112
- 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
113
- 'سعر الوحدة': [4.5, 50, 300, 200, 300],
114
- 'الإجمالي': [4500, 500, 300, 200, 300]
115
- })
116
- components = pd.concat([components, default_components], ignore_index=True)
117
-
118
- elif is_bricks:
119
- # مكونات البلوك
120
- default_components = pd.DataFrame({
121
- 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
122
- 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'],
123
- 'الكمية': [12.5, 0.02, 1, 1, 1],
124
- 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'],
125
- 'سعر الوحدة': [8, 500, 80, 15, 20],
126
- 'الإجمالي': [100, 10, 80, 15, 20]
127
- })
128
- components = pd.concat([components, default_components], ignore_index=True)
129
-
130
- elif is_paint:
131
- # مكونات الدهانات
132
- default_components = pd.DataFrame({
133
- 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
134
- 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'],
135
- 'الكمية': [0.4, 0.1, 1, 1, 1],
136
- 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'],
137
- 'سعر الوحدة': [80, 20, 35, 5, 10],
138
- 'الإجمالي': [32, 2, 35, 5, 10]
139
- })
140
- components = pd.concat([components, default_components], ignore_index=True)
141
-
142
- elif is_insulation:
143
- # مكونات العزل
144
- default_components = pd.DataFrame({
145
- 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
146
- 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'],
147
- 'الكمية': [1.1, 0.2, 1, 1, 1],
148
- 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'],
149
- 'سعر الوحدة': [60, 30, 25, 10, 15],
150
- 'الإجمالي': [66, 6, 25, 10, 15]
151
- })
152
- components = pd.concat([components, default_components], ignore_index=True)
153
-
154
- else:
155
- # مكونات عامة افتراضية
156
- default_components = pd.DataFrame({
157
- 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
158
- 'الوصف': ['مواد أساسية', 'عمالة', 'معدات ومعد مساعدة', 'مصاريف عامة', 'أرباح'],
159
- 'الكمية': [1, 1, 1, 1, 1],
160
- 'الوحدة': [item['الوحدة'], 'وحدة', 'وحدة', 'وحدة', 'وحدة'],
161
- 'سعر الوحدة': [
162
- item['سعر الوحدة'] * 0.6,
163
- item['سعر الوحدة'] * 0.2,
164
- item['سعر الوحدة'] * 0.1,
165
- item['سعر الوحدة'] * 0.05,
166
- item['سعر الوحدة'] * 0.05
167
- ],
168
- 'الإجمالي': [
169
- item['سعر الوحدة'] * 0.6,
170
- item['سعر الوحدة'] * 0.2,
171
- item['سعر الوحدة'] * 0.1,
172
- item['سعر الوحدة'] * 0.05,
173
- item['سعر الوحدة'] * 0.05
174
- ]
175
- })
176
- components = pd.concat([components, default_components], ignore_index=True)
177
-
178
- # حفظ تحليل السعر للبند
179
- st.session_state.items_price_analysis[item_id] = components
180
-
181
- def _render_price_analysis_editor(self, item_id, item):
182
- """عرض محرر تحليل السعر للبند"""
183
- st.markdown("### تحليل السعر")
184
-
185
- # الحصول على مكونات تحليل السعر
186
- components = st.session_state.items_price_analysis[item_id]
187
-
188
- # عرض تحليل السعر في محرر بيانات
189
- st.markdown("#### مكونات السعر")
190
-
191
- edited_components = st.data_editor(
192
- components,
193
- use_container_width=True,
194
- hide_index=True,
195
- num_rows="dynamic",
196
- column_config={
197
- 'نوع التكلفة': st.column_config.SelectboxColumn(
198
- 'نوع التكلفة',
199
- help='فئة التكلفة',
200
- options=self.cost_categories
201
- ),
202
- 'الوحدة': st.column_config.SelectboxColumn(
203
- 'الوحدة',
204
- help='وحدة القياس',
205
- options=self.unit_options + ["وحدة", "ساعة", "يوم"]
206
- ),
207
- 'الكمية': st.column_config.NumberColumn(
208
- 'الكمية',
209
- help='الكمية',
210
- min_value=0.0,
211
- format="%.2f"
212
- ),
213
- 'سعر الوحدة': st.column_config.NumberColumn(
214
- 'سعر الوحدة',
215
- help='سعر الوحدة',
216
- min_value=0.0,
217
- format="%.2f"
218
- ),
219
- 'الإجمالي': st.column_config.NumberColumn(
220
- 'الإجمالي',
221
- help='الإجمالي',
222
- min_value=0.0,
223
- format="%.2f"
224
- )
225
- }
226
- )
227
-
228
- # إعادة حساب الإجمالي لكل مكون
229
- edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة']
230
-
231
- # حفظ التعديلات
232
- st.session_state.items_price_analysis[item_id] = edited_components
233
-
234
- # حساب إجمالي تحليل السعر
235
- total_analysis_price = edited_components['الإجمالي'].sum()
236
- unit_price_from_analysis = total_analysis_price / item['الكمية'] if item['الكمية'] > 0 else 0
237
-
238
- # عرض ملخص تحليل السعر
239
- st.markdown("#### ملخص تحليل السعر")
240
-
241
- col1, col2, col3 = st.columns(3)
242
-
243
- with col1:
244
- st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال")
245
-
246
- with col2:
247
- st.metric("سعر الوحدة من التحليل", f"{unit_price_from_analysis:,.2f} ريال")
248
-
249
- with col3:
250
- # المقارنة مع السعر الأصلي
251
- diff = unit_price_from_analysis - item['سعر الوحدة']
252
- st.metric(
253
- "الفرق عن السعر الأصلي",
254
- f"{diff:,.2f} ريال",
255
- delta=f"{(diff/item['سعر الوحدة']*100) if item['سعر الوحدة'] > 0 else 0:.1f}%"
256
- )
257
-
258
- # تحليل توزيع التكاليف حسب الفئة
259
- cost_by_category = edited_components.groupby('نوع التكلفة')['الإجمالي'].sum().reset_index()
260
-
261
- # عرض مخطط توزيع التكاليف
262
- st.markdown("#### توزيع التكاليف حسب الفئة")
263
-
264
- # عرض توزيع التكاليف في جدول
265
- distribution_df = pd.DataFrame({
266
- 'نوع التكلفة': cost_by_category['نوع التكلفة'],
267
- 'القيمة': cost_by_category['الإجمالي'],
268
- 'النسبة المئوية': (cost_by_category['الإجمالي'] / total_analysis_price * 100).round(2)
269
- })
270
-
271
- st.dataframe(
272
- distribution_df,
273
- use_container_width=True,
274
- hide_index=True,
275
- column_config={
276
- 'القيمة': st.column_config.NumberColumn(
277
- 'القيمة',
278
- help='القيمة',
279
- format="%.2f"
280
- ),
281
- 'النسبة المئوية': st.column_config.ProgressColumn(
282
- 'النسبة المئوية',
283
- help='النسبة المئوية',
284
- format="%.2f%%",
285
- min_value=0,
286
- max_value=100
287
- )
288
- }
289
- )
290
-
291
- # أزرار الإجراءات
292
- col1, col2, col3 = st.columns(3)
293
-
294
- with col1:
295
- if st.button("تحديث سعر البند", use_container_width=True):
296
- # تحديث سعر البند بناءً على تحليل السعر
297
- items = st.session_state.current_pricing['items'].copy()
298
- item_index = items[items['رقم البند'] == item_id].index[0]
299
-
300
- # تحديث ��عر الوحدة والإجمالي
301
- items.at[item_index, 'سعر الوحدة'] = unit_price_from_analysis
302
- items.at[item_index, 'الإجمالي'] = unit_price_from_analysis * items.at[item_index, 'الكمية']
303
-
304
- # حفظ التعديلات في التسعير الحالي
305
- st.session_state.current_pricing['items'] = items
306
-
307
- st.success(f"تم تحديث سعر البند بناءً على تحليل السعر: {unit_price_from_analysis:,.2f} ريال")
308
- time.sleep(0.5)
309
- st.rerun()
310
-
311
- with col2:
312
- if st.button("تصدير تحليل السعر", use_container_width=True):
313
- st.success("تم إرسال تحليل السعر للتصدير بنجاح!")
314
-
315
- with col3:
316
- if st.button("مسح تحليل السعر", use_container_width=True):
317
- # حذف تحليل السعر للبند
318
- if item_id in st.session_state.items_price_analysis:
319
- del st.session_state.items_price_analysis[item_id]
320
-
321
- st.warning("تم مسح تحليل السعر للبند")
322
- time.sleep(0.5)
323
- st.rerun()
324
-
325
- def add_to_pricing_app(self, pricing_app):
326
- """إضافة مكون تحليل الأسعار إلى تطبيق التسعير"""
327
- # إضافة تبويب جديد
328
- if not hasattr(pricing_app, 'tabs'):
329
- pricing_app.tabs = []
330
-
331
- if len(pricing_app.tabs) == 4: # إذا كان هناك 4 تبويبات فقط
332
- pricing_app.tabs.append("تحليل أسعار البنود")
333
-
334
- # إضافة دالة العرض
335
- pricing_app._render_price_analysis_tab = self.render
336
-
337
-
338
- def render_integrated_item_input():
339
- """عرض واجهة إدخال البنود مع تحليل السعر المتكامل"""
340
-
341
- # ضبط CSS لتحسين ظهور الواجهة العربية
342
- st.markdown("""
343
- <style>
344
- input, .stTextArea textarea {
345
- direction: rtl;
346
- text-align: right;
347
- font-family: 'Arial', 'Tahoma', sans-serif !important;
348
- }
349
- .stTextInput > div > div > input {
350
- text-align: right;
351
- direction: rtl;
352
- }
353
- .pricing-analysis-container {
354
- border: 1px solid #e0e0e0;
355
- border-radius: 10px;
356
- padding: 10px;
357
- margin-top: 10px;
358
- background-color: #f9f9f9;
359
- }
360
- </style>
361
- """, unsafe_allow_html=True)
362
-
363
- # تهيئة قائمة الوحدات المتاحة
364
- unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
365
-
366
- # تهيئة فئات التكاليف
367
- cost_categories = [
368
- "مواد",
369
- "عمالة",
370
- "معدات",
371
- "مقاولي الباطن",
372
- "مصاريف عامة",
373
- "أرباح"
374
- ]
375
-
376
- # إنشاء جدول البنود اذا لم يكن موجوداً
377
- if 'manual_items' not in st.session_state:
378
- manual_items = pd.DataFrame(columns=[
379
- 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي'
380
- ])
381
-
382
- # إضافة بضعة صفوف افتراضية
383
- default_items = pd.DataFrame({
384
- 'رقم البند': ["A1", "A2", "A3", "A4", "A5"],
385
- 'وصف البند': [
386
- "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
387
- "توريد وتركيب حديد التسليح للأساسات",
388
- "أعمال العزل المائي للأساسات",
389
- "أعمال الردم والدك للأساسات",
390
- "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة"
391
- ],
392
- 'الوحدة': ["م3", "طن", "م2", "م3", "م3"],
393
- 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0],
394
- 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0],
395
- 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0]
396
- })
397
-
398
- manual_items = pd.concat([manual_items, default_items])
399
- st.session_state.manual_items = manual_items
400
-
401
- # إنشاء جدول تحليل الأسعار اذا لم يكن موجوداً
402
- if 'items_price_analysis' not in st.session_state:
403
- st.session_state.items_price_analysis = {}
404
-
405
- # عرض واجهة إدخال البنود
406
- st.markdown("### إدخال تفاصيل البنود مع تحليل الأسعار")
407
-
408
- # عرض البنود الحالية كجدول للعرض
409
- st.markdown("### جدول البنود الحالية")
410
- st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
411
-
412
- # التبويبات لإضافة بند جديد أو تعديل بند
413
- tabs = st.tabs(["إضافة بند جديد", "تعديل بند حالي"])
414
-
415
- with tabs[0]: # إضافة بند جديد
416
- st.markdown("### إضافة بند جديد مع تحليل السعر")
417
-
418
- col1, col2 = st.columns(2)
419
-
420
- with col1:
421
- new_id = st.text_input("رقم البند", value=f"A{len(st.session_state.manual_items)+1}", key="new_id")
422
- new_desc = st.text_area("وصف البند", value="", key="new_desc")
423
-
424
- with col2:
425
- new_unit = st.selectbox("الوحدة", options=unit_options, key="new_unit")
426
- new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f", key="new_qty")
427
-
428
- # إنشاء تحليل السعر للبند الجديد
429
- st.markdown('<div class="pricing-analysis-container">', unsafe_allow_html=True)
430
- st.markdown("#### تحليل سعر البند")
431
-
432
- # التعرف التلقائي على نوع البند من الوصف
433
- is_concrete = False
434
- is_steel = False
435
- is_bricks = False
436
- is_paint = False
437
- is_insulation = False
438
-
439
- if new_desc:
440
- is_concrete = 'خرسان' in new_desc
441
- is_steel = 'حديد' in new_desc or 'تسليح' in new_desc
442
- is_bricks = 'بلوك' in new_desc or 'طوب' in new_desc
443
- is_paint = 'دهان' in new_desc or 'طلاء' in new_desc
444
- is_insulation = 'عزل' in new_desc
445
-
446
- # تلميح للمستخدم عن التعرف التلقائي
447
- if any([is_concrete, is_steel, is_bricks, is_paint, is_insulation]):
448
- detected_type = ""
449
- if is_concrete:
450
- detected_type = "أعمال خرسانة"
451
- elif is_steel:
452
- detected_type = "أعمال حديد"
453
- elif is_bricks:
454
- detected_type = "أعمال بلوك"
455
- elif is_paint:
456
- detected_type = "أعمال دهانات"
457
- elif is_insulation:
458
- detected_type = "أعمال عزل"
459
-
460
- st.info(f"تم التعرف تلقائياً على نوع البند: {detected_type}")
461
-
462
- # إنشاء مصفوفة فارغة لمكونات البند
463
- if 'new_components' not in st.session_state:
464
- # إنشاء DataFrame فارغ
465
- new_components = pd.DataFrame(columns=[
466
- 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي'
467
- ])
468
-
469
- # إضافة مكونات افتراضية بناءً على نوع البند
470
- if is_concrete:
471
- # مكونات الخرسانة
472
- default_components = pd.DataFrame({
473
- 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
474
- 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'],
475
- 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1],
476
- 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
477
- 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150],
478
- 'الإجمالي': [175, 40, 96, 400, 500, 100, 150]
479
- })
480
- new_components = pd.concat([new_components, default_components], ignore_index=True)
481
-
482
- elif is_steel:
483
- # مكونات الحديد
484
- default_components = pd.DataFrame({
485
- 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
486
- 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'],
487
- 'الكمية': [1000, 10, 1, 1, 1],
488
- 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
489
- 'سعر الوحدة': [4.5, 50, 300, 200, 300],
490
- 'الإجمالي': [4500, 500, 300, 200, 300]
491
- })
492
- new_components = pd.concat([new_components, default_components], ignore_index=True)
493
-
494
- elif is_bricks:
495
- # مكونات البلوك
496
- default_components = pd.DataFrame({
497
- 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
498
- 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'],
499
- 'الكمية': [12.5, 0.02, 1, 1, 1],
500
- 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'],
501
- 'سعر الوحدة': [8, 500, 80, 15, 20],
502
- 'الإجمالي': [100, 10, 80, 15, 20]
503
- })
504
- new_components = pd.concat([new_components, default_components], ignore_index=True)
505
-
506
- elif is_paint:
507
- # مكونات الدهانات
508
- default_components = pd.DataFrame({
509
- 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
510
- 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'],
511
- 'الكمية': [0.4, 0.1, 1, 1, 1],
512
- 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'],
513
- 'سعر الوحدة': [80, 20, 35, 5, 10],
514
- 'الإجمالي': [32, 2, 35, 5, 10]
515
- })
516
- new_components = pd.concat([new_components, default_components], ignore_index=True)
517
-
518
- elif is_insulation:
519
- # مكونات العزل
520
- default_components = pd.DataFrame({
521
- 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
522
- 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'],
523
- 'الكمية': [1.1, 0.2, 1, 1, 1],
524
- 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'],
525
- 'سعر الوحدة': [60, 30, 25, 10, 15],
526
- 'الإجمالي': [66, 6, 25, 10, 15]
527
- })
528
- new_components = pd.concat([new_components, default_components], ignore_index=True)
529
-
530
- else:
531
- # مكونات عامة افتراضية
532
- default_components = pd.DataFrame({
533
- 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
534
- 'الوصف': ['مواد أساسية', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
535
- 'الكمية': [1, 1, 1, 1, 1],
536
- 'الوحدة': [new_unit if new_unit else 'وحدة', 'وحدة', 'وحدة', 'وحدة', 'وحدة'],
537
- 'سعر الوحدة': [100, 50, 30, 20, 20],
538
- 'الإجمالي': [100, 50, 30, 20, 20]
539
- })
540
- new_components = pd.concat([new_components, default_components], ignore_index=True)
541
-
542
- st.session_state.new_components = new_components
543
-
544
- # عرض وتحرير مكونات تحليل السعر
545
- edited_components = st.data_editor(
546
- st.session_state.new_components,
547
- use_container_width=True,
548
- hide_index=True,
549
- num_rows="dynamic",
550
- column_config={
551
- 'نوع التكلفة': st.column_config.SelectboxColumn(
552
- 'نوع التكلفة',
553
- help='فئة التكلفة',
554
- options=cost_categories
555
- ),
556
- 'الوحدة': st.column_config.SelectboxColumn(
557
- 'الوحدة',
558
- help='وحدة القياس',
559
- options=unit_options + ["وحدة", "ساعة", "يوم"]
560
- ),
561
- 'الكمية': st.column_config.NumberColumn(
562
- 'الكمية',
563
- help='الكمية',
564
- min_value=0.0,
565
- format="%.2f"
566
- ),
567
- 'سعر الوحدة': st.column_config.NumberColumn(
568
- 'سعر الوحدة',
569
- help='سعر الوحدة',
570
- min_value=0.0,
571
- format="%.2f"
572
- ),
573
- 'الإجمالي': st.column_config.NumberColumn(
574
- 'الإجمالي',
575
- help='الإجمالي',
576
- min_value=0.0,
577
- format="%.2f"
578
- )
579
- }
580
- )
581
-
582
- # إعادة حساب الإجمالي لكل مكون
583
- edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة']
584
-
585
- # حفظ التعديلات
586
- st.session_state.new_components = edited_components
587
-
588
- # حساب إجمالي تحليل السعر
589
- total_analysis_price = edited_components['الإجمالي'].sum()
590
- unit_price_from_analysis = total_analysis_price / new_qty if new_qty > 0 else 0
591
-
592
- # عرض ملخص تحليل السعر
593
- st.markdown("#### ملخص تحليل السعر")
594
-
595
- col1, col2 = st.columns(2)
596
-
597
- with col1:
598
- st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال")
599
-
600
- with col2:
601
- st.metric("سعر الوحدة المحسوب", f"{unit_price_from_analysis:,.2f} ريال")
602
-
603
- st.markdown('</div>', unsafe_allow_html=True)
604
-
605
- # استخدام السعر المحسوب
606
- use_calculated_price = st.checkbox("استخدام السعر المحسوب من التحليل", value=True)
607
-
608
- # تحديد سعر الوحدة النهائي
609
- if use_calculated_price and new_qty > 0:
610
- new_price = unit_price_from_analysis
611
- else:
612
- new_price = st.number_input("سعر الوحدة", value=unit_price_from_analysis if new_qty > 0 else 0.0, min_value=0.0, format="%.2f", key="new_price")
613
-
614
- # حساب الإجمالي
615
- new_total = new_qty * new_price
616
- st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال")
617
-
618
- # مقارنة السعر المدخل مع السعر المحسوب
619
- if not use_calculated_price and new_qty > 0 and unit_price_from_analysis > 0:
620
- price_diff = new_price - unit_price_from_analysis
621
- diff_percentage = (price_diff / unit_price_from_analysis) * 100
622
-
623
- if abs(diff_percentage) > 5: # إذا كان الفرق أكبر من 5%
624
- if diff_percentage > 0:
625
- st.warning(f"السعر المدخل أعلى من السعر المحسوب بنسبة {diff_percentage:.2f}%")
626
- else:
627
- st.warning(f"السعر المدخل أقل من السعر المحسوب بنسبة {abs(diff_percentage):.2f}%")
628
-
629
- # زر إضافة البند
630
- if st.button("إضافة البند"):
631
- # التحقق من صحة البيانات
632
- if new_id and new_desc and new_qty > 0:
633
- # إنشاء صف جديد
634
- new_row = pd.DataFrame({
635
- 'رقم البند': [new_id],
636
- 'وصف البند': [new_desc],
637
- 'الوحدة': [new_unit],
638
- 'الكمية': [float(new_qty)],
639
- 'سعر الوحدة': [float(new_price)],
640
- 'الإجمالي': [float(new_total)]
641
- })
642
-
643
- # إضافة الصف إلى DataFrame
644
- st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True)
645
-
646
- # حفظ تحليل سعر البند
647
- st.session_state.items_price_analysis[new_id] = st.session_state.new_components.copy()
648
-
649
- # إعادة تهيئة مكونات البند الجديد
650
- if 'new_components' in st.session_state:
651
- del st.session_state.new_components
652
-
653
- st.success("تم إضافة البند وتحليل السعر بنجاح!")
654
- time.sleep(0.5)
655
- st.rerun()
656
- else:
657
- st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.")
658
-
659
- with tabs[1]: # تعديل بند حالي
660
- st.markdown("### تعديل بند حالي مع تحليل السعر")
661
-
662
- # اختيار البند للتعديل
663
- edit_item_id = st.selectbox(
664
- "اختر البند للتعديل",
665
- options=st.session_state.manual_items['رقم البند'].tolist(),
666
- format_func=lambda x: f"{x}: {st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == x]['وصف البند'].values[0][:30]}..."
667
- )
668
-
669
- if edit_item_id:
670
- # الحصول على مؤشر الصف للبند المحدد
671
- idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == edit_item_id].index[0]
672
- row = st.session_state.manual_items.loc[idx]
673
-
674
- # إنشاء نموذج تعديل البند
675
- col1, col2 = st.columns(2)
676
-
677
- with col1:
678
- edited_id = st.text_input("رقم البند (تعديل)", value=row['رقم البند'], key="edit_id")
679
- edited_desc = st.text_area("وصف البند (تعديل)", value=row['وصف البند'], key="edit_desc")
680
-
681
- with col2:
682
- edited_unit = st.selectbox(
683
- "الوحدة (تعديل)",
684
- options=unit_options,
685
- index=unit_options.index(row['الوحدة']) if row['الوحدة'] in unit_options else 0,
686
- key="edit_unit"
687
- )
688
- edited_qty = st.number_input("الكمية (تعديل)", value=float(row['الكمية']), min_value=0.0, format="%.2f", key="edit_qty")
689
-
690
- # إنشاء أو تحرير تحليل السعر ل��بند
691
- st.markdown('<div class="pricing-analysis-container">', unsafe_allow_html=True)
692
- st.markdown("#### تحليل سعر البند")
693
-
694
- # التحقق مما إذا كان البند له تحليل سعر محفوظ
695
- if edit_item_id in st.session_state.items_price_analysis:
696
- # استخدام تحليل السعر المحفوظ
697
- components = st.session_state.items_price_analysis[edit_item_id]
698
- else:
699
- # إنشاء تحليل سعر افتراضي
700
- components = pd.DataFrame(columns=[
701
- 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي'
702
- ])
703
-
704
- # فحص نوع البند من الوصف
705
- is_concrete = 'خرسان' in row['وصف البند']
706
- is_steel = 'حديد' in row['وصف البند'] or 'تسليح' in row['وصف البند']
707
- is_bricks = 'بلوك' in row['وصف البند'] or 'طوب' in row['وصف البند']
708
- is_paint = 'دهان' in row['وصف البند'] or 'طلاء' in row['وصف البند']
709
- is_insulation = 'عزل' in row['وصف البند']
710
-
711
- # إضافة مكونات افتراضية بناءً على نوع البند
712
- if is_concrete:
713
- # مكونات الخرسانة
714
- default_components = pd.DataFrame({
715
- 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
716
- 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'],
717
- 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1],
718
- 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
719
- 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150],
720
- 'الإجمالي': [175, 40, 96, 400, 500, 100, 150]
721
- })
722
- components = pd.concat([components, default_components], ignore_index=True)
723
-
724
- elif is_steel:
725
- # مكونات الحديد
726
- default_components = pd.DataFrame({
727
- 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
728
- 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'],
729
- 'الكمية': [1000, 10, 1, 1, 1],
730
- 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
731
- 'سعر الوحدة': [4.5, 50, 300, 200, 300],
732
- 'الإجمالي': [4500, 500, 300, 200, 300]
733
- })
734
- components = pd.concat([components, default_components], ignore_index=True)
735
-
736
- elif is_bricks:
737
- # مكونات البلوك
738
- default_components = pd.DataFrame({
739
- 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
740
- 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'],
741
- 'الكمية': [12.5, 0.02, 1, 1, 1],
742
- 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'],
743
- 'سعر الوحدة': [8, 500, 80, 15, 20],
744
- 'الإجمالي': [100, 10, 80, 15, 20]
745
- })
746
- components = pd.concat([components, default_components], ignore_index=True)
747
-
748
- elif is_paint:
749
- # مكونات الدهانات
750
- default_components = pd.DataFrame({
751
- 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
752
- 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'],
753
- 'الكمية': [0.4, 0.1, 1, 1, 1],
754
- 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'],
755
- 'سعر الوحدة': [80, 20, 35, 5, 10],
756
- 'الإجمالي': [32, 2, 35, 5, 10]
757
- })
758
- components = pd.concat([components, default_components], ignore_index=True)
759
-
760
- elif is_insulation:
761
- # مكونات العزل
762
- default_components = pd.DataFrame({
763
- 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
764
- 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'],
765
- 'الكمية': [1.1, 0.2, 1, 1, 1],
766
- 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'],
767
- 'سعر الوحدة': [60, 30, 25, 10, 15],
768
- 'الإجمالي': [66, 6, 25, 10, 15]
769
- })
770
- components = pd.concat([components, default_components], ignore_index=True)
771
-
772
- else:
773
- # مكونات عامة افتراضية
774
- default_components = pd.DataFrame({
775
- 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
776
- 'الوصف': ['مواد أساسية', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
777
- 'الكمية': [1, 1, 1, 1, 1],
778
- 'الوحدة': [row['الوحدة'], 'وحدة', 'وحدة', 'وحدة', 'وحدة'],
779
- 'سعر الوحدة': [
780
- row['سعر الوحدة'] * 0.6,
781
- row['سعر الوحدة'] * 0.2,
782
- row['سعر الوحدة'] * 0.1,
783
- row['سعر الوحدة'] * 0.05,
784
- row['سعر الوحدة'] * 0.05
785
- ],
786
- 'الإجمالي': [
787
- row['سعر الوحدة'] * 0.6,
788
- row['سعر الوحدة'] * 0.2,
789
- row['سعر الوحدة'] * 0.1,
790
- row['سعر الوحدة'] * 0.05,
791
- row['سعر الوحدة'] * 0.05
792
- ]
793
- })
794
- components = pd.concat([components, default_components], ignore_index=True)
795
-
796
- # حفظ تحليل السعر
797
- st.session_state.items_price_analysis[edit_item_id] = components
798
-
799
- # عرض وتحرير مكونات تحليل السعر
800
- edited_components = st.data_editor(
801
- components,
802
- use_container_width=True,
803
- hide_index=True,
804
- num_rows="dynamic",
805
- column_config={
806
- 'نوع التكلفة': st.column_config.SelectboxColumn(
807
- 'نوع التكلفة',
808
- help='فئة التكلفة',
809
- options=cost_categories
810
- ),
811
- 'الوحدة': st.column_config.SelectboxColumn(
812
- 'الوحدة',
813
- help='وحدة القياس',
814
- options=unit_options + ["وحدة", "ساعة", "يوم"]
815
- ),
816
- 'الكمية': st.column_config.NumberColumn(
817
- 'الكمية',
818
- help='الكمية',
819
- min_value=0.0,
820
- format="%.2f"
821
- ),
822
- 'سعر الوحدة': st.column_config.NumberColumn(
823
- 'سعر الوحدة',
824
- help='سعر الوحدة',
825
- min_value=0.0,
826
- format="%.2f"
827
- ),
828
- 'الإجمالي': st.column_config.NumberColumn(
829
- 'الإجمالي',
830
- help='الإجمالي',
831
- min_value=0.0,
832
- format="%.2f"
833
- )
834
- }
835
- )
836
-
837
- # إعادة حساب الإجمالي لكل مكون
838
- edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة']
839
-
840
- # حفظ التعديلات
841
- st.session_state.items_price_analysis[edit_item_id] = edited_components
842
-
843
- # حساب إجمالي تحليل السعر
844
- total_analysis_price = edited_components['الإجمالي'].sum()
845
- unit_price_from_analysis = total_analysis_price / edited_qty if edited_qty > 0 else 0
846
-
847
- # عرض ملخص تحليل السعر
848
- st.markdown("#### ملخص تحليل السعر")
849
-
850
- col1, col2 = st.columns(2)
851
-
852
- with col1:
853
- st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال")
854
-
855
- with col2:
856
- st.metric("سعر الوحدة المحسوب", f"{unit_price_from_analysis:,.2f} ريال")
857
-
858
- st.markdown('</div>', unsafe_allow_html=True)
859
-
860
- # استخدام السعر المحسوب
861
- use_calculated_price = st.checkbox("استخدام السعر المحسوب من التحليل", value=True, key="use_calc_edit")
862
-
863
- # تحديد سعر الوحدة النهائي
864
- if use_calculated_price and edited_qty > 0:
865
- edited_price = unit_price_from_analysis
866
- else:
867
- edited_price = st.number_input(
868
- "سعر الوحدة (تعديل)",
869
- value=unit_price_from_analysis if edited_qty > 0 and unit_price_from_analysis > 0 else float(row['سعر الوحدة']),
870
- min_value=0.0,
871
- format="%.2f",
872
- key="edit_price"
873
- )
874
-
875
- # حساب الإجمالي
876
- edited_total = edited_qty * edited_price
877
- st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال")
878
-
879
- # مقارنة السعر المدخل مع السعر المحسوب
880
- if not use_calculated_price and edited_qty > 0 and unit_price_from_analysis > 0:
881
- price_diff = edited_price - unit_price_from_analysis
882
- diff_percentage = (price_diff / unit_price_from_analysis) * 100
883
-
884
- if abs(diff_percentage) > 5: # إذا كان الفرق أكبر من 5%
885
- if diff_percentage > 0:
886
- st.warning(f"السعر المدخل أعلى من السعر المحسوب بنسبة {diff_percentage:.2f}%")
887
- else:
888
- st.warning(f"السعر المدخل أقل من السعر المحسوب بنسبة {abs(diff_percentage):.2f}%")
889
-
890
- # أزرار الإجراءات
891
- col1, col2, col3 = st.columns(3)
892
-
893
- with col1:
894
- if st.button("حفظ التعديلات", use_container_width=True):
895
- # التحقق من صحة البيانات
896
- if edited_id and edited_desc and edited_qty > 0:
897
- # التحقق من تغيير رقم البند
898
- if edited_id != edit_item_id:
899
- # نقل تحليل السعر إلى الرقم الجديد
900
- st.session_state.items_price_analysis[edited_id] = st.session_state.items_price_analysis.pop(edit_item_id)
901
-
902
- # تحديث البند
903
- st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id
904
- st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc
905
- st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit
906
- st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty
907
- st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price
908
- st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total
909
-
910
- st.success("تم تحديث البند وتحليل السعر بنجاح!")
911
- time.sleep(0.5)
912
- st.rerun()
913
- else:
914
- st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.")
915
-
916
- with col2:
917
- if st.button("استعادة القيم الأصلية", use_container_width=True):
918
- # إعادة تحميل الصفحة لاستعادة القيم الأصلية
919
- st.rerun()
920
-
921
- with col3:
922
- if st.button("حذف هذا البند", use_container_width=True):
923
- # حذف تحليل السعر للبند
924
- if edit_item_id in st.session_state.items_price_analysis:
925
- del st.session_state.items_price_analysis[edit_item_id]
926
-
927
- # حذف البند
928
- st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True)
929
-
930
- st.warning("تم حذف البند وتحليل السعر!")
931
- time.sleep(0.5)
932
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/price_analyzer.py DELETED
@@ -1,1695 +0,0 @@
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 DELETED
The diff for this file is too large to render. See raw diff
 
modules/pricing/pricing_app.py.backup DELETED
@@ -1,1242 +0,0 @@
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 random
13
- import os
14
- import time
15
- import io
16
-
17
- from modules.pricing.services.standard_pricing import StandardPricing
18
- from modules.pricing.services.unbalanced_pricing import UnbalancedPricing
19
- from modules.pricing.services.local_content_calculator import LocalContentCalculator
20
- from modules.pricing.services.price_prediction import PricePrediction
21
- from utils.excel_handler import export_to_excel
22
- from utils.helpers import format_number, format_currency
23
-
24
-
25
- class PricingApp:
26
- """وحدة التسعير المتكاملة"""
27
-
28
- def __init__(self):
29
- """تهيئة وحدة التسعير المتكاملة"""
30
- self.pricing_methods = [
31
- "التسعير القياسي",
32
- "التسعير غير المتزن",
33
- "التسعير التنافسي",
34
- "التسعير الموجه بالربحية"
35
- ]
36
-
37
- # تهيئة خدمات التسعير
38
- self.standard_pricing = StandardPricing()
39
- self.unbalanced_pricing = UnbalancedPricing()
40
- self.local_content = LocalContentCalculator()
41
- self.price_prediction = PricePrediction()
42
-
43
- def render(self):
44
- """عرض واجهة وحدة التسعير"""
45
-
46
- st.markdown("<h1 class='module-title'>وحدة التسعير المتكاملة</h1>", unsafe_allow_html=True)
47
-
48
- tabs = st.tabs([
49
- "إنشاء تسعير جديد",
50
- "تحليل سعر البند",
51
- "نموذج التسعير الشامل",
52
- "التسعير غير المتزن",
53
- "المحتوى المحلي"
54
- ])
55
-
56
- with tabs[0]:
57
- self._render_new_pricing_tab()
58
-
59
- with tabs[1]:
60
- self._render_item_analysis_tab()
61
-
62
- with tabs[2]:
63
- self._render_comprehensive_pricing_tab()
64
-
65
- with tabs[3]:
66
- self._render_unbalanced_pricing_tab()
67
-
68
- with tabs[4]:
69
- self._render_local_content_tab()
70
-
71
- def _render_item_analysis_tab(self):
72
- """عرض تبويب تحليل سعر البند"""
73
-
74
- st.markdown("### تحليل سعر البند")
75
-
76
- # التحقق من وجود تسعير حالي
77
- if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
78
- st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
79
- return
80
-
81
- # اختيار البند للتحليل
82
- if 'current_pricing' in st.session_state and st.session_state.current_pricing is not None:
83
- items = st.session_state.current_pricing['items']
84
- item_options = items['رقم البند'].tolist()
85
- selected_item = st.selectbox("اختر البند للتحليل", item_options)
86
-
87
- if selected_item:
88
- item_data = items[items['رقم البند'] == selected_item].iloc[0]
89
-
90
- st.markdown(f"### تحليل البند: {selected_item}")
91
- st.markdown(f"**وصف البند**: {item_data['وصف البند']}")
92
- st.markdown(f"**الوحدة**: {item_data['الوحدة']}")
93
- st.markdown(f"**الكمية**: {item_data['الكمية']}")
94
- st.markdown(f"**سعر الوحدة**: {item_data['سعر الوحدة']:,.2f} ريال")
95
-
96
- # تحليل مكونات السعر
97
- st.markdown("### تحليل مكونات السعر")
98
-
99
- # عناصر التكلفة الافتراضية
100
- cost_components = {
101
- 'المواد': 0.6, # 60% من التكلفة
102
- 'العمالة': 0.25, # 25% من التكلفة
103
- 'المعدات': 0.1, # 10% من التكلفة
104
- 'نفقات عامة': 0.05 # 5% من التكلفة
105
- }
106
-
107
- # حساب تكلفة كل عنصر
108
- unit_price = item_data['سعر الوحدة']
109
- component_values = {k: v * unit_price for k, v in cost_components.items()}
110
-
111
- # عرض مكونات التكلفة في جدول
112
- components_df = pd.DataFrame({
113
- 'العنصر': component_values.keys(),
114
- 'نسبة من التكلفة': [f"{v*100:.1f}%" for v in cost_components.values()],
115
- 'القيمة (ريال)': [f"{v:,.2f}" for v in component_values.values()]
116
- })
117
-
118
- st.table(components_df)
119
-
120
- # رسم بي��ني لمكونات التكلفة
121
- fig = px.pie(
122
- names=list(component_values.keys()),
123
- values=list(component_values.values()),
124
- title='توزيع مكونات التكلفة'
125
- )
126
-
127
- st.plotly_chart(fig)
128
-
129
- # تحليل تاريخي للأسعار
130
- st.markdown("### تحليل تاريخي للأسعار")
131
-
132
- # بيانات تاريخية افتراضية
133
- historical_data = {
134
- 'التاريخ': ['2020-01', '2020-07', '2021-01', '2021-07', '2022-01', '2022-07', '2023-01', '2023-07'],
135
- 'السعر': [
136
- unit_price * 0.7,
137
- unit_price * 0.75,
138
- unit_price * 0.8,
139
- unit_price * 0.85,
140
- unit_price * 0.9,
141
- unit_price * 0.95,
142
- unit_price,
143
- unit_price * 1.05
144
- ]
145
- }
146
-
147
- hist_df = pd.DataFrame(historical_data)
148
-
149
- # رسم بياني للتحليل التاريخي
150
- fig = px.line(
151
- hist_df,
152
- x='التاريخ',
153
- y='السعر',
154
- title='تطور سعر الوحدة عبر الزمن',
155
- markers=True
156
- )
157
-
158
- st.plotly_chart(fig)
159
-
160
- # المقارنة مع الأسعار المرجعية
161
- st.markdown("### المقارنة مع الأسعار المرجعية")
162
-
163
- # بيانات مرجعية افتراضية
164
- reference_data = {
165
- 'المصدر': ['قاعدة البيانات الداخلية', 'دليل الأسعار الاسترشادي', 'متوسط أسعار السوق', 'أسعار المشاريع المماثلة'],
166
- 'السعر المرجعي': [
167
- unit_price * 0.95,
168
- unit_price * 1.05,
169
- unit_price * 1.1,
170
- unit_price * 0.9
171
- ]
172
- }
173
-
174
- ref_df = pd.DataFrame(reference_data)
175
- ref_df['الفرق عن السعر الحالي'] = ref_df['السعر المرجعي'] - unit_price
176
- ref_df['نسبة الفرق'] = (ref_df['الفرق عن السعر الحالي'] / unit_price * 100).round(2).astype(str) + '%'
177
-
178
- st.table(ref_df)
179
-
180
- def _render_new_pricing_tab(self):
181
- """عرض تبويب إنشاء تسعير جديد"""
182
-
183
- st.markdown("### إنشاء تسعير جديد")
184
-
185
- col1, col2 = st.columns(2)
186
-
187
- with col1:
188
- tender_name = st.text_input("اسم المناقصة")
189
- client = st.text_input("الجهة المالكة")
190
- pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods)
191
-
192
- with col2:
193
- tender_number = st.text_input("رقم المناقصة")
194
- location = st.text_input("الموقع")
195
- submission_date = st.date_input("تاريخ التقديم")
196
-
197
- # خيارات بيانات البنود
198
- st.markdown("### بيانات البنود")
199
-
200
- data_source = st.radio(
201
- "مصدر بيانات البنود",
202
- ["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات"]
203
- )
204
-
205
- if data_source == "إدخال يدوي":
206
- # ضبط CSS لتحسين ظهور الواجهة العربية
207
- st.markdown("""
208
- <style>
209
- input, .stTextArea textarea {
210
- direction: rtl;
211
- text-align: right;
212
- font-family: 'Arial', 'Tahoma', sans-serif !important;
213
- }
214
- .stTextInput > div > div > input {
215
- text-align: right;
216
- direction: rtl;
217
- }
218
- </style>
219
- """, unsafe_allow_html=True)
220
-
221
- # تهيئة قائمة الوحدات المتاحة
222
- unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
223
-
224
- # إنشاء بيانات افتراضية إذا لم تكن موجودة
225
- if 'manual_items' not in st.session_state:
226
- # إنشاء DataFrame فارغ
227
- manual_items = pd.DataFrame(columns=[
228
- 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي'
229
- ])
230
-
231
- # إضافة بضعة صفوف افتراضية
232
- default_items = pd.DataFrame({
233
- 'رقم البند': ["A1", "A2", "A3", "A4", "A5"],
234
- 'وصف البند': [
235
- "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
236
- "توريد وتركيب حديد التسليح للأساسات",
237
- "أعمال العزل المائي للأساسات",
238
- "أعمال الردم والدك للأساسات",
239
- "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة"
240
- ],
241
- 'الوحدة': ["م3", "طن", "م2", "م3", "م3"],
242
- 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0],
243
- 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0],
244
- 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0]
245
- })
246
-
247
- manual_items = pd.concat([manual_items, default_items])
248
- st.session_state.manual_items = manual_items
249
-
250
- # عرض واجهة إدخال البنود
251
- st.markdown("### إدخال تفاصيل البنود")
252
-
253
- # التحقق من استخدام طريقة الإدخال البسيطة
254
- use_simple_input = st.checkbox("استخدام طريقة الإدخال البسيطة", value=True)
255
-
256
- if use_simple_input:
257
- # عرض البنود الحالية كجدول للعرض فقط
258
- st.markdown("### جدول البنود الحالية")
259
- st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
260
-
261
- # إضافة بند جديد
262
- st.markdown("### إضافة بند جديد")
263
- col1, col2 = st.columns(2)
264
-
265
- with col1:
266
- new_id = st.text_input("رقم البند", value=f"A{len(st.session_state.manual_items)+1}")
267
- new_desc = st.text_area("وصف البند", value="")
268
-
269
- with col2:
270
- new_unit = st.selectbox("الوحدة", options=unit_options)
271
- new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f")
272
- new_price = st.number_input("سعر الوحدة", value=0.0, min_value=0.0, format="%.2f")
273
-
274
- new_total = new_qty * new_price
275
- st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال")
276
-
277
- if st.button("إضافة البند"):
278
- # التحقق من صحة البيانات
279
- if new_id and new_desc and new_qty > 0:
280
- # إنشاء صف جديد
281
- new_row = pd.DataFrame({
282
- 'رقم البند': [new_id],
283
- 'وصف البند': [new_desc],
284
- 'الوحدة': [new_unit],
285
- 'الكمية': [float(new_qty)],
286
- 'سعر الوحدة': [float(new_price)],
287
- 'الإجمالي': [float(new_total)]
288
- })
289
-
290
- # إضافة الصف إلى DataFrame
291
- st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True)
292
- st.success("تم إضافة البند بنجاح!")
293
- st.rerun()
294
- else:
295
- st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.")
296
-
297
- # تعديل البنود الحالية
298
- st.markdown("### تعديل البنود الحالية")
299
-
300
- # تحديد البند المراد تعديله
301
- item_to_edit = st.selectbox(
302
- "اختر البند للتعديل",
303
- options=st.session_state.manual_items['رقم البند'].tolist(),
304
- format_func=lambda x: f"{x}: {st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == x]['وصف البند'].values[0][:30]}..."
305
- )
306
-
307
- if item_to_edit:
308
- # الحصول على مؤشر الصف للبند المحدد
309
- idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == item_to_edit].index[0]
310
- row = st.session_state.manual_items.loc[idx]
311
-
312
- # إنشاء نموذج تعديل
313
- col1, col2 = st.columns(2)
314
-
315
- with col1:
316
- edited_id = st.text_input("رقم البند (تعديل)", value=row['رقم البند'], key="edit_id")
317
- edited_desc = st.text_area("وصف البند (تعديل)", value=row['وصف البند'], key="edit_desc")
318
-
319
- with col2:
320
- edited_unit = st.selectbox(
321
- "الوحدة (تعديل)",
322
- options=unit_options,
323
- index=unit_options.index(row['الوحدة']) if row['الوحدة'] in unit_options else 0,
324
- key="edit_unit"
325
- )
326
- edited_qty = st.number_input("الكمية (تعديل)", value=float(row['الكمية']), min_value=0.0, format="%.2f", key="edit_qty")
327
- edited_price = st.number_input("سعر الوحدة (تعديل)", value=float(row['سعر الوحدة']), min_value=0.0, format="%.2f", key="edit_price")
328
-
329
- edited_total = edited_qty * edited_price
330
- st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال")
331
-
332
- col1, col2 = st.columns(2)
333
- with col1:
334
- if st.button("حفظ التعديلات"):
335
- # تحديث البند
336
- st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id
337
- st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc
338
- st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit
339
- st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty
340
- st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price
341
- st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total
342
-
343
- st.success("تم تحديث البند بنجاح!")
344
- st.rerun()
345
-
346
- with col2:
347
- if st.button("حذف هذا البند"):
348
- st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True)
349
- st.warning("تم حذف البند!")
350
- st.rerun()
351
-
352
- # المجموع الكلي
353
- total = st.session_state.manual_items['الإجمالي'].sum()
354
- st.metric("المجموع الكلي", f"{total:,.2f} ريال")
355
-
356
- # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية
357
- edited_items = st.session_state.manual_items.copy()
358
-
359
- else:
360
- # عرض رسالة توضح أن طريقة الإدخال البسيطة هي الأفضل
361
- st.warning("لتجنب مشاكل عدم التوافق في أنواع البيانات، يُفضل استخدام طريقة الإدخال البسيطة.")
362
-
363
- # محاولة استخدام المحرر القياسي مع معالجة الأخطاء
364
- try:
365
- # تحويل البيانات إلى الأنواع المناسبة
366
- for col in st.session_state.manual_items.columns:
367
- if col in ['رقم البند', 'وصف البند', 'الوحدة']:
368
- st.session_state.manual_items[col] = st.session_state.manual_items[col].astype(str)
369
-
370
- # عرض المحرر (للقراءة فقط)
371
- st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
372
-
373
- # إنشاء نظام تعديل منفصل
374
- st.markdown("### تعديل أسعار الوحدات")
375
-
376
- for idx, row in st.session_state.manual_items.iterrows():
377
- col1, col2 = st.columns([3, 1])
378
-
379
- with col1:
380
- st.text(f"{row['رقم البند']}: {row['وصف البند'][:50]}")
381
-
382
- with col2:
383
- price = st.number_input(
384
- f"سعر الوحدة ({row['الوحدة']})",
385
- value=float(row['سعر الوحدة']),
386
- min_value=0.0,
387
- key=f"price_{idx}"
388
- )
389
-
390
- # تحديث السعر والإجمالي
391
- st.session_state.manual_items.at[idx, 'سعر الوحدة'] = price
392
- st.session_state.manual_items.at[idx, 'الإجمالي'] = price * row['الكمية']
393
-
394
- # المجموع الكلي
395
- total = st.session_state.manual_items['الإجمالي'].sum()
396
- st.metric("المجموع الكلي", f"{total:,.2f} ريال")
397
-
398
- # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية
399
- edited_items = st.session_state.manual_items.copy()
400
-
401
- except Exception as e:
402
- st.error(f"حدث خطأ: {str(e)}")
403
- st.info("يرجى استخدام طريقة الإدخال البسيطة لتجنب هذه المشكلة.")
404
-
405
- elif data_source == "استيراد من Excel":
406
- uploaded_file = st.file_uploader("رفع ملف Excel", type=["xlsx", "xls"])
407
-
408
- if uploaded_file is not None:
409
- st.success("تم رفع الملف بنجاح")
410
- # محاكاة قراءة الملف
411
- st.markdown("### معاينة البيانات المستوردة")
412
-
413
- # إنشاء بيانات افتراضية
414
- import_items = pd.DataFrame({
415
- 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"],
416
- 'وصف البند': [
417
- "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
418
- "توريد وتركيب حديد التسليح للأساسات",
419
- "أعمال العزل المائي للأساسات",
420
- "أعمال الردم والدك للأساسات",
421
- "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
422
- "توريد وتركيب حديد التسليح للأعمدة",
423
- "أعمال البلوك للجدران"
424
- ],
425
- 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
426
- 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0],
427
- 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
428
- 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
429
- })
430
-
431
- st.dataframe(import_items)
432
-
433
- if st.button("استيراد البيانات"):
434
- st.session_state.manual_items = import_items.copy()
435
- st.session_state.manual_items_modified = True
436
- st.success("تم استيراد البيانات بنجاح!")
437
- st.rerun()
438
-
439
- else: # استيراد من وحدة تحليل المستندات
440
- available_documents = [
441
- "كراسة شروط مشروع توسعة مستشفى الملك فهد",
442
- "جدول كميات صيانة محطات المياه",
443
- "مخططات إنشاء مدرسة ثانوية"
444
- ]
445
-
446
- selected_doc = st.selectbox("اختر المستند", available_documents)
447
-
448
- if st.button("استيراد البيانات من تحليل المستند"):
449
- # محاكاة استيراد البيانات
450
- with st.spinner("جاري استيراد البيانات..."):
451
- time.sleep(2)
452
-
453
- # إنشاء بيانات افتراضية
454
- doc_items = pd.DataFrame({
455
- 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"],
456
- 'وصف البند': [
457
- "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
458
- "توريد وتركيب حديد التسليح للأساسات",
459
- "أعمال العزل المائي للأساسات",
460
- "أعمال الردم والدك للأساسات",
461
- "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
462
- "توريد وتركيب حديد التسليح للأعمدة",
463
- "أعمال البلوك للجدران"
464
- ],
465
- 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
466
- 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0],
467
- 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
468
- 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
469
- })
470
-
471
- st.session_state.manual_items = doc_items.copy()
472
- st.success("تم استيراد البيانات من تحليل المستند بنجاح!")
473
- st.dataframe(doc_items)
474
-
475
- # زر بدء التسعير
476
- if st.button("بدء التسعير"):
477
- # تحقق من صحة البيانات
478
- if 'manual_items' in st.session_state and not st.session_state.manual_items.empty:
479
- # التأكد من حساب الإجمالي قبل الحفظ
480
- st.session_state.manual_items['الإجمالي'] = st.session_state.manual_items['الكمية'] * st.session_state.manual_items['سعر الوحدة']
481
-
482
- # حفظ بيانات التسعير الحالي
483
- st.session_state.current_pricing = {
484
- 'name': tender_name,
485
- 'number': tender_number,
486
- 'client': client,
487
- 'location': location,
488
- 'method': pricing_method,
489
- 'submission_date': submission_date,
490
- 'items': st.session_state.manual_items.copy(),
491
- 'status': 'جديد',
492
- 'created_at': datetime.now()
493
- }
494
-
495
- # الانتقال إلى تبويب نموذج التسعير الشامل
496
- st.success("تم إنشاء التسعير بنجاح! يمكنك الانتقال إلى نموذج التسعير الشامل.")
497
- else:
498
- st.error("يرجى إدخال بيانات البنود أولاً.")
499
-
500
- def _render_comprehensive_pricing_tab(self):
501
- """عرض تبويب نموذج التسعير الشامل"""
502
-
503
- st.markdown("### نموذج التسعير الشامل")
504
-
505
- # التحقق من وجود تسعير حالي
506
- if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
507
- st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
508
- return
509
-
510
- # عرض معلومات التسعير الحالي
511
- pricing = st.session_state.current_pricing
512
-
513
- col1, col2, col3 = st.columns(3)
514
-
515
- with col1:
516
- st.metric("اسم المناقصة", pricing['name'])
517
- st.metric("الجهة المالكة", pricing['client'])
518
-
519
- with col2:
520
- st.metric("رقم المناقصة", pricing['number'])
521
- st.metric("تاريخ التقديم", pricing['submission_date'].strftime("%Y-%m-%d"))
522
-
523
- with col3:
524
- st.metric("طريقة التسعير", pricing['method'])
525
- st.metric("الموقع", pricing['location'])
526
-
527
- # عرض البنود والتسعير
528
- st.markdown("### بنود التسعير")
529
-
530
- items = pricing['items'].copy()
531
-
532
- # إضافة أسعار الوحدة للمحاكاة
533
- if 'سعر الوحدة' in items.columns and (items['سعر الوحدة'] == 0).all():
534
- items['سعر الوحدة'] = [
535
- round(random.uniform(1000, 3000), 2), # الخرسانة
536
- round(random.uniform(5000, 7000), 2), # الحديد
537
- round(random.uniform(100, 200), 2), # العزل
538
- round(random.uniform(50, 100), 2), # الردم
539
- round(random.uniform(1200, 3500), 2), # الخرسانة للأعمدة
540
- ]
541
-
542
- if len(items) > 5:
543
- for i in range(5, len(items)):
544
- items.at[i, 'سعر الوحدة'] = round(random.uniform(500, 5000), 2)
545
-
546
- # حساب الإجمالي
547
- items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
548
-
549
- # عرض البنود
550
- st.dataframe(items, use_container_width=True, hide_index=True)
551
-
552
-
553
- # ✅ التوصية الذكية باستخدام OpenAI
554
- with st.expander("🔍 توليد توصية ذكية باستخدام AI"):
555
- if st.button("🔍 توليد توصية ذكية باستخدام AI", use_container_width=True):
556
- import openai
557
- import os
558
-
559
- client = openai.OpenAI(api_key=os.environ.get("ai"))
560
-
561
- items_df = items.copy()
562
- prompt = f"""قم بتحليل الجدول التالي للبنود في مشروع إنشاء، وقدم توصية ذكية لتحسين التسعير وضمان التوازن المالي. الجدول يحتوي على البنود، الكميات، الأسعار، والإجماليات:\n\n{items_df.to_string(index=False)}\n\nالتوصية:\n"""
563
-
564
- try:
565
- with st.spinner("جاري توليد التوصية..."):
566
- response = client.chat.completions.create(
567
- model="gpt-4",
568
- messages=[
569
- {"role": "system", "content": "أنت خبير في تسعير مشاريع البناء والبنية التحتية."},
570
- {"role": "user", "content": prompt}
571
- ],
572
- temperature=0.4,
573
- max_tokens=500
574
- )
575
-
576
- recommendation = response.choices[0].message.content
577
- st.success("تم توليد التوصية بنجاح!")
578
- st.markdown("#### التوصية الذكية:")
579
- st.info(recommendation)
580
-
581
- except Exception as e:
582
- st.error(f"حدث خطأ أثناء الاتصال بنموذج OpenAI: {e}")
583
- st.info("يجب التأكد من تثبيت أحدث إصدار من مكتبة OpenAI: `pip install openai --upgrade`")
584
-
585
- # واجهة تعديل أسعار الوحدات
586
- st.markdown("### تعديل أسعار الوحدات")
587
-
588
- # تقسيم البنود إلى مجموعتين للعرض
589
- col1, col2 = st.columns(2)
590
- half = len(items) // 2 + len(items) % 2
591
-
592
- with col1:
593
- for idx in range(half):
594
- if idx < len(items):
595
- row = items.iloc[idx]
596
- price = st.number_input(
597
- f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})",
598
- value=float(row['سعر الوحدة']),
599
- min_value=0.0,
600
- key=f"price1_{idx}"
601
- )
602
- items.at[idx, 'سعر الوحدة'] = price
603
- items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية']
604
-
605
- with col2:
606
- for idx in range(half, len(items)):
607
- row = items.iloc[idx]
608
- price = st.number_input(
609
- f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})",
610
- value=float(row['سعر الوحدة']),
611
- min_value=0.0,
612
- key=f"price2_{idx}"
613
- )
614
- items.at[idx, 'سعر الوحدة'] = price
615
- items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية']
616
-
617
- # حساب وعرض إجماليات التسعير
618
- total_price = items['الإجمالي'].sum()
619
-
620
- st.markdown("### إجماليات التسعير")
621
-
622
- col1, col2, col3 = st.columns(3)
623
-
624
- with col1:
625
- st.metric("إجمالي التكاليف المباشرة", f"{total_price:,.2f} ريال")
626
-
627
- with col2:
628
- overhead_percentage = st.slider("نسبة المصاريف العامة والأرباح (%)", 5, 30, 15)
629
- overhead_value = total_price * overhead_percentage / 100
630
- st.metric("المصاريف العامة والأرباح", f"{overhead_value:,.2f} ريال")
631
-
632
- with col3:
633
- grand_total = total_price + overhead_value
634
- st.metric("الإجمالي النهائي", f"{grand_total:,.2f} ريال")
635
-
636
- # رسم بياني لتوزيع التكاليف
637
- st.markdown("### تحليل التكاليف")
638
-
639
- # حساب النسب المئوية لكل بند
640
- pie_data = items.copy()
641
- pie_data['نسبة من إجمالي التكاليف'] = pie_data['الإجمالي'] / total_price * 100
642
-
643
- fig = px.pie(
644
- pie_data,
645
- values='نسبة من إجمالي التكاليف',
646
- names='وصف البند',
647
- title='توزيع التكاليف حسب البنود',
648
- hole=0.4
649
- )
650
-
651
- st.plotly_chart(fig, use_container_width=True)
652
-
653
- # أزرار العمليات
654
- col1, col2, col3 = st.columns(3)
655
-
656
- with col1:
657
- if st.button("حفظ التسعير"):
658
- # تحديث بيانات التسعير الحالي
659
- st.session_state.current_pricing['items'] = items.copy()
660
- st.success("تم حفظ التسعير بنجاح!")
661
-
662
- with col2:
663
- if st.button("تصدير إلى Excel"):
664
- st.success("تم تصدير التسعير إلى Excel بنجاح!")
665
-
666
- with col3:
667
- if st.button("تحليل المخاطر المالية"):
668
- st.success("تم إرسال الطلب إلى وحدة تحليل المخاطر!")
669
-
670
- def _render_unbalanced_pricing_tab(self):
671
- """عرض تبويب التسعير غير المتزن"""
672
-
673
- st.markdown("### التسعير غير المتزن")
674
-
675
- # التحقق من وجود تسعير حالي
676
- if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
677
- st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
678
- return
679
-
680
- # شرح التسعير غير المتزن
681
- with st.expander("ما هو التسعير غير المتزن؟", expanded=False):
682
- st.markdown("""
683
- **التسعير غير المتزن** هو استراتيجية تسعير تقوم على توزيع التكاليف بين بنود المناقصة بشكل غير متساوٍ، مع الحفاظ على إجمالي قيمة العطاء.
684
-
685
- ### استراتيجيات التسعير غير المتزن:
686
-
687
- 1. **التحميل الأمامي (Front Loading)**: زيادة أسعار البنود المبكرة في المشروع للحصول على تدفق نقدي أفضل في بداية المشروع.
688
- 2. **التحميل الخلفي (Back Loading)**: زيادة أسعار البنود المتأخرة في المشروع.
689
- 3. **تحميل البنود المؤكدة**: زيادة أسعار البنود التي من المؤكد تنفيذها بالكميات المحددة.
690
- 4. **تخفيض أسعار البنود المحتملة**: تخفيض أسعار البنود التي قد تزيد كمياتها أثناء التنفيذ.
691
-
692
- ### مزايا التسعير غير المتزن:
693
-
694
- - تحسين التدفق النقدي للمشروع.
695
- - تعظيم الربحية في حالة التغييرات والأوامر التغييرية.
696
- - زيادة فرص الفوز بالمناقصة.
697
-
698
- ### مخاطر التسعير غير المتزن:
699
-
700
- - قد يتم رفض العطاء إذا كان عدم التوازن واضحاً.
701
- - قد تتأثر السمعة سلباً إذا تم استخدامه بشكل مفرط.
702
- - قد يؤدي إلى خسائر إذا لم يتم تنفيذ البنود ذات الأسعار العالية.
703
- """)
704
-
705
- # عرض بنود التسعير الحالي
706
- items = st.session_state.current_pricing['items'].copy()
707
-
708
- # إضافة عمود إستراتيجية التسعير
709
- if 'إستراتيجية التسعير' not in items.columns:
710
- items['إستراتيجية التسعير'] = 'متوازن'
711
-
712
- st.markdown("### إستراتيجية التسعير غير المتزن")
713
-
714
- # اختيار الإستراتيجية
715
- strategy = st.selectbox(
716
- "اختر إستراتيجية التسعير",
717
- [
718
- "تحميل أمامي (Front Loading)",
719
- "تحميل البنود المؤكدة",
720
- "تخفيض البنود المحتمل زيادتها",
721
- "إستراتيجية مخصصة"
722
- ]
723
- )
724
-
725
- # تطبيق الإستراتيجية المختارة
726
- if strategy == "تحميل أمامي (Front Loading)":
727
- # محاكاة تحميل أمامي
728
- items_count = len(items)
729
- early_items = items.iloc[:items_count//3].index
730
- middle_items = items.iloc[items_count//3:2*items_count//3].index
731
- late_items = items.iloc[2*items_count//3:].index
732
-
733
- # تطبيق الزيادة والنقصان
734
- for idx in early_items:
735
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.3 # زيادة 30%
736
- items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
737
-
738
- for idx in middle_items:
739
- items.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
740
-
741
- for idx in late_items:
742
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
743
- items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
744
-
745
- elif strategy == "تحميل البنود المؤكدة":
746
- # محاكاة - اعتبار بعض البنود مؤكدة
747
- confirmed_items = [0, 2, 4] # الأصفار-مستندة
748
- variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items]
749
-
750
- # تطبيق الزيادة والنقصان
751
- for idx in confirmed_items:
752
- if idx < len(items):
753
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 # زيادة 25%
754
- items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
755
-
756
- for idx in variable_items:
757
- if idx < len(items):
758
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 # نقص 15%
759
- items.at[idx, 'إستراتيجية ��لتسعير'] = 'نقص'
760
-
761
- elif strategy == "تخفيض البنود المحتمل زيادتها":
762
- # محاكاة - اعتبار بعض البنود محتمل زيادتها
763
- variable_items = [1, 3] # الأصفار-مستندة
764
- other_items = [idx for idx in range(len(items)) if idx not in variable_items]
765
-
766
- # تطبيق الزيادة والنقصان
767
- for idx in variable_items:
768
- if idx < len(items):
769
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
770
- items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
771
-
772
- for idx in other_items:
773
- if idx < len(items):
774
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 # زيادة 15%
775
- items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
776
-
777
- else: # إستراتيجية مخصصة
778
- st.markdown("### تعديل أسعار البنود يدوياً")
779
- st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً.")
780
-
781
- # حساب الإجمالي بعد التعديل
782
- items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
783
-
784
- # تعيين ألوان للإستراتيجيات
785
- def highlight_strategy(val):
786
- if val == 'زيادة':
787
- return 'background-color: #a8e6cf'
788
- elif val == 'نقص':
789
- return 'background-color: #ff9aa2'
790
- return ''
791
-
792
- # عرض الجدول مع تنسيق
793
- st.markdown("### بنود التسعير غير المتزن")
794
- styled_items = items.style.applymap(highlight_strategy, subset=['إستراتيجية التسعير'])
795
- st.dataframe(styled_items, use_container_width=True)
796
-
797
- # المقارنة بين التسعير المتوازن وغير المتوازن
798
- st.markdown("### مقارنة التسعير المتوازن وغير المتوازن")
799
-
800
- original_items = st.session_state.current_pricing['items'].copy()
801
- original_total = original_items['الإجمالي'].sum()
802
- unbalanced_total = items['الإجمالي'].sum()
803
-
804
- col1, col2, col3 = st.columns(3)
805
-
806
- with col1:
807
- st.metric("إجمالي التسعير المتوازن", f"{original_total:,.2f} ريال")
808
-
809
- with col2:
810
- st.metric("إجمالي التسعير غير المتوازن", f"{unbalanced_total:,.2f} ريال")
811
-
812
- with col3:
813
- diff = unbalanced_total - original_total
814
- st.metric("الفرق", f"{diff:,.2f} ريال", delta=f"{diff/original_total*100:.1f}%")
815
-
816
- # المعايرة للحفاظ على إجمالي التسعير
817
- if abs(diff) > 1: # إذا كان هناك فرق كبير
818
- if st.button("معايرة الأسعار للحفاظ على إجمالي التسعير"):
819
- # تعديل الأسعار للحفاظ على إجمالي التكلفة
820
- adjustment_factor = original_total / unbalanced_total
821
- items['سعر الوحدة'] = items['سعر الوحدة'] * adjustment_factor
822
- items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
823
-
824
- st.success(f"تم تعديل الأسعار للحفاظ على إجمالي التسعير الأصلي ({original_total:,.2f} ريال)")
825
- st.dataframe(items, use_container_width=True)
826
-
827
- # رسم بياني للمقارنة
828
- st.markdown("### تحليل بصري للتسعير غير المتوازن")
829
-
830
- # إعداد البيانات للرسم البياني
831
- chart_data = pd.DataFrame({
832
- 'وصف البند': original_items['وصف البند'],
833
- 'التسعير المتوازن': original_items['الإجمالي'],
834
- 'التسعير غير المتوازن': items['الإجمالي']
835
- })
836
-
837
- # رسم بياني شريطي للمقارنة
838
- fig = go.Figure()
839
-
840
- fig.add_trace(go.Bar(
841
- x=chart_data['وصف البند'],
842
- y=chart_data['التسعير المتوازن'],
843
- name='التسعير المتوازن',
844
- marker_color='rgb(55, 83, 109)'
845
- ))
846
-
847
- fig.add_trace(go.Bar(
848
- x=chart_data['وصف البند'],
849
- y=chart_data['التسعير غير المتوازن'],
850
- name='التسعير غير المتوازن',
851
- marker_color='rgb(26, 118, 255)'
852
- ))
853
-
854
- fig.update_layout(
855
- title='مقارنة بين التسعير المتوازن وغير المتوازن',
856
- xaxis_tickfont_size=14,
857
- yaxis=dict(
858
- title='الإجمالي (ريال)',
859
- titlefont_size=16,
860
- tickfont_size=14,
861
- ),
862
- legend=dict(
863
- x=0,
864
- y=1.0,
865
- bgcolor='rgba(255, 255, 255, 0)',
866
- bordercolor='rgba(255, 255, 255, 0)'
867
- ),
868
- barmode='group',
869
- bargap=0.15,
870
- bargroupgap=0.1
871
- )
872
-
873
- st.plotly_chart(fig, use_container_width=True)
874
-
875
- # زر حفظ التسعير غير المتوازن
876
- if st.button("حفظ التسعير غير المتوازن"):
877
- st.session_state.current_pricing['items'] = items.copy()
878
- st.session_state.current_pricing['method'] = "التسعير غير المتزن"
879
- st.success("تم حفظ التسعير غير المتوازن بنجاح!")
880
-
881
- def _render_local_content_tab(self):
882
- """عرض تبويب المحتوى المحلي"""
883
-
884
- st.markdown("### تحليل المحتوى المحلي")
885
-
886
- # التحقق من وجود تسعير حالي
887
- if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
888
- st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
889
- return
890
-
891
- # شرح المحتوى المحلي
892
- with st.expander("ما هو المحتوى المحلي؟", expanded=False):
893
- st.markdown("""
894
- **المحتوى المحلي** هو نسبة المنتجات والخدمات والقوى العاملة المحلية المستخدمة في المشروع. يهدف إلى زيادة مساهمة المنتجات والخدمات المحلية في المشاريع.
895
-
896
- ### مكونات المحتوى المحلي:
897
-
898
- 1. **المنتجات**: المنتجات والمواد المصنعة محلياً.
899
- 2. **الخدمات**: الخدمات المقدمة من شركات محلية.
900
- 3. **القوى العاملة**: العمالة والكوادر الفنية والإدارية المحلية.
901
-
902
- ### أهمية المحتوى المحلي:
903
-
904
- - تعزيز الاقتصاد المحلي وخلق فرص عمل.
905
- - تحقيق أهداف رؤية 2030 في زيادة المحتوى المحلي.
906
- - التأهل للمشاريع الحكومية التي تتطلب نسبة محتوى محلي محددة.
907
- - الحصول على حوافز وأفضلية في المناقصات الحكومية.
908
-
909
- ### متطلبات المحتوى المحلي:
910
-
911
- - نسبة المحتوى المحلي للقوى العاملة: 80%
912
- - نسبة المحتوى المحلي للمنتجات: 70%
913
- - نسبة المحتوى المحلي للخدمات: 60%
914
- """)
915
-
916
- # عرض لوحة إدخال بيانات المحتوى المحلي
917
- st.markdown("### بيانات المحتوى المحلي")
918
-
919
- # التبويبات لأنواع المحتوى المحلي
920
- lc_tabs = st.tabs(["المنتجات", "الخدمات", "القوى العاملة", "التحليل"])
921
-
922
- with lc_tabs[0]: # المنتجات
923
- st.markdown("#### بيانات المنتجات")
924
-
925
- # إنشاء بيانات افتراضية للمنتجات إذا لم تكن موجودة
926
- if 'local_content_products' not in st.session_state:
927
- st.session_state.local_content_products = pd.DataFrame({
928
- 'المنتج': [
929
- "خرسانة مسلحة",
930
- "حديد تسليح",
931
- "بلوك خرساني",
932
- "عزل مائي",
933
- "دهانات"
934
- ],
935
- 'الكمية': [250, 25, 400, 500, 600],
936
- 'سعر_الوحدة': [1200, 6000, 200, 100, 50],
937
- 'التكلفة_الإجمالية': [300000, 150000, 80000, 50000, 30000],
938
- 'نسبة_المحتوى_المحلي': [0.95, 0.70, 0.98, 0.60, 0.80]
939
- })
940
-
941
- # حساب التكلفة الإجمالية
942
- st.session_state.local_content_products['التكلفة_الإجمالية'] = st.session_state.local_content_products['الكمية'] * st.session_state.local_content_products['سعر_الوحدة']
943
-
944
- # عرض جدول البنود مع إمكانية التعديل
945
- edited_products = st.data_editor(
946
- st.session_state.local_content_products,
947
- use_container_width=True,
948
- hide_index=True,
949
- num_rows="dynamic"
950
- )
951
- st.session_state.local_content_products = edited_products
952
-
953
- # عرض ملخص المنتجات
954
- total_products_cost = edited_products['التكلفة_الإجمالية'].sum()
955
- avg_local_content = (edited_products['التكلفة_الإجمالية'] * edited_products['نسبة_المحتوى_المحلي']).sum() / total_products_cost if total_products_cost > 0 else 0
956
-
957
- st.markdown(f"""
958
- **إجمالي تكلفة المنتجات**: {total_products_cost:,.2f} ريال
959
-
960
- **متوسط نسبة المحتوى المحلي للمنتجات**: {avg_local_content*100:.2f}%
961
-
962
- **المستهدف**: 70%
963
-
964
- **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.7 else "❌ غير ملتزم"}
965
- """)
966
-
967
- with lc_tabs[1]: # الخدمات
968
- st.markdown("#### بيانات الخدمات")
969
-
970
- # إنشاء بيانات افتراضية للخدمات إذا لم تكن موجودة
971
- if 'local_content_services' not in st.session_state:
972
- st.session_state.local_content_services = pd.DataFrame({
973
- 'الخدمة': [
974
- "تصميم معماري",
975
- "إشراف هندسي",
976
- "خدمات نقل",
977
- "خدمات أمن وسلامة",
978
- "صيانة ونظافة"
979
- ],
980
- 'التكلفة': [100000, 120000, 50000, 30000, 20000],
981
- 'نسبة_المحتوى_المحلي': [0.90, 0.85, 0.90, 0.95, 0.95]
982
- })
983
-
984
- # عرض جدول الخدمات مع إمكانية التعديل
985
- edited_services = st.data_editor(
986
- st.session_state.local_content_services,
987
- use_container_width=True,
988
- hide_index=True,
989
- num_rows="dynamic"
990
- )
991
- st.session_state.local_content_services = edited_services
992
-
993
- # عرض ملخص الخدمات
994
- total_services_cost = edited_services['التكلفة'].sum()
995
- avg_local_content = (edited_services['التكلفة'] * edited_services['نسبة_المحتوى_المحلي']).sum() / total_services_cost if total_services_cost > 0 else 0
996
-
997
- st.markdown(f"""
998
- **إجمالي تكلفة الخدمات**: {total_services_cost:,.2f} ريال
999
-
1000
- **متوسط نسبة المحتوى المحلي للخدمات**: {avg_local_content*100:.2f}%
1001
-
1002
- **المستهدف**: 60%
1003
-
1004
- **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.6 else "❌ غير ملتزم"}
1005
- """)
1006
-
1007
- with lc_tabs[2]: # القوى العاملة
1008
- st.markdown("#### بيانات القوى العاملة")
1009
-
1010
- # إنشاء بيانات افتراضية للقوى العاملة إذا لم تكن موجودة
1011
- if 'local_content_labor' not in st.session_state:
1012
- st.session_state.local_content_labor = pd.DataFrame({
1013
- 'فئة_العمالة': [
1014
- "مهندسون",
1015
- "فنيون",
1016
- "عمال بناء",
1017
- "إداريون",
1018
- "مشرفون"
1019
- ],
1020
- 'العدد': [5, 10, 30, 3, 4],
1021
- 'الراتب_الشهري': [15000, 8000, 3000, 10000, 12000],
1022
- 'المدة_بالأشهر': [12, 12, 12, 12, 12],
1023
- 'نسبة_المحتوى_المحلي': [0.75, 0.65, 0.60, 0.90, 0.80]
1024
- })
1025
-
1026
- # حساب التكلفة الإجمالية
1027
- st.session_state.local_content_labor['التكلفة_الإجمالية'] = st.session_state.local_content_labor['العدد'] * st.session_state.local_content_labor['الراتب_الشهري'] * st.session_state.local_content_labor['المدة_بالأشهر']
1028
-
1029
- # عرض جدول القوى العاملة مع إمكانية التعديل
1030
- edited_labor = st.data_editor(
1031
- st.session_state.local_content_labor,
1032
- use_container_width=True,
1033
- hide_index=True,
1034
- num_rows="dynamic"
1035
- )
1036
-
1037
- # إعادة حساب التكلفة الإجمالية بعد التعديل
1038
- edited_labor['التكلفة_الإجمالية'] = edited_labor['العدد'] * edited_labor['الراتب_الشهري'] * edited_labor['المدة_بالأشهر']
1039
- st.session_state.local_content_labor = edited_labor
1040
-
1041
- # عرض ملخص القوى العاملة
1042
- total_labor_cost = edited_labor['التكلفة_الإجمالية'].sum()
1043
- avg_local_content = (edited_labor['التكلفة_الإجمالية'] * edited_labor['نسبة_المحتوى_المحلي']).sum() / total_labor_cost if total_labor_cost > 0 else 0
1044
-
1045
- st.markdown(f"""
1046
- **إجمالي تكلفة القوى العاملة**: {total_labor_cost:,.2f} ريال
1047
-
1048
- **متوسط نسبة المحتوى المحلي للقوى العاملة**: {avg_local_content*100:.2f}%
1049
-
1050
- **المستهدف**: 80%
1051
-
1052
- **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.8 else "❌ غير ملتزم"}
1053
- """)
1054
-
1055
- with lc_tabs[3]: # التحليل
1056
- st.markdown("#### تحليل المحتوى المحلي")
1057
-
1058
- # حساب المحتوى المحلي الإجمالي
1059
- try:
1060
- # تجميع بيانات تحليل المحتوى المحلي
1061
- products_cost = st.session_state.local_content_products['التكلفة_الإجمالية'].sum()
1062
- products_local_content = (st.session_state.local_content_products['التكلفة_الإجمالية'] * st.session_state.local_content_products['نسبة_المحتوى_المحلي']).sum() / products_cost if products_cost > 0 else 0
1063
-
1064
- services_cost = st.session_state.local_content_services['التكلفة'].sum()
1065
- services_local_content = (st.session_state.local_content_services['التكلفة'] * st.session_state.local_content_services['نسبة_المحتوى_المحلي']).sum() / services_cost if services_cost > 0 else 0
1066
-
1067
- labor_cost = st.session_state.local_content_labor['التكلفة_الإجمالية'].sum()
1068
- labor_local_content = (st.session_state.local_content_labor['التكلفة_الإجمالية'] * st.session_state.local_content_labor['نسبة_المحتوى_المحلي']).sum() / labor_cost if labor_cost > 0 else 0
1069
-
1070
- # حساب الوزن النسبي لكل مكون
1071
- total_cost = products_cost + services_cost + labor_cost
1072
- products_weight = products_cost / total_cost if total_cost > 0 else 0
1073
- services_weight = services_cost / total_cost if total_cost > 0 else 0
1074
- labor_weight = labor_cost / total_cost if total_cost > 0 else 0
1075
-
1076
- # حساب المحتوى المحلي الإجمالي
1077
- total_local_content = (products_local_content * products_weight) + (services_local_content * services_weight) + (labor_local_content * labor_weight)
1078
-
1079
- # عرض ملخص المحتوى المحلي
1080
- st.markdown("### ملخص المحتوى المحلي")
1081
-
1082
- col1, col2, col3 = st.columns(3)
1083
-
1084
- with col1:
1085
- st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال")
1086
-
1087
- with col2:
1088
- st.metric("نسبة المحتوى المحلي الإجمالية", f"{total_local_content*100:.2f}%")
1089
-
1090
- with col3:
1091
- target_local_content = 0.7 # 70%
1092
- st.metric("الحالة", "ملتزم" if total_local_content >= target_local_content else "غير ملتزم", delta=f"{(total_local_content - target_local_content)*100:.2f}%")
1093
-
1094
- # عرض رسم بياني للمقارنة
1095
- st.markdown("### تحليل بصري للمحتوى المحلي")
1096
-
1097
- # رسم بياني شريطي لنسب المحتوى المحلي
1098
- categories = ['المنتجات', 'الخدمات', 'القوى العاملة', 'الإجمالي']
1099
- actual_values = [products_local_content * 100, services_local_content * 100, labor_local_content * 100, total_local_content * 100]
1100
- target_values = [70, 60, 80, 70] # المستهدفات
1101
-
1102
- # تهيئة البيانات للرسم البياني
1103
- chart_data = pd.DataFrame({
1104
- 'الفئة': categories,
1105
- 'النسبة الفعلية': actual_values,
1106
- 'النسبة المستهدفة': target_values
1107
- })
1108
-
1109
- # رسم بياني شريطي للمقارنة
1110
- fig = go.Figure()
1111
-
1112
- fig.add_trace(go.Bar(
1113
- x=chart_data['الفئة'],
1114
- y=chart_data['النسبة الفعلية'],
1115
- name='النسبة الفعلية',
1116
- marker_color='rgb(26, 118, 255)'
1117
- ))
1118
-
1119
- fig.add_trace(go.Bar(
1120
- x=chart_data['الفئة'],
1121
- y=chart_data['النسبة المستهدفة'],
1122
- name='النسبة المستهدفة',
1123
- marker_color='rgb(55, 83, 109)'
1124
- ))
1125
-
1126
- fig.update_layout(
1127
- title='مقارنة بين النسب الفعلية والمستهدفة للمحتوى المحلي',
1128
- xaxis_tickfont_size=14,
1129
- yaxis=dict(
1130
- title='النسبة %',
1131
- titlefont_size=16,
1132
- tickfont_size=14,
1133
- ),
1134
- legend=dict(
1135
- x=0,
1136
- y=1.0,
1137
- bgcolor='rgba(255, 255, 255, 0)',
1138
- bordercolor='rgba(255, 255, 255, 0)'
1139
- ),
1140
- barmode='group',
1141
- bargap=0.15,
1142
- bargroupgap=0.1
1143
- )
1144
-
1145
- st.plotly_chart(fig, use_container_width=True)
1146
-
1147
- # عرض توصيات لتحسين نسبة المحتوى المحلي
1148
- st.markdown("### توصيات لتحسين نسبة المحتوى المحلي")
1149
-
1150
- recommendations = []
1151
-
1152
- if products_local_content < 0.7:
1153
- recommendations.append("- زيادة نسبة المحتوى المحلي للمنتجات من خلال:")
1154
- recommendations.append(" - البحث عن موردين محليين للمنتجات ذات النسبة المنخفضة")
1155
- recommendations.append(" - استبدال المنتجات المستوردة ببدائل محلية")
1156
- recommendations.append(" - التعاون مع المصانع المحلية لتوطين صناعة المنتجات")
1157
-
1158
- if services_local_content < 0.6:
1159
- recommendations.append("- زيادة نسبة المحتوى المحلي للخدمات من خلال:")
1160
- recommendations.append(" - التعاقد مع شركات خدمات محلية")
1161
- recommendations.append(" - تحويل الخدمات المستعان بها من الخارج إلى شركات محلية")
1162
- recommendations.append(" - تأهيل الشركات المحلية لتقديم الخدمات المطلوبة")
1163
-
1164
- if labor_local_content < 0.8:
1165
- recommendations.append("- زيادة نسبة المحتوى المحلي للقوى العاملة من خلال:")
1166
- recommendations.append(" - زيادة توظيف الكوادر المحلية")
1167
- recommendations.append(" - تدريب وتأهيل العمالة المحلية")
1168
- recommendations.append(" - استبدال العمالة الأجنبية بكوادر محلية تدريجياً")
1169
-
1170
- if total_local_content < 0.7:
1171
- recommendations.append("- زيادة نسبة المحتوى المحلي الإجمالية من خلال:")
1172
- recommendations.append(" - إعادة توزيع الميزانية لصالح المكونات ذات النسبة العالية من المحتوى المحلي")
1173
- recommendations.append(" - وضع خطة مرحلية لزيادة المحتوى المحلي")
1174
- recommendations.append(" - التعاون مع اللجنة المحلية لزيادة المحتوى المحلي")
1175
-
1176
- if recommendations:
1177
- for rec in recommendations:
1178
- st.markdown(rec)
1179
- else:
1180
- st.success("تهانينا! نسبة المحتوى المحلي متوافقة مع المتطلبات.")
1181
-
1182
- # حساب تأثير المحتوى المحلي على التسعير
1183
- st.markdown("### تأثير المحتوى المحلي على التسعير")
1184
-
1185
- # تحديد عامل تعديل السعر بناءً على نسبة المحتوى المحلي
1186
- price_adjustment_factor = 1.0
1187
-
1188
- if total_local_content >= 0.9:
1189
- price_adjustment_factor = 0.92 # خصم 8% للمحتوى المحلي العالي جداً
1190
- price_discount = "8%"
1191
- elif total_local_content >= 0.8:
1192
- price_adjustment_factor = 0.94 # خصم 6% للمحتوى المحلي العالي
1193
- price_discount = "6%"
1194
- elif total_local_content >= 0.7:
1195
- price_adjustment_factor = 0.96 # خصم 4% للمحتوى المحلي المتوسط
1196
- price_discount = "4%"
1197
- elif total_local_content >= 0.6:
1198
- price_adjustment_factor = 0.98 # خصم 2% ��لمحتوى المحلي المنخفض
1199
- price_discount = "2%"
1200
- else:
1201
- price_adjustment_factor = 1.0 # لا خصم
1202
- price_discount = "0%"
1203
-
1204
- # عرض تأثير المحتوى المحلي على التسعير
1205
- original_total = st.session_state.current_pricing['items']['الإجمالي'].sum()
1206
- adjusted_total = original_total * price_adjustment_factor
1207
- discount_amount = original_total - adjusted_total
1208
-
1209
- col1, col2, col3 = st.columns(3)
1210
-
1211
- with col1:
1212
- st.metric("إجمالي التسعير الأصلي", f"{original_total:,.2f} ريال")
1213
-
1214
- with col2:
1215
- st.metric("نسبة الخصم بسبب المحتوى المحلي", price_discount)
1216
-
1217
- with col3:
1218
- st.metric("إجمالي التسعير بعد الخصم", f"{adjusted_total:,.2f} ريال", delta=f"-{discount_amount:,.2f} ريال")
1219
-
1220
- # أزرار العمليات
1221
- col1, col2 = st.columns(2)
1222
-
1223
- with col1:
1224
- if st.button("حفظ تحليل المحتوى المحلي"):
1225
- # حفظ بيانات المحتوى المحلي في التسعير الحالي
1226
- st.session_state.current_pricing['local_content'] = {
1227
- 'products': st.session_state.local_content_products.copy(),
1228
- 'services': st.session_state.local_content_services.copy(),
1229
- 'labor': st.session_state.local_content_labor.copy(),
1230
- 'total_local_content': total_local_content,
1231
- 'price_adjustment_factor': price_adjustment_factor
1232
- }
1233
-
1234
- st.success("تم حفظ تحليل المحتوى المحلي بنجاح!")
1235
-
1236
- with col2:
1237
- if st.button("تصدير تقرير المحتوى المحلي"):
1238
- st.success("تم تصدير تقرير المحتوى المحلي بنجاح!")
1239
-
1240
- except Exception as e:
1241
- st.error(f"حدث خطأ أثناء تحليل المحتوى المحلي: {str(e)}")
1242
- st.warning("تأكد من إدخال بيانات المحتوى المحلي بشكل صحيح في التبويبات السابقة.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/pricing_engine.py DELETED
@@ -1,430 +0,0 @@
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/pricing/services/construction_cost_calculator.py DELETED
@@ -1,1006 +0,0 @@
1
- """
2
- خدمة حاسبة تكاليف البناء
3
- تقوم هذه الخدمة بحساب تكاليف البناء بشكل تفصيلي بناءً على المكونات المختلفة:
4
- - المواد الخام
5
- - العمالة
6
- - المعدات
7
- - المصاريف الإدارية
8
- - هامش الربح
9
- """
10
-
11
- import pandas as pd
12
- import numpy as np
13
- from datetime import datetime
14
- import os
15
- import json
16
- import sys
17
- from typing import Dict, List, Optional, Union, Any
18
-
19
- # إضافة مسار النظام للوصول لملفات التكوين
20
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")))
21
- try:
22
- import config
23
- except ImportError:
24
- # إذا لم يتم العثور على ملف التكوين، نستخدم قيم افتراضية
25
- class DefaultConfig:
26
- DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../data"))
27
- config = DefaultConfig
28
-
29
- # إنشاء مجلد البيانات إذا لم يكن موجودًا
30
- if not os.path.exists(config.DATA_DIR):
31
- os.makedirs(config.DATA_DIR)
32
-
33
- class ConstructionCostCalculator:
34
- """خدمة حاسبة تكاليف البناء"""
35
-
36
- def __init__(self):
37
- """تهيئة حاسبة تكاليف البناء"""
38
- # تحميل بيانات المواد والأسعار المرجعية
39
- self.material_rates = self._load_material_rates()
40
- self.labor_rates = self._load_labor_rates()
41
- self.equipment_rates = self._load_equipment_rates()
42
-
43
- # النسب الافتراضية للمصاريف الإدارية وهامش الربح
44
- self.default_admin_expenses_percentage = 0.05 # 5%
45
- self.default_profit_margin_percentage = 0.10 # 10%
46
-
47
- # معاملات التعديل الافتراضية
48
- self.default_adjustment_factors = {
49
- 'location_factor': 1.0, # معامل الموقع
50
- 'time_factor': 1.0, # معامل الوقت
51
- 'risk_factor': 1.0, # معامل المخاطر
52
- 'market_factor': 1.0 # معامل السوق
53
- }
54
-
55
- def _load_material_rates(self) -> Dict[str, Dict[str, Any]]:
56
- """تحميل أسعار المواد"""
57
- # محاكاة تحميل البيانات من مصدر بيانات
58
- material_rates = {
59
- # مواد الخرسانة
60
- 'خرسانة جاهزة': {
61
- 'وحدة': 'م3',
62
- 'سعر_الوحدة': 750.0,
63
- 'وصف': 'خرسانة جاهزة بقوة 350 كجم/سم2',
64
- 'فئة': 'أعمال خرسانية'
65
- },
66
- 'حديد تسليح': {
67
- 'وحدة': 'طن',
68
- 'سعر_الوحدة': 5500.0,
69
- 'وصف': 'حديد تسليح قطر 8-32 مم',
70
- 'فئة': 'أعمال خرسانية'
71
- },
72
- 'أسمنت': {
73
- 'وحدة': 'كيس',
74
- 'سعر_الوحدة': 30.0,
75
- 'وصف': 'أسمنت بورتلاندي عادي',
76
- 'فئة': 'أعمال خرسانية'
77
- },
78
- 'رمل': {
79
- 'وحدة': 'م3',
80
- 'سعر_الوحدة': 120.0,
81
- 'وصف': 'رمل خشن للخرسانة',
82
- 'فئة': 'أعمال خرسانية'
83
- },
84
- 'زلط': {
85
- 'وحدة': 'م3',
86
- 'سعر_الوحدة': 150.0,
87
- 'وصف': 'زلط مقاس 10-20 مم للخرسانة',
88
- 'فئة': 'أعمال خرسانية'
89
- },
90
-
91
- # مواد البناء
92
- 'طوب أحمر': {
93
- 'وحدة': '1000 قطعة',
94
- 'سعر_الوحدة': 900.0,
95
- 'وصف': 'طوب أحمر مقاس 25×12×6 سم',
96
- 'فئة': 'أعمال بناء'
97
- },
98
- 'طوب أسمنتي': {
99
- 'وحدة': 'قطعة',
100
- 'سعر_الوحدة': 4.5,
101
- 'وصف': 'بلوك أسمنتي مقاس 20×20×40 سم',
102
- 'فئة': 'أعمال بناء'
103
- },
104
- 'مونة بناء': {
105
- 'وحدة': 'م3',
106
- 'سعر_الوحدة': 350.0,
107
- 'وصف': 'مونة أسمنتية للبناء',
108
- 'فئة': 'أعمال بناء'
109
- },
110
-
111
- # مواد التشطيبات
112
- 'بلاط سيراميك': {
113
- 'وحدة': 'م2',
114
- 'سعر_الوحدة': 120.0,
115
- 'وصف': 'بلاط سيراميك للأرضيات مقاس 40×40 سم',
116
- 'فئة': 'تشطيبات'
117
- },
118
- 'بلاط بورسلين': {
119
- 'وحدة': 'م2',
120
- 'سعر_الوحدة': 180.0,
121
- 'وصف': 'بلاط بورسلين للأرضيات مقاس 60��60 سم',
122
- 'فئة': 'تشطيبات'
123
- },
124
- 'دهانات بلاستيك': {
125
- 'وحدة': 'لتر',
126
- 'سعر_الوحدة': 35.0,
127
- 'وصف': 'دهان بلاستيك أساس وتشطيب',
128
- 'فئة': 'تشطيبات'
129
- },
130
- 'جبس بورد': {
131
- 'وحدة': 'م2',
132
- 'سعر_الوحدة': 95.0,
133
- 'وصف': 'ألواح جبس بورد سمك 12 مم',
134
- 'فئة': 'تشطيبات'
135
- },
136
-
137
- # مواد العزل
138
- 'عزل مائي': {
139
- 'وحدة': 'م2',
140
- 'سعر_الوحدة': 45.0,
141
- 'وصف': 'عزل مائي من البيتومين المؤكسد',
142
- 'فئة': 'أعمال عزل'
143
- },
144
- 'عزل حراري': {
145
- 'وحدة': 'م2',
146
- 'سعر_الوحدة': 65.0,
147
- 'وصف': 'ألواح عزل حراري من البوليسترين سمك 5 سم',
148
- 'فئة': 'أعمال عزل'
149
- }
150
- }
151
-
152
- # محاولة تحميل البيانات من ملف إذا كان متاحًا
153
- try:
154
- file_path = os.path.join(config.DATA_DIR, 'material_rates.json')
155
- if os.path.exists(file_path):
156
- with open(file_path, 'r', encoding='utf-8') as f:
157
- loaded_data = json.load(f)
158
- material_rates.update(loaded_data)
159
- except Exception as e:
160
- print(f"خطأ في تحميل بيانات أسعار المواد: {str(e)}")
161
-
162
- return material_rates
163
-
164
- def _load_labor_rates(self) -> Dict[str, Dict[str, Any]]:
165
- """تحميل أسعار العمالة"""
166
- # محاكاة تحميل البيانات من مصدر بيانات
167
- labor_rates = {
168
- # عمالة الخرسانات
169
- 'نجار مسلح': {
170
- 'وحدة': 'يوم',
171
- 'سعر_الوحدة': 250.0,
172
- 'وصف': 'نجار مسلح لأعمال الشدات والفرم',
173
- 'فئة': 'أعمال خرسانية',
174
- 'إنتاجية_يومية': {
175
- 'شدة أساسات': 12, # متر مربع
176
- 'شدة أعمدة': 10, # متر مربع
177
- 'شدة أسقف': 12 # متر مربع
178
- }
179
- },
180
- 'حداد مسلح': {
181
- 'وحدة': 'يوم',
182
- 'سعر_الوحدة': 250.0,
183
- 'وصف': 'حداد مسلح لأعمال حديد التسليح',
184
- 'فئة': 'أعمال خرسانية',
185
- 'إنتاجية_يومية': {
186
- 'تجهيز وتركيب حديد أساسات': 700, # كجم
187
- 'تجهيز وتركيب حديد أعمدة': 600, # كجم
188
- 'تجهيز وتركيب حديد أسقف': 650 # كجم
189
- }
190
- },
191
- 'عامل خرسانة': {
192
- 'وحدة': 'يوم',
193
- 'سعر_الوحدة': 150.0,
194
- 'وصف': 'عامل لصب وتسوية الخرسانة',
195
- 'فئة': 'أعمال خرسانية',
196
- 'إنتاجية_يومية': {
197
- 'صب خرسانة': 15 # متر مكعب
198
- }
199
- },
200
-
201
- # عمالة البناء
202
- 'بناء': {
203
- 'وحدة': 'يوم',
204
- 'سعر_الوحدة': 200.0,
205
- 'وصف': 'عامل بناء للطوب والبلوك',
206
- 'فئة': 'أعمال بناء',
207
- 'إنتاجية_يومية': {
208
- 'بناء طوب أحمر': 500, # قطعة
209
- 'بناء بلوك أسمنتي': 80 # قطعة
210
- }
211
- },
212
- 'مساعد بناء': {
213
- 'وحدة': 'يوم',
214
- 'سعر_الوحدة': 120.0,
215
- 'وصف': 'مساعد عامل بناء',
216
- 'فئة': 'أعمال بناء',
217
- 'إنتاجية_يومية': {}
218
- },
219
-
220
- # عمالة التشطيبات
221
- 'مبلط': {
222
- 'وحدة': 'يوم',
223
- 'سعر_الوحدة': 250.0,
224
- 'وصف': 'عامل تركيب بلاط وسيراميك',
225
- 'فئة': 'تشطيبات',
226
- 'إنتاجية_يومية': {
227
- 'تركيب سيراميك أرضيات': 15, # متر مربع
228
- 'تركيب سيراميك حوائط': 12, # متر مربع
229
- 'تركيب بورسلين': 12 # متر مربع
230
- }
231
- },
232
- 'نقاش': {
233
- 'وحدة': 'يوم',
234
- 'سعر_الوحدة': 200.0,
235
- 'وصف': 'عامل دهانات',
236
- 'فئة': 'تشطيبات',
237
- 'إنتاجية_يومية': {
238
- 'دهانات بلاستيك': 35, # متر مربع
239
- 'دهانات زيتية': 25 # متر مربع
240
- }
241
- },
242
- 'كهربائي': {
243
- 'وحدة': 'يوم',
244
- 'سعر_الوحدة': 270.0,
245
- 'وصف': 'فني كهرباء',
246
- 'فئة': 'تشطيبات',
247
- 'إنتاجية_يومية': {
248
- 'تأسيس نقاط كهرباء': 15, # نقطة
249
- 'تركيب لوحات توزيع': 2 # لوحة
250
- }
251
- },
252
- 'سباك': {
253
- 'وحدة': 'يوم',
254
- 'سعر_الوحدة': 250.0,
255
- 'وصف': 'فني سباكة',
256
- 'فئة': 'تشطيبات',
257
- 'إنتاجية_يومية': {
258
- 'تأسيس نقاط صرف': 8, # نقطة
259
- 'تأسيس نقاط تغذية': 10, # نقطة
260
- 'تركيب أطقم حمامات': 2 # طقم
261
- }
262
- },
263
-
264
- # مراقبة وإشراف
265
- 'مهندس موقع': {
266
- 'وحدة': 'يوم',
267
- 'سعر_الوحدة': 500.0,
268
- 'وصف': 'مهندس إشراف موقع',
269
- 'فئة': 'إشراف'
270
- },
271
- 'مراقب فني': {
272
- 'وحدة': 'يوم',
273
- 'سعر_الوحدة': 300.0,
274
- 'وصف': 'مراقب فني للتنفيذ',
275
- 'فئة': 'إشراف'
276
- }
277
- }
278
-
279
- # محاولة تحميل البيانات من ملف إذا كان متاحًا
280
- try:
281
- file_path = os.path.join(config.DATA_DIR, 'labor_rates.json')
282
- if os.path.exists(file_path):
283
- with open(file_path, 'r', encoding='utf-8') as f:
284
- loaded_data = json.load(f)
285
- labor_rates.update(loaded_data)
286
- except Exception as e:
287
- print(f"خطأ في تحميل بيانات أسعار العمالة: {str(e)}")
288
-
289
- return labor_rates
290
-
291
- def _load_equipment_rates(self) -> Dict[str, Dict[str, Any]]:
292
- """تحميل أسعار المعدات"""
293
- # محاكاة تحميل البيانات من مصدر بيانات
294
- equipment_rates = {
295
- # معدات الحفر والتسوية
296
- 'حفار صغير': {
297
- 'وحدة': 'يوم',
298
- 'سعر_الوحدة': 1200.0,
299
- 'وصف': 'حفار صغير (بوبكات) بقدرة 70 حصان',
300
- 'فئة': 'معدات حفر',
301
- 'إنتاجية_يومية': {
302
- 'حفر في تربة عادية': 60 # متر مكعب
303
- }
304
- },
305
- 'حفار متوسط': {
306
- 'وحدة': 'يوم',
307
- 'سعر_الوحدة': 2500.0,
308
- 'وصف': 'حفار متوسط الحجم بقدرة 150 حصان',
309
- 'فئة': 'معدات حفر',
310
- 'إنتاجية_يومية': {
311
- 'حفر في تربة عادية': 200 # متر مكعب
312
- }
313
- },
314
- 'لودر': {
315
- 'وحدة': 'يوم',
316
- 'سعر_الوحدة': 2000.0,
317
- 'وصف': 'لودر أمامي لنقل التربة',
318
- 'فئة': 'معدات حفر',
319
- 'إنتاجية_يومية': {
320
- 'تحميل تربة': 300, # متر مكعب
321
- 'تسوية موقع': 1500 # متر مربع
322
- }
323
- },
324
- 'جريدر': {
325
- 'وحدة': 'يوم',
326
- 'سعر_الوحدة': 2200.0,
327
- 'وصف': 'جريدر لتسوية الموقع',
328
- 'فئة': 'معدات حفر',
329
- 'إنتاجية_يومية': {
330
- 'تسوية طرق': 3000 # متر مربع
331
- }
332
- },
333
-
334
- # معدات الخرسانة
335
- 'خلاطة خرسانة': {
336
- 'وحدة': 'يوم',
337
- 'سعر_الوحدة': 350.0,
338
- 'وصف': 'خلاطة خرسانة بسعة 0.5 متر مكعب',
339
- 'فئة': 'معدات خرسانة',
340
- 'إنتاجية_يومية': {
341
- 'خلط خرسانة': 15 # متر مكعب
342
- }
343
- },
344
- 'هزاز خرسانة': {
345
- 'وحدة': 'يوم',
346
- 'سعر_الوحدة': 150.0,
347
- 'وصف': 'هزاز خرسانة كهربائي',
348
- 'فئة': 'معدات خرسانة',
349
- 'إنتاجية_يومية': {
350
- 'دمك خرسانة': 40 # متر مكعب
351
- }
352
- },
353
- 'شاحنة خرسانة جاهزة': {
354
- 'وحدة': 'يوم',
355
- 'سعر_الوحدة': 3000.0,
356
- 'وصف': 'شاحنة خرسانة جاهزة (مكسر) سعة 8 متر مكعب',
357
- 'فئة': 'معدات خرسانة',
358
- 'إنتاجية_يومية': {
359
- 'نقل وصب خرسانة': 50 # متر مكعب
360
- }
361
- },
362
- 'مضخة خرسانة': {
363
- 'وحدة': 'يوم',
364
- 'سعر_الوحدة': 5000.0,
365
- 'وصف': 'مضخة خرسانة بذراع 42 متر',
366
- 'فئة': 'معدات خرسانة',
367
- 'إنتاجية_يومية': {
368
- 'ضخ خرسانة': 120 # متر مكعب
369
- }
370
- },
371
-
372
- # معدات رفع ونقل
373
- 'رافعة برجية': {
374
- 'وحدة': 'شهر',
375
- 'سعر_الوحدة': 35000.0,
376
- 'وصف': 'رافعة برجية بارتفاع 40 متر',
377
- 'فئة': 'معدات رفع',
378
- },
379
- 'ونش شوكة': {
380
- 'وحدة': 'يوم',
381
- 'سعر_الوحدة': 1500.0,
382
- 'وصف': 'ونش شوكة لرفع مواد البناء',
383
- 'فئة': 'معدات رفع',
384
- 'إنتاجية_يومية': {
385
- 'رفع ونقل مواد': 100 # طن
386
- }
387
- },
388
- 'شاحنة نقل': {
389
- 'وحدة': 'يوم',
390
- 'سعر_الوحدة': 1200.0,
391
- 'وصف': 'شاحنة نقل حمولة 20 طن',
392
- 'فئة': 'معدات نقل',
393
- 'إنتاجية_يومية': {
394
- 'نقل مواد': 80 # طن
395
- }
396
- }
397
- }
398
-
399
- # محاولة تحميل البيانات من ملف إذا كان متاحًا
400
- try:
401
- file_path = os.path.join(config.DATA_DIR, 'equipment_rates.json')
402
- if os.path.exists(file_path):
403
- with open(file_path, 'r', encoding='utf-8') as f:
404
- loaded_data = json.load(f)
405
- equipment_rates.update(loaded_data)
406
- except Exception as e:
407
- print(f"خطأ في تحميل بيانات أسعار المعدات: {str(e)}")
408
-
409
- return equipment_rates
410
-
411
- def calculate_item_cost(self, item_data: Dict[str, Any]) -> Dict[str, Any]:
412
- """
413
- حساب تكلفة بند محدد بكافة مكوناته
414
-
415
- المعلمات:
416
- item_data (dict): بيانات البند، تتضمن:
417
- - وصف_البند (str): وصف البند
418
- - الكمية (float): كمية البند
419
- - الوحدة (str): وحدة القياس
420
- - المواد (list): قائمة المواد المستخدمة وكمياتها
421
- - العمالة (list): قائمة العمالة المستخدمة وعددها
422
- - المعدات (list): قائمة المعدات المستخدمة وساعات عملها
423
- - المصاريف_الإدارية (float, optional): نسبة المصاريف الإدارية (افتراضياً 5%)
424
- - هامش_الربح (float, optional): نسبة هامش الربح (افتراضياً 10%)
425
- - عوامل_التعديل (dict, optional): عوامل تعديل التكلفة
426
-
427
- العوائد:
428
- dict: تفاصيل تكلفة البند بكافة عناصرها
429
- """
430
- # استخراج البيانات الأساسية للبند
431
- item_description = item_data.get('وصف_البند', 'بند غير محدد')
432
- quantity = item_data.get('الكمية', 0.0)
433
- unit = item_data.get('الوحدة', 'وحدة')
434
-
435
- # حساب تكلفة المواد
436
- materials_cost = self._calculate_materials_cost(item_data.get('المواد', []))
437
-
438
- # حساب تكلفة العمالة
439
- labor_cost = self._calculate_labor_cost(item_data.get('العمالة', []))
440
-
441
- # حساب تكلفة المعدات
442
- equipment_cost = self._calculate_equipment_cost(item_data.get('المعدات', []))
443
-
444
- # حساب التكلفة المباشرة الإجمالية
445
- direct_cost = materials_cost['الإجمالي'] + labor_cost['الإجمالي'] + equipment_cost['الإجمالي']
446
-
447
- # حساب المصاريف الإدارية
448
- admin_percentage = item_data.get('المصاريف_الإدارية', self.default_admin_expenses_percentage)
449
- admin_cost = direct_cost * admin_percentage
450
-
451
- # حساب هامش الربح
452
- profit_percentage = item_data.get('هامش_الربح', self.default_profit_margin_percentage)
453
- profit_margin = (direct_cost + admin_cost) * profit_percentage
454
-
455
- # حساب التكلفة الإجمالية
456
- total_cost = direct_cost + admin_cost + profit_margin
457
-
458
- # حساب سعر الوحدة
459
- unit_price = total_cost / quantity if quantity > 0 else 0.0
460
-
461
- # تطبيق عوامل التعديل إذا وجدت
462
- adjustment_factors = item_data.get('عوامل_التعديل', self.default_adjustment_factors)
463
- adjustment_factor = self._calculate_adjustment_factor(adjustment_factors)
464
-
465
- adjusted_unit_price = unit_price * adjustment_factor
466
- adjusted_total_cost = total_cost * adjustment_factor
467
-
468
- # إعداد النتائج
469
- result = {
470
- 'وصف_البند': item_description,
471
- 'الكمية': quantity,
472
- 'الوحدة': unit,
473
- 'تكاليف_مباشرة': {
474
- 'المواد': materials_cost,
475
- 'العمالة': labor_cost,
476
- 'المعدات': equipment_cost,
477
- 'إجمالي_تكاليف_مباشرة': direct_cost
478
- },
479
- 'مصاريف_إدارية': {
480
- 'نسبة': admin_percentage * 100,
481
- 'قيمة': admin_cost
482
- },
483
- 'هامش_ربح': {
484
- 'نسبة': profit_percentage * 100,
485
- 'قيمة': profit_margin
486
- },
487
- 'التكلفة_الإجمالية': total_cost,
488
- 'سعر_الوحدة': unit_price,
489
- 'عوامل_التعديل': {
490
- 'المعامل_الإجمالي': adjustment_factor,
491
- 'التفاصيل': adjustment_factors
492
- },
493
- 'السعر_المعدل': {
494
- 'سعر_الوحدة': adjusted_unit_price,
495
- 'إجمالي': adjusted_total_cost
496
- }
497
- }
498
-
499
- return result
500
-
501
- def _calculate_materials_cost(self, materials: List[Dict[str, Any]]) -> Dict[str, Any]:
502
- """
503
- حساب تكلفة المواد
504
-
505
- المعلمات:
506
- materials (list): قائمة المواد المستخدمة وكمياتها
507
- - الاسم (str): اسم المادة
508
- - الكمية (float): الكمية المستخدمة
509
- - الوحدة (str, optional): وحدة القياس
510
- - سعر_الوحدة (float, optional): سعر الوحدة (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده)
511
-
512
- العوائد:
513
- dict: تفاصيل تكلفة المواد
514
- """
515
- materials_details = []
516
- total_cost = 0.0
517
-
518
- for material in materials:
519
- material_name = material.get('الاسم', '')
520
- quantity = material.get('الكمية', 0.0)
521
-
522
- # البحث عن سعر المادة من البيانات المرجعية إذا لم يتم تحديده
523
- if 'سعر_الوحدة' in material:
524
- unit_price = material.get('سعر_الوحدة', 0.0)
525
- unit = material.get('الوحدة', 'وحدة')
526
- elif material_name in self.material_rates:
527
- ref_material = self.material_rates[material_name]
528
- unit_price = ref_material.get('سعر_الوحدة', 0.0)
529
- unit = ref_material.get('وحدة', 'وحدة')
530
- else:
531
- unit_price = 0.0
532
- unit = material.get('الوحدة', 'وحدة')
533
-
534
- # حساب التكلفة
535
- cost = quantity * unit_price
536
- total_cost += cost
537
-
538
- # إضافة التفاصيل
539
- materials_details.append({
540
- 'الاسم': material_name,
541
- 'الكمية': quantity,
542
- 'الوحدة': unit,
543
- 'سعر_الوحدة': unit_price,
544
- 'التكلفة': cost
545
- })
546
-
547
- return {
548
- 'التفاصيل': materials_details,
549
- 'الإجمالي': total_cost
550
- }
551
-
552
- def _calculate_labor_cost(self, labor: List[Dict[str, Any]]) -> Dict[str, Any]:
553
- """
554
- حساب تكلفة العمالة
555
-
556
- المعلمات:
557
- labor (list): قائمة العمالة المستخدمة وعددها
558
- - النوع (str): نوع العامل
559
- - العدد (int): عدد العمال
560
- - المدة (float): مدة العمل بالأيام
561
- - سعر_اليوم (float, optional): أجر اليوم (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده)
562
-
563
- العوائد:
564
- dict: تفاصيل تكلفة العمالة
565
- """
566
- labor_details = []
567
- total_cost = 0.0
568
-
569
- for worker in labor:
570
- worker_type = worker.get('النوع', '')
571
- count = worker.get('العدد', 0)
572
- duration = worker.get('المدة', 0.0)
573
-
574
- # البحث عن سعر العامل من البيانات المرجعية إذا لم يتم تحديده
575
- if 'سعر_اليوم' in worker:
576
- daily_rate = worker.get('سعر_اليوم', 0.0)
577
- elif worker_type in self.labor_rates:
578
- daily_rate = self.labor_rates[worker_type].get('سعر_الوحدة', 0.0)
579
- else:
580
- daily_rate = 0.0
581
-
582
- # حساب التكلفة
583
- cost = count * duration * daily_rate
584
- total_cost += cost
585
-
586
- # إضافة التفاصيل
587
- labor_details.append({
588
- 'النوع': worker_type,
589
- 'العدد': count,
590
- 'المدة': duration,
591
- 'سعر_اليوم': daily_rate,
592
- 'التكلفة': cost
593
- })
594
-
595
- return {
596
- 'التفاصيل': labor_details,
597
- 'الإجمالي': total_cost
598
- }
599
-
600
- def _calculate_equipment_cost(self, equipment: List[Dict[str, Any]]) -> Dict[str, Any]:
601
- """
602
- حساب تكلفة المعدات
603
-
604
- المعلمات:
605
- equipment (list): قائمة المعدات المستخدمة وساعات عملها
606
- - النوع (str): نوع المعدة
607
- - العدد (int): عدد المعدات
608
- - المدة (float): مدة الاستخدام بالأيام
609
- - سعر_اليوم (float, optional): أجر اليوم (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده)
610
-
611
- العوائد:
612
- dict: تفاصيل تكلفة المعدات
613
- """
614
- equipment_details = []
615
- total_cost = 0.0
616
-
617
- for equip in equipment:
618
- equip_type = equip.get('النوع', '')
619
- count = equip.get('العدد', 0)
620
- duration = equip.get('المدة', 0.0)
621
-
622
- # البحث عن سعر المعدة من البيانات المرجعية إذا لم يتم تحديده
623
- if 'سعر_اليوم' in equip:
624
- daily_rate = equip.get('سعر_اليوم', 0.0)
625
- elif equip_type in self.equipment_rates:
626
- daily_rate = self.equipment_rates[equip_type].get('سعر_الوحدة', 0.0)
627
- else:
628
- daily_rate = 0.0
629
-
630
- # حساب التكلفة
631
- cost = count * duration * daily_rate
632
- total_cost += cost
633
-
634
- # إضافة التفاصيل
635
- equipment_details.append({
636
- 'النوع': equip_type,
637
- 'العدد': count,
638
- 'المدة': duration,
639
- 'سعر_اليوم': daily_rate,
640
- 'التكلفة': cost
641
- })
642
-
643
- return {
644
- 'التفاصيل': equipment_details,
645
- 'الإجمالي': total_cost
646
- }
647
-
648
- def _calculate_adjustment_factor(self, factors: Dict[str, float]) -> float:
649
- """
650
- حساب المعامل الإجمالي لتعديل التكلفة
651
-
652
- المعلمات:
653
- factors (dict): عوامل التعديل
654
-
655
- العوائد:
656
- float: المعامل الإجمالي
657
- """
658
- # دمج العوامل المحددة مع العوامل الافتراضية
659
- effective_factors = self.default_adjustment_factors.copy()
660
- effective_factors.update(factors)
661
-
662
- # حساب المعامل الإجمالي
663
- total_factor = 1.0
664
- for factor in effective_factors.values():
665
- total_factor *= factor
666
-
667
- return total_factor
668
-
669
- def calculate_project_cost(self, project_data: Dict[str, Any]) -> Dict[str, Any]:
670
- """
671
- حساب التكلفة الإجمالية لمشروع بناء كامل
672
-
673
- المعلمات:
674
- project_data (dict): بيانات المشروع، تتضمن:
675
- - اسم_المشروع (str): اسم المشروع
676
- - وصف_المشروع (str): وصف المشروع
677
- - البنود (list): قائمة بنود المشروع
678
- - المصاريف_الإدارية (float, optional): نسبة المصاريف الإدارية الإجمالية (افتراضياً 5%)
679
- - هامش_الربح (float, optional): نسبة هامش الربح الإجمالي (افتراضياً 10%)
680
- - عوامل_التعديل (dict, optional): عوامل تعديل التكلفة للمشروع
681
-
682
- العوائد:
683
- dict: تفاصيل تكلفة المشروع بكافة عناصرها
684
- """
685
- # استخراج البيانات الأساسية للمشروع
686
- project_name = project_data.get('اسم_المشروع', 'مشروع غير محدد')
687
- project_description = project_data.get('وصف_المشروع', '')
688
- items = project_data.get('البنود', [])
689
-
690
- # استخراج النسب الإجمالية
691
- admin_percentage = project_data.get('المصاريف_الإدارية', self.default_admin_expenses_percentage)
692
- profit_percentage = project_data.get('هامش_الربح', self.default_profit_margin_percentage)
693
-
694
- # حساب تكلفة كل بند
695
- items_costs = []
696
- total_direct_cost = 0.0
697
- total_materials_cost = 0.0
698
- total_labor_cost = 0.0
699
- total_equipment_cost = 0.0
700
-
701
- for item_data in items:
702
- # تحديث نسب المصاريف والربح للبند إذا لم تكن محددة
703
- if 'المصاريف_الإدارية' not in item_data:
704
- item_data['المصاريف_الإدارية'] = admin_percentage
705
-
706
- if 'هامش_الربح' not in item_data:
707
- item_data['هامش_الربح'] = profit_percentage
708
-
709
- # حساب تكلفة البند
710
- item_cost = self.calculate_item_cost(item_data)
711
- items_costs.append(item_cost)
712
-
713
- # تحديث الإجماليات
714
- total_materials_cost += item_cost['تكاليف_مباشرة']['المواد']['الإجمالي']
715
- total_labor_cost += item_cost['تكاليف_مباشرة']['العمالة']['الإجمالي']
716
- total_equipment_cost += item_cost['تكاليف_مباشرة']['المعدات']['الإجمالي']
717
- total_direct_cost += item_cost['تكاليف_مباشرة']['إجمالي_تكاليف_مباشرة']
718
-
719
- # حساب المصاريف الإدارية
720
- admin_cost = total_direct_cost * admin_percentage
721
-
722
- # حساب هامش الربح
723
- profit_margin = (total_direct_cost + admin_cost) * profit_percentage
724
-
725
- # حساب التكلفة الإجمالية
726
- total_cost = total_direct_cost + admin_cost + profit_margin
727
-
728
- # تطبيق عوامل التعديل إذا وجدت
729
- adjustment_factors = project_data.get('عوامل_التعديل', self.default_adjustment_factors)
730
- adjustment_factor = self._calculate_adjustment_factor(adjustment_factors)
731
-
732
- adjusted_total_cost = total_cost * adjustment_factor
733
-
734
- # إعداد النتائج
735
- result = {
736
- 'اسم_المشروع': project_name,
737
- 'وصف_المشروع': project_description,
738
- 'تكاليف_مباشرة': {
739
- 'المواد': {
740
- 'الإجمالي': total_materials_cost,
741
- 'النسبة_المئوية': (total_materials_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0
742
- },
743
- 'العمالة': {
744
- 'الإجمالي': total_labor_cost,
745
- 'النسبة_المئوية': (total_labor_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0
746
- },
747
- 'المعدات': {
748
- 'الإجمالي': total_equipment_cost,
749
- 'النسبة_المئوية': (total_equipment_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0
750
- },
751
- 'إجمالي_تكاليف_مباشرة': total_direct_cost
752
- },
753
- 'مصاريف_إدارية': {
754
- 'نسبة': admin_percentage * 100,
755
- 'قيمة': admin_cost
756
- },
757
- 'هامش_ربح': {
758
- 'نسبة': profit_percentage * 100,
759
- 'قيمة': profit_margin
760
- },
761
- 'التكلفة_الإجمالية': total_cost,
762
- 'عوامل_التعديل': {
763
- 'المعامل_الإجمالي': adjustment_factor,
764
- 'التفاصيل': adjustment_factors
765
- },
766
- 'التكلفة_النهائية_المعدلة': adjusted_total_cost,
767
- 'تفاصيل_البنود': items_costs,
768
- 'عدد_البنود': len(items)
769
- }
770
-
771
- return result
772
-
773
- def get_rate_info(self, item_type: str, item_name: str) -> Dict[str, Any]:
774
- """
775
- الحصول على معلومات تفصيلية عن معدل وسعر عنصر محدد (مادة، عمالة، معدة)
776
-
777
- المعلمات:
778
- item_type (str): نوع العنصر - 'مادة'، 'عمالة'، 'معدة'
779
- item_name (str): اسم العنصر
780
-
781
- العوائد:
782
- dict: معلومات تفصيلية عن العنصر
783
- """
784
- # تحديد القاموس المناسب حسب نوع العنصر
785
- if item_type == 'مادة':
786
- rates_dict = self.material_rates
787
- elif item_type == 'عمالة':
788
- rates_dict = self.labor_rates
789
- elif item_type == 'معدة':
790
- rates_dict = self.equipment_rates
791
- else:
792
- return {'خطأ': 'نوع العنصر غير صحيح'}
793
-
794
- # البحث عن العنصر في القاموس
795
- if item_name in rates_dict:
796
- return rates_dict[item_name]
797
- else:
798
- return {'خطأ': 'العنصر غير موجود'}
799
-
800
- def get_all_rates(self, item_type: str = None, category: str = None) -> Dict[str, Any]:
801
- """
802
- الحصول على قوائم معدلات الأسعار (لجميع المواد أو العمالة أو المعدات)
803
-
804
- المعلمات:
805
- item_type (str, optional): نوع العنصر - 'مادة'، 'عمالة'، 'معدة'، أو None لجميع الأنواع
806
- category (str, optional): فئة محددة للتصفية
807
-
808
- العوائد:
809
- dict: قوائم معدلات الأسعار
810
- """
811
- result = {}
812
-
813
- # جمع المواد حسب الفئة
814
- if item_type is None or item_type == 'مادة':
815
- materials = {}
816
- for name, info in self.material_rates.items():
817
- if category is None or info.get('فئة') == category:
818
- materials[name] = info
819
- result['المواد'] = materials
820
-
821
- # جمع العمالة حسب الفئة
822
- if item_type is None or item_type == 'عمالة':
823
- labor = {}
824
- for name, info in self.labor_rates.items():
825
- if category is None or info.get('فئة') == category:
826
- labor[name] = info
827
- result['العمالة'] = labor
828
-
829
- # جمع المعدات حسب الفئة
830
- if item_type is None or item_type == 'معدة':
831
- equipment = {}
832
- for name, info in self.equipment_rates.items():
833
- if category is None or info.get('فئة') == category:
834
- equipment[name] = info
835
- result['المعدات'] = equipment
836
-
837
- return result
838
-
839
- def generate_sample_project_data(self) -> Dict[str, Any]:
840
- """
841
- توليد بيانات نموذجية لمشروع بناء صغير للاختبار
842
-
843
- العوائد:
844
- dict: بيانات المشروع النموذجية
845
- """
846
- # إنشاء بيانات المشروع
847
- project_data = {
848
- 'اسم_المشروع': 'مبنى سكني صغير',
849
- 'وصف_المشروع': 'مبنى سكني مكون من دور أرضي بمساحة 250 متر مربع',
850
- 'المصاريف_الإدارية': 0.05, # 5%
851
- 'هامش_الربح': 0.10, # 10%
852
- 'عوامل_التعديل': {
853
- 'location_factor': 1.2, # معامل الموقع (منطقة مرتفعة التكلفة)
854
- 'time_factor': 1.0, # معامل الوقت
855
- 'risk_factor': 1.05, # معامل المخاطر
856
- 'market_factor': 1.0 # معامل السوق
857
- },
858
- 'البنود': [
859
- # الأساسات
860
- {
861
- 'وصف_البند': 'حفر الأساسات بعمق 2 متر',
862
- 'الكمية': 150.0,
863
- 'الوحدة': 'م3',
864
- 'المواد': [],
865
- 'العمالة': [
866
- {'النوع': 'عامل خرسانة', 'العدد': 4, 'المدة': 3}
867
- ],
868
- 'المعدات': [
869
- {'النوع': 'حفار متوسط', 'العدد': 1, 'المدة': 2}
870
- ]
871
- },
872
- {
873
- 'وصف_البند': 'توريد وصب خرسانة عادية للأساسات',
874
- 'الكمية': 25.0,
875
- 'الوحدة': 'م3',
876
- 'المواد': [
877
- {'الاسم': 'خرسانة جاهزة', 'الكمية': 25.0}
878
- ],
879
- 'العمالة': [
880
- {'النوع': 'عامل خرسانة', 'العدد': 6, 'المدة': 1}
881
- ],
882
- 'المعدات': [
883
- {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5}
884
- ]
885
- },
886
- {
887
- 'وصف_البند': 'توريد وتركيب حديد تسليح للأساسات',
888
- 'الكمية': 3.5,
889
- 'الوحدة': 'طن',
890
- 'المواد': [
891
- {'الاسم': 'حديد تسليح', 'الكمية': 3.5}
892
- ],
893
- 'العمالة': [
894
- {'النوع': 'حداد مسلح', 'العدد': 4, 'المدة': 3}
895
- ],
896
- 'المعدات': []
897
- },
898
- {
899
- 'وصف_البند': 'نجارة وفك شدة الأساسات',
900
- 'الكمية': 120.0,
901
- 'الوحدة': 'م2',
902
- 'المواد': [],
903
- 'العمالة': [
904
- {'النوع': 'نجار مسلح', 'العدد': 4, 'المدة': 3}
905
- ],
906
- 'المعدات': []
907
- },
908
- {
909
- 'وصف_البند': 'توريد وصب خرسانة مسلحة للأساسات',
910
- 'الكمية': 30.0,
911
- 'الوحدة': 'م3',
912
- 'المواد': [
913
- {'الاسم': 'خرسانة جاهزة', 'الكمية': 30.0}
914
- ],
915
- 'العمالة': [
916
- {'النوع': 'عامل خرسانة', 'العدد': 6, 'المدة': 1}
917
- ],
918
- 'المعدات': [
919
- {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5},
920
- {'النوع': 'هزاز خرسانة', 'العدد': 2, 'المدة': 1}
921
- ]
922
- },
923
-
924
- # الأعمدة والأسقف
925
- {
926
- 'وصف_البند': 'توريد وتركيب حديد تسليح للأعمدة',
927
- 'الكمية': 2.8,
928
- 'الوحدة': 'طن',
929
- 'المواد': [
930
- {'الاسم': 'حديد تسليح', 'الكمية': 2.8}
931
- ],
932
- 'العمالة': [
933
- {'النوع': 'حداد مسلح', 'العدد': 4, 'المدة': 3}
934
- ],
935
- 'المعدات': []
936
- },
937
- {
938
- 'وصف_البند': 'نجارة وفك شدة الأعمدة',
939
- 'الكمية': 85.0,
940
- 'الوحدة': 'م2',
941
- 'المواد': [],
942
- 'العمالة': [
943
- {'النوع': 'نجار مسلح', 'العدد': 3, 'المدة': 3}
944
- ],
945
- 'المعدات': []
946
- },
947
- {
948
- 'وصف_البند': 'توريد وصب خرسانة مسلحة للأعمدة',
949
- 'الكمية': 12.0,
950
- 'الوحدة': 'م3',
951
- 'المواد': [
952
- {'الاسم': 'خرسانة جاهزة', 'الكمية': 12.0}
953
- ],
954
- 'العمالة': [
955
- {'النوع': 'عامل خرسانة', 'العدد': 4, 'المدة': 1}
956
- ],
957
- 'المعدات': [
958
- {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5},
959
- {'النوع': 'هزاز خرسانة', 'العدد': 2, 'المدة': 1}
960
- ]
961
- },
962
-
963
- # أعمال البناء
964
- {
965
- 'وصف_البند': 'توريد وبناء حوائط من الطوب الأحمر',
966
- 'الكمية': 220.0,
967
- 'الوحدة': 'م2',
968
- 'المواد': [
969
- {'الاسم': 'طوب أحمر', 'الكمية': 16.5} # بالألف
970
- ],
971
- 'العمالة': [
972
- {'النوع': 'بناء', 'العدد': 4, 'المدة': 8},
973
- {'النوع': 'مساعد بناء', 'العدد': 4, 'المدة': 8}
974
- ],
975
- 'المعدات': []
976
- },
977
-
978
- # أعمال التشطيبات
979
- {
980
- 'وصف_البند': 'توريد وتركيب بلاط سيراميك للأرضيات',
981
- 'الكمية': 250.0,
982
- 'الوحدة': 'م2',
983
- 'المواد': [
984
- {'الاسم': 'بلاط سيراميك', 'الكمية': 250.0}
985
- ],
986
- 'العمالة': [
987
- {'النوع': 'مبلط', 'العدد': 4, 'المدة': 7}
988
- ],
989
- 'المعدات': []
990
- },
991
- {
992
- 'وصف_البند': 'توريد وتنفيذ دهانات للحوائط',
993
- 'الكمية': 450.0,
994
- 'الوحدة': 'م2',
995
- 'المواد': [
996
- {'ا��اسم': 'دهانات بلاستيك', 'الكمية': 90.0} # بالتر
997
- ],
998
- 'العمالة': [
999
- {'النوع': 'نقاش', 'العدد': 3, 'المدة': 8}
1000
- ],
1001
- 'المعدات': []
1002
- }
1003
- ]
1004
- }
1005
-
1006
- return project_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/services/construction_templates.py DELETED
@@ -1,748 +0,0 @@
1
- """
2
- كتالوج بنود نموذجية للمقاولات
3
- يحتوي هذا الملف على قائمة كاملة من النماذج الجاهزة للبنود الشائعة في مشاريع المقاولات، مثل:
4
- - أعمال الخرسانة بأنواعها
5
- - المناهل وأنواع المواسير
6
- - التركيبات المختلفة
7
- - الطرق والأسفلت
8
- - وغيرها من أعمال المقاولات
9
- """
10
-
11
- import os
12
- import json
13
- import sys
14
- from typing import Dict, List, Any, Optional
15
- from datetime import datetime
16
-
17
- # إضافة مسار النظام للوصول لملفات التكوين
18
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")))
19
- try:
20
- import config
21
- except ImportError:
22
- # إذا لم يتم العثور على ملف التكوين، نستخدم قيم افتراضية
23
- class DefaultConfig:
24
- DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../data"))
25
- config = DefaultConfig
26
-
27
- # إنشاء مجلد البيانات إذا لم يكن موجودًا
28
- if not os.path.exists(config.DATA_DIR):
29
- os.makedirs(config.DATA_DIR)
30
-
31
-
32
- class ConstructionTemplates:
33
- """كتالوج بنود نموذجية للمقاولات"""
34
-
35
- def __init__(self):
36
- """تهيئة كتالوج البنود النموذجية"""
37
- self.templates_file = os.path.join(config.DATA_DIR, 'construction_templates.json')
38
- self.market_prices_file = os.path.join(config.DATA_DIR, 'saudi_market_prices.json')
39
-
40
- # تحميل قوالب البنود النموذجية
41
- self.templates = self._load_templates()
42
-
43
- # تحميل أسعار السوق السعودي
44
- self.market_prices = self._load_market_prices()
45
-
46
- def _load_templates(self) -> Dict[str, Dict[str, Any]]:
47
- """تحميل قوالب البنود النموذجية من الملف"""
48
- if os.path.exists(self.templates_file):
49
- try:
50
- with open(self.templates_file, 'r', encoding='utf-8') as f:
51
- return json.load(f)
52
- except Exception as e:
53
- print(f"خطأ في تحميل قوالب البنود النموذجية: {str(e)}")
54
-
55
- # إنشاء بيانات افتراضية إذا لم يتم العثور على الملف
56
- default_templates = self._create_default_templates()
57
-
58
- # حفظ البيانات الافتراضية
59
- self._save_templates(default_templates)
60
-
61
- return default_templates
62
-
63
- def _save_templates(self, templates: Dict[str, Dict[str, Any]]) -> None:
64
- """حفظ قوالب البنود النموذجية إلى الملف"""
65
- try:
66
- with open(self.templates_file, 'w', encoding='utf-8') as f:
67
- json.dump(templates, f, ensure_ascii=False, indent=4)
68
- except Exception as e:
69
- print(f"خطأ في حفظ قوالب البنود النموذجية: {str(e)}")
70
-
71
- def _load_market_prices(self) -> Dict[str, Dict[str, Any]]:
72
- """تحميل أسعار السوق السعودي من الملف"""
73
- if os.path.exists(self.market_prices_file):
74
- try:
75
- with open(self.market_prices_file, 'r', encoding='utf-8') as f:
76
- return json.load(f)
77
- except Exception as e:
78
- print(f"خطأ في تحميل أسعار السوق السعودي: {str(e)}")
79
-
80
- # إنشاء بيانات افتراضية إذا لم يتم العثور على الملف
81
- default_prices = self._create_default_market_prices()
82
-
83
- # حفظ البيانات الافتراضية
84
- self._save_market_prices(default_prices)
85
-
86
- return default_prices
87
-
88
- def _save_market_prices(self, prices: Dict[str, Dict[str, Any]]) -> None:
89
- """حفظ أسعار السوق السعودي إلى الملف"""
90
- try:
91
- with open(self.market_prices_file, 'w', encoding='utf-8') as f:
92
- json.dump(prices, f, ensure_ascii=False, indent=4)
93
- except Exception as e:
94
- print(f"خطأ في حفظ أسعار السوق السعودي: {str(e)}")
95
-
96
- def _create_default_templates(self) -> Dict[str, Dict[str, Any]]:
97
- """إنشاء قوالب افتراضية للبنود النموذجية"""
98
- templates = {
99
- "categories": {
100
- "أعمال_خرسانية": {
101
- "name": "أعمال خرسانية",
102
- "description": "بنود أعمال الخرسانة المسلحة والعادية",
103
- "icon": "building"
104
- },
105
- "أعمال_صحية": {
106
- "name": "أعمال صحية",
107
- "description": "بنود أعمال المناهل والمواسير والتركيبات الصحية",
108
- "icon": "pipe"
109
- },
110
- "أعمال_طرق": {
111
- "name": "أعمال طرق",
112
- "description": "بنود أعمال الطرق والأسفلت والرصف",
113
- "icon": "road"
114
- },
115
- "أعمال_كهربائية": {
116
- "name": "أعمال كهربائية",
117
- "description": "بنود أعمال الكهرباء والإنارة",
118
- "icon": "zap"
119
- },
120
- "أعمال_ميكانيكية": {
121
- "name": "أعمال ميكانيكية",
122
- "description": "بنود أعمال التكييف والتهوية والتبريد",
123
- "icon": "thermometer"
124
- }
125
- },
126
- "templates": {
127
- # نماذج أعمال خرسانية
128
- "خرسانة_مسلحة_أساسات": {
129
- "category": "أعمال_خرسانية",
130
- "name": "خرسانة مسلحة للأساسات",
131
- "description": "توريد وصب خرسانة مسلحة للأساسات بقوة لا تقل عن 300 كجم/سم2",
132
- "unit": "م3",
133
- "components": {
134
- "materials": [
135
- {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0},
136
- {"الاسم": "حديد تسليح", "الكمية": 0.12, "الوحدة": "طن", "سعر_الوحدة": 5500.0}
137
- ],
138
- "labor": [
139
- {"النوع": "عامل خرسانة", "العدد": 4, "المدة": 0.3, "سعر_اليوم": 150.0},
140
- {"النوع": "نجار مسلح", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 250.0},
141
- {"النوع": "حداد مسلح", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 250.0}
142
- ],
143
- "equipment": [
144
- {"النوع": "هزاز خرسانة", "العدد": 1, "المدة": 0.3, "سعر_اليوم": 150.0}
145
- ]
146
- },
147
- "admin_expenses": 0.05,
148
- "profit_margin": 0.10,
149
- "tags": ["خرسانة", "أساسات", "مسلحة"]
150
- },
151
- "خرسانة_مسلحة_أعمدة": {
152
- "category": "أعمال_خرسانية",
153
- "name": "خرسانة مسلحة للأعمدة",
154
- "description": "توريد وصب خرسانة مسلحة للأعمدة بقوة لا تقل عن 350 كجم/سم2",
155
- "unit": "م3",
156
- "components": {
157
- "materials": [
158
- {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0},
159
- {"الاسم": "حديد تسليح", "الكمية": 0.18, "الوحدة": "طن", "سعر_الوحدة": 5500.0}
160
- ],
161
- "labor": [
162
- {"النوع": "عامل خرسانة", "العدد": 4, "المدة": 0.4, "سعر_اليوم": 150.0},
163
- {"النوع": "نجار مسلح", "العدد": 2, "المدة": 0.7, "سعر_اليوم": 250.0},
164
- {"النوع": "حداد مسلح", "العدد": 2, "المدة": 0.7, "سعر_اليوم": 250.0}
165
- ],
166
- "equipment": [
167
- {"النوع": "هزاز خرسانة", "العدد": 2, "المدة": 0.4, "سعر_اليوم": 150.0}
168
- ]
169
- },
170
- "admin_expenses": 0.05,
171
- "profit_margin": 0.10,
172
- "tags": ["خرسانة", "أعمدة", "مسلحة"]
173
- },
174
- "خرسانة_مسلحة_أسقف": {
175
- "category": "أعمال_خرسانية",
176
- "name": "خرسانة مسلحة للأسقف",
177
- "description": "توريد وصب خرسانة مسلحة للأسقف والبلاطات بقوة لا تقل عن 350 كجم/سم2",
178
- "unit": "م3",
179
- "components": {
180
- "materials": [
181
- {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0},
182
- {"الاسم": "حديد تسليح", "الكمية": 0.16, "الوحدة": "طن", "سعر_الوحدة": 5500.0}
183
- ],
184
- "labor": [
185
- {"النوع": "عامل خرسانة", "العدد": 5, "المدة": 0.5, "سعر_اليوم": 150.0},
186
- {"النوع": "نجار مسلح", "العدد": 3, "المدة": 0.8, "سعر_اليوم": 250.0},
187
- {"النوع": "حداد مسلح", "العدد": 3, "المدة": 0.8, "سعر_اليوم": 250.0}
188
- ],
189
- "equipment": [
190
- {"النوع": "هزاز خرسانة", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 150.0}
191
- ]
192
- },
193
- "admin_expenses": 0.05,
194
- "profit_margin": 0.10,
195
- "tags": ["خرسانة", "أسقف", "بلاطات", "مسلحة"]
196
- },
197
-
198
- # نماذج أعمال صحية
199
- "منهل_تفتيش_خرساني": {
200
- "category": "أعمال_صحية",
201
- "name": "منهل تفتيش خرساني",
202
- "description": "توريد وتركيب منهل تفتيش خرساني قطر 1 متر وعمق 2 متر",
203
- "unit": "عدد",
204
- "components": {
205
- "materials": [
206
- {"الاسم": "خرسانة جاهزة", "الكمية": 1.5, "الوحدة": "م3", "سعر_الوحدة": 750.0},
207
- {"الاسم": "حديد تسليح", "الكمية": 0.15, "الوحدة": "طن", "سعر_الوحدة": 5500.0},
208
- {"الاسم": "غطاء منهل حديد", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 1500.0}
209
- ],
210
- "labor": [
211
- {"النوع": "عامل خرسانة", "العدد": 3, "المدة": 1, "سعر_اليوم": 150.0},
212
- {"النوع": "نجار مسلح", "العدد": 2, "المدة": 1, "سعر_اليوم": 250.0},
213
- {"النوع": "حداد مسلح", "العدد": 1, "المدة": 1, "سعر_اليوم": 250.0},
214
- {"النوع": "سباك", "العدد": 2, "المدة": 1, "سعر_اليوم": 250.0}
215
- ],
216
- "equipment": [
217
- {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.5, "سعر_اليوم": 1200.0}
218
- ]
219
- },
220
- "admin_expenses": 0.05,
221
- "profit_margin": 0.12,
222
- "tags": ["صرف صحي", "منهل", "تفتيش"]
223
- },
224
- "مواسير_بلاستيك_قطر_200_مم": {
225
- "category": "أعمال_صحية",
226
- "name": "مواسير بلاستيك قطر 200 مم",
227
- "description": "توريد وتركيب مواسير بلاستيك UPVC قطر 200 مم لشبكات الصرف الصحي",
228
- "unit": "م.ط",
229
- "components": {
230
- "materials": [
231
- {"الاسم": "مواسير بلاستيك UPVC قطر 200 مم", "الكمية": 1.05, "الوحدة": "م.ط", "سعر_الوحدة": 180.0},
232
- {"الاسم": "وصلات ومثبتات", "الكمية": 1, "الوحدة": "مجموعة", "سعر_الوحدة": 35.0},
233
- {"الاسم": "مواد لاصقة", "الكمية": 0.1, "الوحدة": "لتر", "سعر_الوحدة": 120.0}
234
- ],
235
- "labor": [
236
- {"النوع": "سباك", "العدد": 2, "المدة": 0.2, "سعر_اليوم": 250.0},
237
- {"النوع": "مساعد سباك", "العدد": 2, "المدة": 0.2, "سعر_اليوم": 120.0}
238
- ],
239
- "equipment": [
240
- {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.1, "سعر_اليوم": 1200.0}
241
- ]
242
- },
243
- "admin_expenses": 0.05,
244
- "profit_margin": 0.12,
245
- "tags": ["صرف صحي", "مواسير", "بلاستيك"]
246
- },
247
-
248
- # نماذج أعمال طرق
249
- "طبقة_أساس_للطرق": {
250
- "category": "أعمال_طرق",
251
- "name": "طبقة أساس للطرق",
252
- "description": "توريد وفرد ودمك طبقة أساس للطرق سمك 20 سم، درجة دمك 98%",
253
- "unit": "م3",
254
- "components": {
255
- "materials": [
256
- {"الاسم": "مواد طبقة أساس", "الكمية": 1.25, "الوحدة": "م3", "سعر_الوحدة": 90.0},
257
- {"الاسم": "مياه للدمك", "الكمية": 0.2, "الوحدة": "م3", "سعر_الوحدة": 10.0}
258
- ],
259
- "labor": [
260
- {"النوع": "عامل طرق", "العدد": 4, "المدة": 0.05, "سعر_اليوم": 150.0},
261
- {"النوع": "مراقب فني", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 300.0}
262
- ],
263
- "equipment": [
264
- {"النوع": "جريدر", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 2200.0},
265
- {"النوع": "رصاصة دمك", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 1800.0},
266
- {"النوع": "شاحنة نقل", "العدد": 2, "المدة": 0.05, "سعر_اليوم": 1200.0}
267
- ]
268
- },
269
- "admin_expenses": 0.05,
270
- "profit_margin": 0.12,
271
- "tags": ["طرق", "أساس", "دمك"]
272
- },
273
- "طبقة_إسفلت_سطحية": {
274
- "category": "أعمال_طرق",
275
- "name": "طبقة إسفلت سطحية",
276
- "description": "توريد وفرد ودمك طبقة إسفلت سطحية سمك 5 سم",
277
- "unit": "م2",
278
- "components": {
279
- "materials": [
280
- {"الاسم": "خلطة إسفلتية ساخنة", "الكمية": 0.125, "الوحدة": "طن", "سعر_الوحدة": 400.0},
281
- {"الاسم": "مواد رش تأسيسي", "الكمية": 0.5, "الوحدة": "لتر", "سعر_الوحدة": 8.0}
282
- ],
283
- "labor": [
284
- {"النوع": "عامل طرق", "العدد": 6, "المدة": 0.01, "سعر_اليوم": 150.0},
285
- {"النوع": "مراقب فني", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 300.0}
286
- ],
287
- "equipment": [
288
- {"النوع": "فرادة إسفلت", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 4000.0},
289
- {"النوع": "رصاصة دمك", "العدد": 2, "المدة": 0.01, "سعر_اليوم": 1800.0},
290
- {"النوع": "سيارة رش إسفلت", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 2000.0},
291
- {"النوع": "شاحنة نقل", "العدد": 4, "المدة": 0.01, "سعر_اليوم": 1200.0}
292
- ]
293
- },
294
- "admin_expenses": 0.05,
295
- "profit_margin": 0.12,
296
- "tags": ["طرق", "إسفلت", "سطحية"]
297
- },
298
-
299
- # نماذج أعمال كهربائية
300
- "عمود_إنارة_10_متر": {
301
- "category": "أعمال_كهربائية",
302
- "name": "عمود إنارة 10 متر",
303
- "description": "توريد وتركيب عمود إنارة جلفانيزي بارتفاع 10 متر مع ذراع مفردة وكشاف LED بقدرة 150 واط",
304
- "unit": "عدد",
305
- "components": {
306
- "materials": [
307
- {"الاسم": "عمود إنارة جلفانيزي 10 متر", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 3500.0},
308
- {"الاسم": "ذراع إنارة مفردة", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 450.0},
309
- {"الاسم": "كشاف LED 150 واط", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 850.0},
310
- {"الاسم": "كابل كهرباء 3×4 مم²", "الكمية": 15, "الوحدة": "م.ط", "سعر_الوحدة": 32.0},
311
- {"الاسم": "قاعدة خرسانية مسلحة", "الكمية": 0.25, "الوحدة": "م3", "سعر_الوحدة": 750.0}
312
- ],
313
- "labor": [
314
- {"النوع": "كهربائي", "العدد": 2, "المدة": 1, "سعر_اليوم": 270.0},
315
- {"النوع": "مساعد كهربائي", "العدد": 2, "المدة": 1, "سعر_اليوم": 120.0},
316
- {"النوع": "عامل خرسانة", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 150.0}
317
- ],
318
- "equipment": [
319
- {"النوع": "ونش شوكة", "العدد": 1, "المدة": 0.5, "سعر_اليوم": 1500.0},
320
- {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.2, "سعر_اليوم": 1200.0}
321
- ]
322
- },
323
- "admin_expenses": 0.05,
324
- "profit_margin": 0.12,
325
- "tags": ["كهرباء", "إنارة", "LED"]
326
- }
327
- }
328
- }
329
-
330
- return templates
331
-
332
- def _create_default_market_prices(self) -> Dict[str, Dict[str, Any]]:
333
- """إنشاء بيانات افتراضية لأسعار السوق السعودي"""
334
- current_date = datetime.now().strftime("%Y-%m-%d")
335
-
336
- prices = {
337
- "metadata": {
338
- "last_update": current_date,
339
- "source": "أسعار السوق السعودي الافتراضية",
340
- "disclaimer": "هذه الأسعار تقريبية وقد تختلف حسب المنطقة والكميات والموردين"
341
- },
342
- "materials": {
343
- # مواد الخرسانة
344
- "خرسانة_جاهزة": {
345
- "name": "خرسانة جاهزة",
346
- "unit": "م3",
347
- "current_price": 750.0,
348
- "previous_price": 730.0,
349
- "price_trend": "up",
350
- "category": "أعمال خرسانية",
351
- "specifications": "خرسانة جاهزة بقوة 350 كجم/سم2",
352
- "note": "السعر يشمل توريد فقط، الضخ بتكلفة إضافية",
353
- "price_history": [
354
- {"date": "2023-06-01", "price": 700.0},
355
- {"date": "2023-09-01", "price": 715.0},
356
- {"date": "2023-12-01", "price": 730.0},
357
- {"date": current_date, "price": 750.0}
358
- ]
359
- },
360
- "حديد_تسليح": {
361
- "name": "حديد تسليح",
362
- "unit": "طن",
363
- "current_price": 5500.0,
364
- "previous_price": 5200.0,
365
- "price_trend": "up",
366
- "category": "أعمال خرسانية",
367
- "specifications": "حديد تسليح قطر 8-32 مم، انتاج سابك",
368
- "note": "السعر يتغير بشكل دوري حسب أسعار الحديد العالمية",
369
- "price_history": [
370
- {"date": "2023-06-01", "price": 4800.0},
371
- {"date": "2023-09-01", "price": 5000.0},
372
- {"date": "2023-12-01", "price": 5200.0},
373
- {"date": current_date, "price": 5500.0}
374
- ]
375
- },
376
- "أسمنت": {
377
- "name": "أسمنت",
378
- "unit": "كيس",
379
- "current_price": 30.0,
380
- "previous_price": 28.0,
381
- "price_trend": "up",
382
- "category": "أعمال خرسانية",
383
- "specifications": "أسمنت بورتلاندي عادي، كيس 50 كجم",
384
- "note": "السعر للكميات الكبيرة",
385
- "price_history": [
386
- {"date": "2023-06-01", "price": 25.0},
387
- {"date": "2023-09-01", "price": 27.0},
388
- {"date": "2023-12-01", "price": 28.0},
389
- {"date": current_date, "price": 30.0}
390
- ]
391
- },
392
-
393
- # مواد الطرق والإسفلت
394
- "خلطة_إسفلتية_ساخنة": {
395
- "name": "خلطة إسفلتية ساخنة",
396
- "unit": "طن",
397
- "current_price": 400.0,
398
- "previous_price": 380.0,
399
- "price_trend": "up",
400
- "category": "أعمال طرق",
401
- "specifications": "خلطة إسفلتية ساخنة للطبقة السطحية",
402
- "note": "السعر يشمل التوريد من المصنع، النقل بتكلفة إضافية",
403
- "price_history": [
404
- {"date": "2023-06-01", "price": 350.0},
405
- {"date": "2023-09-01", "price": 370.0},
406
- {"date": "2023-12-01", "price": 380.0},
407
- {"date": current_date, "price": 400.0}
408
- ]
409
- },
410
-
411
- # مواد صحية
412
- "مواسير_بلاستيك_UPVC": {
413
- "name": "مواسير بلاستيك UPVC قطر 200 مم",
414
- "unit": "م.ط",
415
- "current_price": 180.0,
416
- "previous_price": 165.0,
417
- "price_trend": "up",
418
- "category": "أعمال صحية",
419
- "specifications": "مواسير بلاستيك UPVC قطر 200 مم لشبكات الصرف الصحي",
420
- "note": "السعر للكميات الكبيرة",
421
- "price_history": [
422
- {"date": "2023-06-01", "price": 150.0},
423
- {"date": "2023-09-01", "price": 160.0},
424
- {"date": "2023-12-01", "price": 165.0},
425
- {"date": current_date, "price": 180.0}
426
- ]
427
- },
428
-
429
- # مواد كهربائية
430
- "كشاف_LED": {
431
- "name": "كشاف LED 150 واط",
432
- "unit": "عدد",
433
- "current_price": 850.0,
434
- "previous_price": 820.0,
435
- "price_trend": "up",
436
- "category": "أعمال كهربائية",
437
- "specifications": "كشاف إنارة LED بقدرة 150 واط للاستخدام الخارجي، IP65",
438
- "note": "السعر شامل الضريبة",
439
- "price_history": [
440
- {"date": "2023-06-01", "price": 780.0},
441
- {"date": "2023-09-01", "price": 800.0},
442
- {"date": "2023-12-01", "price": 820.0},
443
- {"date": current_date, "price": 850.0}
444
- ]
445
- }
446
- },
447
- "labor": {
448
- "عامل_خرسانة": {
449
- "name": "عامل خرسانة",
450
- "unit": "يوم",
451
- "current_price": 150.0,
452
- "previous_price": 140.0,
453
- "price_trend": "up",
454
- "category": "عمالة",
455
- "specifications": "عامل لصب وتسوية الخرسانة",
456
- "note": "أجرة اليوم الواحد لا تشمل السكن والمواصلات",
457
- "price_history": [
458
- {"date": "2023-06-01", "price": 130.0},
459
- {"date": "2023-09-01", "price": 135.0},
460
- {"date": "2023-12-01", "price": 140.0},
461
- {"date": current_date, "price": 150.0}
462
- ]
463
- },
464
- "مهندس_موقع": {
465
- "name": "مهندس موقع",
466
- "unit": "يوم",
467
- "current_price": 500.0,
468
- "previous_price": 480.0,
469
- "price_trend": "up",
470
- "category": "إشراف",
471
- "specifications": "مهندس إشراف موقع",
472
- "note": "أجرة اليوم الواحد لا تشمل السكن والمواصلات",
473
- "price_history": [
474
- {"date": "2023-06-01", "price": 450.0},
475
- {"date": "2023-09-01", "price": 470.0},
476
- {"date": "2023-12-01", "price": 480.0},
477
- {"date": current_date, "price": 500.0}
478
- ]
479
- }
480
- },
481
- "equipment": {
482
- "حفار_صغير": {
483
- "name": "حفار صغير",
484
- "unit": "يوم",
485
- "current_price": 1200.0,
486
- "previous_price": 1150.0,
487
- "price_trend": "up",
488
- "category": "معدات حفر",
489
- "specifications": "حفار صغير (بوبكات) بقدرة 70 حصان",
490
- "note": "السعر يشمل المشغل والوقود",
491
- "price_history": [
492
- {"date": "2023-06-01", "price": 1100.0},
493
- {"date": "2023-09-01", "price": 1120.0},
494
- {"date": "2023-12-01", "price": 1150.0},
495
- {"date": current_date, "price": 1200.0}
496
- ]
497
- },
498
- "فرادة_إسفلت": {
499
- "name": "فرادة إسفلت",
500
- "unit": "يوم",
501
- "current_price": 4000.0,
502
- "previous_price": 3800.0,
503
- "price_trend": "up",
504
- "category": "معدات طرق",
505
- "specifications": "فرادة إسفلت بعرض 3 متر",
506
- "note": "السعر يشمل المشغل والوقود",
507
- "price_history": [
508
- {"date": "2023-06-01", "price": 3500.0},
509
- {"date": "2023-09-01", "price": 3650.0},
510
- {"date": "2023-12-01", "price": 3800.0},
511
- {"date": current_date, "price": 4000.0}
512
- ]
513
- }
514
- }
515
- }
516
-
517
- return prices
518
-
519
- def get_all_templates(self) -> Dict[str, Dict[str, Any]]:
520
- """الحصول على جميع القوالب النموذجية"""
521
- return self.templates
522
-
523
- def get_templates_by_category(self, category_id: str) -> List[Dict[str, Any]]:
524
- """الحصول على القوالب النموذجية حسب الفئة"""
525
- result = []
526
-
527
- # التحقق من وجود الفئة
528
- if category_id not in self.templates["categories"]:
529
- return result
530
-
531
- # جمع القوالب التي تنتمي إلى الفئة المحددة
532
- for template_id, template in self.templates["templates"].items():
533
- if template["category"] == category_id:
534
- template_copy = template.copy()
535
- template_copy["id"] = template_id
536
- result.append(template_copy)
537
-
538
- return result
539
-
540
- def get_template_by_id(self, template_id: str) -> Optional[Dict[str, Any]]:
541
- """الحصول على قالب نموذجي بواسطة المعرف"""
542
- if template_id in self.templates["templates"]:
543
- template = self.templates["templates"][template_id].copy()
544
- template["id"] = template_id
545
- return template
546
-
547
- return None
548
-
549
- def add_template(self, template_data: Dict[str, Any]) -> str:
550
- """إضافة قالب نموذجي جديد"""
551
- # إنشاء معرف فريد للقالب
552
- template_name = template_data.get("name", "").strip()
553
- if not template_name:
554
- raise ValueError("يجب تحديد اسم القالب")
555
-
556
- # تحويل الاسم إلى معرف (باستبدال المسافات بالشرطات السفلية وإزالة الأحرف الخاصة)
557
- import re
558
- template_id = re.sub(r'[^\w\s]', '', template_name)
559
- template_id = template_id.replace(" ", "_")
560
-
561
- # إضافة رقم عشوائي لتجنب التكرار
562
- import random
563
- if template_id in self.templates["templates"]:
564
- template_id = f"{template_id}_{random.randint(1000, 9999)}"
565
-
566
- # إضافة القالب إلى القائمة
567
- self.templates["templates"][template_id] = template_data
568
-
569
- # حفظ التغييرات
570
- self._save_templates(self.templates)
571
-
572
- return template_id
573
-
574
- def update_template(self, template_id: str, template_data: Dict[str, Any]) -> bool:
575
- """تحديث قالب نموذجي موجود"""
576
- if template_id not in self.templates["templates"]:
577
- return False
578
-
579
- # تحديث القالب
580
- self.templates["templates"][template_id] = template_data
581
-
582
- # حفظ التغييرات
583
- self._save_templates(self.templates)
584
-
585
- return True
586
-
587
- def delete_template(self, template_id: str) -> bool:
588
- """حذف قالب نموذجي"""
589
- if template_id not in self.templates["templates"]:
590
- return False
591
-
592
- # حذف القالب
593
- del self.templates["templates"][template_id]
594
-
595
- # حفظ التغييرات
596
- self._save_templates(self.templates)
597
-
598
- return True
599
-
600
- def get_market_prices(self, category: Optional[str] = None, item_type: Optional[str] = None) -> Dict[str, Any]:
601
- """الحصول على أسعار السوق السعودي"""
602
- result = {
603
- "metadata": self.market_prices["metadata"]
604
- }
605
-
606
- # تحديد نوع العناصر المطلوبة
607
- sections = []
608
- if item_type:
609
- if item_type in ["materials", "المواد"]:
610
- sections = ["materials"]
611
- elif item_type in ["labor", "العمالة"]:
612
- sections = ["labor"]
613
- elif item_type in ["equipment", "المعدات"]:
614
- sections = ["equipment"]
615
- else:
616
- sections = ["materials", "labor", "equipment"]
617
-
618
- # جمع العناصر
619
- for section in sections:
620
- result[section] = {}
621
- for item_id, item_data in self.market_prices[section].items():
622
- if not category or (item_data.get("category", "") == category):
623
- result[section][item_id] = item_data
624
-
625
- return result
626
-
627
- def update_market_price(self, item_type: str, item_id: str, new_price: float) -> bool:
628
- """تحديث سعر في قائمة أسعار السوق"""
629
- section = ""
630
- if item_type in ["materials", "المواد"]:
631
- section = "materials"
632
- elif item_type in ["labor", "العمالة"]:
633
- section = "labor"
634
- elif item_type in ["equipment", "المعدات"]:
635
- section = "equipment"
636
- else:
637
- return False
638
-
639
- if item_id not in self.market_prices[section]:
640
- return False
641
-
642
- # تحديث السعر
643
- current_price = self.market_prices[section][item_id]["current_price"]
644
- self.market_prices[section][item_id]["previous_price"] = current_price
645
- self.market_prices[section][item_id]["current_price"] = new_price
646
-
647
- # تحديد اتجاه السعر
648
- if new_price > current_price:
649
- self.market_prices[section][item_id]["price_trend"] = "up"
650
- elif new_price < current_price:
651
- self.market_prices[section][item_id]["price_trend"] = "down"
652
- else:
653
- self.market_prices[section][item_id]["price_trend"] = "stable"
654
-
655
- # إضافة السعر الجديد إلى تاريخ الأسعار
656
- current_date = datetime.now().strftime("%Y-%m-%d")
657
- self.market_prices[section][item_id]["price_history"].append({
658
- "date": current_date,
659
- "price": new_price
660
- })
661
-
662
- # تحديث تاريخ آخر تحديث
663
- self.market_prices["metadata"]["last_update"] = current_date
664
-
665
- # حفظ التغييرات
666
- self._save_market_prices(self.market_prices)
667
-
668
- return True
669
-
670
- def add_market_price_item(self, item_type: str, item_data: Dict[str, Any]) -> str:
671
- """إضافة عنصر جديد إلى قائمة أسعار السوق"""
672
- section = ""
673
- if item_type in ["materials", "المواد"]:
674
- section = "materials"
675
- elif item_type in ["labor", "العمالة"]:
676
- section = "labor"
677
- elif item_type in ["equipment", "المعدات"]:
678
- section = "equipment"
679
- else:
680
- raise ValueError("نوع العنصر غير صحيح")
681
-
682
- # التحقق من البيانات الأساسية
683
- if "name" not in item_data or "current_price" not in item_data or "unit" not in item_data:
684
- raise ValueError("يجب تحديد الاسم والسعر الحالي والوحدة")
685
-
686
- # إنشاء معرف فريد للعنصر
687
- item_name = item_data["name"].strip()
688
- import re
689
- item_id = re.sub(r'[^\w\s]', '', item_name)
690
- item_id = item_id.replace(" ", "_")
691
-
692
- # إضافة رقم عشوائي لتجنب التكرار
693
- import random
694
- if item_id in self.market_prices[section]:
695
- item_id = f"{item_id}_{random.randint(1000, 9999)}"
696
-
697
- # إعداد بيانات العنصر
698
- current_date = datetime.now().strftime("%Y-%m-%d")
699
- new_item = {
700
- "name": item_name,
701
- "unit": item_data["unit"],
702
- "current_price": item_data["current_price"],
703
- "previous_price": item_data.get("previous_price", item_data["current_price"]),
704
- "price_trend": "stable",
705
- "category": item_data.get("category", ""),
706
- "specifications": item_data.get("specifications", ""),
707
- "note": item_data.get("note", ""),
708
- "price_history": [
709
- {"date": current_date, "price": item_data["current_price"]}
710
- ]
711
- }
712
-
713
- # إضافة العنصر إلى القائمة
714
- self.market_prices[section][item_id] = new_item
715
-
716
- # تحديث تاريخ آخر تحديث
717
- self.market_prices["metadata"]["last_update"] = current_date
718
-
719
- # حفظ التغييرات
720
- self._save_market_prices(self.market_prices)
721
-
722
- return item_id
723
-
724
- def convert_template_to_item(self, template_id: str) -> Dict[str, Any]:
725
- """تحويل قالب نموذجي إلى بند للاستخدام في حاسبة تكاليف البناء"""
726
- template = self.get_template_by_id(template_id)
727
- if not template:
728
- raise ValueError("القالب غير موجود")
729
-
730
- # تحويل القالب إلى صيغة بند
731
- item = {
732
- "وصف_البند": template["description"],
733
- "الكمية": 1.0,
734
- "الوحدة": template["unit"],
735
- "المواد": template["components"]["materials"],
736
- "العمالة": template["components"]["labor"],
737
- "المعدات": template["components"]["equipment"],
738
- "المصاريف_الإدارية": template["admin_expenses"],
739
- "هامش_الربح": template["profit_margin"],
740
- "عوامل_التعديل": {
741
- "location_factor": 1.0,
742
- "time_factor": 1.0,
743
- "risk_factor": 1.0,
744
- "market_factor": 1.0
745
- }
746
- }
747
-
748
- return item
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/services/local_content_calculator.py DELETED
@@ -1,577 +0,0 @@
1
- """
2
- خدمة حساب المحتوى المحلي
3
- """
4
- import pandas as pd
5
- import numpy as np
6
- from datetime import datetime
7
- import os
8
- import config
9
-
10
- class LocalContentCalculator:
11
- """خدمة حساب وتحسين المحتوى المحلي"""
12
-
13
- def __init__(self):
14
- """تهيئة خدمة حساب المحتوى المحلي"""
15
- # تحميل بيانات المواد المحلية ونسب المحتوى المحلي
16
- self.local_products = self._load_local_products()
17
- self.local_services = self._load_local_services()
18
- self.local_labor = self._load_local_labor()
19
-
20
- # تحديد الأوزان النسبية لمكونات المحتوى المحلي
21
- self.component_weights = {
22
- 'القوى العاملة': 0.3, # 30% من وزن المحتوى المحلي
23
- 'المنتجات': 0.5, # 50% من وزن المحتوى المحلي
24
- 'الخدمات': 0.2 # 20% من وزن المحتوى المحلي
25
- }
26
-
27
- # تحديد المستهدفات (متطلبات المحتوى المحلي)
28
- self.targets = {
29
- 'القوى العاملة': 0.8, # 80% محتوى محلي للقوى العاملة
30
- 'المنتجات': 0.7, # 70% محتوى محلي للمنتجات
31
- 'الخدمات': 0.6 # 60% محتوى محلي للخدمات
32
- }
33
-
34
- def _load_local_products(self):
35
- """تحميل بيانات المنتجات المحلية ونسب المحتوى المحلي"""
36
- # محاكاة تحميل البيانات من مصدر بيانات
37
- local_products = {
38
- 'خرسانة': {
39
- 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي
40
- 'بديل_محلي': True,
41
- 'مصدر': 'محلي',
42
- 'ملاحظات': 'منتج محلي بالكامل'
43
- },
44
- 'حديد تسليح': {
45
- 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي
46
- 'بديل_محلي': True,
47
- 'مصدر': 'محلي/مستورد',
48
- 'ملاحظات': 'متوفر من مصانع محلية ومستورد'
49
- },
50
- 'عزل مائي': {
51
- 'نسبة_المحتوى_المحلي': 0.60, # 60% محتوى محلي
52
- 'بديل_محلي': True,
53
- 'مصدر': 'محلي/مستورد',
54
- 'ملاحظات': 'منتج محلي متوفر بجودة معقولة'
55
- },
56
- 'بلوك خرساني': {
57
- 'نسبة_المحتوى_المحلي': 0.98, # 98% محتوى محلي
58
- 'بديل_محلي': True,
59
- 'مصدر': 'محلي',
60
- 'ملاحظات': 'منتج محلي بالكامل'
61
- },
62
- 'رخام': {
63
- 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي
64
- 'بديل_محلي': True,
65
- 'مصدر': 'محلي',
66
- 'ملاحظات': 'متوفر من محاجر محلية'
67
- },
68
- 'أثاث مكتبي': {
69
- 'نسبة_المحتوى_المحلي': 0.75, # 75% محتوى محلي
70
- 'بديل_محلي': True,
71
- 'مصدر': 'محلي',
72
- 'ملاحظات': 'يُصنع محليًا ويستخدم بعض المكونات المستوردة'
73
- },
74
- 'أجهزة تكييف': {
75
- 'نسبة_المحتوى_المحلي': 0.40, # 40% محتوى محلي
76
- 'بديل_محلي': True,
77
- 'مصدر': 'محلي/مستورد',
78
- 'ملاحظات': 'تجميع محلي مع مكونات مستوردة'
79
- },
80
- 'أنظمة إضاءة': {
81
- 'نسبة_المحتوى_المحلي': 0.55, # 55% محتوى محلي
82
- 'بديل_محلي': True,
83
- 'مصدر': 'محلي/مستورد',
84
- 'ملاحظات': 'متوفر محليًا وبجودة متفاوتة'
85
- },
86
- 'زجاج': {
87
- 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي
88
- 'بديل_محلي': True,
89
- 'مصدر': 'محلي/مستورد',
90
- 'ملاحظات': 'إنتاج محلي بمواصفات جيدة'
91
- },
92
- 'أسلاك كهربائية': {
93
- 'نسبة_المحتوى_المحلي': 0.85, # 85% محتوى محلي
94
- 'بديل_محلي': True,
95
- 'مصدر': 'محلي',
96
- 'ملاحظات': 'تصنيع محلي بجودة عالية'
97
- }
98
- }
99
-
100
- # محاولة تحميل البيانات من ملف إذا كان متاحًا
101
- try:
102
- file_path = os.path.join(config.DATA_DIR, 'local_products.csv')
103
- if os.path.exists(file_path):
104
- df = pd.read_csv(file_path, encoding='utf-8')
105
- local_products = {}
106
- for _, row in df.iterrows():
107
- local_products[row['اسم_المنتج']] = {
108
- 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'],
109
- 'بديل_محلي': row['بديل_محلي'],
110
- 'مصدر': row['مصدر'],
111
- 'ملاحظات': row['ملاحظات']
112
- }
113
- except Exception as e:
114
- print(f"خطأ في تحميل بيانات المنتجات المحلية: {str(e)}")
115
-
116
- return local_products
117
-
118
- def _load_local_services(self):
119
- """تحميل بيانات الخدمات المحلية ونسب المحتوى المحلي"""
120
- # محاكاة تحميل البيانات من مصدر بيانات
121
- local_services = {
122
- 'تصميم معماري': {
123
- 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي
124
- 'بديل_محلي': True,
125
- 'مصدر': 'محلي',
126
- 'ملاحظات': 'متوفرة من مكاتب استشارية محلية'
127
- },
128
- 'إشراف هندسي': {
129
- 'نسبة_المحتوى_المحلي': 0.85, # 85% محتوى محلي
130
- 'بديل_محلي': True,
131
- 'مصدر': 'محلي',
132
- 'ملاحظات': 'متوفر من شركات محلية'
133
- },
134
- 'خدمات تنسيق المواقع': {
135
- 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي
136
- 'بديل_محلي': True,
137
- 'مصدر': 'محلي',
138
- 'ملاحظات': 'شركات محلية متخصصة'
139
- },
140
- 'خدمات أمن وسلامة': {
141
- 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي
142
- 'بديل_محلي': True,
143
- 'مصدر': 'محلي',
144
- 'ملاحظات': 'شركات محلية متخصصة'
145
- },
146
- 'استشارات بيئية': {
147
- 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي
148
- 'بديل_محلي': True,
149
- 'مصدر': 'محلي/دولي',
150
- 'ملاحظات': 'متوفرة محليًا مع بعض الخبرات الأجنبية'
151
- },
152
- 'دراسات جدوى': {
153
- 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي
154
- 'بديل_محلي': True,
155
- 'مصدر': 'محلي/دولي',
156
- 'ملاحظات': 'متوفرة من مكاتب استشارية محلية'
157
- },
158
- 'خدمات نقل': {
159
- 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي
160
- 'بديل_محلي': True,
161
- 'مصدر': 'محلي',
162
- 'ملاحظات': 'شركات نقل محلية متعددة'
163
- },
164
- 'صيانة ونظافة': {
165
- 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي
166
- 'بديل_محلي': True,
167
- 'مصدر': 'محلي',
168
- 'ملاحظات': 'شركات محلية متخصصة'
169
- }
170
- }
171
-
172
- # محاولة تحميل البيانات من ملف إذا كان متاحًا
173
- try:
174
- file_path = os.path.join(config.DATA_DIR, 'local_services.csv')
175
- if os.path.exists(file_path):
176
- df = pd.read_csv(file_path, encoding='utf-8')
177
- local_services = {}
178
- for _, row in df.iterrows():
179
- local_services[row['اسم_الخدمة']] = {
180
- 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'],
181
- 'بديل_محلي': row['بديل_محلي'],
182
- 'مصدر': row['مصدر'],
183
- 'ملاحظات': row['ملاحظات']
184
- }
185
- except Exception as e:
186
- print(f"خطأ في تحميل بيانات الخدمات المحلية: {str(e)}")
187
-
188
- return local_services
189
-
190
- def _load_local_labor(self):
191
- """تحميل بيانات القوى العاملة المحلية ونسب المحتوى المحلي"""
192
- # محاكاة تحميل البيانات من مصدر بيانات
193
- local_labor = {
194
- 'عمال بناء': {
195
- 'نسبة_المحتوى_المحلي': 0.60, # 60% محتوى محلي
196
- 'بديل_محلي': True,
197
- 'مصدر': 'محلي/أجنبي',
198
- 'ملاحظات': 'متوفر محليًا مع نسبة من العمالة الأجنبية'
199
- },
200
- 'مهندسون': {
201
- 'نسبة_المحتوى_المحلي': 0.75, # 75% محتوى محلي
202
- 'بديل_محلي': True,
203
- 'مصدر': 'محلي/أجنبي',
204
- 'ملاحظات': 'كفاءات محلية متوفرة'
205
- },
206
- 'فنيون': {
207
- 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي
208
- 'بديل_محلي': True,
209
- 'مصدر': 'محلي/أجنبي',
210
- 'ملاحظات': 'متوفر محليًا بنسب متفاوتة'
211
- },
212
- 'إداريون': {
213
- 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي
214
- 'بديل_محلي': True,
215
- 'مصدر': 'محلي',
216
- 'ملاحظات': 'معظمهم من الكوادر المحلية'
217
- },
218
- 'مشرفون': {
219
- 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي
220
- 'بديل_محلي': True,
221
- 'مصدر': 'محلي',
222
- 'ملاحظات': 'معظمهم من الكوادر المحلية'
223
- },
224
- 'مصممون': {
225
- 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي
226
- 'بديل_محلي': True,
227
- 'مصدر': 'محلي/أجنبي',
228
- 'ملاحظات': 'كفاءات محلية مع بعض الخبرات الأجنبية'
229
- },
230
- 'عمال مهرة': {
231
- 'نسبة_المحتوى_المحلي': 0.55, # 55% محتوى محلي
232
- 'بديل_محلي': True,
233
- 'مصدر': 'محلي/أجنبي',
234
- 'ملاحظات': 'نسبة من العمالة الأجنبية ذات الخبرة'
235
- }
236
- }
237
-
238
- # محاولة تحميل البيانات من ملف إذا كان متاحًا
239
- try:
240
- file_path = os.path.join(config.DATA_DIR, 'local_labor.csv')
241
- if os.path.exists(file_path):
242
- df = pd.read_csv(file_path, encoding='utf-8')
243
- local_labor = {}
244
- for _, row in df.iterrows():
245
- local_labor[row['فئة_العمالة']] = {
246
- 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'],
247
- 'بديل_محلي': row['بديل_محلي'],
248
- 'مصدر': row['مصدر'],
249
- 'ملاحظات': row['ملاحظات']
250
- }
251
- except Exception as e:
252
- print(f"خطأ في تحميل بيانات القوى العاملة المحلية: {str(e)}")
253
-
254
- return local_labor
255
-
256
- def calculate_project_local_content(self, project_data):
257
- """
258
- حساب نسبة المحتوى المحلي للمشروع
259
-
260
- المعلمات:
261
- project_data: بيانات المشروع، تتضمن مكونات المنتجات والخدمات والقوى العاملة
262
-
263
- إرجاع:
264
- نسبة المحتوى المحلي الإجمالية، وتفاصيل حسب كل مكون
265
- """
266
- # تهيئة نتائج الحساب
267
- results = {
268
- 'نسبة_المحتوى_المحلي_الإجمالية': 0,
269
- 'تفاصيل_المكونات': {
270
- 'المنتجات': {'نسبة': 0, 'تفاصيل': {}},
271
- 'الخدمات': {'نسبة': 0, 'تفاصيل': {}},
272
- 'القوى العاملة': {'نسبة': 0, 'تفاصيل': {}}
273
- },
274
- 'ملخص_المحتوى_المحلي': {},
275
- 'توصيات_التحسين': []
276
- }
277
-
278
- # حساب نسبة المحتوى المحلي للمنتجات
279
- if 'المنتجات' in project_data:
280
- products_local_content = self._calculate_products_local_content(project_data['المنتجات'])
281
- results['تفاصيل_المكونات']['المنتجات'] = products_local_content
282
-
283
- # حساب نسبة المحتوى المحلي للخدمات
284
- if 'الخدمات' in project_data:
285
- services_local_content = self._calculate_services_local_content(project_data['الخدمات'])
286
- results['تفاصيل_المكونات']['الخدمات'] = services_local_content
287
-
288
- # حساب نسبة المحتوى المحلي للقوى العاملة
289
- if 'القوى العاملة' in project_data:
290
- labor_local_content = self._calculate_labor_local_content(project_data['القوى العاملة'])
291
- results['تفاصيل_المكونات']['القوى العاملة'] = labor_local_content
292
-
293
- # حساب النسبة الإجمالية للمحتوى المحلي بناءً على الأوزان النسبية
294
- total_local_content = 0
295
- for component, weight in self.component_weights.items():
296
- if component in results['تفاصيل_المكونات']:
297
- component_percentage = results['تفاصيل_المكونات'][component]['نسبة']
298
- total_local_content += component_percentage * weight
299
-
300
- results['نسبة_المحتوى_المحلي_الإجمالية'] = total_local_content
301
-
302
- # تحديد ملخص المحتوى المحلي ومقارنته بالمستهدف
303
- for component, target in self.targets.items():
304
- if component in results['تفاصيل_المكونات']:
305
- actual = results['تفاصيل_المكونات'][component]['نسبة']
306
- status = 'مطابق' if actual >= target else 'غير مطابق'
307
- gap = round((target - actual) * 100, 2) if actual < target else 0
308
-
309
- results['ملخص_المحتوى_المحلي'][component] = {
310
- 'المستهدف': target * 100,
311
- 'الفعلي': round(actual * 100, 2),
312
- 'الحالة': status,
313
- 'الفجوة (%)': gap
314
- }
315
-
316
- # توليد توصيات لتحسين نسبة المحتوى المحلي
317
- results['توصيات_التحسين'] = self._generate_improvement_recommendations(results)
318
-
319
- return results
320
-
321
- def _calculate_products_local_content(self, products_data):
322
- """
323
- حساب نسبة المحتوى المحلي للمنتجات
324
-
325
- المعلمات:
326
- products_data: بيانات المنتجات المستخدمة في المشروع
327
-
328
- إرجاع:
329
- تفاصيل نسبة المحتوى المحلي للمنتجات
330
- """
331
- total_cost = 0
332
- local_content_value = 0
333
- details = {}
334
-
335
- for product_name, product_info in products_data.items():
336
- quantity = product_info.get('الكمية', 0)
337
- unit_price = product_info.get('سعر_الوحدة', 0)
338
- total_product_cost = quantity * unit_price
339
-
340
- # البحث عن نسبة المحتوى المحلي للمنتج
341
- local_content_percentage = 0
342
- if product_name in self.local_products:
343
- local_content_percentage = self.local_products[product_name]['نسبة_المحتوى_المحلي']
344
-
345
- # حساب قيمة المحتوى المحلي للمنتج
346
- product_local_content_value = total_product_cost * local_content_percentage
347
-
348
- # تحديث الإجماليات
349
- total_cost += total_product_cost
350
- local_content_value += product_local_content_value
351
-
352
- # تسجيل التفاصيل
353
- details[product_name] = {
354
- 'الكمية': quantity,
355
- 'سعر_الوحدة': unit_price,
356
- 'التكلفة_الإجمالية': total_product_cost,
357
- 'نسبة_المحتوى_المحلي': local_content_percentage,
358
- 'قيمة_المحتوى_المحلي': product_local_content_value,
359
- 'مصدر': self.local_products.get(product_name, {}).get('مصدر', 'غير معروف'),
360
- 'ملاحظات': self.local_products.get(product_name, {}).get('ملاحظات', '')
361
- }
362
-
363
- # حساب النسبة الإجمالية للمحتوى المحلي للمنتجات
364
- local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0
365
-
366
- return {
367
- 'نسبة': local_content_percentage,
368
- 'إجمالي_التكلفة': total_cost,
369
- 'قيمة_المحتوى_المحلي': local_content_value,
370
- 'تفاصيل': details
371
- }
372
-
373
- def _calculate_services_local_content(self, services_data):
374
- """
375
- حساب نسبة المحتوى المحلي للخدمات
376
-
377
- المعلمات:
378
- services_data: بيانات الخدمات المستخدمة في المشروع
379
-
380
- إرجاع:
381
- تفاصيل نسبة المحتوى المحلي للخدمات
382
- """
383
- total_cost = 0
384
- local_content_value = 0
385
- details = {}
386
-
387
- for service_name, service_info in services_data.items():
388
- cost = service_info.get('التكلفة', 0)
389
-
390
- # البحث عن نسبة المحتوى المحلي للخدمة
391
- local_content_percentage = 0
392
- if service_name in self.local_services:
393
- local_content_percentage = self.local_services[service_name]['نسبة_المحتوى_المحلي']
394
-
395
- # حساب قيمة المحتوى المحلي للخدمة
396
- service_local_content_value = cost * local_content_percentage
397
-
398
- # تحديث الإجماليات
399
- total_cost += cost
400
- local_content_value += service_local_content_value
401
-
402
- # تسجيل التفاصيل
403
- details[service_name] = {
404
- 'التكلفة': cost,
405
- 'نسبة_المحتوى_المحلي': local_content_percentage,
406
- 'قيمة_المحتوى_المحلي': service_local_content_value,
407
- 'مصدر': self.local_services.get(service_name, {}).get('مصدر', 'غير معروف'),
408
- 'ملاحظات': self.local_services.get(service_name, {}).get('ملاحظات', '')
409
- }
410
-
411
- # حساب النسبة الإجمالية للمحتوى المحلي للخدمات
412
- local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0
413
-
414
- return {
415
- 'نسبة': local_content_percentage,
416
- 'إجمالي_التكلفة': total_cost,
417
- 'قيمة_المحتوى_المحلي': local_content_value,
418
- 'تفاصيل': details
419
- }
420
-
421
- def _calculate_labor_local_content(self, labor_data):
422
- """
423
- حساب نسبة المحتوى المحلي للقوى العاملة
424
-
425
- المعلمات:
426
- labor_data: بيانات القوى العاملة المستخدمة في المشروع
427
-
428
- إرجاع:
429
- تفاصيل نسبة المحتوى المحلي للقوى العاملة
430
- """
431
- total_cost = 0
432
- local_content_value = 0
433
- details = {}
434
-
435
- for labor_type, labor_info in labor_data.items():
436
- count = labor_info.get('العدد', 0)
437
- monthly_salary = labor_info.get('الراتب_الشهري', 0)
438
- duration_months = labor_info.get('المدة_بالأشهر', 0)
439
-
440
- total_labor_cost = count * monthly_salary * duration_months
441
-
442
- # البحث عن نسبة المحتوى المحلي للقوى العاملة
443
- local_content_percentage = 0
444
- if labor_type in self.local_labor:
445
- local_content_percentage = self.local_labor[labor_type]['نسبة_المحتوى_المحلي']
446
-
447
- # حساب قيمة المحتوى المحلي للقوى العاملة
448
- labor_local_content_value = total_labor_cost * local_content_percentage
449
-
450
- # تحديث الإجماليات
451
- total_cost += total_labor_cost
452
- local_content_value += labor_local_content_value
453
-
454
- # تسجيل التفاصيل
455
- details[labor_type] = {
456
- 'العدد': count,
457
- 'الراتب_الشهري': monthly_salary,
458
- 'المدة_بالأشهر': duration_months,
459
- 'التكلفة_الإجمالية': total_labor_cost,
460
- 'نسبة_المحتوى_المحلي': local_content_percentage,
461
- 'قيمة_المحتوى_المحلي': labor_local_content_value,
462
- 'مصدر': self.local_labor.get(labor_type, {}).get('مصدر', 'غير معروف'),
463
- 'ملاحظات': self.local_labor.get(labor_type, {}).get('ملاحظات', '')
464
- }
465
-
466
- # حساب النسبة الإجمالية للمحتوى المحلي للقوى العاملة
467
- local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0
468
-
469
- return {
470
- 'نسبة': local_content_percentage,
471
- 'إجمالي_التكلفة': total_cost,
472
- 'قيمة_المحتوى_المحلي': local_content_value,
473
- 'تفاصيل': details
474
- }
475
-
476
- def _generate_improvement_recommendations(self, results):
477
- """
478
- توليد توصيات لتحسين نسبة المحتوى المحلي
479
-
480
- المعلمات:
481
- results: نتائج حساب المحتوى المحلي
482
-
483
- إرجاع:
484
- قائمة بالتوصيات لتحسين نسبة المحتوى المحلي
485
- """
486
- recommendations = []
487
-
488
- # تحليل المكونات التي تحتاج إلى تحسين
489
- for component, summary in results['ملخص_المحتوى_المحلي'].items():
490
- if summary['الحالة'] == 'غير مطابق':
491
- if component == 'المنتجات':
492
- # تحديد المنتجات ذات المحتوى المحلي المنخفض
493
- low_content_products = []
494
- for product, details in results['تفاصيل_المكونات']['المنتجات']['تفاصيل'].items():
495
- if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50%
496
- low_content_products.append({
497
- 'اسم': product,
498
- 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'],
499
- 'التكلفة_الإجمالية': details['التكلفة_الإجمالية']
500
- })
501
-
502
- elif component == 'الخدمات':
503
- # تحديد البنود ذات المحتوى المحلي المنخفض
504
- low_content_services = []
505
- for service, details in results['تفاصيل_المكونات']['الخدمات']['تفاصيل'].items():
506
- if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50%
507
- low_content_services.append({
508
- 'اسم': service,
509
- 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'],
510
- 'التكلفة': details['التكلفة']
511
- })
512
-
513
- elif component == 'القوى العاملة':
514
- # تحديد فئات العمالة ذات المحتوى المحلي المنخفض
515
- low_content_labor = []
516
- for labor_type, details in results['تفاصيل_المكونات']['القوى العاملة']['تفاصيل'].items():
517
- if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50%
518
- low_content_labor.append({
519
- 'اسم': labor_type,
520
- 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'],
521
- 'التكلفة_الإجمالية': details['التكلفة_الإجمالية']
522
- })
523
-
524
- # إنشاء توصيات لتحسين المحتوى المحلي
525
- # توصيات للمنتجات
526
- if 'المنتجات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['المنتجات']['الحالة'] == 'غير مطابق':
527
- low_content_products = []
528
- for product, details in results['تفاصيل_المكونات']['المنتجات']['تفاصيل'].items():
529
- if details['نسبة_المحتوى_المحلي'] < 0.5:
530
- low_content_products.append({
531
- 'اسم': product,
532
- 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي']
533
- })
534
-
535
- if low_content_products:
536
- recommendations.append(f"استبدال المنتجات ذات المحتوى المحلي المنخفض: {', '.join([p['اسم'] for p in low_content_products[:3]])}")
537
- recommendations.append("البحث عن موردين محليين للمنتجات ذات الأولوية العالية")
538
-
539
- # توصيات للخدمات
540
- if 'الخدمات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['الخدمات']['الحالة'] == 'غير مطابق':
541
- low_content_services = []
542
- for service, details in results['تفاصيل_المكونات']['الخدمات']['تفاصيل'].items():
543
- if details['نسبة_المحتوى_المحلي'] < 0.5:
544
- low_content_services.append({
545
- 'اسم': service,
546
- 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي']
547
- })
548
-
549
- if low_content_services:
550
- recommendations.append(f"تحسين نسبة المحتوى المحلي للخدمات: {', '.join([s['اسم'] for s in low_content_services[:3]])}")
551
- recommendations.append("التعاقد مع شركات خدمية محلية")
552
-
553
- # توصيات للقوى العاملة
554
- if 'القوى العاملة' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['القوى العاملة']['الحالة'] == 'غير مطابق':
555
- low_content_labor = []
556
- for labor_type, details in results['تفاصيل_المكونات']['القوى العاملة']['تفاصيل'].items():
557
- if details['نسبة_المحتوى_المحلي'] < 0.5:
558
- low_content_labor.append({
559
- 'اسم': labor_type,
560
- 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي']
561
- })
562
-
563
- if low_content_labor:
564
- recommendations.append(f"زيادة توظيف العمالة المحلية في الفئات: {', '.join([l['اسم'] for l in low_content_labor[:3]])}")
565
- recommendations.append("الاستثما�� في برامج تدريب وتأهيل الكوادر المحلية")
566
-
567
- # توصيات عامة
568
- if 'المنتجات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['المنتجات'].get('الفجوة (%)', 0) > 10:
569
- recommendations.append(f"خطة تطوير المحتوى المحلي للمنتجات لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['المنتجات']['الفجوة (%)']}%")
570
-
571
- if 'الخدمات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['الخدمات'].get('الفجوة (%)', 0) > 10:
572
- recommendations.append(f"خطة تطوير المحتوى المحلي للخدمات لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['الخدمات']['الفجوة (%)']}%")
573
-
574
- if 'القوى العاملة' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['القوى العاملة'].get('الفجوة (%)', 0) > 10:
575
- recommendations.append(f"خطة تطوير المحتوى المحلي للقوى العاملة لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['القوى العاملة']['الفجوة (%)']}%")
576
-
577
- return recommendations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/services/price_prediction.py DELETED
@@ -1,444 +0,0 @@
1
- """
2
- خدمة التنبؤ بالأسعار
3
- """
4
-
5
- import pandas as pd
6
- import numpy as np
7
- import joblib
8
- import os
9
- from datetime import datetime, timedelta
10
- from sklearn.ensemble import RandomForestRegressor
11
- from sklearn.model_selection import train_test_split
12
- from sklearn.preprocessing import StandardScaler
13
- from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
14
-
15
- import config
16
-
17
-
18
- class PricePrediction:
19
- """خدمة التنبؤ بالأسعار باستخدام التعلم الآلي"""
20
-
21
- def __init__(self):
22
- """تهيئة خدمة التنبؤ بالأسعار"""
23
- self.model_path = config.PRICE_PREDICTION_MODEL
24
- self.model = self._load_model()
25
- self.scaler = None
26
- self.materials_data = self._load_materials_data()
27
- self.market_indices = self._load_market_indices()
28
-
29
- def _load_model(self):
30
- """تحميل نموذج التنبؤ المدرب مسبقاً"""
31
- try:
32
- if os.path.exists(self.model_path):
33
- model = joblib.load(self.model_path)
34
- return model
35
- else:
36
- # إذا لم يكن النموذج موجوداً، قم بإنشاء نموذج جديد
37
- model = RandomForestRegressor(
38
- n_estimators=100,
39
- max_depth=15,
40
- min_samples_split=5,
41
- min_samples_leaf=2,
42
- random_state=42
43
- )
44
- return model
45
- except Exception as e:
46
- print(f"خطأ في تحميل نموذج التنبؤ: {str(e)}")
47
- return RandomForestRegressor(random_state=42)
48
-
49
- def _load_materials_data(self):
50
- """تحميل بيانات المواد وأسعارها التاريخية"""
51
- # محاكاة تحميل البيانات من مصدر بيانات
52
- materials_data = {
53
- 'خرسانة': {
54
- 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
55
- 'سعر': [750, 740, 735, 730, 720, 715, 710, 700, 695, 690, 685, 680],
56
- 'وحدة': 'م3'
57
- },
58
- 'حديد تسليح': {
59
- 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
60
- 'سعر': [5500, 5450, 5400, 5350, 5300, 5250, 5200, 5150, 5100, 5050, 5000, 4950],
61
- 'وحدة': 'طن'
62
- },
63
- 'إسمنت': {
64
- 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
65
- 'سعر': [25, 25, 24.5, 24.5, 24, 24, 23.5, 23.5, 23, 23, 22.5, 22.5],
66
- 'وحدة': 'كيس'
67
- },
68
- 'رمل': {
69
- 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
70
- 'سعر': [140, 140, 135, 135, 130, 130, 125, 125, 120, 120, 115, 115],
71
- 'وحدة': 'م3'
72
- },
73
- 'بلوك خرساني': {
74
- 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
75
- 'سعر': [11, 11, 10.5, 10.5, 10, 10, 9.5, 9.5, 9, 9, 8.5, 8.5],
76
- 'وحدة': 'قطعة'
77
- }
78
- }
79
- return materials_data
80
-
81
- def _load_market_indices(self):
82
- """تحميل مؤشرات السوق المؤثرة على الأسعار"""
83
- # محاكاة تحميل البيانات من مصدر بيانات
84
- market_indices = {
85
- 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
86
- 'مؤشر_البناء': [105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94],
87
- 'مؤشر_النفط': [80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69],
88
- 'مؤشر_سعر_الصرف': [3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75],
89
- 'مؤشر_التضخم': [2.5, 2.4, 2.3, 2.2, 2.1, 2.0, 1.9, 1.8, 1.7, 1.6, 1.5, 1.4]
90
- }
91
- return market_indices
92
-
93
- def train(self, training_data=None):
94
- """
95
- تدريب نموذج التنبؤ بالأسعار
96
-
97
- المعلمات:
98
- training_data: بيانات التدريب (اختياري)، إذا لم يتم توفيرها سيتم استخدام البيانات المتاحة
99
-
100
- إرجاع:
101
- مؤشرات أداء النموذج
102
- """
103
- # تجهيز بيانات التدريب
104
- if training_data is None:
105
- # استخدام البيانات المتاحة لتوليد مجموعة تدريب
106
- X, y = self._prepare_training_data()
107
- else:
108
- X, y = self._extract_features_target(training_data)
109
-
110
- # تقسيم البيانات إلى تدريب واختبار
111
- X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
112
-
113
- # تطبيع البيانات
114
- self.scaler = StandardScaler()
115
- X_train_scaled = self.scaler.fit_transform(X_train)
116
- X_test_scaled = self.scaler.transform(X_test)
117
-
118
- # تدريب النموذج
119
- self.model.fit(X_train_scaled, y_train)
120
-
121
- # تقييم النموذج
122
- y_pred = self.model.predict(X_test_scaled)
123
-
124
- # حساب مؤشرات الأداء
125
- mae = mean_absolute_error(y_test, y_pred)
126
- rmse = np.sqrt(mean_squared_error(y_test, y_pred))
127
- r2 = r2_score(y_test, y_pred)
128
-
129
- # حفظ النموذج
130
- try:
131
- joblib.dump(self.model, self.model_path)
132
- joblib.dump(self.scaler, os.path.join(os.path.dirname(self.model_path), 'price_scaler.pkl'))
133
- except Exception as e:
134
- print(f"خطأ في حفظ النموذج: {str(e)}")
135
-
136
- return {
137
- 'mae': mae,
138
- 'rmse': rmse,
139
- 'r2': r2
140
- }
141
-
142
- def _prepare_training_data(self):
143
- """تجهيز بيانات التدريب من البيانات المتاحة"""
144
- # توليد بيانات تدريب افتراضية
145
- data = []
146
- target = []
147
-
148
- # استخدام بيانات المواد وأسعارها التاريخية
149
- for material_name, material_info in self.materials_data.items():
150
- for i in range(len(material_info['تاريخ'])):
151
- # استخراج المؤشرات في التاريخ المقابل
152
- date_index = self.market_indices['تاريخ'].index(material_info['تاريخ'][i]) if material_info['تاريخ'][i] in self.market_indices['تاريخ'] else 0
153
-
154
- # تكوين ميزات التدريب (المؤشرات السوقية والشهر)
155
- features = [
156
- material_info['تاريخ'][i].month, # الشهر
157
- self.market_indices['مؤشر_البناء'][date_index],
158
- self.market_indices['مؤشر_النفط'][date_index],
159
- self.market_indices['مؤشر_سعر_الصرف'][date_index],
160
- self.market_indices['مؤشر_التضخم'][date_index]
161
- ]
162
-
163
- # إضافة معرّف للمادة (تمثيل رقمي)
164
- material_id = list(self.materials_data.keys()).index(material_name)
165
- features.append(material_id)
166
-
167
- data.append(features)
168
- target.append(material_info['سعر'][i])
169
-
170
- # إضافة ضوضاء عشوائية لزيادة حجم البيانات
171
- for _ in range(5):
172
- noisy_features = features.copy()
173
- for j in range(1, 5): # إضافة ضوضاء للمؤشرات فقط
174
- noisy_features[j] += np.random.normal(0, 0.5)
175
-
176
- noisy_price = material_info['سعر'][i] * (1 + np.random.normal(0, 0.02)) # ضوضاء 2%
177
-
178
- data.append(noisy_features)
179
- target.append(noisy_price)
180
-
181
- return np.array(data), np.array(target)
182
-
183
- def _extract_features_target(self, training_data):
184
- """استخراج الميزات والأهداف من بيانات التدريب"""
185
- # استخراج الميزات والأهداف من البيانات المقدمة
186
- features = []
187
- target = []
188
-
189
- for item in training_data:
190
- features.append([
191
- item['date'].month, # الشهر
192
- item['building_index'],
193
- item['oil_index'],
194
- item['exchange_rate'],
195
- item['inflation_rate'],
196
- item['material_id']
197
- ])
198
- target.append(item['price'])
199
-
200
- return np.array(features), np.array(target)
201
-
202
- def predict_prices(self, materials, prediction_date=None, market_conditions=None):
203
- """
204
- التنبؤ بأسعار المواد
205
-
206
- المعلمات:
207
- materials: قائمة المواد المطلوب التنبؤ بأسعارها
208
- prediction_date: تاريخ التنبؤ (اختياري)، إذا لم يتم توفيره سيتم استخدام التاريخ الحالي
209
- market_conditions: ظروف السوق (اختياري)، إذا لم يتم توفيرها سيتم استخدام آخر قيم متاحة
210
-
211
- إرجاع:
212
- قاموس بأسعار المواد المتنبأ بها
213
- """
214
- if prediction_date is None:
215
- prediction_date = datetime.now()
216
-
217
- if market_conditions is None:
218
- # استخدام آخر قيم متاحة للمؤشرات
219
- market_conditions = {
220
- 'مؤشر_البناء': self.market_indices['مؤشر_البناء'][0],
221
- 'مؤشر_النفط': self.market_indices['مؤشر_النفط'][0],
222
- 'مؤشر_سعر_الصرف': self.market_indices['مؤشر_سعر_الصرف'][0],
223
- 'مؤشر_التضخم': self.market_indices['مؤشر_التضخم'][0]
224
- }
225
-
226
- # التحقق من وجود المواد في البيانات
227
- material_names = list(self.materials_data.keys())
228
- valid_materials = [m for m in materials if m in material_names]
229
-
230
- if not valid_materials:
231
- return {}
232
-
233
- # تحميل المعايير إذا كانت متوفرة
234
- scaler_path = os.path.join(os.path.dirname(self.model_path), 'price_scaler.pkl')
235
- if self.scaler is None and os.path.exists(scaler_path):
236
- try:
237
- self.scaler = joblib.load(scaler_path)
238
- except Exception as e:
239
- print(f"خطأ في تحميل المعايير: {str(e)}")
240
- # إنشاء معايير جديدة
241
- X, _ = self._prepare_training_data()
242
- self.scaler = StandardScaler()
243
- self.scaler.fit(X)
244
-
245
- # إعداد ميزات التنبؤ
246
- features = []
247
- for material in valid_materials:
248
- material_id = material_names.index(material)
249
-
250
- material_features = [
251
- prediction_date.month, # الشهر
252
- market_conditions['مؤشر_البناء'],
253
- market_conditions['مؤشر_النفط'],
254
- market_conditions['مؤشر_سعر_الصرف'],
255
- market_conditions['مؤشر_التضخم'],
256
- material_id
257
- ]
258
-
259
- features.append(material_features)
260
-
261
- # تطبيع الميزات
262
- if self.scaler is not None:
263
- features_scaled = self.scaler.transform(features)
264
- else:
265
- features_scaled = features
266
-
267
- # التنبؤ بالأسعار
268
- predicted_prices = self.model.predict(features_scaled)
269
-
270
- # إرجاع النتائج
271
- results = {}
272
- for i, material in enumerate(valid_materials):
273
- # تطبيق عامل تصحيح (2% عشوائية)
274
- correction_factor = 1.0 + np.random.uniform(-0.02, 0.02)
275
- price = max(0, predicted_prices[i] * correction_factor)
276
-
277
- results[material] = {
278
- 'سعر': price,
279
- 'وحدة': self.materials_data[material]['وحدة'],
280
- 'تاريخ_التنبؤ': prediction_date.strftime('%Y-%m-%d'),
281
- 'هامش_الخطأ': '±5%' # تقدير هامش الخطأ
282
- }
283
-
284
- return results
285
-
286
- def get_price_trends(self, material, periods=6):
287
- """
288
- الحصول على اتجاهات الأسعار المستقبلية
289
-
290
- المعلمات:
291
- material: المادة المطلوب التنبؤ باتجاهات أسعارها
292
- periods: عدد الفترات المستقبلية (الشهور)
293
-
294
- إرجاع:
295
- قائمة بالأسعار المتوقعة للفترات المستقبلية
296
- """
297
- if material not in self.materials_data:
298
- return []
299
-
300
- # الحصول على التاريخ الحالي
301
- current_date = datetime.now()
302
-
303
- # التنبؤ بالأسعار للفترات المستقبلية
304
- price_trends = []
305
-
306
- for i in range(periods):
307
- prediction_date = current_date + timedelta(days=30 * (i + 1))
308
-
309
- # افتراض تغيرات طفيفة في المؤشرات مع مرور الوقت
310
- market_conditions = {
311
- 'مؤشر_البناء': self.market_indices['مؤشر_البناء'][0] * (1 + 0.01 * i), # زيادة 1% شهرياً
312
- 'مؤشر_النفط': self.market_indices['مؤشر_النفط'][0] * (1 + 0.005 * i), # زيادة 0.5% شهرياً
313
- 'مؤشر_سعر_الصرف': self.market_indices['مؤشر_سعر_الصرف'][0], # ثابت
314
- 'مؤشر_التضخم': self.market_indices['مؤشر_التضخم'][0] * (1 + 0.01 * i) # زيادة 1% شهرياً
315
- }
316
-
317
- # التنبؤ بالسعر
318
- predicted_price = self.predict_prices([material], prediction_date, market_conditions)
319
-
320
- price_trends.append({
321
- 'تاريخ': prediction_date.strftime('%Y-%m'),
322
- 'سعر': predicted_price[material]['سعر'] if material in predicted_price else 0
323
- })
324
-
325
- return price_trends
326
-
327
- def analyze_factors(self, material):
328
- """
329
- تحليل العوامل المؤثرة على سعر المادة
330
-
331
- المعلمات:
332
- material: المادة المطلوب تحليلها
333
-
334
- إرجاع:
335
- قاموس ب��لعوامل المؤثرة وأهميتها النسبية
336
- """
337
- if material not in self.materials_data or not hasattr(self.model, 'feature_importances_'):
338
- return {}
339
-
340
- # الحصول على أهمية الميزات من النموذج
341
- feature_importances = self.model.feature_importances_
342
-
343
- # أسماء الميزات
344
- feature_names = ['الشهر', 'مؤشر البناء', 'مؤشر النفط', 'سعر الصرف', 'معدل التضخم', 'نوع المادة']
345
-
346
- # ترتيب الميزات حسب الأهمية
347
- importance_pairs = [(name, importance) for name, importance in zip(feature_names, feature_importances)]
348
- importance_pairs.sort(key=lambda x: x[1], reverse=True)
349
-
350
- # إرجاع العوامل المؤثرة وأهميتها
351
- factors = {}
352
- for name, importance in importance_pairs:
353
- factors[name] = round(importance * 100, 2) # تحويل إلى نسبة مئوية
354
-
355
- return {
356
- 'العوامل_المؤثرة': factors,
357
- 'المادة': material,
358
- 'وحدة': self.materials_data[material]['وحدة'],
359
- 'سعر_حالي': self.materials_data[material]['سعر'][0],
360
- 'اتجاه_السعر': self._get_price_trend(material)
361
- }
362
-
363
- def _get_price_trend(self, material):
364
- """تحديد اتجاه سعر المادة بناءً على البيانات التاريخية"""
365
- if material not in self.materials_data:
366
- return "غير معروف"
367
-
368
- prices = self.materials_data[material]['سعر']
369
- if len(prices) < 2:
370
- return "غير معروف"
371
-
372
- # حساب متوسط التغير الشهري
373
- price_changes = [(prices[i] - prices[i+1]) / prices[i+1] * 100 for i in range(len(prices)-1)]
374
- avg_monthly_change = sum(price_changes) / len(price_changes)
375
-
376
- if avg_monthly_change > 1:
377
- return "ارتفاع حاد"
378
- elif avg_monthly_change > 0.2:
379
- return "ارتفاع معتدل"
380
- elif avg_monthly_change > -0.2:
381
- return "استقرار"
382
- elif avg_monthly_change > -1:
383
- return "انخفاض معتدل"
384
- else:
385
- return "انخفاض حاد"
386
-
387
- def export_price_forecast(self, materials, periods=6, output_file=None):
388
- """
389
- تصدير توقعات الأسعار إلى ملف
390
-
391
- المعلمات:
392
- materials: قائمة المواد المطلوب التنبؤ بأسعارها
393
- periods: عدد الفترات المستقبلية (الشهور)
394
- output_file: مسار ملف الإخراج (اختياري)
395
-
396
- إرجاع:
397
- مسار الملف المصدر أو البيانات مباشرة إذا لم يتم تحديد ملف
398
- """
399
- # التحقق من وجود المواد في البيانات
400
- valid_materials = [m for m in materials if m in self.materials_data]
401
-
402
- if not valid_materials:
403
- return None
404
-
405
- # إعداد بيانات التوقعات
406
- forecast_data = []
407
-
408
- for material in valid_materials:
409
- # الحصول على اتجاهات الأسعار
410
- price_trends = self.get_price_trends(material, periods)
411
-
412
- for trend in price_trends:
413
- forecast_data.append({
414
- 'المادة': material,
415
- 'الوحدة': self.materials_data[material]['وحدة'],
416
- 'التاريخ': trend['تاريخ'],
417
- 'السعر المتوقع': trend['سعر'],
418
- 'هامش الخطأ': '±5%'
419
- })
420
-
421
- # تحويل البيانات إلى DataFrame
422
- forecast_df = pd.DataFrame(forecast_data)
423
-
424
- # تصدير البيانات إلى ملف إذا تم تحديده
425
- if output_file:
426
- try:
427
- ext = os.path.splitext(output_file)[1].lower()
428
-
429
- if ext == '.csv':
430
- forecast_df.to_csv(output_file, index=False, encoding='utf-8-sig')
431
- elif ext in ['.xlsx', '.xls']:
432
- forecast_df.to_excel(output_file, index=False)
433
- elif ext == '.json':
434
- forecast_df.to_json(output_file, orient='records', force_ascii=False)
435
- else:
436
- print(f"تنسيق غير مدعوم: {ext}")
437
- return None
438
-
439
- return output_file
440
- except Exception as e:
441
- print(f"خطأ في تصدير توقعات الأسعار: {str(e)}")
442
- return None
443
-
444
- return forecast_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/services/standard_pricing.py DELETED
@@ -1,232 +0,0 @@
1
- """
2
- خدمة التسعير القياسي
3
- """
4
-
5
- import pandas as pd
6
- import numpy as np
7
- from datetime import datetime
8
- import os
9
- import config
10
-
11
-
12
- class StandardPricing:
13
- """خدمة التسعير القياسي للبنود"""
14
-
15
- def __init__(self):
16
- """تهيئة خدمة التسعير القياسي"""
17
- # تحميل بيانات المواد والأسعار المرجعية
18
- self.material_prices = self._load_material_prices()
19
- self.labor_rates = self._load_labor_rates()
20
- self.equipment_rates = self._load_equipment_rates()
21
-
22
- def _load_material_prices(self):
23
- """تحميل أسعار المواد"""
24
- # محاكاة تحميل البيانات من مصدر بيانات
25
- material_prices = {
26
- 'خرسانة': {
27
- 'م3': 750.0, # سعر المتر المكعب بالريال
28
- 'وحدة_قياسية': 'م3',
29
- 'آخر_تحديث': datetime(2025, 3, 1)
30
- },
31
- 'حديد تسليح': {
32
- 'طن': 5500.0, # سعر الطن بالريال
33
- 'وحدة_قياسية': 'طن',
34
- 'آخر_تحديث': datetime(2025, 3, 1)
35
- },
36
- 'عزل مائي': {
37
- 'م2': 80.0, # سعر المتر المربع بالريال
38
- 'وحدة_قياسية': 'م2',
39
- 'آخر_تحديث': datetime(2025, 3, 1)
40
- },
41
- 'بلوك خرساني': {
42
- '20سم': 11.0, # سعر البلكة بالريال
43
- 'وحدة_قياسية': 'عدد',
44
- 'آخر_تحديث': datetime(2025, 3, 1)
45
- },
46
- 'رمل': {
47
- 'م3': 140.0, # سعر المتر المكعب بالريال
48
- 'وحدة_قياسية': 'م3',
49
- 'آخر_تحديث': datetime(2025, 3, 1)
50
- },
51
- 'اسمنت': {
52
- 'كيس': 25.0, # سعر الكيس بالريال
53
- 'وحدة_قياسية': 'كيس',
54
- 'آخر_تحديث': datetime(2025, 3, 1)
55
- }
56
- }
57
- return material_prices
58
-
59
- def _load_labor_rates(self):
60
- """تحميل معدلات أجور العمالة"""
61
- # محاكاة تحميل البيانات من مصدر بيانات
62
- labor_rates = {
63
- 'عامل': {
64
- 'يومي': 150.0, # الأجر اليومي بالريال
65
- 'وحدة_قياسية': 'يوم',
66
- 'آخر_تحديث': datetime(2025, 3, 1)
67
- },
68
- 'نجار': {
69
- 'يومي': 250.0, # الأجر اليومي بالريال
70
- 'وحدة_قياسية': 'يوم',
71
- 'آخر_تحديث': datetime(2025, 3, 1)
72
- },
73
- 'حداد': {
74
- 'يومي': 250.0, # الأجر اليومي بالريال
75
- 'وحدة_قياسية': 'يوم',
76
- 'آخر_تحديث': datetime(2025, 3, 1)
77
- },
78
- 'سباك': {
79
- 'يومي': 300.0, # الأجر اليومي بالريال
80
- 'وحدة_قياسية': 'يوم',
81
- 'آخر_تحديث': datetime(2025, 3, 1)
82
- },
83
- 'كهربائي': {
84
- 'يومي': 300.0, # الأجر اليومي بالريال
85
- 'وحدة_قياسية': 'يوم',
86
- 'آخر_تحديث': datetime(2025, 3, 1)
87
- },
88
- 'مراقب': {
89
- 'يومي': 400.0, # الأجر اليومي بالريال
90
- 'وحدة_قياسية': 'يوم',
91
- 'آخر_تحديث': datetime(2025, 3, 1)
92
- }
93
- }
94
- return labor_rates
95
-
96
- def _load_equipment_rates(self):
97
- """تحميل معدلات تأجير المعدات"""
98
- # محاكاة تحميل البيانات من مصدر بيانات
99
- equipment_rates = {
100
- 'خلاطة خرسانة': {
101
- 'يومي': 800.0, # الإيجار اليومي بالريال
102
- 'وحدة_قياسية': 'يوم',
103
- 'آخر_تحديث': datetime(2025, 3, 1)
104
- },
105
- 'هزاز خرسانة': {
106
- 'يومي': 150.0, # الإيجار اليومي بالريال
107
- 'وحدة_قياسية': 'يوم',
108
- 'آخر_تحديث': datetime(2025, 3, 1)
109
- },
110
- 'حفارة': {
111
- 'يومي': 1500.0, # الإيجار اليومي بالريال
112
- 'وحدة_قياسية': 'يوم',
113
- 'آخر_تحديث': datetime(2025, 3, 1)
114
- },
115
- 'لودر': {
116
- 'يومي': 1200.0, # الإيجار اليومي بالريال
117
- 'وحدة_قياسية': 'يوم',
118
- 'آخر_تحديث': datetime(2025, 3, 1)
119
- },
120
- 'رافعة': {
121
- 'يومي': 2000.0, # الإيجار اليومي بالريال
122
- 'وحدة_قياسية': 'يوم',
123
- 'آخر_تحديث': datetime(2025, 3, 1)
124
- },
125
- 'شاحنة نقل': {
126
- 'يومي': 900.0, # الإيجار اليومي بالريال
127
- 'وحدة_قياسية': 'يوم',
128
- 'آخر_تحديث': datetime(2025, 3, 1)
129
- }
130
- }
131
- return equipment_rates
132
-
133
- def calculate_prices(self, items_df):
134
- """حساب الأسعار للبنود باستخدام التسعير القياسي"""
135
- # نسخة من البيانات المدخلة للعمل عليها
136
- df = items_df.copy()
137
-
138
- # التأكد من وجود العمود المطلوب
139
- if 'سعر الوحدة' not in df.columns:
140
- df['سعر الوحدة'] = 0.0
141
-
142
- if 'الإجمالي' not in df.columns:
143
- df['الإجمالي'] = 0.0
144
-
145
- # حساب أسعار الوحدات لكل بند
146
- for idx, row in df.iterrows():
147
- # حساب سعر الوحدة بناءً على وصف البند
148
- unit_price = self._estimate_unit_price(row['وصف البند'], row['الوحدة'])
149
- df.at[idx, 'سعر الوحدة'] = unit_price
150
-
151
- # حساب الإجمالي لكل بند
152
- df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
153
-
154
- return df
155
-
156
- def _estimate_unit_price(self, description, unit):
157
- """تقدير سعر الوحدة بناءً على وصف البند ووحدة القياس"""
158
- description = description.lower()
159
-
160
- # تقدير سعر الوحدة بناءً على وصف البند
161
- if 'خرسان' in description:
162
- if 'أساسات' in description:
163
- return 1200.0 if unit == 'م3' else 0.0
164
- elif 'أعمدة' in description:
165
- return 1800.0 if unit == 'م3' else 0.0
166
- elif 'سقف' in description:
167
- return 1500.0 if unit == 'م3' else 0.0
168
- else:
169
- return 1400.0 if unit == 'م3' else 0.0
170
-
171
- elif 'حديد' in description and 'تسليح' in description:
172
- if 'أساسات' in description:
173
- return 6000.0 if unit == 'طن' else 0.0
174
- elif 'أعمدة' in description or 'سقف' in description:
175
- return 6500.0 if unit == 'طن' else 0.0
176
- else:
177
- return 6200.0 if unit == 'طن' else 0.0
178
-
179
- elif 'عزل' in description:
180
- if 'مائي' in description:
181
- return 120.0 if unit == 'م2' else 0.0
182
- elif 'حراري' in description:
183
- return 90.0 if unit == 'م2' else 0.0
184
- else:
185
- return 100.0 if unit == 'م2' else 0.0
186
-
187
- elif 'ردم' in description or 'حفر' in description:
188
- return 75.0 if unit == 'م3' else 0.0
189
-
190
- elif 'بلوك' in description or 'طوب' in description:
191
- return 250.0 if unit == 'م2' else 0.0
192
-
193
- elif 'لياسة' in description or 'بياض' in description:
194
- return 80.0 if unit == 'م2' else 0.0
195
-
196
- elif 'دهان' in description or 'طلاء' in description:
197
- return 65.0 if unit == 'م2' else 0.0
198
-
199
- elif 'سيراميك' in description or 'بلاط' in description:
200
- return 180.0 if unit == 'م2' else 0.0
201
-
202
- elif 'كهرباء' in description:
203
- return 150.0 if unit == 'نقطة' else 500.0
204
-
205
- # قيمة افتراضية إذا لم تتطابق مع أي وصف
206
- return 100.0
207
-
208
- def adjust_prices_for_factors(self, items_df, factors=None):
209
- """تعديل الأسعار بناءً على عوامل مؤثرة"""
210
- # نسخة من البيانات المدخلة للعمل عليها
211
- df = items_df.copy()
212
-
213
- # إذا لم يتم تحديد عوامل، استخدم العوامل الافتراضية
214
- if factors is None:
215
- factors = {
216
- 'location_factor': 1.0, # معامل الموقع
217
- 'time_factor': 1.0, # معامل الوقت
218
- 'risk_factor': 1.1, # معامل المخاطر
219
- 'market_factor': 1.05 # معامل السوق
220
- }
221
-
222
- # حساب المعامل الإجمالي
223
- total_factor = (factors['location_factor'] * factors['time_factor'] *
224
- factors['risk_factor'] * factors['market_factor'])
225
-
226
- # تعديل سعر الوحدة بناءً على المعامل ا��إجمالي
227
- df['سعر الوحدة'] = df['سعر الوحدة'] * total_factor
228
-
229
- # حساب الإجمالي بعد التعديل
230
- df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
231
-
232
- return df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/services/templates_catalog/__init__.py DELETED
@@ -1,3 +0,0 @@
1
- from .templates_catalog import TemplatesCatalog
2
-
3
- __all__ = ['TemplatesCatalog']
 
 
 
 
modules/pricing/services/templates_catalog/templates_catalog.py DELETED
@@ -1,949 +0,0 @@
1
- """
2
- كتالوج قوالب البناء والمقاولات
3
- واجهة مستخدم متكاملة لعرض واستخدام نماذج بنود البناء الجاهزة
4
- """
5
-
6
- import os
7
- import sys
8
- import json
9
- import pandas as pd
10
- import streamlit as st
11
- from typing import Dict, List, Any, Optional
12
-
13
- # إضافة مسار النظام للوصول للملفات المشتركة
14
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")))
15
-
16
- # استيراد مكونات واجهة المستخدم
17
- from utils.components.header import render_header
18
- # استيراد الدالة مباشرة من الملف
19
- from utils.components.credits import display_credits
20
- # استخدام display_credits كبديل لـ render_credits
21
- render_credits = display_credits
22
- from utils.helpers import format_number, format_currency, styled_button, filter_dataframe
23
-
24
- # النموذج المستخدم عند إضافة بند نموذجي جديد من صفحة التسعير
25
- class NewTemplateForm:
26
- """نموذج إضافة بند نموذجي جديد من صفحة التسعير"""
27
- pass
28
-
29
- class TemplatesCatalog:
30
- """كتالوج قوالب البناء والمقاولات"""
31
-
32
- def __init__(self, construction_templates):
33
- """تهيئة كتالوج قوالب البناء والمقاولات"""
34
- self.construction_templates = construction_templates
35
-
36
- # تهيئة حالة الجلسة للطلبات والعروض
37
- if 'material_requests' not in st.session_state:
38
- st.session_state.material_requests = []
39
-
40
- if 'equipment_requests' not in st.session_state:
41
- st.session_state.equipment_requests = []
42
-
43
- if 'material_offers' not in st.session_state:
44
- st.session_state.material_offers = []
45
-
46
- if 'equipment_offers' not in st.session_state:
47
- st.session_state.equipment_offers = []
48
-
49
- # تهيئة قوائم الأسعار المرجعية
50
- if 'reference_price_list' not in st.session_state:
51
- st.session_state.reference_price_list = []
52
-
53
- # تعبئة قوائم الأسعار المرجعية من كتالوج البنود النموذجية
54
- self._populate_reference_price_list()
55
-
56
- def render(self):
57
- """عرض واجهة كتالوج القوالب"""
58
- # عرض الشعار والعنوان الرئيسي
59
- render_header("كتالوج بنود المقاولات النموذجية")
60
-
61
- # تبويبات الكتالوج
62
- tabs = st.tabs(["تصفح البنود النموذجية", "طلب تسعير مواد جديدة", "طلب تسعير معدات جديدة", "قوائم الأسعار المرجعية"])
63
-
64
- with tabs[0]:
65
- self._render_templates_browser()
66
-
67
- with tabs[1]:
68
- self._render_material_request_form()
69
-
70
- with tabs[2]:
71
- self._render_equipment_request_form()
72
-
73
- with tabs[3]:
74
- self._render_reference_price_list()
75
-
76
- def _render_templates_browser(self):
77
- """عرض واجهة تصفح كتالوج القوالب"""
78
- st.markdown("""
79
- <div class='custom-box info-box'>
80
- <h3>🗃️ نماذج بنود المقاولات الجاهزة للاستخدام</h3>
81
- <p>يمكنك الاختيار من بين مجموعة متنوعة من نماذج البنود المعرفة مسبقًا والجاهزة للاستخدام في مشاريعك وعروض أسعارك.</p>
82
- <p>تشمل النماذج تفاصيل كاملة عن:</p>
83
- <ul>
84
- <li>المواد المستخدمة وكمياتها</li>
85
- <li>العمالة المطلوبة ومدة العمل</li>
86
- <li>المعدات اللازمة وتكلفتها</li>
87
- <li>المصروفات الإدارية وهامش الربح</li>
88
- </ul>
89
- </div>
90
- """, unsafe_allow_html=True)
91
-
92
- # قسم البحث والتصفية
93
- st.markdown("### تصفية البنود النموذجية")
94
-
95
- # الحصول على الفئات من الكتالوج
96
- templates = self.construction_templates.get_all_templates()
97
- categories = templates.get("categories", {})
98
-
99
- # إنشاء قائمة الفئات
100
- category_list = [{"id": cat_id, "name": cat_data.get("name", cat_id)} for cat_id, cat_data in categories.items()]
101
- category_names = ["الكل"] + [cat["name"] for cat in category_list]
102
-
103
- # تصفية حسب الفئة
104
- col1, col2 = st.columns(2)
105
- with col1:
106
- selected_category_name = st.selectbox("فئة البنود", category_names, index=0)
107
-
108
- with col2:
109
- search_query = st.text_input("بحث في النماذج", placeholder="اكتب كلمة للبحث...")
110
-
111
- # تحديد الفئة المختارة
112
- selected_category_id = None
113
- if selected_category_name != "الكل":
114
- for cat in category_list:
115
- if cat["name"] == selected_category_name:
116
- selected_category_id = cat["id"]
117
- break
118
-
119
- # الحصول على النماذج المصفاة
120
- filtered_templates = []
121
- all_templates = templates.get("templates", {})
122
-
123
- for template_id, template in all_templates.items():
124
- # التصفية حسب الفئة إذا تم اختيار فئة محددة
125
- if selected_category_id and template.get("category") != selected_category_id:
126
- continue
127
-
128
- # التصفية حسب البحث إذا تم إدخال نص للبحث
129
- if search_query:
130
- template_name = template.get("name", "")
131
- template_desc = template.get("description", "")
132
- searchable_text = f"{template_name} {template_desc}"
133
- if search_query.lower() not in searchable_text.lower():
134
- continue
135
-
136
- # إضافة النموذج إلى القائمة المصفاة
137
- template_copy = template.copy()
138
- template_copy["id"] = template_id
139
- filtered_templates.append(template_copy)
140
-
141
- # عرض النماذج المصفاة
142
- self._render_templates_list(filtered_templates)
143
-
144
- # عرض نموذج إضافة قالب جديد
145
- with st.expander("إضافة قالب نموذجي جديد"):
146
- self._render_new_template_form()
147
-
148
- # عرض الحقوق
149
- render_credits()
150
-
151
- def _render_templates_list(self, templates: List[Dict[str, Any]]):
152
- """عرض قائمة النماذج المصفاة"""
153
-
154
- if not templates:
155
- st.warning("لا توجد نماذج بنود متاحة تطابق معايير البحث.")
156
- return
157
-
158
- # تحويل النماذج إلى DataFrame
159
- templates_data = []
160
- for template in templates:
161
- # حساب التكلفة التقديرية للنموذج
162
- estimated_cost = self._calculate_template_cost(template)
163
-
164
- templates_data.append({
165
- "الرقم التعريفي": template["id"],
166
- "اسم النموذج": template.get("name", ""),
167
- "الوصف": template.get("description", "")[:50] + "..." if len(template.get("description", "")) > 50 else template.get("description", ""),
168
- "التكلفة التقديرية": estimated_cost,
169
- "الوحدة": template.get("unit", ""),
170
- "الفئة": template.get("category", "")
171
- })
172
-
173
- templates_df = pd.DataFrame(templates_data)
174
-
175
- # تنسيق الجدول
176
- def highlight_row(row):
177
- """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
178
- color = '#F0F8FF' if row.name % 2 == 0 else 'white'
179
- return ['background-color: %s' % color] * len(row)
180
-
181
- styled_df = templates_df.style.apply(highlight_row, axis=1)
182
- styled_df = styled_df.format({
183
- "التكلفة التقديرية": "{:,.2f} ريال"
184
- })
185
-
186
- # عرض الجدول
187
- st.markdown("### قائمة النماذج المتاحة")
188
- st.dataframe(styled_df, use_container_width=True, hide_index=True)
189
-
190
- # عرض تفاصيل النموذج المحدد
191
- selected_template_id = st.selectbox(
192
- "اختر نموذجًا لعرض التفاصيل",
193
- options=[t["الرقم التعريفي"] for t in templates_data],
194
- format_func=lambda x: next((t["اسم النموذج"] for t in templates_data if t["الرقم التعريفي"] == x), x)
195
- )
196
-
197
- if selected_template_id:
198
- # الحصول على النموذج المحدد
199
- selected_template = next((t for t in templates if t["id"] == selected_template_id), None)
200
- if selected_template:
201
- self._render_template_details(selected_template)
202
-
203
- def _render_template_details(self, template: Dict[str, Any]):
204
- """عرض تفاصيل النموذج المحدد"""
205
- st.markdown(f"### تفاصيل نموذج: {template.get('name', '')}")
206
-
207
- # معلومات النموذج الأساسية
208
- col1, col2, col3 = st.columns(3)
209
- with col1:
210
- st.markdown(f"**الوصف:** {template.get('description', '')}")
211
- with col2:
212
- st.markdown(f"**وحدة القياس:** {template.get('unit', '')}")
213
- with col3:
214
- # حساب التكلفة التقديرية للنموذج
215
- estimated_cost = self._calculate_template_cost(template)
216
- st.markdown(f"**التكلفة التقديرية:** {estimated_cost:,.2f} ريال")
217
-
218
- # تبويبات لعرض مكونات النموذج
219
- tabs = st.tabs(["المواد", "العمالة", "المعدات", "التكاليف"])
220
-
221
- # تبويب المواد
222
- with tabs[0]:
223
- self._render_materials_tab(template)
224
-
225
- # تبويب العمالة
226
- with tabs[1]:
227
- self._render_labor_tab(template)
228
-
229
- # تبويب المعدات
230
- with tabs[2]:
231
- self._render_equipment_tab(template)
232
-
233
- # تبويب التكاليف
234
- with tabs[3]:
235
- self._render_costs_tab(template)
236
-
237
- # أزرار العمليات
238
- col1, col2, col3 = st.columns(3)
239
-
240
- with col1:
241
- if styled_button("إضافة إلى حاسبة التكاليف", key=f"add_to_calc_{template['id']}", type="primary", icon="➕"):
242
- st.session_state.selected_template_for_calculator = template["id"]
243
- st.success("تم إضافة النموذج إلى حاسبة التكاليف!")
244
-
245
- with col2:
246
- if styled_button("استخدام في بند جديد", key=f"use_in_new_item_{template['id']}", type="success", icon="🔄"):
247
- st.session_state.selected_template_for_new_item = template["id"]
248
- st.success("تم اختيار النموذج لاستخدامه في بند جديد!")
249
-
250
- with col3:
251
- if styled_button("تحرير النموذج", key=f"edit_template_{template['id']}", type="secondary", icon="✏️"):
252
- st.session_state.template_to_edit = template["id"]
253
-
254
- def _render_materials_tab(self, template: Dict[str, Any]):
255
- """عرض تبويب المواد"""
256
- components = template.get("components", {})
257
- materials = components.get("materials", [])
258
-
259
- if not materials:
260
- st.info("لا توجد مواد محددة لهذا النموذج.")
261
- return
262
-
263
- # تحويل المواد إلى DataFrame
264
- materials_data = []
265
- total_materials_cost = 0
266
-
267
- for material in materials:
268
- material_cost = material.get("الكمية", 0) * material.get("سعر_الوحدة", 0)
269
- total_materials_cost += material_cost
270
-
271
- materials_data.append({
272
- "اسم المادة": material.get("الاسم", ""),
273
- "الكمية": material.get("الكمية", 0),
274
- "الوحدة": material.get("الوحدة", ""),
275
- "سعر الوحدة": material.get("سعر_الوحدة", 0),
276
- "التكلفة": material_cost
277
- })
278
-
279
- materials_df = pd.DataFrame(materials_data)
280
-
281
- # تنسيق الجدول
282
- def highlight_row(row):
283
- """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
284
- color = '#F0F8FF' if row.name % 2 == 0 else 'white'
285
- return ['background-color: %s' % color] * len(row)
286
-
287
- styled_df = materials_df.style.apply(highlight_row, axis=1)
288
- styled_df = styled_df.format({
289
- "الكمية": "{:.2f}",
290
- "سعر الوحدة": "{:,.2f} ريال",
291
- "التكلفة": "{:,.2f} ريال"
292
- })
293
-
294
- # عرض الجدول
295
- st.dataframe(styled_df, use_container_width=True, hide_index=True)
296
- st.markdown(f"**إجمالي تكلفة المواد:** {total_materials_cost:,.2f} ريال")
297
-
298
- def _render_labor_tab(self, template: Dict[str, Any]):
299
- """عرض تبويب العمالة"""
300
- components = template.get("components", {})
301
- labor = components.get("labor", [])
302
-
303
- if not labor:
304
- st.info("لا توجد عمالة محددة لهذا النموذج.")
305
- return
306
-
307
- # تحويل العمالة إلى DataFrame
308
- labor_data = []
309
- total_labor_cost = 0
310
-
311
- for worker in labor:
312
- labor_cost = worker.get("العدد", 0) * worker.get("المدة", 0) * worker.get("سعر_اليوم", 0)
313
- total_labor_cost += labor_cost
314
-
315
- labor_data.append({
316
- "نوع العامل": worker.get("النوع", ""),
317
- "العدد": worker.get("العدد", 0),
318
- "المدة (يوم)": worker.get("المدة", 0),
319
- "سعر اليوم": worker.get("سعر_اليوم", 0),
320
- "التكلفة": labor_cost
321
- })
322
-
323
- labor_df = pd.DataFrame(labor_data)
324
-
325
- # تنسيق الجدول
326
- def highlight_row(row):
327
- """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
328
- color = '#F0F8FF' if row.name % 2 == 0 else 'white'
329
- return ['background-color: %s' % color] * len(row)
330
-
331
- styled_df = labor_df.style.apply(highlight_row, axis=1)
332
- styled_df = styled_df.format({
333
- "المدة (يوم)": "{:.2f}",
334
- "سعر اليوم": "{:,.2f} ريال",
335
- "التكلفة": "{:,.2f} ريال"
336
- })
337
-
338
- # عرض الجدول
339
- st.dataframe(styled_df, use_container_width=True, hide_index=True)
340
- st.markdown(f"**إجمالي تكلفة العمالة:** {total_labor_cost:,.2f} ريال")
341
-
342
- def _render_equipment_tab(self, template: Dict[str, Any]):
343
- """عرض تبويب المعدات"""
344
- components = template.get("components", {})
345
- equipment = components.get("equipment", [])
346
-
347
- if not equipment:
348
- st.info("لا توجد معدات محددة لهذا النموذج.")
349
- return
350
-
351
- # تحويل المعدات إلى DataFrame
352
- equipment_data = []
353
- total_equipment_cost = 0
354
-
355
- for eq in equipment:
356
- equipment_cost = eq.get("العدد", 0) * eq.get("المدة", 0) * eq.get("سعر_اليوم", 0)
357
- total_equipment_cost += equipment_cost
358
-
359
- equipment_data.append({
360
- "نوع المعدة": eq.get("النوع", ""),
361
- "العدد": eq.get("العدد", 0),
362
- "المدة (يوم)": eq.get("المدة", 0),
363
- "سعر اليوم": eq.get("سعر_اليوم", 0),
364
- "التكلفة": equipment_cost
365
- })
366
-
367
- equipment_df = pd.DataFrame(equipment_data)
368
-
369
- # تنسيق الجدول
370
- def highlight_row(row):
371
- """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
372
- color = '#F0F8FF' if row.name % 2 == 0 else 'white'
373
- return ['background-color: %s' % color] * len(row)
374
-
375
- styled_df = equipment_df.style.apply(highlight_row, axis=1)
376
- styled_df = styled_df.format({
377
- "المدة (يوم)": "{:.2f}",
378
- "سعر اليوم": "{:,.2f} ريال",
379
- "التكلفة": "{:,.2f} ريال"
380
- })
381
-
382
- # عرض الجدول
383
- st.dataframe(styled_df, use_container_width=True, hide_index=True)
384
- st.markdown(f"**إجمالي تكلفة المعدات:** {total_equipment_cost:,.2f} ريال")
385
-
386
- def _render_costs_tab(self, template: Dict[str, Any]):
387
- """عرض تبويب التكاليف"""
388
- # حساب إجمالي التكاليف المباشرة
389
- direct_cost = self._calculate_direct_cost(template)
390
-
391
- # المصاريف الإدارية
392
- admin_expenses_pct = template.get("admin_expenses", 0.05)
393
- admin_expenses = direct_cost * admin_expenses_pct
394
-
395
- # هامش الربح
396
- profit_margin_pct = template.get("profit_margin", 0.1)
397
- profit_margin = direct_cost * profit_margin_pct
398
-
399
- # إجمالي التكلفة
400
- total_cost = direct_cost + admin_expenses + profit_margin
401
-
402
- # عرض ملخص التكاليف
403
- st.markdown("#### ملخص التكاليف")
404
-
405
- col1, col2 = st.columns(2)
406
-
407
- with col1:
408
- st.markdown(f"**التكاليف المباشرة:** {direct_cost:,.2f} ريال")
409
- st.markdown(f"**المصاريف الإدارية ({admin_expenses_pct*100:.0f}%):** {admin_expenses:,.2f} ريال")
410
- st.markdown(f"**هامش الربح ({profit_margin_pct*100:.0f}%):** {profit_margin:,.2f} ريال")
411
-
412
- with col2:
413
- # رسم بياني دائري لتوزيع التكاليف
414
- import plotly.express as px
415
-
416
- # حساب تكاليف المكونات
417
- components = template.get("components", {})
418
-
419
- materials_cost = sum(
420
- mat.get("الكمية", 0) * mat.get("سعر_الوحدة", 0)
421
- for mat in components.get("materials", [])
422
- )
423
-
424
- labor_cost = sum(
425
- worker.get("العدد", 0) * worker.get("المدة", 0) * worker.get("سعر_اليوم", 0)
426
- for worker in components.get("labor", [])
427
- )
428
-
429
- equipment_cost = sum(
430
- eq.get("العدد", 0) * eq.get("المدة", 0) * eq.get("سعر_اليوم", 0)
431
- for eq in components.get("equipment", [])
432
- )
433
-
434
- # إنشاء البيانات للرسم البياني
435
- cost_distribution = [
436
- {"النوع": "المواد", "القيمة": materials_cost},
437
- {"النوع": "العمالة", "القيمة": labor_cost},
438
- {"النوع": "المعدات", "القيمة": equipment_cost},
439
- {"النوع": "المصاريف الإدارية", "القيمة": admin_expenses},
440
- {"النوع": "هامش الربح", "القيمة": profit_margin}
441
- ]
442
-
443
- cost_df = pd.DataFrame(cost_distribution)
444
-
445
- fig = px.pie(
446
- cost_df,
447
- values="القيمة",
448
- names="النوع",
449
- title="توزيع التكاليف",
450
- color_discrete_sequence=px.colors.sequential.Teal,
451
- hole=0.4
452
- )
453
-
454
- fig.update_traces(textposition='inside', textinfo='percent+label')
455
-
456
- fig.update_layout(
457
- annotations=[dict(text=f"{total_cost:,.0f} ريال", x=0.5, y=0.5, font_size=14, showarrow=False)],
458
- font=dict(family="Almarai, Arial", size=12),
459
- margin=dict(t=30, b=30, l=10, r=10)
460
- )
461
-
462
- st.plotly_chart(fig, use_container_width=True)
463
-
464
- st.markdown(f"**السعر النهائي:** {total_cost:,.2f} ريال لكل {template.get('unit', 'وحدة')}")
465
-
466
- def _render_new_template_form(self):
467
- """عرض نموذج إضافة قالب جديد"""
468
- st.markdown("### إضافة قالب نموذجي جديد")
469
-
470
- # معلومات القالب الأساسية
471
- col1, col2 = st.columns(2)
472
-
473
- with col1:
474
- template_name = st.text_input("اسم القالب", key="new_template_name")
475
- template_description = st.text_area("وصف القالب", key="new_template_description")
476
-
477
- with col2:
478
- # الحصول على الفئات من الكتالوج
479
- templates = self.construction_templates.get_all_templates()
480
- categories = templates.get("categories", {})
481
-
482
- # إنشاء قائمة الفئات
483
- category_list = [{"id": cat_id, "name": cat_data.get("name", cat_id)} for cat_id, cat_data in categories.items()]
484
- category_options = [cat["id"] for cat in category_list]
485
- category_labels = [cat["name"] for cat in category_list]
486
-
487
- category_index = 0
488
- if category_options:
489
- template_category = st.selectbox(
490
- "فئة القالب",
491
- options=category_options,
492
- format_func=lambda x: next((cat["name"] for cat in category_list if cat["id"] == x), x),
493
- key="new_template_category"
494
- )
495
- else:
496
- template_category = st.text_input("فئة القالب (لا توجد فئات محددة)", key="new_template_category_text")
497
-
498
- template_unit = st.selectbox(
499
- "وحدة القياس",
500
- options=["م²", "م³", "م.ط", "عدد", "طن", "كجم", "لتر", "يوم", "ساعة", "مقطوعية"],
501
- index=0,
502
- key="new_template_unit"
503
- )
504
-
505
- # إضافة المواد
506
- st.markdown("#### المواد المستخدمة")
507
-
508
- # إنشاء مصفوفة لتخزين المواد
509
- if "new_template_materials" not in st.session_state:
510
- st.session_state.new_template_materials = []
511
-
512
- # عرض المواد المضافة حاليًا
513
- if st.session_state.new_template_materials:
514
- materials_df = pd.DataFrame(st.session_state.new_template_materials)
515
- st.dataframe(materials_df, hide_index=True)
516
-
517
- # إضافة مادة جديدة
518
- st.markdown("##### 🧱 إضافة مادة جديدة")
519
- st.markdown('<div style="border: 1px solid #e0e0e0; border-radius: 5px; padding: 15px; background-color: #f8f9fa;">', unsafe_allow_html=True)
520
-
521
- col1, col2 = st.columns(2)
522
- with col1:
523
- material_name = st.text_input("اسم المادة", key="new_template_material_name")
524
- material_quantity = st.number_input("الكمية", min_value=0.0, step=0.1, key="new_template_material_quantity")
525
-
526
- with col2:
527
- material_unit = st.selectbox(
528
- "وحدة القياس",
529
- options=["م²", "م³", "م.ط", "عدد", "طن", "كجم", "لتر", "قطعة", "كيس", "لوح"],
530
- key="new_template_material_unit"
531
- )
532
- material_price = st.number_input("سعر الوحدة", min_value=0.0, step=1.0, key="new_template_material_price")
533
-
534
- st.markdown('</div>', unsafe_allow_html=True)
535
-
536
- if st.button("إضافة المادة", key="add_new_template_material"):
537
- if material_name and material_quantity > 0 and material_price > 0:
538
- st.session_state.new_template_materials.append({
539
- "الاسم": material_name,
540
- "الكمية": material_quantity,
541
- "الوحدة": material_unit,
542
- "سعر_الوحدة": material_price
543
- })
544
- st.rerun()
545
- else:
546
- st.error("يرجى ملء جميع الحقول المطلوبة للمادة.")
547
-
548
- # إضافة العمالة
549
- st.markdown("#### العمالة المطلوبة")
550
-
551
- # إنشاء مصفوفة لتخزين العمالة
552
- if "new_template_labor" not in st.session_state:
553
- st.session_state.new_template_labor = []
554
-
555
- # عرض العمالة المضافة حاليًا
556
- if st.session_state.new_template_labor:
557
- labor_df = pd.DataFrame(st.session_state.new_template_labor)
558
- st.dataframe(labor_df, hide_index=True)
559
-
560
- # إضافة عامل جديد
561
- st.markdown("##### 👷 إضافة عامل جديد")
562
- st.markdown('<div style="border: 1px solid #e0e0e0; border-radius: 5px; padding: 15px; background-color: #f8f9fa;">', unsafe_allow_html=True)
563
-
564
- col1, col2 = st.columns(2)
565
- with col1:
566
- labor_type = st.text_input("نوع العامل", key="new_template_labor_type")
567
- labor_count = st.number_input("العدد", min_value=1, step=1, key="new_template_labor_count")
568
-
569
- with col2:
570
- labor_duration = st.number_input("المدة (يوم)", min_value=0.1, step=0.1, key="new_template_labor_duration")
571
- labor_price = st.number_input("سعر اليوم", min_value=0.0, step=10.0, key="new_template_labor_price")
572
-
573
- st.markdown('</div>', unsafe_allow_html=True)
574
-
575
- if st.button("إضافة العامل", key="add_new_template_labor"):
576
- if labor_type and labor_count > 0 and labor_duration > 0 and labor_price > 0:
577
- st.session_state.new_template_labor.append({
578
- "النوع": labor_type,
579
- "العدد": labor_count,
580
- "المدة": labor_duration,
581
- "سعر_اليوم": labor_price
582
- })
583
- st.rerun()
584
- else:
585
- st.error("يرجى ملء جميع الحقول المطلوبة للعامل.")
586
-
587
- # إضافة المعدات
588
- st.markdown("#### المعدات اللازمة")
589
-
590
- # إنشاء مصفوفة لتخزين المعدات
591
- if "new_template_equipment" not in st.session_state:
592
- st.session_state.new_template_equipment = []
593
-
594
- # عرض المعدات المضافة حاليًا
595
- if st.session_state.new_template_equipment:
596
- equipment_df = pd.DataFrame(st.session_state.new_template_equipment)
597
- st.dataframe(equipment_df, hide_index=True)
598
-
599
- # إضافة معدة جديدة
600
- st.markdown("##### 🚜 إضافة معدة جديدة")
601
- st.markdown('<div style="border: 1px solid #e0e0e0; border-radius: 5px; padding: 15px; background-color: #f8f9fa;">', unsafe_allow_html=True)
602
-
603
- col1, col2 = st.columns(2)
604
- with col1:
605
- equipment_type = st.text_input("نوع المعدة", key="new_template_equipment_type")
606
- equipment_count = st.number_input("العدد", min_value=1, step=1, key="new_template_equipment_count")
607
-
608
- with col2:
609
- equipment_duration = st.number_input("المدة (يوم)", min_value=0.1, step=0.1, key="new_template_equipment_duration")
610
- equipment_price = st.number_input("سعر اليوم", min_value=0.0, step=50.0, key="new_template_equipment_price")
611
-
612
- st.markdown('</div>', unsafe_allow_html=True)
613
-
614
- if st.button("إضافة المعدة", key="add_new_template_equipment"):
615
- if equipment_type and equipment_count > 0 and equipment_duration > 0 and equipment_price > 0:
616
- st.session_state.new_template_equipment.append({
617
- "النوع": equipment_type,
618
- "العدد": equipment_count,
619
- "المدة": equipment_duration,
620
- "سعر_اليوم": equipment_price
621
- })
622
- st.rerun()
623
- else:
624
- st.error("يرجى ملء جميع الحقول المطلوبة للمعدة.")
625
-
626
- # المصاريف الإدارية وهامش الربح
627
- st.markdown("#### المصاريف الإدارية وهامش الربح")
628
-
629
- col1, col2 = st.columns(2)
630
- with col1:
631
- admin_expenses = st.slider("نسبة المصاريف الإدارية (%)", min_value=0, max_value=20, value=5, step=1, key="new_template_admin_expenses") / 100
632
-
633
- with col2:
634
- profit_margin = st.slider("نسبة هامش الربح (%)", min_value=0, max_value=30, value=10, step=1, key="new_template_profit_margin") / 100
635
-
636
- # الكلمات المفتاحية
637
- st.markdown("#### الكلمات المفتاحية")
638
- tags_input = st.text_input("الكلمات المفتاحية (مفصولة بفواصل)", key="new_template_tags")
639
- tags = [tag.strip() for tag in tags_input.split(",")] if tags_input else []
640
-
641
- # زر إنشاء القالب
642
- if styled_button("إنشاء القالب النموذجي", key="create_new_template", type="primary", full_width=True, icon="✅"):
643
- # التحقق من صحة البيانات
644
- if not template_name:
645
- st.error("يرجى إدخال اسم القالب.")
646
- elif not template_description:
647
- st.error("يرجى إدخال وصف القالب.")
648
- elif not st.session_state.new_template_materials:
649
- st.error("يرجى إضافة مادة واحدة على الأقل.")
650
- else:
651
- # إنشاء القالب الجديد
652
- new_template = {
653
- "name": template_name,
654
- "description": template_description,
655
- "category": template_category,
656
- "unit": template_unit,
657
- "components": {
658
- "materials": st.session_state.new_template_materials,
659
- "labor": st.session_state.new_template_labor,
660
- "equipment": st.session_state.new_template_equipment
661
- },
662
- "admin_expenses": admin_expenses,
663
- "profit_margin": profit_margin,
664
- "tags": tags
665
- }
666
-
667
- # إضافة القالب إلى الكتالوج
668
- try:
669
- template_id = self.construction_templates.add_template(new_template)
670
- st.success(f"تم إنشاء القالب النموذجي بنجاح! المعرف: {template_id}")
671
-
672
- # إعادة تعيين البيانات
673
- st.session_state.new_template_materials = []
674
- st.session_state.new_template_labor = []
675
- st.session_state.new_template_equipment = []
676
-
677
- # إعادة تحميل الصفحة
678
- st.rerun()
679
- except Exception as e:
680
- st.error(f"حدث خطأ أثناء إنشاء القالب: {str(e)}")
681
-
682
- def _calculate_template_cost(self, template: Dict[str, Any]) -> float:
683
- """حساب التكلفة التقديرية للنموذج"""
684
- # حساب التكاليف المباشرة
685
- direct_cost = self._calculate_direct_cost(template)
686
-
687
- # المصاريف الإدارية
688
- admin_expenses = direct_cost * template.get("admin_expenses", 0.05)
689
-
690
- # هامش الربح
691
- profit_margin = direct_cost * template.get("profit_margin", 0.1)
692
-
693
- # إجمالي التكلفة
694
- total_cost = direct_cost + admin_expenses + profit_margin
695
-
696
- return total_cost
697
-
698
- def _populate_reference_price_list(self):
699
- """تعبئة قوائم الأسعار المرجعية من كتالوج البنود النموذجية"""
700
- templates = self.construction_templates.get_all_templates()
701
- all_templates = templates.get("templates", {})
702
-
703
- for template_id, template in all_templates.items():
704
- # إضافة البند النموذجي إلى قائمة الأسعار المرجعية
705
- self._add_template_to_reference_list(template_id, template)
706
-
707
- def _add_template_to_reference_list(self, template_id: str, template: Dict[str, Any]):
708
- """إضافة بند نموذجي إلى قائمة الأسعار المرجعية"""
709
- # حساب التكلفة التقديرية للنموذج
710
- estimated_cost = self._calculate_template_cost(template)
711
-
712
- # إنشاء عنصر القائمة المرجعية
713
- reference_item = {
714
- "id": template_id,
715
- "name": template.get("name", ""),
716
- "description": template.get("description", ""),
717
- "unit": template.get("unit", ""),
718
- "estimated_cost": estimated_cost,
719
- "category": template.get("category", ""),
720
- "type": "بند نموذجي",
721
- "source": "كتالوج البنود",
722
- "date_added": pd.Timestamp.now().strftime("%Y-%m-%d"),
723
- "is_active": True
724
- }
725
-
726
- # إضافة العنصر إلى قائمة الأسعار المرجعية إذا لم يكن موجوداً بالفعل
727
- existing_item = next((item for item in st.session_state.reference_price_list if item["id"] == template_id), None)
728
- if existing_item:
729
- # تحديث العنصر الموجود
730
- existing_index = st.session_state.reference_price_list.index(existing_item)
731
- st.session_state.reference_price_list[existing_index] = reference_item
732
- else:
733
- # إضافة عنصر جديد
734
- st.session_state.reference_price_list.append(reference_item)
735
-
736
- def _render_reference_price_list(self):
737
- """عرض قوائم الأسعار المرجعية"""
738
- st.markdown("""
739
- <div class='custom-box info-box'>
740
- <h3>📊 قوائم الأسعار المرجعية</h3>
741
- <p>قوائم الأسعار المرجعية تحتوي على أسعار المواد والخدمات المستخدمة في المشاريع.</p>
742
- <p>يمكنك استخدام هذه القوائم في عمليات التسعير والتخطيط للمشاريع الجديدة.</p>
743
- </div>
744
- """, unsafe_allow_html=True)
745
-
746
- # تبويبات قوائم الأسعار
747
- tabs = st.tabs(["البنود النموذجية", "المواد", "العمالة", "المعدات", "إدارة القوائم"])
748
-
749
- # تبويب البنود النموذجية
750
- with tabs[0]:
751
- self._render_reference_templates_list()
752
-
753
- # تبويب المواد
754
- with tabs[1]:
755
- self._render_reference_materials_list()
756
-
757
- # تبويب العمالة
758
- with tabs[2]:
759
- self._render_reference_labor_list()
760
-
761
- # تبويب المعدات
762
- with tabs[3]:
763
- self._render_reference_equipment_list()
764
-
765
- # تبويب إدارة القوائم
766
- with tabs[4]:
767
- self._render_reference_list_management()
768
-
769
- def _render_reference_templates_list(self):
770
- """عرض قائمة البنود النموذجية في الأسعار المرجعية"""
771
- st.markdown("### قائمة البنود النموذجية المرجعية")
772
-
773
- # فلترة العناصر للحصول على البنود النموذجية فقط
774
- template_items = [item for item in st.session_state.reference_price_list if item["type"] == "بند نموذجي"]
775
-
776
- if not template_items:
777
- st.info("لا توجد بنود نموذجية في قائمة الأسعار المرجعية.")
778
- return
779
-
780
- # إنشاء DataFrame للعرض
781
- df_data = []
782
- for item in template_items:
783
- df_data.append({
784
- "الرقم التعريفي": item["id"],
785
- "اسم البند": item["name"],
786
- "الوصف": item["description"][:50] + "..." if len(item["description"]) > 50 else item["description"],
787
- "الوحدة": item["unit"],
788
- "السعر التقديري": item["estimated_cost"],
789
- "الفئة": item["category"],
790
- "المصدر": item["source"]
791
- })
792
-
793
- df = pd.DataFrame(df_data)
794
-
795
- # تنسيق الجدول
796
- def highlight_row(row):
797
- color = '#F0F8FF' if row.name % 2 == 0 else 'white'
798
- return ['background-color: %s' % color] * len(row)
799
-
800
- styled_df = df.style.apply(highlight_row, axis=1)
801
- styled_df = styled_df.format({
802
- "السعر التقديري": "{:,.2f} ريال"
803
- })
804
-
805
- # عرض الجدول
806
- st.dataframe(styled_df, use_container_width=True, hide_index=True)
807
-
808
- # إضافة زر لإضافة بند من القائمة المرجعية إلى حاسبة التكاليف
809
- selected_item_id = st.selectbox(
810
- "اختر بنداً لإضافته إلى حاسبة التكاليف",
811
- options=[item["id"] for item in template_items],
812
- format_func=lambda x: next((item["name"] for item in template_items if item["id"] == x), x)
813
- )
814
-
815
- if selected_item_id:
816
- if styled_button("إضافة إلى حاسبة التكاليف", key=f"add_ref_to_calc_{selected_item_id}", type="primary", icon="➕"):
817
- st.session_state.selected_template_for_calculator = selected_item_id
818
- st.success("تم إضافة البند المرجعي إلى حاسبة التكاليف!")
819
-
820
- def _render_reference_materials_list(self):
821
- """عرض قائمة المواد في الأسعار المرجعية"""
822
- st.markdown("### قائمة المواد المرجعية")
823
- st.info("سيتم تطوير هذا القسم قريباً. يمكنك إضافة مواد من طلبات التسعير التي تم الرد عليها.")
824
-
825
- def _render_reference_labor_list(self):
826
- """عرض قائمة العمالة في الأسعار المرجعية"""
827
- st.markdown("### قائمة العمالة المرجعية")
828
- st.info("سيتم تطوير هذا القسم قريباً.")
829
-
830
- def _render_reference_equipment_list(self):
831
- """عرض قائمة المعدات في الأسعار المرجعية"""
832
- st.markdown("### قائمة المعدات المرجعية")
833
- st.info("سيتم تطوير هذا القسم قريباً. يمكنك إضافة معدات من طلبات ��لتسعير التي تم الرد عليها.")
834
-
835
- def _render_reference_list_management(self):
836
- """عرض إدارة قوائم الأسعار المرجعية"""
837
- st.markdown("### إدارة قوائم الأسعار المرجعية")
838
-
839
- col1, col2 = st.columns(2)
840
-
841
- with col1:
842
- st.markdown(f"**إجمالي عدد العناصر في القوائم المرجعية:** {len(st.session_state.reference_price_list)}")
843
- st.markdown(f"**عدد البنود النموذجية:** {len([item for item in st.session_state.reference_price_list if item['type'] == 'بند نموذجي'])}")
844
-
845
- with col2:
846
- if styled_button("تحديث جميع القوائم المرجعية", key="update_all_reference_lists", type="primary", icon="🔄"):
847
- self._populate_reference_price_list()
848
- st.success("تم تحديث قوائم الأسعار المرجعية بنجاح!")
849
-
850
- st.markdown("### إضافة عنصر جديد يدوياً")
851
- st.markdown("##### 🛠️ إضافة عنصر جديد إلى قوائم الأسعار المرجعية")
852
- st.markdown('<div style="border: 1px solid #e0e0e0; border-radius: 5px; padding: 15px; background-color: #f8f9fa;">', unsafe_allow_html=True)
853
-
854
- item_type = st.selectbox(
855
- "نوع العنصر",
856
- options=["مادة", "عمالة", "معدات"],
857
- key="new_reference_item_type"
858
- )
859
-
860
- item_name = st.text_input("اسم العنصر", key="new_reference_item_name")
861
- item_desc = st.text_area("وصف العنصر", key="new_reference_item_desc")
862
- item_unit = st.text_input("وحدة القياس", key="new_reference_item_unit")
863
- item_price = st.number_input("السعر", min_value=0.0, step=0.1, key="new_reference_item_price")
864
-
865
- st.markdown('</div>', unsafe_allow_html=True)
866
-
867
- if styled_button("إضافة إلى القائمة المرجعية", key="add_manual_reference_item", type="success", icon="➕"):
868
- if not item_name:
869
- st.error("الرجاء إدخال اسم العنصر")
870
- elif not item_unit:
871
- st.error("الرجاء إدخال وحدة القياس")
872
- elif item_price <= 0:
873
- st.error("الرجاء إدخال سعر صحيح")
874
- else:
875
- # إنشاء معرف فريد للعنصر
876
- item_id = f"MAN-{item_type[:1]}-{len(st.session_state.reference_price_list) + 1:04d}"
877
-
878
- # إنشاء العنصر
879
- new_item = {
880
- "id": item_id,
881
- "name": item_name,
882
- "description": item_desc,
883
- "unit": item_unit,
884
- "estimated_cost": item_price,
885
- "category": "أخرى",
886
- "type": item_type,
887
- "source": "إدخال يدوي",
888
- "date_added": pd.Timestamp.now().strftime("%Y-%m-%d"),
889
- "is_active": True
890
- }
891
-
892
- # إضافة العنصر إلى القائمة
893
- st.session_state.reference_price_list.append(new_item)
894
- st.success(f"تم إضافة العنصر {item_name} إلى القائمة المرجعية بنجاح!")
895
-
896
- def _calculate_direct_cost(self, template: Dict[str, Any]) -> float:
897
- """حساب التكاليف المباشرة للنموذج"""
898
- components = template.get("components", {})
899
-
900
- # حساب تكلفة المواد
901
- materials_cost = sum(
902
- mat.get("الكمية", 0) * mat.get("سعر_الوحدة", 0)
903
- for mat in components.get("materials", [])
904
- )
905
-
906
- # حساب تكلفة العمالة
907
- labor_cost = sum(
908
- worker.get("العدد", 0) * worker.get("المدة", 0) * worker.get("سعر_اليوم", 0)
909
- for worker in components.get("labor", [])
910
- )
911
-
912
- # حساب تكلفة المعدات
913
- equipment_cost = sum(
914
- eq.get("العدد", 0) * eq.get("المدة", 0) * eq.get("سعر_اليوم", 0)
915
- for eq in components.get("equipment", [])
916
- )
917
- direct_cost = materials_cost + labor_cost + equipment_cost
918
-
919
- return direct_cost
920
-
921
-
922
- # دالة لتشغيل الكتالوج مباشرة في حالة تنفيذ الملف بشكل مستقل
923
- def main():
924
- """تشغيل كتالوج القوالب بشكل مستقل"""
925
- from modules.pricing.services.construction_templates import ConstructionTemplates
926
-
927
- # تهيئة الواجهة
928
- st.set_page_config(
929
- page_title="كتالوج بنود المقاولات النموذجية",
930
- page_icon="🗃️",
931
- layout="wide",
932
- initial_sidebar_state="collapsed",
933
- menu_items={
934
- 'Get Help': 'mailto:[email protected]',
935
- 'Report a bug': 'mailto:[email protected]',
936
- 'About': 'كتالوج بنود المقاولات النموذجية - جزء من نظام WAHBi AI لتحليل المناقصات'
937
- }
938
- )
939
-
940
- # تهيئة كائن الكتالوج
941
- construction_templates = ConstructionTemplates()
942
- templates_catalog = TemplatesCatalog(construction_templates)
943
-
944
- # عرض الكتالوج
945
- templates_catalog.render()
946
-
947
- # تشغيل الكتالوج مباشرة عند تنفيذ الملف
948
- if __name__ == "__main__":
949
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/services/unbalanced_pricing.py DELETED
@@ -1,213 +0,0 @@
1
- """
2
- خدمة التسعير غير المتزن
3
- """
4
-
5
- import pandas as pd
6
- import numpy as np
7
- from datetime import datetime
8
- import os
9
- import config
10
-
11
-
12
- class UnbalancedPricing:
13
- """خدمة التسعير غير المتزن للبنود"""
14
-
15
- def __init__(self):
16
- """تهيئة خدمة التسعير غير المتزن"""
17
- self.strategies = {
18
- 'front_loading': self.apply_front_loading,
19
- 'back_loading': self.apply_back_loading,
20
- 'confirmed_items': self.apply_confirmed_items_loading,
21
- 'variable_items': self.apply_variable_items_discount
22
- }
23
-
24
- def apply_strategy(self, items_df, strategy, params=None):
25
- """تطبيق استراتيجية تسعير غير متزن على البنود"""
26
- # نسخة من البيانات المدخلة للعمل عليها
27
- df = items_df.copy()
28
-
29
- # إضافة عمود إستراتيجية التسعير إذا لم يكن موجوداً
30
- if 'إستراتيجية التسعير' not in df.columns:
31
- df['إستراتيجية التسعير'] = 'متوازن'
32
-
33
- # تطبيق الإستراتيجية المطلوبة
34
- if strategy in self.strategies:
35
- df = self.strategies[strategy](df, params)
36
- else:
37
- # إذا كانت الإستراتيجية غير معروفة، أعد البيانات بدون تغيير
38
- pass
39
-
40
- # حساب الإجمالي بعد التعديل
41
- df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
42
-
43
- return df
44
-
45
- def apply_front_loading(self, items_df, params=None):
46
- """تطبيق استراتيجية التحميل الأمامي (Front Loading)"""
47
- df = items_df.copy()
48
-
49
- # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
50
- if params is None:
51
- params = {
52
- 'early_increase': 1.3, # زيادة 30% للبنود المبكرة
53
- 'late_decrease': 0.7, # تخفيض 30% للبنود المتأخرة
54
- 'early_percentage': 0.33, # نسبة البنود المبكرة 33%
55
- 'late_percentage': 0.33 # نسبة البنود المتأخرة 33%
56
- }
57
-
58
- # تحديد البنود المبكرة والمتأخرة والمتوسطة
59
- items_count = len(df)
60
- early_count = int(items_count * params['early_percentage'])
61
- late_count = int(items_count * params['late_percentage'])
62
-
63
- early_items = df.iloc[:early_count].index
64
- middle_items = df.iloc[early_count:items_count-late_count].index
65
- late_items = df.iloc[items_count-late_count:].index
66
-
67
- # تطبيق الزيادة والنقصان
68
- for idx in early_items:
69
- df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['early_increase']
70
- df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
71
-
72
- for idx in middle_items:
73
- df.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
74
-
75
- for idx in late_items:
76
- df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['late_decrease']
77
- df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
78
-
79
- return df
80
-
81
- def apply_back_loading(self, items_df, params=None):
82
- """تطبيق استراتيجية التحميل الخلفي (Back Loading)"""
83
- df = items_df.copy()
84
-
85
- # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
86
- if params is None:
87
- params = {
88
- 'early_decrease': 0.7, # تخفيض 30% للبنود المبكرة
89
- 'late_increase': 1.3, # زيادة 30% للبنود المتأخرة
90
- 'early_percentage': 0.33, # نسبة البنود المبكرة 33%
91
- 'late_percentage': 0.33 # نسبة البنود المتأخرة 33%
92
- }
93
-
94
- # تحديد البنود المبكرة والمتأخرة والمتوسطة
95
- items_count = len(df)
96
- early_count = int(items_count * params['early_percentage'])
97
- late_count = int(items_count * params['late_percentage'])
98
-
99
- early_items = df.iloc[:early_count].index
100
- middle_items = df.iloc[early_count:items_count-late_count].index
101
- late_items = df.iloc[items_count-late_count:].index
102
-
103
- # تطبيق الزيادة والنقصان
104
- for idx in early_items:
105
- df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['early_decrease']
106
- df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
107
-
108
- for idx in middle_items:
109
- df.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
110
-
111
- for idx in late_items:
112
- df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['late_increase']
113
- df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
114
-
115
- return df
116
-
117
- def apply_confirmed_items_loading(self, items_df, params=None):
118
- """تطبيق استراتيجية تحميل البنود المؤكدة"""
119
- df = items_df.copy()
120
-
121
- # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
122
- if params is None:
123
- params = {
124
- 'confirmed_increase': 1.25, # زيادة 25% للبنود المؤكدة
125
- 'others_decrease': 0.85, # تخفيض 15% للبنود الأخرى
126
- 'confirmed_items_indices': [] # قائمة مؤشرات البنود المؤكدة
127
- }
128
-
129
- # إذا لم يتم تحديد البنود المؤكدة، استخدم قواعد اختيار افتراضية
130
- if not params['confirmed_items_indices']:
131
- # البنود التي تحتوي على كلمات مثل "أساسات" أو "هيكل" عادة ما تكون مؤكدة
132
- confirmed_items = []
133
- for idx, row in df.iterrows():
134
- description = row['وصف البند'].lower()
135
- if any(term in description for term in ['أساس', 'خرسان', 'هيكل', 'إنشائي']):
136
- confirmed_items.append(idx)
137
- else:
138
- confirmed_items = params['confirmed_items_indices']
139
-
140
- # تحديد البنود غير المؤكدة
141
- all_indices = set(range(len(df)))
142
- confirmed_indices = set(confirmed_items)
143
- variable_indices = list(all_indices - confirmed_indices)
144
-
145
- # تطبيق الزيادة والنقصان
146
- for idx in confirmed_items:
147
- df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['confirmed_increase']
148
- df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
149
-
150
- for idx in variable_indices:
151
- df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['others_decrease']
152
- df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
153
-
154
- return df
155
-
156
- def apply_variable_items_discount(self, items_df, params=None):
157
- """تطبيق استراتيجية تخفيض البنود المحتمل زيادتها"""
158
- df = items_df.copy()
159
-
160
- # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
161
- if params is None:
162
- params = {
163
- 'variable_decrease': 0.7, # تخفيض 30% للبنود المحتمل زيادتها
164
- 'others_increase': 1.15, # زيادة 15% للبنود الأخرى
165
- 'variable_items_indices': [] # قائمة مؤشرات البنود المحتمل زيادتها
166
- }
167
-
168
- # إذا لم يتم تحديد البنود المحتمل زيادتها، استخدم قواعد اختيار افتراضية
169
- if not params['variable_items_indices']:
170
- # البنود التي تحتوي على كلمات مثل "حفر" أو "ردم" عادة ما تكون محتمل زيادتها
171
- variable_items = []
172
- for idx, row in df.iterrows():
173
- description = row['وصف البند'].lower()
174
- if any(term in description for term in ['حفر', 'ردم', 'تمديد', 'صرف', 'مياه']):
175
- variable_items.append(idx)
176
- else:
177
- variable_items = params['variable_items_indices']
178
-
179
- # تحديد البنود الأخرى
180
- all_indices = set(range(len(df)))
181
- variable_indices = set(variable_items)
182
- other_indices = list(all_indices - variable_indices)
183
-
184
- # تطبيق الزيادة والنقصان
185
- for idx in variable_items:
186
- df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['variable_decrease']
187
- df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
188
-
189
- for idx in other_indices:
190
- df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['others_increase']
191
- df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
192
-
193
- return df
194
-
195
- def calibrate_prices(self, original_df, unbalanced_df):
196
- """معايرة الأسعار للحفاظ على إجمالي التسعير الأصلي"""
197
- # حساب الإجماليات
198
- original_total = original_df['الإجمالي'].sum()
199
- unbalanced_total = unbalanced_df['الإجمالي'].sum()
200
-
201
- # نسخة من البيانات المدخلة للعمل عليها
202
- df = unbalanced_df.copy()
203
-
204
- # حساب معامل التعديل
205
- adjustment_factor = original_total / unbalanced_total if unbalanced_total > 0 else 1.0
206
-
207
- # تعديل الأسعار
208
- df['سعر الوحدة'] = df['سعر الوحدة'] * adjustment_factor
209
-
210
- # حساب الإجمالي بعد التعديل
211
- df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
212
-
213
- return df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/pricing/specs_analyzer.py DELETED
@@ -1,527 +0,0 @@
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 random
13
- import os
14
- import time
15
- import io
16
-
17
- from modules.pricing.services.standard_pricing import StandardPricing
18
- from modules.pricing.services.unbalanced_pricing import UnbalancedPricing
19
- from modules.pricing.services.local_content import LocalContentCalculator
20
- from modules.pricing.services.price_prediction import PricePrediction
21
- from utils.excel_handler import export_to_excel
22
- from utils.helpers import format_number, format_currency
23
-
24
-
25
- class PricingApp:
26
- """وحدة التسعير المتكاملة"""
27
-
28
- def __init__(self):
29
- self.pricing_methods = [
30
- "التسعير القياسي",
31
- "التسعير غير المتزن",
32
- "التسعير التنافسي",
33
- "التسعير الموجه بالربحية"
34
- ]
35
-
36
- # تهيئة خدمات التسعير
37
- self.standard_pricing = StandardPricing()
38
- self.unbalanced_pricing = UnbalancedPricing()
39
- self.local_content = LocalContentCalculator()
40
- self.price_prediction = PricePrediction()
41
-
42
- def render(self):
43
- """عرض واجهة وحدة التسعير"""
44
-
45
- st.markdown("<h1 class='module-title'>وحدة التسعير المتكاملة</h1>", unsafe_allow_html=True)
46
-
47
- tabs = st.tabs([
48
- "إنشاء تسعير جديد",
49
- "نموذج التسعير الشامل",
50
- "التسعير غير المتزن",
51
- "المحتوى المحلي"
52
- ])
53
-
54
- with tabs[0]:
55
- self._render_new_pricing_tab()
56
-
57
- with tabs[1]:
58
- self._render_comprehensive_pricing_tab()
59
-
60
- with tabs[2]:
61
- self._render_unbalanced_pricing_tab()
62
-
63
- with tabs[3]:
64
- self._render_local_content_tab()
65
-
66
- def _render_new_pricing_tab(self):
67
- """عرض تبويب إنشاء تسعير جديد"""
68
-
69
- st.markdown("### إنشاء تسعير جديد")
70
-
71
- col1, col2 = st.columns(2)
72
-
73
- with col1:
74
- tender_name = st.text_input("اسم المناقصة")
75
- client = st.text_input("الجهة المالكة")
76
- pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods)
77
-
78
- with col2:
79
- tender_number = st.text_input("رقم المناقصة")
80
- location = st.text_input("الموقع")
81
- submission_date = st.date_input("تاريخ التقديم")
82
-
83
- # خيارات بيانات البنود
84
- st.markdown("### بيانات البنود")
85
-
86
- data_source = st.radio(
87
- "مصدر بيانات البنود",
88
- ["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات"]
89
- )
90
-
91
- if data_source == "إدخال يدوي":
92
- # إنشاء بيانات افتراضية
93
- if 'manual_items' not in st.session_state:
94
- st.session_state.manual_items = pd.DataFrame({
95
- 'رقم البند': [f"A{i}" for i in range(1, 6)],
96
- 'وصف البند': [
97
- "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
98
- "توريد وتركيب حديد التسليح للأساسات",
99
- "أعمال العزل المائي للأساسات",
100
- "أعمال الردم والدك للأساسات",
101
- "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة"
102
- ],
103
- 'الوحدة': ["م3", "طن", "م2", "م3", "م3"],
104
- 'الكمية': [250, 25, 500, 300, 120],
105
- 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0],
106
- 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0]
107
- })
108
-
109
- # عرض جدول البنود مع إمكانية التعديل
110
- edited_items = st.data_editor(
111
- st.session_state.manual_items,
112
- use_container_width=True,
113
- hide_index=True,
114
- num_rows="dynamic"
115
- )
116
- st.session_state.manual_items = edited_items
117
-
118
- elif data_source == "استيراد من Excel":
119
- uploaded_file = st.file_uploader("رفع ملف Excel", type=["xlsx", "xls"])
120
-
121
- if uploaded_file is not None:
122
- st.success("تم رفع الملف بنجاح")
123
- # محاكاة قراءة الملف
124
- st.markdown("### معاينة البيانات المستوردة")
125
-
126
- # إنشاء بيانات افتراضية
127
- import_items = pd.DataFrame({
128
- 'رقم البند': [f"A{i}" for i in range(1, 8)],
129
- 'وصف البند': [
130
- "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
131
- "توريد وتركيب حديد التسليح للأساسات",
132
- "أعمال العزل المائي للأساسات",
133
- "أعمال الردم والدك للأساسات",
134
- "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
135
- "توريد وتركيب حديد التسليح للأعمدة",
136
- "أعمال البلوك للجدران"
137
- ],
138
- 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
139
- 'الكمية': [250, 25, 500, 300, 120, 10, 400],
140
- 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
141
- 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
142
- })
143
-
144
- st.dataframe(import_items)
145
-
146
- if st.button("استيراد البيانات"):
147
- st.session_state.manual_items = import_items.copy()
148
- st.session_state.manual_items_modified = True
149
- st.success("تم استيراد البيانات بنجاح!")
150
-
151
- else: # استيراد من وحدة تحليل المستندات
152
- available_documents = [
153
- "كراسة شروط مشروع توسعة مستشفى الملك فهد",
154
- "جدول كميات صيانة محطات المياه",
155
- "مخططات إنشاء مدرسة ثانوية"
156
- ]
157
-
158
- selected_doc = st.selectbox("اختر المستند", available_documents)
159
-
160
- if st.button("استيراد البيانات من تحليل المستند"):
161
- # محاكاة استيراد البيانات
162
- with st.spinner("جاري استيراد البيانات..."):
163
- time.sleep(2)
164
-
165
- # إنشاء بيانات افتراضية
166
- doc_items = pd.DataFrame({
167
- 'رقم البند': [f"A{i}" for i in range(1, 8)],
168
- 'وصف البند': [
169
- "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
170
- "توريد وتركيب حديد التسليح للأساسات",
171
- "أعمال العزل المائي للأساسات",
172
- "أعمال الردم والدك للأساسات",
173
- "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
174
- "توريد وتركيب حديد التسليح للأعمدة",
175
- "أعمال البلوك للجدران"
176
- ],
177
- 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
178
- 'الكمية': [250, 25, 500, 300, 120, 10, 400],
179
- 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
180
- 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
181
- })
182
-
183
- st.session_state.manual_items = doc_items.copy()
184
- st.success("تم استيراد البيانات من تحليل المستند بنجاح!")
185
- st.dataframe(doc_items)
186
-
187
- # زر بدء التسعير
188
- if st.button("بدء التسعير"):
189
- # تحقق من صحة البيانات
190
- if 'manual_items' in st.session_state and not st.session_state.manual_items.empty:
191
- # حفظ بيانات التسعير الحالي
192
- st.session_state.current_pricing = {
193
- 'name': tender_name,
194
- 'number': tender_number,
195
- 'client': client,
196
- 'location': location,
197
- 'method': pricing_method,
198
- 'submission_date': submission_date,
199
- 'items': st.session_state.manual_items.copy(),
200
- 'status': 'جديد',
201
- 'created_at': datetime.now()
202
- }
203
-
204
- # الانتقال إلى تبويب نموذج التسعير الشامل
205
- st.success("تم إ��شاء التسعير بنجاح! يمكنك الانتقال إلى نموذج التسعير الشامل.")
206
- else:
207
- st.error("يرجى إدخال بيانات البنود أولاً.")
208
-
209
- def _render_comprehensive_pricing_tab(self):
210
- """عرض تبويب نموذج التسعير الشامل"""
211
-
212
- st.markdown("### نموذج التسعير الشامل")
213
-
214
- # التحقق من وجود تسعير حالي
215
- if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
216
- st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
217
- return
218
-
219
- # عرض معلومات التسعير الحالي
220
- pricing = st.session_state.current_pricing
221
-
222
- col1, col2, col3 = st.columns(3)
223
-
224
- with col1:
225
- st.metric("اسم المناقصة", pricing['name'])
226
- st.metric("الجهة المالكة", pricing['client'])
227
-
228
- with col2:
229
- st.metric("رقم المناقصة", pricing['number'])
230
- st.metric("تاريخ التقديم", pricing['submission_date'].strftime("%Y-%m-%d"))
231
-
232
- with col3:
233
- st.metric("طريقة التسعير", pricing['method'])
234
- st.metric("الموقع", pricing['location'])
235
-
236
- # عرض البنود والتسعير
237
- st.markdown("### بنود التسعير")
238
-
239
- items = pricing['items'].copy()
240
-
241
- # إضافة أسعار الوحدة للمحاكاة
242
- if 'سعر الوحدة' in items.columns and (items['سعر الوحدة'] == 0).all():
243
- items['سعر الوحدة'] = [
244
- round(random.uniform(1000, 3000), 2), # الخرسانة
245
- round(random.uniform(5000, 7000), 2), # الحديد
246
- round(random.uniform(100, 200), 2), # العزل
247
- round(random.uniform(50, 100), 2), # الردم
248
- round(random.uniform(1200, 3500), 2), # الخرسانة للأعمدة
249
- ]
250
-
251
- if len(items) > 5:
252
- for i in range(5, len(items)):
253
- items.at[i, 'سعر الوحدة'] = round(random.uniform(500, 5000), 2)
254
-
255
- # حساب الإجمالي
256
- items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
257
-
258
- # عرض الجدول مع إمكانية التعديل
259
- edited_items = st.data_editor(
260
- items,
261
- use_container_width=True,
262
- hide_index=True,
263
- disabled=('رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'الإجمالي')
264
- )
265
-
266
- # حساب الإجمالي بعد التعديل
267
- edited_items['الإجمالي'] = edited_items['الكمية'] * edited_items['سعر الوحدة']
268
- st.session_state.current_pricing['items'] = edited_items
269
-
270
- # حساب وعرض إجماليات التسعير
271
- total_price = edited_items['الإجمالي'].sum()
272
-
273
- st.markdown("### إجماليات التسعير")
274
-
275
- col1, col2, col3 = st.columns(3)
276
-
277
- with col1:
278
- st.metric("إجمالي التكاليف المباشرة", f"{total_price:,.2f} ريال")
279
-
280
- with col2:
281
- overhead_percentage = st.slider("نسبة المصاريف العامة والأرباح (%)", 5, 30, 15)
282
- overhead_value = total_price * overhead_percentage / 100
283
- st.metric("المصاريف العامة والأرباح", f"{overhead_value:,.2f} ريال")
284
-
285
- with col3:
286
- grand_total = total_price + overhead_value
287
- st.metric("الإجمالي النهائي", f"{grand_total:,.2f} ريال")
288
-
289
- # رسم بياني لتوزيع التكاليف
290
- st.markdown("### تحليل التكاليف")
291
-
292
- # حساب النسب المئوية لكل بند
293
- pie_data = edited_items.copy()
294
- pie_data['نسبة من إجمالي التكاليف'] = pie_data['الإجمالي'] / total_price * 100
295
-
296
- fig = px.pie(
297
- pie_data,
298
- values='نسبة من إجمالي التكاليف',
299
- names='وصف البند',
300
- title='توزيع التكاليف حسب البنود',
301
- hole=0.4
302
- )
303
-
304
- st.plotly_chart(fig, use_container_width=True)
305
-
306
- # أزرار العمليات
307
- col1, col2, col3 = st.columns(3)
308
-
309
- with col1:
310
- if st.button("حفظ التسعير"):
311
- st.success("تم حفظ التسعير بنجاح!")
312
-
313
- with col2:
314
- if st.button("تصدير إلى Excel"):
315
- st.success("تم تصدير التسعير إلى Excel بنجاح!")
316
-
317
- with col3:
318
- if st.button("تحليل المخاطر المالية"):
319
- st.success("تم إرسال الطلب إلى وحدة تحليل المخاطر!")
320
-
321
- def _render_unbalanced_pricing_tab(self):
322
- """عرض تبويب التسعير غير المتزن"""
323
-
324
- st.markdown("### التسعير غير المتزن")
325
-
326
- # التحقق من وجود تسعير حالي
327
- if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
328
- st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
329
- return
330
-
331
- # شرح التسعير غير المتزن
332
- with st.expander("ما هو التسعير غير المتزن؟", expanded=False):
333
- st.markdown("""
334
- **التسعير غير المتزن** هو استراتيجية تسعير تقوم على توزيع التكاليف بين بنود المناقصة بشكل غير متساوٍ، مع الحفاظ على إجمالي قيمة العطاء.
335
-
336
- ### استراتيجيات التسعير غير المتزن:
337
-
338
- 1. **التحميل الأمامي (Front Loading)**: زيادة أسعار البنود المبكرة في المشروع للحصول على تدفق نقدي أفضل في بداية المشروع.
339
- 2. **التحميل الخلفي (Back Loading)**: زيادة أسعار البنود المتأخرة في المشروع.
340
- 3. **تحميل البنود المؤكدة**: زيادة أسعار البنود التي من المؤكد تنفيذها بالكميات المحددة.
341
- 4. **تخفيض أسعار البنود المحتملة**: تخفيض أسعار البنود التي قد تزيد كمياتها أثناء التنفيذ.
342
-
343
- ### مزايا التسعير غير المتزن:
344
-
345
- - تحسين التدفق النقدي للمشروع.
346
- - تعظيم الربحية في حالة التغييرات والأوامر التغييرية.
347
- - زيادة فرص الفوز بالمناقصة.
348
-
349
- ### مخاطر التسعير غير المتزن:
350
-
351
- - قد يتم رفض العطاء إذا كان عدم التوازن واضحاً.
352
- - قد تتأثر السمعة سلباً إذا تم استخدامه بشكل مفرط.
353
- - قد يؤدي إلى خسائر إذا لم يتم تنفيذ البنود ذات الأسعار العالية.
354
- """)
355
-
356
- # عرض بنود التسعير الحالي
357
- items = st.session_state.current_pricing['items'].copy()
358
-
359
- # إضافة عمود إستراتيجية التسعير
360
- if 'إستراتيجية التسعير' not in items.columns:
361
- items['إستراتيجية التسعير'] = 'متوازن'
362
-
363
- st.markdown("### إستراتيجية التسعير غير المتزن")
364
-
365
- # اختيار الإستراتيجية
366
- strategy = st.selectbox(
367
- "اختر إستراتيجية التسعير",
368
- [
369
- "تحميل أمامي (Front Loading)",
370
- "تحميل البنود المؤكدة",
371
- "تخفيض البنود المحتمل زيادتها",
372
- "إستراتيجية مخصصة"
373
- ]
374
- )
375
-
376
- # تطبيق الإستراتيجية المختارة
377
- if strategy == "تحميل أمامي (Front Loading)":
378
- # محاكاة تحميل أمامي
379
- items_count = len(items)
380
- early_items = items.iloc[:items_count//3].index
381
- middle_items = items.iloc[items_count//3:2*items_count//3].index
382
- late_items = items.iloc[2*items_count//3:].index
383
-
384
- # تطبيق الزيادة والنقصان
385
- for idx in early_items:
386
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.3 # زيادة 30%
387
- items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
388
-
389
- for idx in middle_items:
390
- items.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
391
-
392
- for idx in late_items:
393
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
394
- items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
395
-
396
- elif strategy == "تحميل البنود المؤكدة":
397
- # محاكاة - اعتبار بعض البنود مؤكدة
398
- confirmed_items = [0, 2, 4] # الأصفار-مستندة
399
- variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items]
400
-
401
- # تطبيق الزيادة والنقصان
402
- for idx in confirmed_items:
403
- if idx < len(items):
404
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 # زيادة 25%
405
- items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
406
-
407
- for idx in variable_items:
408
- if idx < len(items):
409
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 # نقص 15%
410
- items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
411
-
412
- elif strategy == "تخفيض البنود المحتمل زيادتها":
413
- # محاكاة - اعتبار بعض البنود محتمل زيادتها
414
- variable_items = [1, 3] # الأصفار-مستندة
415
- other_items = [idx for idx in range(len(items)) if idx not in variable_items]
416
-
417
- # تطبيق الزيادة والنقصان
418
- for idx in variable_items:
419
- if idx < len(items):
420
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
421
- items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
422
-
423
- for idx in other_items:
424
- if idx < len(items):
425
- items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 # زيادة 15%
426
- items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
427
-
428
- else: # إستراتيجية مخصصة
429
- st.markdown("### تعديل أسعار البنود يدوياً")
430
- st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً.")
431
-
432
- # حساب الإجمالي بعد التعديل
433
- items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
434
-
435
- # تعيين ألوان للإستراتيجيات
436
- def highlight_strategy(val):
437
- if val == 'زيادة':
438
- return 'background-color: #a8e6cf'
439
- elif val == 'نقص':
440
- return 'background-color: #ff9aa2'
441
- return ''
442
-
443
- # عرض الجدول مع تنسيق
444
- st.markdown("### بنود التسعير غير المتزن")
445
- styled_items = items.style.applymap(highlight_strategy, subset=['إستراتيجية التسعير'])
446
- st.dataframe(styled_items, use_container_width=True)
447
-
448
- # المقارنة بين التسعير المتوازن وغير المتوازن
449
- st.markdown("### مقارنة التسعير المتوازن وغير المتوازن")
450
-
451
- original_items = st.session_state.current_pricing['items'].copy()
452
- original_total = original_items['الإجمالي'].sum()
453
- unbalanced_total = items['الإجمالي'].sum()
454
-
455
- col1, col2, col3 = st.columns(3)
456
-
457
- with col1:
458
- st.metric("إجمالي التسعير المتوازن", f"{original_total:,.2f} ريال")
459
-
460
- with col2:
461
- st.metric("إجمالي التسعير غير المتوازن", f"{unbalanced_total:,.2f} ريال")
462
-
463
- with col3:
464
- diff = unbalanced_total - original_total
465
- st.metric("الفرق", f"{diff:,.2f} ريال", delta=f"{diff/original_total*100:.1f}%")
466
-
467
- # المعايرة للحفاظ على إجمالي التسعير
468
- if abs(diff) > 1: # إذا كان هناك فرق كبير
469
- if st.button("معايرة الأسعار للحفاظ على إجمالي التسعير"):
470
- # تعديل الأسعار للحفاظ على إجمالي التكلفة
471
- adjustment_factor = original_total / unbalanced_total
472
- items['سعر الوحدة'] = items['سعر الوحدة'] * adjustment_factor
473
- items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
474
-
475
- st.success(f"تم تعديل الأسعار للحفاظ على إجمالي التسعير الأصلي ({original_total:,.2f} ريال)")
476
- st.dataframe(items, use_container_width=True)
477
-
478
- # رسم بياني للمقارنة
479
- st.markdown("### تحليل بصري للتسعير غير المتوازن")
480
-
481
- # إعداد البيانات للرسم البياني
482
- chart_data = pd.DataFrame({
483
- 'وصف البند': original_items['وصف البند'],
484
- 'التسعير المتوازن': original_items['الإجمالي'],
485
- 'التسعير غير المتوازن': items['��لإجمالي']
486
- })
487
-
488
- # رسم بياني شريطي للمقارنة
489
- fig = go.Figure()
490
-
491
- fig.add_trace(go.Bar(
492
- x=chart_data['وصف البند'],
493
- y=chart_data['التسعير المتوازن'],
494
- name='التسعير المتوازن',
495
- marker_color='rgb(55, 83, 109)'
496
- ))
497
-
498
- fig.add_trace(go.Bar(
499
- x=chart_data['وصف البند'],
500
- y=chart_data['التسعير غير المتوازن'],
501
- name='التسعير غير المتوازن',
502
- marker_color='rgb(26, 118, 255)'
503
- ))
504
-
505
- fig.update_layout(
506
- title='مقارنة بين التسعير المتوازن وغير المتوازن',
507
- xaxis_tickfont_size=14,
508
- yaxis=dict(
509
- title='الإجمالي (ريال)',
510
- titlefont_size=16,
511
- tickfont_size=14,
512
- ),
513
- legend=dict(
514
- x=0,
515
- y=1.0,
516
- bgcolor='rgba(255, 255, 255, 0)',
517
- bordercolor='rgba(255, 255, 255, 0)'
518
- ),
519
- barmode='group',
520
- bargap=0.15,
521
- bargroupgap=0.1
522
- )
523
-
524
- st.plotly_chart(fig, use_container_width=True)
525
-
526
- # زر حفظ التسعير غير المتوازن
527
- if st.button("
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/project_management/project_management_app.py DELETED
@@ -1,666 +0,0 @@
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()