diff --git a/.gitattributes b/.gitattributes
index 349260ce73c49f9df7ac494efe5a2834f8a3e267..ee35be280ac52ef693ac193ba1d6ef6f032a65dc 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,36 +1,37 @@
-*.7z filter=lfs diff=lfs merge=lfs -text
-*.arrow filter=lfs diff=lfs merge=lfs -text
-*.bin filter=lfs diff=lfs merge=lfs -text
-*.bz2 filter=lfs diff=lfs merge=lfs -text
-*.ckpt filter=lfs diff=lfs merge=lfs -text
-*.ftz filter=lfs diff=lfs merge=lfs -text
-*.gz filter=lfs diff=lfs merge=lfs -text
-*.h5 filter=lfs diff=lfs merge=lfs -text
-*.joblib filter=lfs diff=lfs merge=lfs -text
-*.lfs.* filter=lfs diff=lfs merge=lfs -text
-*.mlmodel filter=lfs diff=lfs merge=lfs -text
-*.model filter=lfs diff=lfs merge=lfs -text
-*.msgpack filter=lfs diff=lfs merge=lfs -text
-*.npy filter=lfs diff=lfs merge=lfs -text
-*.npz filter=lfs diff=lfs merge=lfs -text
-*.onnx filter=lfs diff=lfs merge=lfs -text
-*.ot filter=lfs diff=lfs merge=lfs -text
-*.parquet filter=lfs diff=lfs merge=lfs -text
-*.pb filter=lfs diff=lfs merge=lfs -text
-*.pickle filter=lfs diff=lfs merge=lfs -text
-*.pkl filter=lfs diff=lfs merge=lfs -text
-*.pt filter=lfs diff=lfs merge=lfs -text
-*.pth filter=lfs diff=lfs merge=lfs -text
-*.rar filter=lfs diff=lfs merge=lfs -text
-*.safetensors filter=lfs diff=lfs merge=lfs -text
-saved_model/**/* filter=lfs diff=lfs merge=lfs -text
-*.tar.* filter=lfs diff=lfs merge=lfs -text
-*.tar filter=lfs diff=lfs merge=lfs -text
-*.tflite filter=lfs diff=lfs merge=lfs -text
-*.tgz filter=lfs diff=lfs merge=lfs -text
-*.wasm filter=lfs diff=lfs merge=lfs -text
-*.xz filter=lfs diff=lfs merge=lfs -text
-*.zip filter=lfs diff=lfs merge=lfs -text
-*.zst filter=lfs diff=lfs merge=lfs -text
-*tfevents* filter=lfs diff=lfs merge=lfs -text
-images/logo.png filter=lfs diff=lfs merge=lfs -text
+*.7z filter=lfs diff=lfs merge=lfs -text
+*.arrow filter=lfs diff=lfs merge=lfs -text
+*.bin filter=lfs diff=lfs merge=lfs -text
+*.bz2 filter=lfs diff=lfs merge=lfs -text
+*.ckpt filter=lfs diff=lfs merge=lfs -text
+*.ftz filter=lfs diff=lfs merge=lfs -text
+*.gz filter=lfs diff=lfs merge=lfs -text
+*.h5 filter=lfs diff=lfs merge=lfs -text
+*.joblib filter=lfs diff=lfs merge=lfs -text
+*.lfs.* filter=lfs diff=lfs merge=lfs -text
+*.mlmodel filter=lfs diff=lfs merge=lfs -text
+*.model filter=lfs diff=lfs merge=lfs -text
+*.msgpack filter=lfs diff=lfs merge=lfs -text
+*.npy filter=lfs diff=lfs merge=lfs -text
+*.npz filter=lfs diff=lfs merge=lfs -text
+*.onnx filter=lfs diff=lfs merge=lfs -text
+*.ot filter=lfs diff=lfs merge=lfs -text
+*.parquet filter=lfs diff=lfs merge=lfs -text
+*.pb filter=lfs diff=lfs merge=lfs -text
+*.pickle filter=lfs diff=lfs merge=lfs -text
+*.pkl filter=lfs diff=lfs merge=lfs -text
+*.pt filter=lfs diff=lfs merge=lfs -text
+*.pth filter=lfs diff=lfs merge=lfs -text
+*.rar filter=lfs diff=lfs merge=lfs -text
+*.safetensors filter=lfs diff=lfs merge=lfs -text
+saved_model/**/* filter=lfs diff=lfs merge=lfs -text
+*.tar.* filter=lfs diff=lfs merge=lfs -text
+*.tar filter=lfs diff=lfs merge=lfs -text
+*.tflite filter=lfs diff=lfs merge=lfs -text
+*.tgz filter=lfs diff=lfs merge=lfs -text
+*.wasm filter=lfs diff=lfs merge=lfs -text
+*.xz filter=lfs diff=lfs merge=lfs -text
+*.zip filter=lfs diff=lfs merge=lfs -text
+*.zst filter=lfs diff=lfs merge=lfs -text
+*tfevents* filter=lfs diff=lfs merge=lfs -text
+images/logo.png filter=lfs diff=lfs merge=lfs -text
+assets/images/logo.png filter=lfs diff=lfs merge=lfs -text
diff --git a/app.py b/app.py
index 8dd3884559fa417d382768967c9cf5a3447faec4..54ee350463f9e2d354e7aa16e8950338752af5d2 100644
--- a/app.py
+++ b/app.py
@@ -9,6 +9,7 @@ import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
+from datetime import datetime
# إعداد المسارات
sys.path.append(str(Path(__file__).parent.parent))
@@ -29,7 +30,8 @@ from modules.document_comparison.document_comparison_app import DocumentComparis
from modules.translation.translation_app import TranslationApp
from modules.ai_assistant.ai_app import AIAssistantApp
from modules.data_analysis.data_analysis_app import DataAnalysisApp
-from pricing_system.integrated_app import IntegratedApp
+from pricing_system.modules.pricing_strategies.pricing_strategies import PricingStrategies #added import
+from pricing_system.integrated_app import IntegratedApp #added import
from styling.enhanced_ui import UIEnhancer
# تهيئة مدير التكوين
@@ -46,7 +48,7 @@ config_manager.set_page_config_if_needed(
'Report a bug': "https://www.example.com/bug",
'About': "### نظام تحليل المناقصات\nالإصدار 2.0.0"
}
-)
+)
# تطبيق التنسيق العام
ui_enhancer = UIEnhancer(page_title="نظام تحليل المناقصات", page_icon="📊")
@@ -116,9 +118,47 @@ elif selected == "تحليل المستندات":
document_app.run()
elif selected == "نظام التسعير":
+ import streamlit as st
+ from pricing_system.integrated_app import IntegratedApp
+
+ # تهيئة النظام المتكامل
integrated_pricing = IntegratedApp()
+
+ # إعداد التكوين مرة واحدة في بداية التطبيق
+ config_manager.set_page_config_if_needed(
+ page_title="نظام التسعير المتكامل",
+ page_icon="💰",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ # عرض الشعار وعنوان النظام
+ st.markdown("""
+
+
+
نظام التسعير المتكامل
+
+ """, unsafe_allow_html=True)
+
+ # تشغيل النظام المتكامل
integrated_pricing.run()
+
elif selected == "الموارد والتكاليف":
resources_app = ResourcesApp()
resources_app.run()
@@ -155,6 +195,11 @@ elif selected == "تحليل البيانات":
data_analysis_app = DataAnalysisApp()
data_analysis_app.run()
+elif selected == "الجدول الزمني":
+ from modules.scheduling.schedule_app import ScheduleApp
+ schedule_app = ScheduleApp()
+ schedule_app.run()
+
elif selected == "الإعدادات":
ui_enhancer.create_header("الإعدادات", "إعدادات النظام والحساب")
st.markdown("### إعدادات النظام")
@@ -179,6 +224,4 @@ elif selected == "الإعدادات":
with tabs[3]:
st.text_input("مفتاح OpenAI API", type="password")
st.text_input("مفتاح Google Maps API", type="password")
- st.button("حفظ مفاتيح API")
-
-
+ st.button("حفظ مفاتيح API")
\ No newline at end of file
diff --git a/assets/images/logo.png b/assets/images/logo.png
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..29bbaee982b5e26b206689d5c8e63a429209cc12 100644
--- a/assets/images/logo.png
+++ b/assets/images/logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b2ae470957cdfc79acecd9de70185303143df843e0870c33131b820831e07261
+size 106225
diff --git a/config.py b/config.py
index f8c4dc4ef601f6f3115ea9391ad69f177e47124c..04ac08d2914932554381d03d196fd37da8647b61 100644
--- a/config.py
+++ b/config.py
@@ -1,229 +1,229 @@
-"""
-ملف الإعدادات لنظام إدارة المناقصات
-"""
-
-import os
-import json
-from pathlib import Path
-
-class AppConfig:
- """فئة إعدادات التطبيق"""
-
- def __init__(self):
- """تهيئة الإعدادات"""
- # المسارات الأساسية
- self.app_dir = os.path.dirname(os.path.abspath(__file__))
- self.assets_dir = os.path.join(self.app_dir, "assets")
- self.data_dir = os.path.join(self.app_dir, "data")
-
- # إنشاء المجلدات إذا لم تكن موجودة
- Path(self.assets_dir).mkdir(parents=True, exist_ok=True)
- Path(self.data_dir).mkdir(parents=True, exist_ok=True)
-
- # مسارات الأصول
- self.icons_dir = os.path.join(self.assets_dir, "icons")
- self.images_dir = os.path.join(self.assets_dir, "images")
- self.fonts_dir = os.path.join(self.assets_dir, "fonts")
-
- # إنشاء مجلدات الأصول إذا لم تكن موجودة
- Path(self.icons_dir).mkdir(parents=True, exist_ok=True)
- Path(self.images_dir).mkdir(parents=True, exist_ok=True)
- Path(self.fonts_dir).mkdir(parents=True, exist_ok=True)
-
- # مسارات البيانات
- self.database_file = os.path.join(self.data_dir, "database.db")
- self.settings_file = os.path.join(self.data_dir, "settings.json")
- self.charts_dir = os.path.join(self.data_dir, "charts")
-
- # إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
- Path(self.charts_dir).mkdir(parents=True, exist_ok=True)
-
- # تحميل الإعدادات
- self.settings = self._load_settings()
-
- def _load_settings(self):
- """تحميل الإعدادات من ملف JSON"""
- default_settings = {
- "app": {
- "name": "نظام إدارة المناقصات",
- "version": "1.0.0",
- "language": "ar",
- "theme": "light",
- "font": "Cairo",
- "font_size": 12
- },
- "database": {
- "type": "sqlite",
- "path": self.database_file
- },
- "ui": {
- "window_width": 1200,
- "window_height": 800,
- "sidebar_width": 250
- },
- "notifications": {
- "enabled": True,
- "email_enabled": True,
- "email_server": "smtp.example.com",
- "email_port": 587,
- "email_username": "",
- "email_password": ""
- },
- "reports": {
- "default_format": "pdf",
- "default_path": os.path.join(self.data_dir, "reports")
- },
- "backup": {
- "auto_backup": True,
- "backup_frequency": "weekly",
- "backup_path": os.path.join(self.data_dir, "backups"),
- "max_backups": 10
- }
- }
-
- # إذا كان ملف الإعدادات موجودًا، قم بتحميله
- if os.path.exists(self.settings_file):
- try:
- with open(self.settings_file, "r", encoding="utf-8") as f:
- settings = json.load(f)
-
- # دمج الإعدادات المحملة مع الإعدادات الافتراضية
- self._merge_settings(default_settings, settings)
- return default_settings
- except Exception as e:
- print(f"خطأ في تحميل الإعدادات: {str(e)}")
- return default_settings
- else:
- # إنشاء ملف الإعدادات الافتراضية
- self._save_settings(default_settings)
- return default_settings
-
- def _merge_settings(self, default_settings, loaded_settings):
- """دمج الإعدادات المحملة مع الإعدادات الافتراضية"""
- for key, value in loaded_settings.items():
- if key in default_settings:
- if isinstance(value, dict) and isinstance(default_settings[key], dict):
- self._merge_settings(default_settings[key], value)
- else:
- default_settings[key] = value
-
- def _save_settings(self, settings=None):
- """حفظ الإعدادات إلى ملف JSON"""
- if settings is None:
- settings = self.settings
-
- try:
- with open(self.settings_file, "w", encoding="utf-8") as f:
- json.dump(settings, f, ensure_ascii=False, indent=4)
- return True
- except Exception as e:
- print(f"خطأ في حفظ الإعدادات: {str(e)}")
- return False
-
- def get_setting(self, section, key, default=None):
- """الحصول على قيمة إعداد معين"""
- try:
- return self.settings[section][key]
- except KeyError:
- return default
-
- def set_setting(self, section, key, value):
- """تعيين قيمة إعداد معين"""
- if section not in self.settings:
- self.settings[section] = {}
-
- self.settings[section][key] = value
- self._save_settings()
-
- def get_app_name(self):
- """الحصول على اسم التطبيق"""
- return self.get_setting("app", "name", "نظام إدارة المناقصات")
-
- def get_app_version(self):
- """الحصول على إصدار التطبيق"""
- return self.get_setting("app", "version", "1.0.0")
-
- def get_language(self):
- """الحصول على لغة التطبيق"""
- return self.get_setting("app", "language", "ar")
-
- def set_language(self, language):
- """تعيين لغة التطبيق"""
- self.set_setting("app", "language", language)
-
- def get_theme(self):
- """الحصول على نمط التطبيق"""
- return self.get_setting("app", "theme", "light")
-
- def set_theme(self, theme):
- """تعيين نمط التطبيق"""
- self.set_setting("app", "theme", theme)
-
- def get_font(self):
- """الحصول على خط التطبيق"""
- return self.get_setting("app", "font", "Cairo")
-
- def set_font(self, font):
- """تعيين خط التطبيق"""
- self.set_setting("app", "font", font)
-
- def get_font_size(self):
- """الحصول على حجم خط التطبيق"""
- return self.get_setting("app", "font_size", 12)
-
- def set_font_size(self, font_size):
- """تعيين حجم خط التطبيق"""
- self.set_setting("app", "font_size", font_size)
-
- def get_window_size(self):
- """الحصول على حجم نافذة التطبيق"""
- width = self.get_setting("ui", "window_width", 1200)
- height = self.get_setting("ui", "window_height", 800)
- return (width, height)
-
- def set_window_size(self, width, height):
- """تعيين حجم نافذة التطبيق"""
- self.set_setting("ui", "window_width", width)
- self.set_setting("ui", "window_height", height)
-
- def get_sidebar_width(self):
- """الحصول على عرض الشريط الجانبي"""
- return self.get_setting("ui", "sidebar_width", 250)
-
- def set_sidebar_width(self, width):
- """تعيين عرض الشريط الجانبي"""
- self.set_setting("ui", "sidebar_width", width)
-
- def get_database_config(self):
- """الحصول على إعدادات قاعدة البيانات"""
- return self.settings.get("database", {
- "type": "sqlite",
- "path": self.database_file
- })
-
- def get_notifications_config(self):
- """الحصول على إعدادات الإشعارات"""
- return self.settings.get("notifications", {
- "enabled": True,
- "email_enabled": True,
- "email_server": "smtp.example.com",
- "email_port": 587,
- "email_username": "",
- "email_password": ""
- })
-
- def get_reports_config(self):
- """الحصول على إعدادات التقارير"""
- return self.settings.get("reports", {
- "default_format": "pdf",
- "default_path": os.path.join(self.data_dir, "reports")
- })
-
- def get_backup_config(self):
- """الحصول على إعدادات النسخ الاحتياطي"""
- return self.settings.get("backup", {
- "auto_backup": True,
- "backup_frequency": "weekly",
- "backup_path": os.path.join(self.data_dir, "backups"),
- "max_backups": 10
- })
+"""
+ملف الإعدادات لنظام إدارة المناقصات
+"""
+
+import os
+import json
+from pathlib import Path
+
+class AppConfig:
+ """فئة إعدادات التطبيق"""
+
+ def __init__(self):
+ """تهيئة الإعدادات"""
+ # المسارات الأساسية
+ self.app_dir = os.path.dirname(os.path.abspath(__file__))
+ self.assets_dir = os.path.join(self.app_dir, "assets")
+ self.data_dir = os.path.join(self.app_dir, "data")
+
+ # إنشاء المجلدات إذا لم تكن موجودة
+ Path(self.assets_dir).mkdir(parents=True, exist_ok=True)
+ Path(self.data_dir).mkdir(parents=True, exist_ok=True)
+
+ # مسارات الأصول
+ self.icons_dir = os.path.join(self.assets_dir, "icons")
+ self.images_dir = os.path.join(self.assets_dir, "images")
+ self.fonts_dir = os.path.join(self.assets_dir, "fonts")
+
+ # إنشاء مجلدات الأصول إذا لم تكن موجودة
+ Path(self.icons_dir).mkdir(parents=True, exist_ok=True)
+ Path(self.images_dir).mkdir(parents=True, exist_ok=True)
+ Path(self.fonts_dir).mkdir(parents=True, exist_ok=True)
+
+ # مسارات البيانات
+ self.database_file = os.path.join(self.data_dir, "database.db")
+ self.settings_file = os.path.join(self.data_dir, "settings.json")
+ self.charts_dir = os.path.join(self.data_dir, "charts")
+
+ # إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
+ Path(self.charts_dir).mkdir(parents=True, exist_ok=True)
+
+ # تحميل الإعدادات
+ self.settings = self._load_settings()
+
+ def _load_settings(self):
+ """تحميل الإعدادات من ملف JSON"""
+ default_settings = {
+ "app": {
+ "name": "نظام إدارة المناقصات",
+ "version": "1.0.0",
+ "language": "ar",
+ "theme": "light",
+ "font": "Cairo",
+ "font_size": 12
+ },
+ "database": {
+ "type": "sqlite",
+ "path": self.database_file
+ },
+ "ui": {
+ "window_width": 1200,
+ "window_height": 800,
+ "sidebar_width": 250
+ },
+ "notifications": {
+ "enabled": True,
+ "email_enabled": True,
+ "email_server": "smtp.example.com",
+ "email_port": 587,
+ "email_username": "",
+ "email_password": ""
+ },
+ "reports": {
+ "default_format": "pdf",
+ "default_path": os.path.join(self.data_dir, "reports")
+ },
+ "backup": {
+ "auto_backup": True,
+ "backup_frequency": "weekly",
+ "backup_path": os.path.join(self.data_dir, "backups"),
+ "max_backups": 10
+ }
+ }
+
+ # إذا كان ملف الإعدادات موجودًا، قم بتحميله
+ if os.path.exists(self.settings_file):
+ try:
+ with open(self.settings_file, "r", encoding="utf-8") as f:
+ settings = json.load(f)
+
+ # دمج الإعدادات المحملة مع الإعدادات الافتراضية
+ self._merge_settings(default_settings, settings)
+ return default_settings
+ except Exception as e:
+ print(f"خطأ في تحميل الإعدادات: {str(e)}")
+ return default_settings
+ else:
+ # إنشاء ملف الإعدادات الافتراضية
+ self._save_settings(default_settings)
+ return default_settings
+
+ def _merge_settings(self, default_settings, loaded_settings):
+ """دمج الإعدادات المحملة مع الإعدادات الافتراضية"""
+ for key, value in loaded_settings.items():
+ if key in default_settings:
+ if isinstance(value, dict) and isinstance(default_settings[key], dict):
+ self._merge_settings(default_settings[key], value)
+ else:
+ default_settings[key] = value
+
+ def _save_settings(self, settings=None):
+ """حفظ الإعدادات إلى ملف JSON"""
+ if settings is None:
+ settings = self.settings
+
+ try:
+ with open(self.settings_file, "w", encoding="utf-8") as f:
+ json.dump(settings, f, ensure_ascii=False, indent=4)
+ return True
+ except Exception as e:
+ print(f"خطأ في حفظ الإعدادات: {str(e)}")
+ return False
+
+ def get_setting(self, section, key, default=None):
+ """الحصول على قيمة إعداد معين"""
+ try:
+ return self.settings[section][key]
+ except KeyError:
+ return default
+
+ def set_setting(self, section, key, value):
+ """تعيين قيمة إعداد معين"""
+ if section not in self.settings:
+ self.settings[section] = {}
+
+ self.settings[section][key] = value
+ self._save_settings()
+
+ def get_app_name(self):
+ """الحصول على اسم التطبيق"""
+ return self.get_setting("app", "name", "نظام إدارة المناقصات")
+
+ def get_app_version(self):
+ """الحصول على إصدار التطبيق"""
+ return self.get_setting("app", "version", "1.0.0")
+
+ def get_language(self):
+ """الحصول على لغة التطبيق"""
+ return self.get_setting("app", "language", "ar")
+
+ def set_language(self, language):
+ """تعيين لغة التطبيق"""
+ self.set_setting("app", "language", language)
+
+ def get_theme(self):
+ """الحصول على نمط التطبيق"""
+ return self.get_setting("app", "theme", "light")
+
+ def set_theme(self, theme):
+ """تعيين نمط التطبيق"""
+ self.set_setting("app", "theme", theme)
+
+ def get_font(self):
+ """الحصول على خط التطبيق"""
+ return self.get_setting("app", "font", "Cairo")
+
+ def set_font(self, font):
+ """تعيين خط التطبيق"""
+ self.set_setting("app", "font", font)
+
+ def get_font_size(self):
+ """الحصول على حجم خط التطبيق"""
+ return self.get_setting("app", "font_size", 12)
+
+ def set_font_size(self, font_size):
+ """تعيين حجم خط التطبيق"""
+ self.set_setting("app", "font_size", font_size)
+
+ def get_window_size(self):
+ """الحصول على حجم نافذة التطبيق"""
+ width = self.get_setting("ui", "window_width", 1200)
+ height = self.get_setting("ui", "window_height", 800)
+ return (width, height)
+
+ def set_window_size(self, width, height):
+ """تعيين حجم نافذة التطبيق"""
+ self.set_setting("ui", "window_width", width)
+ self.set_setting("ui", "window_height", height)
+
+ def get_sidebar_width(self):
+ """الحصول على عرض الشريط الجانبي"""
+ return self.get_setting("ui", "sidebar_width", 250)
+
+ def set_sidebar_width(self, width):
+ """تعيين عرض الشريط الجانبي"""
+ self.set_setting("ui", "sidebar_width", width)
+
+ def get_database_config(self):
+ """الحصول على إعدادات قاعدة البيانات"""
+ return self.settings.get("database", {
+ "type": "sqlite",
+ "path": self.database_file
+ })
+
+ def get_notifications_config(self):
+ """الحصول على إعدادات الإشعارات"""
+ return self.settings.get("notifications", {
+ "enabled": True,
+ "email_enabled": True,
+ "email_server": "smtp.example.com",
+ "email_port": 587,
+ "email_username": "",
+ "email_password": ""
+ })
+
+ def get_reports_config(self):
+ """الحصول على إعدادات التقارير"""
+ return self.settings.get("reports", {
+ "default_format": "pdf",
+ "default_path": os.path.join(self.data_dir, "reports")
+ })
+
+ def get_backup_config(self):
+ """الحصول على إعدادات النسخ الاحتياطي"""
+ return self.settings.get("backup", {
+ "auto_backup": True,
+ "backup_frequency": "weekly",
+ "backup_path": os.path.join(self.data_dir, "backups"),
+ "max_backups": 10
+ })
diff --git a/config_manager.py b/config_manager.py
index d567e94a2a6625f4cac109c8fc0355e2eaa40fa8..e176ae696e9a9b1d2ac4862264f2e30755378527 100644
--- a/config_manager.py
+++ b/config_manager.py
@@ -1,37 +1,37 @@
-"""
-مدير تكوين تطبيق Streamlit
-يستخدم لمنع استدعاء set_page_config() أكثر من مرة في التطبيق
-"""
-
-class ConfigManager:
- """مدير تكوين التطبيق لمنع استدعاء set_page_config() أكثر من مرة"""
-
- _instance = None
- _page_config_set = False
-
- def __new__(cls):
- if cls._instance is None:
- cls._instance = super(ConfigManager, cls).__new__(cls)
- return cls._instance
-
- def set_page_config_if_needed(self, **kwargs):
- """
- تعيين تكوين الصفحة إذا لم يتم تعيينه بالفعل
-
- المعلمات:
- **kwargs: معلمات لدالة st.set_page_config()
-
- العوائد:
- bool: True إذا تم تعيين التكوين، False إذا كان التكوين معينًا بالفعل
- """
- import streamlit as st
-
- if not ConfigManager._page_config_set:
- st.set_page_config(**kwargs)
- ConfigManager._page_config_set = True
- return True
- return False
-
- def is_page_config_set(self):
- """التحقق مما إذا كان تكوين الصفحة قد تم تعيينه بالفعل"""
- return ConfigManager._page_config_set
+"""
+مدير تكوين تطبيق Streamlit
+يستخدم لمنع استدعاء set_page_config() أكثر من مرة في التطبيق
+"""
+
+class ConfigManager:
+ """مدير تكوين التطبيق لمنع استدعاء set_page_config() أكثر من مرة"""
+
+ _instance = None
+ _page_config_set = False
+
+ def __new__(cls):
+ if cls._instance is None:
+ cls._instance = super(ConfigManager, cls).__new__(cls)
+ return cls._instance
+
+ def set_page_config_if_needed(self, **kwargs):
+ """
+ تعيين تكوين الصفحة إذا لم يتم تعيينه بالفعل
+
+ المعلمات:
+ **kwargs: معلمات لدالة st.set_page_config()
+
+ العوائد:
+ bool: True إذا تم تعيين التكوين، False إذا كان التكوين معينًا بالفعل
+ """
+ import streamlit as st
+
+ if not ConfigManager._page_config_set:
+ st.set_page_config(**kwargs)
+ ConfigManager._page_config_set = True
+ return True
+ return False
+
+ def is_page_config_set(self):
+ """التحقق مما إذا كان تكوين الصفحة قد تم تعيينه بالفعل"""
+ return ConfigManager._page_config_set
diff --git a/data/exports/boq_20250403_133827.xlsx b/data/exports/boq_20250403_133827.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..5d3da73463b7ad7e5137e1871d6ff4ea30ddbc51
Binary files /dev/null and b/data/exports/boq_20250403_133827.xlsx differ
diff --git a/data/exports/boq_20250403_135418.xlsx b/data/exports/boq_20250403_135418.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..8b484593c9263c42352845a03fb11148b572feb8
Binary files /dev/null and b/data/exports/boq_20250403_135418.xlsx differ
diff --git a/data/exports/boq_20250403_135819.xlsx b/data/exports/boq_20250403_135819.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..707998aa099c57fc18d239451b4497be9f9fd8f6
Binary files /dev/null and b/data/exports/boq_20250403_135819.xlsx differ
diff --git a/data/exports/boq_20250403_140953.xlsx b/data/exports/boq_20250403_140953.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..f0de2eb79528bfa53621d30b169bf8cfb03eb6b6
Binary files /dev/null and b/data/exports/boq_20250403_140953.xlsx differ
diff --git a/data/exports/boq_20250403_143659.xlsx b/data/exports/boq_20250403_143659.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..6122a519dc3e3a849873b0cbbe38f0a900a0dfa3
Binary files /dev/null and b/data/exports/boq_20250403_143659.xlsx differ
diff --git a/database/db_connector.py b/database/db_connector.py
index 79e3337f31e54c12937a566c0e48ef8755cd4b0f..a00a6f0d6b5abb4fc93aef3fff09ed859ca534c7 100644
--- a/database/db_connector.py
+++ b/database/db_connector.py
@@ -45,6 +45,19 @@ class DatabaseConnector:
def _create_tables(self):
"""إنشاء جداول قاعدة البيانات"""
+ # جدول المشاريع المحفوظة
+ self.cursor.execute('''
+ CREATE TABLE IF NOT EXISTS saved_projects (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ project_name TEXT NOT NULL,
+ project_code TEXT,
+ project_description TEXT,
+ boq_data TEXT NOT NULL,
+ total_cost REAL NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ ''')
+
# جدول المستخدمين
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
@@ -320,4 +333,4 @@ class DatabaseConnector:
"""إغلاق الاتصال"""
if self.connection:
self.connection.close()
- logger.info("تم إغلاق الاتصال بقاعدة البيانات")
+ logger.info("تم إغلاق الاتصال بقاعدة البيانات")
\ No newline at end of file
diff --git a/database/models.py b/database/models.py
index 58f5ec86527d1a6c1beb8729e6250cb012569e55..20331635eb922468647981b8ab7e1d467acb68ff 100644
--- a/database/models.py
+++ b/database/models.py
@@ -378,88 +378,84 @@ class Document:
return False
+"""
+نماذج قاعدة البيانات للتسعير
+"""
+import sqlite3
+from datetime import datetime
+
class PricingItem:
"""نموذج بند التسعير"""
-
- def __init__(self, id=None, project_id=None, item_number=None, description=None, unit=None, quantity=None, unit_price=None, total_price=None, created_by=None):
- """تهيئة نموذج بند التسعير"""
- self.id = id
- self.project_id = project_id
- self.item_number = item_number
- self.description = description
- self.unit = unit
- self.quantity = quantity
- self.unit_price = unit_price
- self.total_price = total_price
- self.created_by = created_by
- self.created_at = None
- self.updated_at = None
-
- @staticmethod
- def get_by_project(project_id, db):
- """الحصول على بنود التسعير بواسطة معرف المشروع"""
- try:
- query = "SELECT * FROM pricing_items WHERE project_id = ?"
- results = db.fetch_all(query, (project_id,))
-
- items = []
- for result in results:
- item = PricingItem()
- item.id = result[0]
- item.project_id = result[1]
- item.item_number = result[2]
- item.description = result[3]
- item.unit = result[4]
- item.quantity = result[5]
- item.unit_price = result[6]
- item.total_price = result[7]
- item.created_by = result[8]
- item.created_at = result[9]
- item.updated_at = result[10]
-
- items.append(item)
-
- return items
- except Exception as e:
- logger.error(f"خطأ في الحصول على بنود التسعير: {str(e)}")
- return []
-
- def save(self, db):
- """حفظ بند التسعير"""
- try:
- if self.id:
- # تحديث بند موجود
- data = {
- 'project_id': self.project_id,
- 'item_number': self.item_number,
- 'description': self.description,
- 'unit': self.unit,
- 'quantity': self.quantity,
- 'unit_price': self.unit_price,
- 'total_price': self.total_price,
- 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- }
-
- db.update('pricing_items', data, f"id = {self.id}")
- return self.id
- else:
- # إنشاء بند جديد
- data = {
- 'project_id': self.project_id,
- 'item_number': self.item_number,
- 'description': self.description,
- 'unit': self.unit,
- 'quantity': self.quantity,
- 'unit_price': self.unit_price,
- 'total_price': self.total_price,
- 'created_by': self.created_by
- }
-
- self.id = db.insert('pricing_items', data)
- return self.id
- except Exception as e:
- logger.error(f"خطأ في حفظ بند التسعير: {str(e)}")
- return None
+ def __init__(self, db):
+ self.db = db
+
+ def create_table(self):
+ """إنشاء جدول بنود التسعير"""
+ self.db.execute("""
+ CREATE TABLE IF NOT EXISTS pricing_items (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ project_id INTEGER,
+ code TEXT NOT NULL,
+ description TEXT NOT NULL,
+ unit TEXT NOT NULL,
+ quantity REAL NOT NULL,
+ unit_price REAL NOT NULL,
+ total_price REAL NOT NULL,
+ category TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (project_id) REFERENCES projects (id)
+ )
+ """)
+
+ def add_item(self, project_id, item_data):
+ """إضافة بند جديد"""
+ sql = """
+ INSERT INTO pricing_items (
+ project_id, code, description, unit,
+ quantity, unit_price, total_price, category
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ """
+ values = (
+ project_id,
+ item_data['code'],
+ item_data['description'],
+ item_data['unit'],
+ item_data['quantity'],
+ item_data['unit_price'],
+ item_data['total_price'],
+ item_data.get('category')
+ )
+ return self.db.execute(sql, values)
+
+ def get_project_items(self, project_id):
+ """جلب بنود المشروع"""
+ sql = "SELECT * FROM pricing_items WHERE project_id = ?"
+ return self.db.fetch_all(sql, (project_id,))
+
+ def update_item(self, item_id, item_data):
+ """تحديث بند"""
+ sql = """
+ UPDATE pricing_items
+ SET code=?, description=?, unit=?, quantity=?,
+ unit_price=?, total_price=?, category=?
+ WHERE id=?
+ """
+ values = (
+ item_data['code'],
+ item_data['description'],
+ item_data['unit'],
+ item_data['quantity'],
+ item_data['unit_price'],
+ item_data['total_price'],
+ item_data.get('category'),
+ item_id
+ )
+ return self.db.execute(sql, values)
+
+ def delete_item(self, item_id):
+ """حذف بند"""
+ sql = "DELETE FROM pricing_items WHERE id = ?"
+ return self.db.execute(sql, (item_id,))
class Risk:
@@ -546,6 +542,7 @@ class Risk:
return None
+
class Report:
"""نموذج التقرير"""
@@ -623,4 +620,4 @@ class Report:
return self.id
except Exception as e:
logger.error(f"خطأ في حفظ التقرير: {str(e)}")
- return None
+ return None
\ No newline at end of file
diff --git a/docs/architecture.md b/docs/architecture.md
index acbeb7a3cd8faf62b0b3799d67d74738fdfc77f5..f3ce58bed466c34f5c283095975522572d8672aa 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,136 +1,136 @@
-# هيكلية النظام المحسنة لنظام إدارة المناقصات
-
-## نظرة عامة
-هذا المستند يوضح هيكلية النظام المحسنة لنظام إدارة المناقصات، والذي يتضمن الوحدات التالية:
-- وحدة التسعير المتكاملة
-- وحدة الذكاء الاصطناعي
-- وحدة تحليل البيانات
-- وحدة الموارد
-
-## هيكلية المجلدات
-
-```
-tender_system/
-├── app.py # نقطة الدخول الرئيسية للتطبيق
-├── config.py # إعدادات التطبيق
-├── requirements.txt # متطلبات المكتبات
-├── README.md # توثيق النظام
-├── assets/ # الأصول الثابتة
-│ ├── images/ # الصور
-│ ├── icons/ # الأيقونات
-│ └── fonts/ # الخطوط
-├── data/ # البيانات
-│ ├── templates/ # قوالب البيانات
-│ └── charts/ # بيانات الرسوم البيانية
-├── database/ # قاعدة البيانات
-│ ├── db_connector.py # موصل قاعدة البيانات
-│ └── models.py # نماذج البيانات
-├── modules/ # وحدات النظام
-│ ├── pricing/ # وحدة التسعير
-│ │ ├── pricing_app.py # تطبيق التسعير
-│ │ └── services/ # خدمات التسعير
-│ │ ├── standard_pricing.py
-│ │ ├── unbalanced_pricing.py
-│ │ ├── local_content_calculator.py
-│ │ ├── price_prediction.py
-│ │ ├── construction_cost_calculator.py
-│ │ └── construction_templates.py
-│ ├── ai_assistant/ # وحدة الذكاء الاصطناعي
-│ │ ├── ai_app.py # تطبيق الذكاء الاصطناعي
-│ │ └── services/ # خدمات الذكاء الاصطناعي
-│ │ ├── openai_service.py
-│ │ ├── anthropic_service.py
-│ │ ├── local_llm_service.py
-│ │ └── prompt_templates.py
-│ ├── document_analysis/ # وحدة تحليل المستندات
-│ │ ├── document_app.py # تطبيق تحليل المستندات
-│ │ └── services/ # خدمات تحليل المستندات
-│ │ ├── text_extractor.py
-│ │ ├── item_extractor.py
-│ │ └── document_parser.py
-│ ├── resources/ # وحدة الموارد
-│ │ ├── resources_app.py # تطبيق الموارد
-│ │ └── services/ # خدمات الموارد
-│ │ ├── material_manager.py
-│ │ ├── labor_manager.py
-│ │ ├── equipment_manager.py
-│ │ └── subcontractor_manager.py
-│ ├── project_management/ # وحدة إدارة المشاريع
-│ │ └── project_app.py # تطبيق إدارة المشاريع
-│ └── reports/ # وحدة التقارير
-│ └── reports_app.py # تطبيق التقارير
-├── styling/ # التنسيق
-│ ├── theme.py # سمات التطبيق
-│ ├── icons.py # أيقونات التطبيق
-│ └── charts.py # تنسيق الرسوم البيانية
-├── utils/ # أدوات مساعدة
-│ ├── excel_handler.py # معالج ملفات Excel
-│ ├── pdf_handler.py # معالج ملفات PDF
-│ ├── helpers.py # دوال مساعدة
-│ └── auth.py # المصادقة
-└── tests/ # اختبارات
- ├── test_pricing.py # اختبارات وحدة التسعير
- ├── test_ai.py # اختبارات وحدة الذكاء الاصطناعي
- ├── test_document.py # اختبارات وحدة تحليل المستندات
- └── test_resources.py # اختبارات وحدة الموارد
-```
-
-## تفاصيل الوحدات
-
-### 1. وحدة التسعير المتكاملة
-- **الوظائف الرئيسية**:
- - إنشاء تسعير جديد
- - تحليل سعر البند
- - نموذج التسعير الشامل
- - التسعير غير المتزن
- - المحتوى المحلي
- - حاسبة تكاليف البناء
- - الأدوات المساعدة
-
-### 2. وحدة الذكاء الاصطناعي
-- **الوظائف الرئيسية**:
- - تحليل المستندات باستخدام الذكاء الاصطناعي
- - توليد توصيات ذكية للتسعير
- - تحليل المخاطر باستخدام الذكاء الاصطناعي
- - مساعد المحادثة الذكي
- - تلخيص المستندات
- - استخراج المعلومات الرئيسية
-
-### 3. وحدة تحليل البيانات
-- **الوظائف الرئيسية**:
- - استخراج النصوص من المستندات
- - استخراج الجداول والبنود
- - تحليل المستندات
- - تحويل المستندات إلى بيانات منظمة
- - تحليل الصور والمخططات
-
-### 4. وحدة الموارد
-- **الوظائف الرئيسية**:
- - إدارة المواد
- - إدارة العمالة
- - إدارة المعدات
- - إدارة المقاولين من الباطن
- - تحليل تكاليف الموارد
- - تخطيط الموارد
-
-## واجهة المستخدم
-- تستخدم إطار عمل Streamlit لبناء واجهة مستخدم تفاعلية
-- تدعم اللغة العربية بشكل كامل
-- تتضمن تبويبات لكل وحدة من وحدات النظام
-- تدعم الوضعين الفاتح والداكن
-- تتضمن رسومات بيانية تفاعلية باستخدام Plotly
-
-## تكامل الوحدات
-- تتكامل وحدة التسعير مع وحدة الموارد لاستخدام بيانات الأسعار
-- تتكامل وحدة تحليل البيانات مع وحدة التسعير لاستخراج بنود المناقصة
-- تتكامل وحدة الذكاء الاصطناعي مع جميع الوحدات لتقديم توصيات ذكية
-- تتكامل جميع الوحدات مع قاعدة البيانات المركزية
-
-## التقنيات المستخدمة
-- **لغة البرمجة**: Python
-- **إطار عمل واجهة المستخدم**: Streamlit
-- **معالجة البيانات**: Pandas, NumPy
-- **الرسوم البيانية**: Plotly, Matplotlib
-- **الذكاء الاصطناعي**: OpenAI API, Anthropic API, Transformers
-- **معالجة المستندات**: PyPDF2, python-docx, pdf2image
-- **قاعدة البيانات**: SQLAlchemy
+# هيكلية النظام المحسنة لنظام إدارة المناقصات
+
+## نظرة عامة
+هذا المستند يوضح هيكلية النظام المحسنة لنظام إدارة المناقصات، والذي يتضمن الوحدات التالية:
+- وحدة التسعير المتكاملة
+- وحدة الذكاء الاصطناعي
+- وحدة تحليل البيانات
+- وحدة الموارد
+
+## هيكلية المجلدات
+
+```
+tender_system/
+├── app.py # نقطة الدخول الرئيسية للتطبيق
+├── config.py # إعدادات التطبيق
+├── requirements.txt # متطلبات المكتبات
+├── README.md # توثيق النظام
+├── assets/ # الأصول الثابتة
+│ ├── images/ # الصور
+│ ├── icons/ # الأيقونات
+│ └── fonts/ # الخطوط
+├── data/ # البيانات
+│ ├── templates/ # قوالب البيانات
+│ └── charts/ # بيانات الرسوم البيانية
+├── database/ # قاعدة البيانات
+│ ├── db_connector.py # موصل قاعدة البيانات
+│ └── models.py # نماذج البيانات
+├── modules/ # وحدات النظام
+│ ├── pricing/ # وحدة التسعير
+│ │ ├── pricing_app.py # تطبيق التسعير
+│ │ └── services/ # خدمات التسعير
+│ │ ├── standard_pricing.py
+│ │ ├── unbalanced_pricing.py
+│ │ ├── local_content_calculator.py
+│ │ ├── price_prediction.py
+│ │ ├── construction_cost_calculator.py
+│ │ └── construction_templates.py
+│ ├── ai_assistant/ # وحدة الذكاء الاصطناعي
+│ │ ├── ai_app.py # تطبيق الذكاء الاصطناعي
+│ │ └── services/ # خدمات الذكاء الاصطناعي
+│ │ ├── openai_service.py
+│ │ ├── anthropic_service.py
+│ │ ├── local_llm_service.py
+│ │ └── prompt_templates.py
+│ ├── document_analysis/ # وحدة تحليل المستندات
+│ │ ├── document_app.py # تطبيق تحليل المستندات
+│ │ └── services/ # خدمات تحليل المستندات
+│ │ ├── text_extractor.py
+│ │ ├── item_extractor.py
+│ │ └── document_parser.py
+│ ├── resources/ # وحدة الموارد
+│ │ ├── resources_app.py # تطبيق الموارد
+│ │ └── services/ # خدمات الموارد
+│ │ ├── material_manager.py
+│ │ ├── labor_manager.py
+│ │ ├── equipment_manager.py
+│ │ └── subcontractor_manager.py
+│ ├── project_management/ # وحدة إدارة المشاريع
+│ │ └── project_app.py # تطبيق إدارة المشاريع
+│ └── reports/ # وحدة التقارير
+│ └── reports_app.py # تطبيق التقارير
+├── styling/ # التنسيق
+│ ├── theme.py # سمات التطبيق
+│ ├── icons.py # أيقونات التطبيق
+│ └── charts.py # تنسيق الرسوم البيانية
+├── utils/ # أدوات مساعدة
+│ ├── excel_handler.py # معالج ملفات Excel
+│ ├── pdf_handler.py # معالج ملفات PDF
+│ ├── helpers.py # دوال مساعدة
+│ └── auth.py # المصادقة
+└── tests/ # اختبارات
+ ├── test_pricing.py # اختبارات وحدة التسعير
+ ├── test_ai.py # اختبارات وحدة الذكاء الاصطناعي
+ ├── test_document.py # اختبارات وحدة تحليل المستندات
+ └── test_resources.py # اختبارات وحدة الموارد
+```
+
+## تفاصيل الوحدات
+
+### 1. وحدة التسعير المتكاملة
+- **الوظائف الرئيسية**:
+ - إنشاء تسعير جديد
+ - تحليل سعر البند
+ - نموذج التسعير الشامل
+ - التسعير غير المتزن
+ - المحتوى المحلي
+ - حاسبة تكاليف البناء
+ - الأدوات المساعدة
+
+### 2. وحدة الذكاء الاصطناعي
+- **الوظائف الرئيسية**:
+ - تحليل المستندات باستخدام الذكاء الاصطناعي
+ - توليد توصيات ذكية للتسعير
+ - تحليل المخاطر باستخدام الذكاء الاصطناعي
+ - مساعد المحادثة الذكي
+ - تلخيص المستندات
+ - استخراج المعلومات الرئيسية
+
+### 3. وحدة تحليل البيانات
+- **الوظائف الرئيسية**:
+ - استخراج النصوص من المستندات
+ - استخراج الجداول والبنود
+ - تحليل المستندات
+ - تحويل المستندات إلى بيانات منظمة
+ - تحليل الصور والمخططات
+
+### 4. وحدة الموارد
+- **الوظائف الرئيسية**:
+ - إدارة المواد
+ - إدارة العمالة
+ - إدارة المعدات
+ - إدارة المقاولين من الباطن
+ - تحليل تكاليف الموارد
+ - تخطيط الموارد
+
+## واجهة المستخدم
+- تستخدم إطار عمل Streamlit لبناء واجهة مستخدم تفاعلية
+- تدعم اللغة العربية بشكل كامل
+- تتضمن تبويبات لكل وحدة من وحدات النظام
+- تدعم الوضعين الفاتح والداكن
+- تتضمن رسومات بيانية تفاعلية باستخدام Plotly
+
+## تكامل الوحدات
+- تتكامل وحدة التسعير مع وحدة الموارد لاستخدام بيانات الأسعار
+- تتكامل وحدة تحليل البيانات مع وحدة التسعير لاستخراج بنود المناقصة
+- تتكامل وحدة الذكاء الاصطناعي مع جميع الوحدات لتقديم توصيات ذكية
+- تتكامل جميع الوحدات مع قاعدة البيانات المركزية
+
+## التقنيات المستخدمة
+- **لغة البرمجة**: Python
+- **إطار عمل واجهة المستخدم**: Streamlit
+- **معالجة البيانات**: Pandas, NumPy
+- **الرسوم البيانية**: Plotly, Matplotlib
+- **الذكاء الاصطناعي**: OpenAI API, Anthropic API, Transformers
+- **معالجة المستندات**: PyPDF2, python-docx, pdf2image
+- **قاعدة البيانات**: SQLAlchemy
diff --git a/docs/missing_modules_analysis.md b/docs/missing_modules_analysis.md
index ef18e8f9d7b0aef7ae4205093459aa3413a6fe10..f3c604ad6acd61eb5228c3193a990a324cfb08d8 100644
--- a/docs/missing_modules_analysis.md
+++ b/docs/missing_modules_analysis.md
@@ -1,88 +1,88 @@
-# تحليل الوحدات الناقصة والميزات المطلوبة
-
-## الوحدات الناقصة في النظام الحالي
-
-بناءً على تحليل دليل المستخدم والمتطلبات المقدمة، تم تحديد الوحدات والميزات التالية التي يجب إضافتها أو تحسينها في النظام:
-
-### 1. وحدة الخرائط والمواقع
-- خرائط تفاعلية لمواقع المشاريع
-- عرض المشاريع على الخريطة بناءً على الموقع الجغرافي
-- إمكانية تحديد المسافات والمناطق
-
-### 2. وحدة الإشعارات الذكية
-- نظام إشعارات متكامل للتنبيهات المهمة
-- إشعارات للمواعيد النهائية والمهام
-- إشعارات لتغييرات حالة المناقصات والمشاريع
-
-### 3. وحدة مقارنة المستندات
-- أدوات متطورة لمقارنة المستندات
-- تحديد الاختلافات بين إصدارات المستندات
-- تحليل التغييرات في الشروط والبنود
-
-### 4. وحدة الترجمة
-- ترجمة المستندات والنصوص بين اللغات
-- دعم خاص للترجمة بين العربية والإنجليزية
-- ترجمة المصطلحات الفنية والقانونية
-
-### 5. تحسين وحدة الذكاء الاصطناعي
-- تكامل مع نماذج الذكاء الاصطناعي المتقدمة
-- العمل في بيئة هجينة (محلية وسحابية)
-- طلب مفاتيح API من قسم الأمان
-
-### 6. تحسين وحدة التسعير المتكامل
-- تطوير نظام تسعير شامل ومتكامل
-- تحسين واجهة المستخدم وسهولة الاستخدام
-- إضافة ميزات تحليل الأسعار المتقدمة
-
-### 7. تحسين وحدة إدارة المشاريع
-- تطوير وحدة متكاملة لإدارة المشاريع المرساة
-- متابعة تنفيذ المشاريع ومراحلها
-- إدارة الموارد والجداول الزمنية للمشاريع
-
-## تحسينات الواجهة المرئية المطلوبة
-
-### 1. تحسين التصميم العام
-- تطوير واجهة مستخدم أكثر احترافية
-- تحسين الألوان والتباين
-- تنسيق متناسق بين جميع الوحدات
-
-### 2. تحسين القوائم والتنقل
-- تطوير شريط قوائم رئيسي متكامل
-- تحسين تجربة التنقل بين الوحدات
-- إضافة اختصارات للوظائف الأكثر استخداماً
-
-### 3. تحسين عرض الشعار والهوية
-- تحسين عرض شعار الشركة
-- تطبيق هوية بصرية موحدة في جميع أجزاء النظام
-- إضافة خيارات تخصيص الواجهة
-
-### 4. تحسين لوحات المعلومات
-- تطوير لوحات معلومات تفاعلية وجذابة
-- تحسين عرض المؤشرات والإحصائيات
-- إضافة رسوم بيانية متقدمة
-
-## متطلبات التكامل والاتساق
-
-### 1. تكامل الوحدات
-- ضمان التكامل السلس بين جميع وحدات النظام
-- مشاركة البيانات بين الوحدات المختلفة
-- واجهة مستخدم موحدة عبر جميع الوحدات
-
-### 2. اتساق البيانات
-- ضمان اتساق البيانات بين جميع أجزاء النظام
-- تزامن البيانات بين الوحدات المختلفة
-- منع تكرار البيانات وتضاربها
-
-### 3. أداء النظام
-- تحسين سرعة استجابة النظام
-- تحسين كفاءة استخدام الموارد
-- تحسين تجربة المستخدم العامة
-
-## الاعتماديات والمتطلبات الفنية
-
-يجب تحديث ملف المتطلبات ليشمل جميع المكتبات اللازمة للوحدات الجديدة والمحسنة، بما في ذلك:
-- مكتبات الخرائط والتحليل المكاني
-- مكتبات الترجمة ومعالجة اللغات
-- مكتبات الذكاء الاصطناعي والتعلم الآلي
-- مكتبات الرسوم البيانية والتصور
-- مكتبات واجهة المستخدم المتقدمة
+# تحليل الوحدات الناقصة والميزات المطلوبة
+
+## الوحدات الناقصة في النظام الحالي
+
+بناءً على تحليل دليل المستخدم والمتطلبات المقدمة، تم تحديد الوحدات والميزات التالية التي يجب إضافتها أو تحسينها في النظام:
+
+### 1. وحدة الخرائط والمواقع
+- خرائط تفاعلية لمواقع المشاريع
+- عرض المشاريع على الخريطة بناءً على الموقع الجغرافي
+- إمكانية تحديد المسافات والمناطق
+
+### 2. وحدة الإشعارات الذكية
+- نظام إشعارات متكامل للتنبيهات المهمة
+- إشعارات للمواعيد النهائية والمهام
+- إشعارات لتغييرات حالة المناقصات والمشاريع
+
+### 3. وحدة مقارنة المستندات
+- أدوات متطورة لمقارنة المستندات
+- تحديد الاختلافات بين إصدارات المستندات
+- تحليل التغييرات في الشروط والبنود
+
+### 4. وحدة الترجمة
+- ترجمة المستندات والنصوص بين اللغات
+- دعم خاص للترجمة بين العربية والإنجليزية
+- ترجمة المصطلحات الفنية والقانونية
+
+### 5. تحسين وحدة الذكاء الاصطناعي
+- تكامل مع نماذج الذكاء الاصطناعي المتقدمة
+- العمل في بيئة هجينة (محلية وسحابية)
+- طلب مفاتيح API من قسم الأمان
+
+### 6. تحسين وحدة التسعير المتكامل
+- تطوير نظام تسعير شامل ومتكامل
+- تحسين واجهة المستخدم وسهولة الاستخدام
+- إضافة ميزات تحليل الأسعار المتقدمة
+
+### 7. تحسين وحدة إدارة المشاريع
+- تطوير وحدة متكاملة لإدارة المشاريع المرساة
+- متابعة تنفيذ المشاريع ومراحلها
+- إدارة الموارد والجداول الزمنية للمشاريع
+
+## تحسينات الواجهة المرئية المطلوبة
+
+### 1. تحسين التصميم العام
+- تطوير واجهة مستخدم أكثر احترافية
+- تحسين الألوان والتباين
+- تنسيق متناسق بين جميع الوحدات
+
+### 2. تحسين القوائم والتنقل
+- تطوير شريط قوائم رئيسي متكامل
+- تحسين تجربة التنقل بين الوحدات
+- إضافة اختصارات للوظائف الأكثر استخداماً
+
+### 3. تحسين عرض الشعار والهوية
+- تحسين عرض شعار الشركة
+- تطبيق هوية بصرية موحدة في جميع أجزاء النظام
+- إضافة خيارات تخصيص الواجهة
+
+### 4. تحسين لوحات المعلومات
+- تطوير لوحات معلومات تفاعلية وجذابة
+- تحسين عرض المؤشرات والإحصائيات
+- إضافة رسوم بيانية متقدمة
+
+## متطلبات التكامل والاتساق
+
+### 1. تكامل الوحدات
+- ضمان التكامل السلس بين جميع وحدات النظام
+- مشاركة البيانات بين الوحدات المختلفة
+- واجهة مستخدم موحدة عبر جميع الوحدات
+
+### 2. اتساق البيانات
+- ضمان اتساق البيانات بين جميع أجزاء النظام
+- تزامن البيانات بين الوحدات المختلفة
+- منع تكرار البيانات وتضاربها
+
+### 3. أداء النظام
+- تحسين سرعة استجابة النظام
+- تحسين كفاءة استخدام الموارد
+- تحسين تجربة المستخدم العامة
+
+## الاعتماديات والمتطلبات الفنية
+
+يجب تحديث ملف المتطلبات ليشمل جميع المكتبات اللازمة للوحدات الجديدة والمحسنة، بما في ذلك:
+- مكتبات الخرائط والتحليل المكاني
+- مكتبات الترجمة ومعالجة اللغات
+- مكتبات الذكاء الاصطناعي والتعلم الآلي
+- مكتبات الرسوم البيانية والتصور
+- مكتبات واجهة المستخدم المتقدمة
diff --git a/docs/pricing_module_design.md b/docs/pricing_module_design.md
index 5592a878d3c94cac5e22a25e11d668e5b607ece1..9386198dae63822f7a2ef7eae54b935e2868c28d 100644
--- a/docs/pricing_module_design.md
+++ b/docs/pricing_module_design.md
@@ -1,714 +1,714 @@
-# تصميم وحدة التسعير المتكاملة وتحليل الأسعار
-
-## هيكل قاعدة البيانات
-
-### جدول فئات البنود (pricing_categories)
-```sql
-CREATE TABLE pricing_categories (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- description TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-```
-
-### جدول وحدات القياس (measurement_units)
-```sql
-CREATE TABLE measurement_units (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- symbol TEXT NOT NULL,
- description TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-```
-
-### جدول بنود التسعير الأساسية (pricing_items_base)
-```sql
-CREATE TABLE pricing_items_base (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- code TEXT NOT NULL,
- name TEXT NOT NULL,
- description TEXT,
- category_id INTEGER,
- unit_id INTEGER,
- base_price REAL NOT NULL,
- last_updated_date TEXT,
- price_source TEXT,
- notes TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (category_id) REFERENCES pricing_categories (id),
- FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
-);
-```
-
-### جدول تاريخ أسعار البنود (pricing_items_history)
-```sql
-CREATE TABLE pricing_items_history (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- base_item_id INTEGER,
- price REAL NOT NULL,
- price_date TEXT NOT NULL,
- price_source TEXT,
- notes TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id)
-);
-```
-
-### جدول بنود التسعير للمشاريع (project_pricing_items)
-```sql
-CREATE TABLE project_pricing_items (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- project_id INTEGER,
- base_item_id INTEGER,
- item_number TEXT NOT NULL,
- description TEXT NOT NULL,
- unit_id INTEGER,
- quantity REAL NOT NULL,
- unit_price REAL NOT NULL,
- total_price REAL NOT NULL,
- direct_cost REAL,
- indirect_cost REAL,
- profit_margin REAL,
- risk_factor REAL,
- notes TEXT,
- created_by INTEGER,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (project_id) REFERENCES projects (id),
- FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id),
- FOREIGN KEY (unit_id) REFERENCES measurement_units (id),
- FOREIGN KEY (created_by) REFERENCES users (id)
-);
-```
-
-### جدول مكونات بنود التسعير (pricing_item_components)
-```sql
-CREATE TABLE pricing_item_components (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- pricing_item_id INTEGER,
- component_type TEXT NOT NULL, -- 'material', 'labor', 'equipment', 'subcontractor', 'other'
- component_name TEXT NOT NULL,
- unit_id INTEGER,
- quantity REAL NOT NULL,
- unit_price REAL NOT NULL,
- total_price REAL NOT NULL,
- notes TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (pricing_item_id) REFERENCES project_pricing_items (id),
- FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
-);
-```
-
-### جدول عوامل التعديل (adjustment_factors)
-```sql
-CREATE TABLE adjustment_factors (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- description TEXT,
- factor_type TEXT NOT NULL, -- 'inflation', 'location', 'risk', 'market', 'other'
- value REAL NOT NULL,
- start_date TEXT,
- end_date TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-```
-
-### جدول نماذج التسعير (pricing_templates)
-```sql
-CREATE TABLE pricing_templates (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- description TEXT,
- created_by INTEGER,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (created_by) REFERENCES users (id)
-);
-```
-
-### جدول بنود نماذج التسعير (pricing_template_items)
-```sql
-CREATE TABLE pricing_template_items (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- template_id INTEGER,
- base_item_id INTEGER,
- item_number TEXT NOT NULL,
- description TEXT NOT NULL,
- unit_id INTEGER,
- unit_price REAL NOT NULL,
- notes TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (template_id) REFERENCES pricing_templates (id),
- FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id),
- FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
-);
-```
-
-### جدول تنبؤات الأسعار (price_forecasts)
-```sql
-CREATE TABLE price_forecasts (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- base_item_id INTEGER,
- forecast_date TEXT NOT NULL,
- forecast_price REAL NOT NULL,
- forecast_model TEXT,
- confidence_level REAL,
- scenario TEXT, -- 'optimistic', 'baseline', 'pessimistic'
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id)
-);
-```
-
-## هيكل الكلاسات
-
-### 1. مدير التسعير (PricingManager)
-```python
-class PricingManager:
- """فئة مدير التسعير الرئيسية"""
-
- def __init__(self, db_connector, config):
- """تهيئة مدير التسعير"""
- self.db = db_connector
- self.config = config
- self.item_manager = PricingItemManager(db_connector)
- self.cost_calculator = CostCalculator(db_connector)
- self.price_analyzer = PriceAnalyzer(db_connector)
- self.price_forecaster = PriceForecaster(db_connector)
- self.report_generator = PricingReportGenerator(db_connector)
-
- def initialize_database(self):
- """تهيئة قاعدة البيانات للتسعير"""
- # إنشاء الجداول إذا لم تكن موجودة
- pass
-
- def load_default_data(self):
- """تحميل البيانات الافتراضية"""
- # تحميل الفئات ووحدات القياس الافتراضية
- pass
-
- def get_project_pricing_summary(self, project_id):
- """الحصول على ملخص التسعير للمشروع"""
- pass
-
- def import_pricing_data(self, file_path, import_type):
- """استيراد بيانات التسعير من ملف خارجي"""
- pass
-
- def export_pricing_data(self, project_id, export_type, file_path):
- """تصدير بيانات التسعير إلى ملف خارجي"""
- pass
-```
-
-### 2. مدير بنود التسعير (PricingItemManager)
-```python
-class PricingItemManager:
- """فئة إدارة بنود التسعير"""
-
- def __init__(self, db_connector):
- """تهيئة مدير بنود التسعير"""
- self.db = db_connector
-
- def get_all_base_items(self, filters=None):
- """الحصول على جميع البنود الأساسية"""
- pass
-
- def get_base_item_by_id(self, item_id):
- """الحصول على بند أساسي بواسطة المعرف"""
- pass
-
- def add_base_item(self, item_data):
- """إضافة بند أساسي جديد"""
- pass
-
- def update_base_item(self, item_id, item_data):
- """تحديث بند أساسي"""
- pass
-
- def delete_base_item(self, item_id):
- """حذف بند أساسي"""
- pass
-
- def get_project_items(self, project_id):
- """الحصول على بنود المشروع"""
- pass
-
- def add_project_item(self, project_id, item_data):
- """إضافة بند للمشروع"""
- pass
-
- def update_project_item(self, item_id, item_data):
- """تحديث بند المشروع"""
- pass
-
- def delete_project_item(self, item_id):
- """حذف بند المشروع"""
- pass
-
- def get_item_components(self, item_id):
- """الحصول على مكونات البند"""
- pass
-
- def add_item_component(self, item_id, component_data):
- """إضافة مكون للبند"""
- pass
-
- def update_item_component(self, component_id, component_data):
- """تحديث مكون البند"""
- pass
-
- def delete_item_component(self, component_id):
- """حذف مكون البند"""
- pass
-
- def get_categories(self):
- """الحصول على فئات البنود"""
- pass
-
- def get_measurement_units(self):
- """الحصول على وحدات القياس"""
- pass
-
- def get_templates(self):
- """الحصول على نماذج التسعير"""
- pass
-
- def get_template_items(self, template_id):
- """الحصول على بنود النموذج"""
- pass
-
- def apply_template_to_project(self, project_id, template_id):
- """تطبيق نموذج على مشروع"""
- pass
-```
-
-### 3. حاسبة التكاليف (CostCalculator)
-```python
-class CostCalculator:
- """فئة حساب التكاليف"""
-
- def __init__(self, db_connector):
- """تهيئة حاسبة التكاليف"""
- self.db = db_connector
-
- def calculate_direct_costs(self, project_id):
- """حساب التكاليف المباشرة"""
- pass
-
- def calculate_indirect_costs(self, project_id, indirect_cost_percentage):
- """حساب التكاليف غير المباشرة"""
- pass
-
- def calculate_profit_margin(self, project_id, profit_percentage):
- """حساب هامش الربح"""
- pass
-
- def calculate_risk_contingency(self, project_id, risk_factors):
- """حساب احتياطي المخاطر"""
- pass
-
- def calculate_taxes_and_fees(self, project_id, tax_rates):
- """حساب الضرائب والرسوم"""
- pass
-
- def calculate_total_cost(self, project_id):
- """حساب التكلفة الإجمالية"""
- pass
-
- def calculate_unit_rates(self, project_id):
- """حساب معدلات الوحدات"""
- pass
-
- def apply_adjustment_factors(self, project_id, factors):
- """تطبيق عوامل التعديل"""
- pass
-
- def calculate_cost_breakdown(self, project_id):
- """حساب تفصيل التكاليف"""
- pass
-```
-
-### 4. محلل الأسعار (PriceAnalyzer)
-```python
-class PriceAnalyzer:
- """فئة تحليل الأسعار"""
-
- def __init__(self, db_connector):
- """تهيئة محلل الأسعار"""
- self.db = db_connector
-
- def get_price_history(self, item_id):
- """الحصول على تاريخ الأسعار"""
- pass
-
- def analyze_price_trends(self, item_id, start_date, end_date):
- """تحليل اتجاهات الأسعار"""
- pass
-
- def compare_prices(self, items, date=None):
- """مقارنة الأسعار"""
- pass
-
- def calculate_price_volatility(self, item_id, period):
- """حساب تقلب الأسعار"""
- pass
-
- def perform_sensitivity_analysis(self, project_id, variable_items, ranges):
- """إجراء تحليل الحساسية"""
- pass
-
- def analyze_price_correlations(self, items):
- """تحليل ارتباطات الأسعار"""
- pass
-
- def compare_with_market_prices(self, items):
- """مقارنة مع أسعار السوق"""
- pass
-
- def analyze_cost_drivers(self, project_id):
- """تحليل محركات التكلفة"""
- pass
-
- def generate_price_analysis_charts(self, analysis_type, params):
- """إنشاء رسوم بيانية لتحليل الأسعار"""
- pass
-```
-
-### 5. متنبئ الأسعار (PriceForecaster)
-```python
-class PriceForecaster:
- """فئة التنبؤ بالأسعار"""
-
- def __init__(self, db_connector):
- """تهيئة متنبئ الأسعار"""
- self.db = db_connector
-
- def forecast_price(self, item_id, forecast_date, model_type='arima'):
- """التنبؤ بالسعر"""
- pass
-
- def generate_price_scenarios(self, item_id, forecast_date):
- """إنشاء سيناريوهات الأسعار"""
- pass
-
- def calculate_inflation_impact(self, project_id, inflation_rate, duration):
- """حساب تأثير التضخم"""
- pass
-
- def forecast_project_costs(self, project_id, forecast_date):
- """التنبؤ بتكاليف المشروع"""
- pass
-
- def evaluate_forecast_accuracy(self, item_id):
- """تقييم دقة التنبؤ"""
- pass
-
- def generate_forecast_charts(self, item_id, forecast_date):
- """إنشاء رسوم بيانية للتنبؤ"""
- pass
-```
-
-### 6. مولد تقارير التسعير (PricingReportGenerator)
-```python
-class PricingReportGenerator:
- """فئة إنشاء تقارير التسعير"""
-
- def __init__(self, db_connector):
- """تهيئة مولد تقارير التسعير"""
- self.db = db_connector
-
- def generate_cost_summary_report(self, project_id):
- """إنشاء تقرير ملخص التكاليف"""
- pass
-
- def generate_detailed_items_report(self, project_id):
- """إنشاء تقرير تفصيلي للبنود"""
- pass
-
- def generate_price_comparison_report(self, items, parameters):
- """إنشاء تقرير مقارنة الأسعار"""
- pass
-
- def generate_sensitivity_analysis_report(self, project_id, parameters):
- """إنشاء تقرير تحليل الحساسية"""
- pass
-
- def generate_price_forecast_report(self, items, forecast_date):
- """إنشاء تقرير التنبؤ بالأسعار"""
- pass
-
- def generate_price_risk_report(self, project_id):
- """إنشاء تقرير مخاطر الأسعار"""
- pass
-
- def export_report_to_pdf(self, report_data, file_path):
- """تصدير التقرير إلى PDF"""
- pass
-
- def export_report_to_excel(self, report_data, file_path):
- """تصدير التقرير إلى Excel"""
- pass
-```
-
-## تصميم واجهة المستخدم
-
-### 1. الشاشة الرئيسية لوحدة التسعير
-
-```
-+--------------------------------------------------+
-| وحدة التسعير |
-+--------------------------------------------------+
-| |
-| +----------------+ +----------------------+ |
-| | المناقصات | | إحصائيات التسعير | |
-| | الحالية | | | |
-| | | | | |
-| | | | | |
-| | | | | |
-| +----------------+ +----------------------+ |
-| |
-| +----------------+ +----------------------+ |
-| | الوصول | | آخر التحديثات | |
-| | السريع | | | |
-| | | | | |
-| | | | | |
-| | | | | |
-| +----------------+ +----------------------+ |
-| |
-+--------------------------------------------------+
-```
-
-### 2. شاشة إدارة بنود التسعير
-
-```
-+--------------------------------------------------+
-| إدارة بنود التسعير |
-+--------------------------------------------------+
-| بحث: [ ] [تصفية▼] [تصدير] |
-+--------------------------------------------------+
-| # | الكود | الوصف | الوحدة | الكمية | السعر | المجموع |
-+--------------------------------------------------+
-| 1 | | | | | | |
-| 2 | | | | | | |
-| 3 | | | | | | |
-| 4 | | | | | | |
-| 5 | | | | | | |
-+--------------------------------------------------+
-| [إضافة بند] [حذف المحدد] [استيراد من Excel] |
-+--------------------------------------------------+
-| المجموع الكلي: |
-+--------------------------------------------------+
-```
-
-### 3. شاشة تفاصيل البند
-
-```
-+--------------------------------------------------+
-| تفاصيل البند |
-+--------------------------------------------------+
-| الكود: [ ] الوصف: [ ] |
-| الفئة: [ ▼] الوحدة: [ ▼] |
-+--------------------------------------------------+
-| مكونات البند: |
-+--------------------------------------------------+
-| النوع | الوصف | الوحدة | الكمية | السعر | المجموع |
-+--------------------------------------------------+
-| مواد | | | | | |
-| عمالة | | | | | |
-| معدات | | | | | |
-| أخرى | | | | | |
-+--------------------------------------------------+
-| [إضافة مكون] [حذف المحدد] |
-+--------------------------------------------------+
-| التكلفة المباشرة: |
-| التكلفة غير المباشرة: |
-| هامش الربح: |
-| احتياطي المخاطر: |
-| السعر النهائي: |
-+--------------------------------------------------+
-| [حفظ] [إلغاء] |
-+--------------------------------------------------+
-```
-
-### 4. شاشة تحليل الأسعار
-
-```
-+--------------------------------------------------+
-| تحليل الأسعار |
-+--------------------------------------------------+
-| [اختيار البند▼] [الفترة الزمنية▼] [تحليل] |
-+--------------------------------------------------+
-| |
-| |
-| |
-| (رسم بياني للأسعار) |
-| |
-| |
-| |
-+--------------------------------------------------+
-| إحصائيات: |
-| - متوسط السعر: |
-| - أعلى سعر: |
-| - أدنى سعر: |
-| - معدل التغير: |
-| - التقلب: |
-+--------------------------------------------------+
-| [مقارنة مع بنود أخرى] [تصدير التحليل] |
-+--------------------------------------------------+
-```
-
-### 5. شاشة التنبؤ بالأسعار
-
-```
-+--------------------------------------------------+
-| التنبؤ بالأسعار |
-+--------------------------------------------------+
-| [اختيار البند▼] [تاريخ التنبؤ] [نموذج التنبؤ▼] |
-+--------------------------------------------------+
-| |
-| |
-| |
-| (رسم بياني للتنبؤ بالأسعار) |
-| |
-| |
-| |
-+--------------------------------------------------+
-| السيناريوهات: |
-| - متفائل: |
-| - متوسط: |
-| - متشائم: |
-+--------------------------------------------------+
-| عوامل التأثير: |
-| - التضخم: |
-| - تغيرات السوق: |
-| - العوامل الموسمية: |
-+--------------------------------------------------+
-| [تطبيق على المشروع] [تصدير التنبؤ] |
-+--------------------------------------------------+
-```
-
-### 6. شاشة تحليل الحساسية
-
-```
-+--------------------------------------------------+
-| تحليل الحساسية |
-+--------------------------------------------------+
-| المشروع: [ ▼] |
-+--------------------------------------------------+
-| المتغيرات: |
-| [✓] أسعار المواد الخام (±20%) |
-| [✓] تكلفة العمالة (±15%) |
-| [✓] تكلفة المعدات (±10%) |
-| [ ] المصاريف العامة (±5%) |
-+--------------------------------------------------+
-| |
-| |
-| (رسم بياني لتحليل الحساسية) |
-| |
-| |
-| |
-+--------------------------------------------------+
-| النتائج: |
-| - أكثر العوامل تأثيراً: |
-| - نطاق التغير المتوقع: |
-| - توصيات: |
-+--------------------------------------------------+
-| [تحديث التحليل] [تصدير النتائج] |
-+--------------------------------------------------+
-```
-
-### 7. شاشة التقارير
-
-```
-+--------------------------------------------------+
-| التقارير |
-+--------------------------------------------------+
-| [نوع التقرير▼] [المشروع▼] [إنشاء تقرير] |
-+--------------------------------------------------+
-| التقارير المتاحة: |
-| |
-| ○ ملخص التكاليف |
-| ○ تفصيل البنود |
-| ○ مقارنة الأسعار |
-| ○ تحليل الحساسية |
-| ○ التنبؤ بالأسعار |
-| ○ مخاطر الأسعار |
-| |
-+--------------------------------------------------+
-| خيارات التقرير: |
-| |
-| [✓] تضمين الرسوم البيانية |
-| [✓] تضمين التوصيات |
-| [ ] تضمين البيانات التفصيلية |
-| |
-+--------------------------------------------------+
-| [PDF] [Excel] [طباعة] |
-+--------------------------------------------------+
-```
-
-## تكامل النظام
-
-### 1. تكامل مع وحدة تحليل المستندات
-- استخراج بنود التسعير من وثائق المناقصة
-- تحديد الكميات والمواصفات من المستندات
-- مقارنة البنود المستخرجة مع قاعدة البيانات
-
-### 2. تكامل مع وحدة تحليل المخاطر
-- تحديد المخاطر المرتبطة بالتسعير
-- تقييم تأثير المخاطر على التكاليف
-- تحديد احتياطي المخاطر المناسب
-
-### 3. تكامل مع وحدة إدارة المشاريع
-- متابعة التكاليف الفعلية مقابل المخططة
-- تحديث التنبؤات بناءً على بيانات المشروع الفعلية
-- تحليل انحرافات التكاليف
-
-### 4. تكامل مع وحدة التقارير
-- إنشاء تقارير متكاملة تشمل بيانات التسعير
-- دمج تحليلات التسعير في تقارير المشروع
-- توفير لوحات معلومات متكاملة
-
-## خطة التنفيذ التفصيلية
-
-### المرحلة 1: إعداد البنية التحتية (3 أيام)
-- تصميم وإنشاء جداول قاعدة البيانات
-- إعداد هيكل الملفات والمجلدات
-- تهيئة البيئة التطويرية
-
-### المرحلة 2: تنفيذ الوظائف الأساسية (5 أيام)
-- تنفيذ فئة مدير التسعير
-- تنفيذ فئة مدير بنود التسعير
-- تنفيذ فئة حاسبة التكاليف
-- إنشاء واجهات المستخدم الأساسية
-
-### المرحلة 3: تنفيذ وظائف التحليل (7 أيام)
-- تنفيذ فئة محلل الأسعار
-- تنفيذ فئة متنبئ الأسعار
-- إنشاء الرسوم البيانية والتحليلات
-- تنفيذ واجهات المستخدم للتحليل
-
-### المرحلة 4: تنفيذ التقارير والتكامل (5 أيام)
-- تنفيذ فئة مولد تقارير التسعير
-- تكامل مع الوحدات الأخرى
-- إنشاء واجهات المستخدم للتقارير
-- اختبار التكامل
-
-### المرحلة 5: الاختبار والتحسين (3 أيام)
-- اختبار جميع الوظائف
-- تحسين الأداء
-- إصلاح الأخطاء
-- تحسين واجهة المستخدم
-
-### المرحلة 6: التوثيق والتسليم (2 أيام)
-- إعداد وثائق المستخدم
-- إعداد وثائق المطور
-- تجهيز النسخة النهائية
-- تسليم النظام
+# تصميم وحدة التسعير المتكاملة وتحليل الأسعار
+
+## هيكل قاعدة البيانات
+
+### جدول فئات البنود (pricing_categories)
+```sql
+CREATE TABLE pricing_categories (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ description TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+### جدول وحدات القياس (measurement_units)
+```sql
+CREATE TABLE measurement_units (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ symbol TEXT NOT NULL,
+ description TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+### جدول بنود التسعير الأساسية (pricing_items_base)
+```sql
+CREATE TABLE pricing_items_base (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ code TEXT NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ category_id INTEGER,
+ unit_id INTEGER,
+ base_price REAL NOT NULL,
+ last_updated_date TEXT,
+ price_source TEXT,
+ notes TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES pricing_categories (id),
+ FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
+);
+```
+
+### جدول تاريخ أسعار البنود (pricing_items_history)
+```sql
+CREATE TABLE pricing_items_history (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ base_item_id INTEGER,
+ price REAL NOT NULL,
+ price_date TEXT NOT NULL,
+ price_source TEXT,
+ notes TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id)
+);
+```
+
+### جدول بنود التسعير للمشاريع (project_pricing_items)
+```sql
+CREATE TABLE project_pricing_items (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ project_id INTEGER,
+ base_item_id INTEGER,
+ item_number TEXT NOT NULL,
+ description TEXT NOT NULL,
+ unit_id INTEGER,
+ quantity REAL NOT NULL,
+ unit_price REAL NOT NULL,
+ total_price REAL NOT NULL,
+ direct_cost REAL,
+ indirect_cost REAL,
+ profit_margin REAL,
+ risk_factor REAL,
+ notes TEXT,
+ created_by INTEGER,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (project_id) REFERENCES projects (id),
+ FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id),
+ FOREIGN KEY (unit_id) REFERENCES measurement_units (id),
+ FOREIGN KEY (created_by) REFERENCES users (id)
+);
+```
+
+### جدول مكونات بنود التسعير (pricing_item_components)
+```sql
+CREATE TABLE pricing_item_components (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ pricing_item_id INTEGER,
+ component_type TEXT NOT NULL, -- 'material', 'labor', 'equipment', 'subcontractor', 'other'
+ component_name TEXT NOT NULL,
+ unit_id INTEGER,
+ quantity REAL NOT NULL,
+ unit_price REAL NOT NULL,
+ total_price REAL NOT NULL,
+ notes TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (pricing_item_id) REFERENCES project_pricing_items (id),
+ FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
+);
+```
+
+### جدول عوامل التعديل (adjustment_factors)
+```sql
+CREATE TABLE adjustment_factors (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ description TEXT,
+ factor_type TEXT NOT NULL, -- 'inflation', 'location', 'risk', 'market', 'other'
+ value REAL NOT NULL,
+ start_date TEXT,
+ end_date TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+### جدول نماذج التسعير (pricing_templates)
+```sql
+CREATE TABLE pricing_templates (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ description TEXT,
+ created_by INTEGER,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (created_by) REFERENCES users (id)
+);
+```
+
+### جدول بنود نماذج التسعير (pricing_template_items)
+```sql
+CREATE TABLE pricing_template_items (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ template_id INTEGER,
+ base_item_id INTEGER,
+ item_number TEXT NOT NULL,
+ description TEXT NOT NULL,
+ unit_id INTEGER,
+ unit_price REAL NOT NULL,
+ notes TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (template_id) REFERENCES pricing_templates (id),
+ FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id),
+ FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
+);
+```
+
+### جدول تنبؤات الأسعار (price_forecasts)
+```sql
+CREATE TABLE price_forecasts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ base_item_id INTEGER,
+ forecast_date TEXT NOT NULL,
+ forecast_price REAL NOT NULL,
+ forecast_model TEXT,
+ confidence_level REAL,
+ scenario TEXT, -- 'optimistic', 'baseline', 'pessimistic'
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id)
+);
+```
+
+## هيكل الكلاسات
+
+### 1. مدير التسعير (PricingManager)
+```python
+class PricingManager:
+ """فئة مدير التسعير الرئيسية"""
+
+ def __init__(self, db_connector, config):
+ """تهيئة مدير التسعير"""
+ self.db = db_connector
+ self.config = config
+ self.item_manager = PricingItemManager(db_connector)
+ self.cost_calculator = CostCalculator(db_connector)
+ self.price_analyzer = PriceAnalyzer(db_connector)
+ self.price_forecaster = PriceForecaster(db_connector)
+ self.report_generator = PricingReportGenerator(db_connector)
+
+ def initialize_database(self):
+ """تهيئة قاعدة البيانات للتسعير"""
+ # إنشاء الجداول إذا لم تكن موجودة
+ pass
+
+ def load_default_data(self):
+ """تحميل البيانات الافتراضية"""
+ # تحميل الفئات ووحدات القياس الافتراضية
+ pass
+
+ def get_project_pricing_summary(self, project_id):
+ """الحصول على ملخص التسعير للمشروع"""
+ pass
+
+ def import_pricing_data(self, file_path, import_type):
+ """استيراد بيانات التسعير من ملف خارجي"""
+ pass
+
+ def export_pricing_data(self, project_id, export_type, file_path):
+ """تصدير بيانات التسعير إلى ملف خارجي"""
+ pass
+```
+
+### 2. مدير بنود التسعير (PricingItemManager)
+```python
+class PricingItemManager:
+ """فئة إدارة بنود التسعير"""
+
+ def __init__(self, db_connector):
+ """تهيئة مدير بنود التسعير"""
+ self.db = db_connector
+
+ def get_all_base_items(self, filters=None):
+ """الحصول على جميع البنود الأساسية"""
+ pass
+
+ def get_base_item_by_id(self, item_id):
+ """الحصول على بند أساسي بواسطة المعرف"""
+ pass
+
+ def add_base_item(self, item_data):
+ """إضافة بند أساسي جديد"""
+ pass
+
+ def update_base_item(self, item_id, item_data):
+ """تحديث بند أساسي"""
+ pass
+
+ def delete_base_item(self, item_id):
+ """حذف بند أساسي"""
+ pass
+
+ def get_project_items(self, project_id):
+ """الحصول على بنود المشروع"""
+ pass
+
+ def add_project_item(self, project_id, item_data):
+ """إضافة بند للمشروع"""
+ pass
+
+ def update_project_item(self, item_id, item_data):
+ """تحديث بند المشروع"""
+ pass
+
+ def delete_project_item(self, item_id):
+ """حذف بند المشروع"""
+ pass
+
+ def get_item_components(self, item_id):
+ """الحصول على مكونات البند"""
+ pass
+
+ def add_item_component(self, item_id, component_data):
+ """إضافة مكون للبند"""
+ pass
+
+ def update_item_component(self, component_id, component_data):
+ """تحديث مكون البند"""
+ pass
+
+ def delete_item_component(self, component_id):
+ """حذف مكون البند"""
+ pass
+
+ def get_categories(self):
+ """الحصول على فئات البنود"""
+ pass
+
+ def get_measurement_units(self):
+ """الحصول على وحدات القياس"""
+ pass
+
+ def get_templates(self):
+ """الحصول على نماذج التسعير"""
+ pass
+
+ def get_template_items(self, template_id):
+ """الحصول على بنود النموذج"""
+ pass
+
+ def apply_template_to_project(self, project_id, template_id):
+ """تطبيق نموذج على مشروع"""
+ pass
+```
+
+### 3. حاسبة التكاليف (CostCalculator)
+```python
+class CostCalculator:
+ """فئة حساب التكاليف"""
+
+ def __init__(self, db_connector):
+ """تهيئة حاسبة التكاليف"""
+ self.db = db_connector
+
+ def calculate_direct_costs(self, project_id):
+ """حساب التكاليف المباشرة"""
+ pass
+
+ def calculate_indirect_costs(self, project_id, indirect_cost_percentage):
+ """حساب التكاليف غير المباشرة"""
+ pass
+
+ def calculate_profit_margin(self, project_id, profit_percentage):
+ """حساب هامش الربح"""
+ pass
+
+ def calculate_risk_contingency(self, project_id, risk_factors):
+ """حساب احتياطي المخاطر"""
+ pass
+
+ def calculate_taxes_and_fees(self, project_id, tax_rates):
+ """حساب الضرائب والرسوم"""
+ pass
+
+ def calculate_total_cost(self, project_id):
+ """حساب التكلفة الإجمالية"""
+ pass
+
+ def calculate_unit_rates(self, project_id):
+ """حساب معدلات الوحدات"""
+ pass
+
+ def apply_adjustment_factors(self, project_id, factors):
+ """تطبيق عوامل التعديل"""
+ pass
+
+ def calculate_cost_breakdown(self, project_id):
+ """حساب تفصيل التكاليف"""
+ pass
+```
+
+### 4. محلل الأسعار (PriceAnalyzer)
+```python
+class PriceAnalyzer:
+ """فئة تحليل الأسعار"""
+
+ def __init__(self, db_connector):
+ """تهيئة محلل الأسعار"""
+ self.db = db_connector
+
+ def get_price_history(self, item_id):
+ """الحصول على تاريخ الأسعار"""
+ pass
+
+ def analyze_price_trends(self, item_id, start_date, end_date):
+ """تحليل اتجاهات الأسعار"""
+ pass
+
+ def compare_prices(self, items, date=None):
+ """مقارنة الأسعار"""
+ pass
+
+ def calculate_price_volatility(self, item_id, period):
+ """حساب تقلب الأسعار"""
+ pass
+
+ def perform_sensitivity_analysis(self, project_id, variable_items, ranges):
+ """إجراء تحليل الحساسية"""
+ pass
+
+ def analyze_price_correlations(self, items):
+ """تحليل ارتباطات الأسعار"""
+ pass
+
+ def compare_with_market_prices(self, items):
+ """مقارنة مع أسعار السوق"""
+ pass
+
+ def analyze_cost_drivers(self, project_id):
+ """تحليل محركات التكلفة"""
+ pass
+
+ def generate_price_analysis_charts(self, analysis_type, params):
+ """إنشاء رسوم بيانية لتحليل الأسعار"""
+ pass
+```
+
+### 5. متنبئ الأسعار (PriceForecaster)
+```python
+class PriceForecaster:
+ """فئة التنبؤ بالأسعار"""
+
+ def __init__(self, db_connector):
+ """تهيئة متنبئ الأسعار"""
+ self.db = db_connector
+
+ def forecast_price(self, item_id, forecast_date, model_type='arima'):
+ """التنبؤ بالسعر"""
+ pass
+
+ def generate_price_scenarios(self, item_id, forecast_date):
+ """إنشاء سيناريوهات الأسعار"""
+ pass
+
+ def calculate_inflation_impact(self, project_id, inflation_rate, duration):
+ """حساب تأثير التضخم"""
+ pass
+
+ def forecast_project_costs(self, project_id, forecast_date):
+ """التنبؤ بتكاليف المشروع"""
+ pass
+
+ def evaluate_forecast_accuracy(self, item_id):
+ """تقييم دقة التنبؤ"""
+ pass
+
+ def generate_forecast_charts(self, item_id, forecast_date):
+ """إنشاء رسوم بيانية للتنبؤ"""
+ pass
+```
+
+### 6. مولد تقارير التسعير (PricingReportGenerator)
+```python
+class PricingReportGenerator:
+ """فئة إنشاء تقارير التسعير"""
+
+ def __init__(self, db_connector):
+ """تهيئة مولد تقارير التسعير"""
+ self.db = db_connector
+
+ def generate_cost_summary_report(self, project_id):
+ """إنشاء تقرير ملخص التكاليف"""
+ pass
+
+ def generate_detailed_items_report(self, project_id):
+ """إنشاء تقرير تفصيلي للبنود"""
+ pass
+
+ def generate_price_comparison_report(self, items, parameters):
+ """إنشاء تقرير مقارنة الأسعار"""
+ pass
+
+ def generate_sensitivity_analysis_report(self, project_id, parameters):
+ """إنشاء تقرير تحليل الحساسية"""
+ pass
+
+ def generate_price_forecast_report(self, items, forecast_date):
+ """إنشاء تقرير التنبؤ بالأسعار"""
+ pass
+
+ def generate_price_risk_report(self, project_id):
+ """إنشاء تقرير مخاطر الأسعار"""
+ pass
+
+ def export_report_to_pdf(self, report_data, file_path):
+ """تصدير التقرير إلى PDF"""
+ pass
+
+ def export_report_to_excel(self, report_data, file_path):
+ """تصدير التقرير إلى Excel"""
+ pass
+```
+
+## تصميم واجهة المستخدم
+
+### 1. الشاشة الرئيسية لوحدة التسعير
+
+```
++--------------------------------------------------+
+| وحدة التسعير |
++--------------------------------------------------+
+| |
+| +----------------+ +----------------------+ |
+| | المناقصات | | إحصائيات التسعير | |
+| | الحالية | | | |
+| | | | | |
+| | | | | |
+| | | | | |
+| +----------------+ +----------------------+ |
+| |
+| +----------------+ +----------------------+ |
+| | الوصول | | آخر التحديثات | |
+| | السريع | | | |
+| | | | | |
+| | | | | |
+| | | | | |
+| +----------------+ +----------------------+ |
+| |
++--------------------------------------------------+
+```
+
+### 2. شاشة إدارة بنود التسعير
+
+```
++--------------------------------------------------+
+| إدارة بنود التسعير |
++--------------------------------------------------+
+| بحث: [ ] [تصفية▼] [تصدير] |
++--------------------------------------------------+
+| # | الكود | الوصف | الوحدة | الكمية | السعر | المجموع |
++--------------------------------------------------+
+| 1 | | | | | | |
+| 2 | | | | | | |
+| 3 | | | | | | |
+| 4 | | | | | | |
+| 5 | | | | | | |
++--------------------------------------------------+
+| [إضافة بند] [حذف المحدد] [استيراد من Excel] |
++--------------------------------------------------+
+| المجموع الكلي: |
++--------------------------------------------------+
+```
+
+### 3. شاشة تفاصيل البند
+
+```
++--------------------------------------------------+
+| تفاصيل البند |
++--------------------------------------------------+
+| الكود: [ ] الوصف: [ ] |
+| الفئة: [ ▼] الوحدة: [ ▼] |
++--------------------------------------------------+
+| مكونات البند: |
++--------------------------------------------------+
+| النوع | الوصف | الوحدة | الكمية | السعر | المجموع |
++--------------------------------------------------+
+| مواد | | | | | |
+| عمالة | | | | | |
+| معدات | | | | | |
+| أخرى | | | | | |
++--------------------------------------------------+
+| [إضافة مكون] [حذف المحدد] |
++--------------------------------------------------+
+| التكلفة المباشرة: |
+| التكلفة غير المباشرة: |
+| هامش الربح: |
+| احتياطي المخاطر: |
+| السعر النهائي: |
++--------------------------------------------------+
+| [حفظ] [إلغاء] |
++--------------------------------------------------+
+```
+
+### 4. شاشة تحليل الأسعار
+
+```
++--------------------------------------------------+
+| تحليل الأسعار |
++--------------------------------------------------+
+| [اختيار البند▼] [الفترة الزمنية▼] [تحليل] |
++--------------------------------------------------+
+| |
+| |
+| |
+| (رسم بياني للأسعار) |
+| |
+| |
+| |
++--------------------------------------------------+
+| إحصائيات: |
+| - متوسط السعر: |
+| - أعلى سعر: |
+| - أدنى سعر: |
+| - معدل التغير: |
+| - التقلب: |
++--------------------------------------------------+
+| [مقارنة مع بنود أخرى] [تصدير التحليل] |
++--------------------------------------------------+
+```
+
+### 5. شاشة التنبؤ بالأسعار
+
+```
++--------------------------------------------------+
+| التنبؤ بالأسعار |
++--------------------------------------------------+
+| [اختيار البند▼] [تاريخ التنبؤ] [نموذج التنبؤ▼] |
++--------------------------------------------------+
+| |
+| |
+| |
+| (رسم بياني للتنبؤ بالأسعار) |
+| |
+| |
+| |
++--------------------------------------------------+
+| السيناريوهات: |
+| - متفائل: |
+| - متوسط: |
+| - متشائم: |
++--------------------------------------------------+
+| عوامل التأثير: |
+| - التضخم: |
+| - تغيرات السوق: |
+| - العوامل الموسمية: |
++--------------------------------------------------+
+| [تطبيق على المشروع] [تصدير التنبؤ] |
++--------------------------------------------------+
+```
+
+### 6. شاشة تحليل الحساسية
+
+```
++--------------------------------------------------+
+| تحليل الحساسية |
++--------------------------------------------------+
+| المشروع: [ ▼] |
++--------------------------------------------------+
+| المتغيرات: |
+| [✓] أسعار المواد الخام (±20%) |
+| [✓] تكلفة العمالة (±15%) |
+| [✓] تكلفة المعدات (±10%) |
+| [ ] المصاريف العامة (±5%) |
++--------------------------------------------------+
+| |
+| |
+| (رسم بياني لتحليل الحساسية) |
+| |
+| |
+| |
++--------------------------------------------------+
+| النتائج: |
+| - أكثر العوامل تأثيراً: |
+| - نطاق التغير المتوقع: |
+| - توصيات: |
++--------------------------------------------------+
+| [تحديث التحليل] [تصدير النتائج] |
++--------------------------------------------------+
+```
+
+### 7. شاشة التقارير
+
+```
++--------------------------------------------------+
+| التقارير |
++--------------------------------------------------+
+| [نوع التقرير▼] [المشروع▼] [إنشاء تقرير] |
++--------------------------------------------------+
+| التقارير المتاحة: |
+| |
+| ○ ملخص التكاليف |
+| ○ تفصيل البنود |
+| ○ مقارنة الأسعار |
+| ○ تحليل الحساسية |
+| ○ التنبؤ بالأسعار |
+| ○ مخاطر الأسعار |
+| |
++--------------------------------------------------+
+| خيارات التقرير: |
+| |
+| [✓] تضمين الرسوم البيانية |
+| [✓] تضمين التوصيات |
+| [ ] تضمين البيانات التفصيلية |
+| |
++--------------------------------------------------+
+| [PDF] [Excel] [طباعة] |
++--------------------------------------------------+
+```
+
+## تكامل النظام
+
+### 1. تكامل مع وحدة تحليل المستندات
+- استخراج بنود التسعير من وثائق المناقصة
+- تحديد الكميات والمواصفات من المستندات
+- مقارنة البنود المستخرجة مع قاعدة البيانات
+
+### 2. تكامل مع وحدة تحليل المخاطر
+- تحديد المخاطر المرتبطة بالتسعير
+- تقييم تأثير المخاطر على التكاليف
+- تحديد احتياطي المخاطر المناسب
+
+### 3. تكامل مع وحدة إدارة المشاريع
+- متابعة التكاليف الفعلية مقابل المخططة
+- تحديث التنبؤات بناءً على بيانات المشروع الفعلية
+- تحليل انحرافات التكاليف
+
+### 4. تكامل مع وحدة التقارير
+- إنشاء تقارير متكاملة تشمل بيانات التسعير
+- دمج تحليلات التسعير في تقارير المشروع
+- توفير لوحات معلومات متكاملة
+
+## خطة التنفيذ التفصيلية
+
+### المرحلة 1: إعداد البنية التحتية (3 أيام)
+- تصميم وإنشاء جداول قاعدة البيانات
+- إعداد هيكل الملفات والمجلدات
+- تهيئة البيئة التطويرية
+
+### المرحلة 2: تنفيذ الوظائف الأساسية (5 أيام)
+- تنفيذ فئة مدير التسعير
+- تنفيذ فئة مدير بنود التسعير
+- تنفيذ فئة حاسبة التكاليف
+- إنشاء واجهات المستخدم الأساسية
+
+### المرحلة 3: تنفيذ وظائف التحليل (7 أيام)
+- تنفيذ فئة محلل الأسعار
+- تنفيذ فئة متنبئ الأسعار
+- إنشاء الرسوم البيانية والتحليلات
+- تنفيذ واجهات المستخدم للتحليل
+
+### المرحلة 4: تنفيذ التقارير والتكامل (5 أيام)
+- تنفيذ فئة مولد تقارير التسعير
+- تكامل مع الوحدات الأخرى
+- إنشاء واجهات المستخدم للتقارير
+- اختبار التكامل
+
+### المرحلة 5: الاختبار والتحسين (3 أيام)
+- اختبار جميع الوظائف
+- تحسين الأداء
+- إصلاح الأخطاء
+- تحسين واجهة المستخدم
+
+### المرحلة 6: التوثيق والتسليم (2 أيام)
+- إعداد وثائق المستخدم
+- إعداد وثائق المطور
+- تجهيز النسخة النهائية
+- تسليم النظام
diff --git a/docs/pricing_module_requirements.md b/docs/pricing_module_requirements.md
index 683f21615da46beb5124d14f57cf768f1dad98db..58a33bcd334a48c06561fafc097bdfa25d7e7f72 100644
--- a/docs/pricing_module_requirements.md
+++ b/docs/pricing_module_requirements.md
@@ -1,155 +1,155 @@
-# متطلبات وحدة التسعير المتكاملة وتحليل الأسعار
-
-## الهدف
-تطوير وحدة متكاملة للتسعير وتحليل الأسعار في نظام إدارة المناقصات لتمكين المستخدمين من إدارة عمليات التسعير بشكل أكثر دقة وفعالية، وتوفير أدوات تحليلية متقدمة لاتخاذ قرارات أفضل.
-
-## المتطلبات الوظيفية
-
-### 1. إدارة بنود التسعير
-- إنشاء وتحرير وحذف بنود التسعير
-- تصنيف البنود حسب الفئات (مواد، عمالة، معدات، مصاريف عامة)
-- دعم الوحدات المختلفة (متر مربع، متر مكعب، عدد، طن، إلخ)
-- إمكانية استيراد بنود التسعير من ملفات Excel
-- إمكانية تصدير بنود التسعير إلى ملفات Excel
-
-### 2. حساب التكاليف
-- حساب تكلفة المواد المباشرة
-- حساب تكلفة العمالة المباشرة
-- حساب تكلفة المعدات
-- حساب المصاريف العامة والإدارية
-- حساب هامش الربح
-- حساب الضرائب والرسوم
-
-### 3. تحليل الأسعار
-- مقارنة الأسعار التاريخية للبنود
-- تحليل تغيرات الأسعار عبر الزمن
-- تحليل حساسية الأسعار للمتغيرات المختلفة
-- تحليل المخاطر المرتبطة بتغيرات الأسعار
-- مقارنة الأسعار مع أسعار السوق
-- تحليل تأثير تغير أسعار المواد الخام على التكلفة الإجمالية
-
-### 4. التنبؤ بالأسعار
-- التنبؤ بتغيرات الأسعار المستقبلية
-- حساب معدلات التضخم المتوقعة
-- تقدير تأثير العوامل الاقتصادية على الأسعار
-- إنشاء سيناريوهات مختلفة للأسعار (متفائل، متوسط، متشائم)
-
-### 5. تقارير التسعير
-- تقرير ملخص التكاليف
-- تقرير تفصيلي للبنود
-- تقرير مقارنة الأسعار
-- تقرير تحليل الحساسية
-- تقرير التنبؤ بالأسعار
-- تقرير المخاطر المرتبطة بالأسعار
-
-### 6. لوحة معلومات التسعير
-- عرض مؤشرات الأداء الرئيسية للتسعير
-- عرض الرسوم البيانية لتحليل الأسعار
-- عرض تنبيهات لتغيرات الأسعار الكبيرة
-- عرض مقارنات مع المناقصات السابقة
-
-## المتطلبات غير الوظيفية
-
-### 1. الأداء
-- سرعة استجابة عالية عند التعامل مع كميات كبيرة من البيانات
-- قدرة على معالجة آلاف البنود في المناقصة الواحدة
-
-### 2. قابلية الاستخدام
-- واجهة مستخدم بديهية وسهلة الاستخدام
-- إمكانية تخصيص العرض حسب احتياجات المستخدم
-- توفير أدوات مساعدة وشروحات للمستخدمين
-
-### 3. التكامل
-- تكامل مع وحدة تحليل المستندات لاستخراج بنود التسعير من وثائق المناقصة
-- تكامل مع وحدة تحليل المخاطر لتقييم مخاطر التسعير
-- تكامل مع وحدة إدارة المشاريع لمتابعة التكاليف الفعلية مقابل المخططة
-
-### 4. الأمان
-- تحديد صلاحيات الوصول لبيانات التسعير
-- تسجيل جميع التغييرات على بيانات التسعير
-- حماية البيانات الحساسة المتعلقة بالتسعير
-
-## التقنيات المقترحة
-
-### 1. تخزين البيانات
-- استخدام قاعدة بيانات SQLite لتخزين بيانات التسعير
-- تصميم جداول مناسبة لتخزين البنود والتكاليف والأسعار التاريخية
-
-### 2. تحليل البيانات
-- استخدام مكتبة Pandas لمعالجة وتحليل بيانات الأسعار
-- استخدام مكتبة NumPy للعمليات الحسابية المتقدمة
-- استخدام مكتبة SciPy للتحليل الإحصائي
-
-### 3. التنبؤ بالأسعار
-- استخدام مكتبة Statsmodels للنماذج الإحصائية
-- استخدام مكتبة Prophet للتنبؤ بالسلاسل الزمنية
-- استخدام مكتبة scikit-learn لنماذج التعلم الآلي
-
-### 4. العرض المرئي
-- استخدام مكتبة Matplotlib لإنشاء الرسوم البيانية الأساسية
-- استخدام مكتبة Seaborn للرسوم البيانية الإحصائية المتقدمة
-- استخدام مكتبة Plotly للرسوم البيانية التفاعلية
-
-## الواجهة المقترحة
-
-### 1. الشاشة الرئيسية لوحدة التسعير
-- قائمة بالمناقصات الحالية
-- ملخص لإحصائيات التسعير
-- الوصول السريع للوظائف الشائعة
-
-### 2. شاشة إدارة بنود التسعير
-- جدول لعرض وتحرير البنود
-- أدوات للتصفية والبحث
-- أزرار للإضافة والحذف والاستيراد والتصدير
-
-### 3. شاشة حساب التكاليف
-- نموذج لإدخال معلومات التكاليف
-- عرض ملخص للتكاليف حسب الفئات
-- حاسبة تفاعلية للتكاليف
-
-### 4. شاشة تحليل الأسعار
-- رسوم بيانية لتحليل الأسعار
-- أدوات للمقارنة والتحليل
-- خيارات لتخصيص التحليل
-
-### 5. شاشة التنبؤ بالأسعار
-- نماذج للتنبؤ بالأسعار
-- عرض السيناريوهات المختلفة
-- تحليل الحساسية للمتغيرات
-
-### 6. شاشة تقارير التسعير
-- قائمة بالتقارير المتاحة
-- خيارات لتخصيص التقارير
-- أدوات لتصدير التقارير
-
-## خطة التنفيذ
-
-### المرحلة 1: تصميم قاعدة البيانات وهيكل الوحدة
-- تصميم جداول قاعدة البيانات
-- تصميم هيكل الكلاسات والوظائف
-- تصميم واجهة المستخدم
-
-### المرحلة 2: تنفيذ إدارة بنود التسعير وحساب التكاليف
-- تنفيذ وظائف إدارة البنود
-- تنفيذ وظائف حساب التكاليف
-- تنفيذ واجهة المستخدم لهذه الوظائف
-
-### المرحلة 3: تنفيذ تحليل الأسعار والتنبؤ
-- تنفيذ وظائف تحليل الأسعار
-- تنفيذ وظائف التنبؤ بالأسعار
-- تنفيذ واجهة المستخدم لهذه الوظائف
-
-### المرحلة 4: تنفيذ التقارير ولوحة المعلومات
-- تنفيذ وظائف إنشاء التقارير
-- تنفيذ لوحة معلومات التسعير
-- تنفيذ واجهة المستخدم لهذه الوظائف
-
-### المرحلة 5: التكامل والاختبار
-- تكامل الوحدة مع النظام الحالي
-- اختبار الوظائف والأداء
-- تصحيح الأخطاء وتحسين الأداء
-
-### المرحلة 6: التوثيق والتسليم
-- توثيق الوحدة وكيفية استخدامها
-- إعداد أمثلة ودروس تعليمية
-- تسليم الوحدة للمستخدم النهائي
+# متطلبات وحدة التسعير المتكاملة وتحليل الأسعار
+
+## الهدف
+تطوير وحدة متكاملة للتسعير وتحليل الأسعار في نظام إدارة المناقصات لتمكين المستخدمين من إدارة عمليات التسعير بشكل أكثر دقة وفعالية، وتوفير أدوات تحليلية متقدمة لاتخاذ قرارات أفضل.
+
+## المتطلبات الوظيفية
+
+### 1. إدارة بنود التسعير
+- إنشاء وتحرير وحذف بنود التسعير
+- تصنيف البنود حسب الفئات (مواد، عمالة، معدات، مصاريف عامة)
+- دعم الوحدات المختلفة (متر مربع، متر مكعب، عدد، طن، إلخ)
+- إمكانية استيراد بنود التسعير من ملفات Excel
+- إمكانية تصدير بنود التسعير إلى ملفات Excel
+
+### 2. حساب التكاليف
+- حساب تكلفة المواد المباشرة
+- حساب تكلفة العمالة المباشرة
+- حساب تكلفة المعدات
+- حساب المصاريف العامة والإدارية
+- حساب هامش الربح
+- حساب الضرائب والرسوم
+
+### 3. تحليل الأسعار
+- مقارنة الأسعار التاريخية للبنود
+- تحليل تغيرات الأسعار عبر الزمن
+- تحليل حساسية الأسعار للمتغيرات المختلفة
+- تحليل المخاطر المرتبطة بتغيرات الأسعار
+- مقارنة الأسعار مع أسعار السوق
+- تحليل تأثير تغير أسعار المواد الخام على التكلفة الإجمالية
+
+### 4. التنبؤ بالأسعار
+- التنبؤ بتغيرات الأسعار المستقبلية
+- حساب معدلات التضخم المتوقعة
+- تقدير تأثير العوامل الاقتصادية على الأسعار
+- إنشاء سيناريوهات مختلفة للأسعار (متفائل، متوسط، متشائم)
+
+### 5. تقارير التسعير
+- تقرير ملخص التكاليف
+- تقرير تفصيلي للبنود
+- تقرير مقارنة الأسعار
+- تقرير تحليل الحساسية
+- تقرير التنبؤ بالأسعار
+- تقرير المخاطر المرتبطة بالأسعار
+
+### 6. لوحة معلومات التسعير
+- عرض مؤشرات الأداء الرئيسية للتسعير
+- عرض الرسوم البيانية لتحليل الأسعار
+- عرض تنبيهات لتغيرات الأسعار الكبيرة
+- عرض مقارنات مع المناقصات السابقة
+
+## المتطلبات غير الوظيفية
+
+### 1. الأداء
+- سرعة استجابة عالية عند التعامل مع كميات كبيرة من البيانات
+- قدرة على معالجة آلاف البنود في المناقصة الواحدة
+
+### 2. قابلية الاستخدام
+- واجهة مستخدم بديهية وسهلة الاستخدام
+- إمكانية تخصيص العرض حسب احتياجات المستخدم
+- توفير أدوات مساعدة وشروحات للمستخدمين
+
+### 3. التكامل
+- تكامل مع وحدة تحليل المستندات لاستخراج بنود التسعير من وثائق المناقصة
+- تكامل مع وحدة تحليل المخاطر لتقييم مخاطر التسعير
+- تكامل مع وحدة إدارة المشاريع لمتابعة التكاليف الفعلية مقابل المخططة
+
+### 4. الأمان
+- تحديد صلاحيات الوصول لبيانات التسعير
+- تسجيل جميع التغييرات على بيانات التسعير
+- حماية البيانات الحساسة المتعلقة بالتسعير
+
+## التقنيات المقترحة
+
+### 1. تخزين البيانات
+- استخدام قاعدة بيانات SQLite لتخزين بيانات التسعير
+- تصميم جداول مناسبة لتخزين البنود والتكاليف والأسعار التاريخية
+
+### 2. تحليل البيانات
+- استخدام مكتبة Pandas لمعالجة وتحليل بيانات الأسعار
+- استخدام مكتبة NumPy للعمليات الحسابية المتقدمة
+- استخدام مكتبة SciPy للتحليل الإحصائي
+
+### 3. التنبؤ بالأسعار
+- استخدام مكتبة Statsmodels للنماذج الإحصائية
+- استخدام مكتبة Prophet للتنبؤ بالسلاسل الزمنية
+- استخدام مكتبة scikit-learn لنماذج التعلم الآلي
+
+### 4. العرض المرئي
+- استخدام مكتبة Matplotlib لإنشاء الرسوم البيانية الأساسية
+- استخدام مكتبة Seaborn للرسوم البيانية الإحصائية المتقدمة
+- استخدام مكتبة Plotly للرسوم البيانية التفاعلية
+
+## الواجهة المقترحة
+
+### 1. الشاشة الرئيسية لوحدة التسعير
+- قائمة بالمناقصات الحالية
+- ملخص لإحصائيات التسعير
+- الوصول السريع للوظائف الشائعة
+
+### 2. شاشة إدارة بنود التسعير
+- جدول لعرض وتحرير البنود
+- أدوات للتصفية والبحث
+- أزرار للإضافة والحذف والاستيراد والتصدير
+
+### 3. شاشة حساب التكاليف
+- نموذج لإدخال معلومات التكاليف
+- عرض ملخص للتكاليف حسب الفئات
+- حاسبة تفاعلية للتكاليف
+
+### 4. شاشة تحليل الأسعار
+- رسوم بيانية لتحليل الأسعار
+- أدوات للمقارنة والتحليل
+- خيارات لتخصيص التحليل
+
+### 5. شاشة التنبؤ بالأسعار
+- نماذج للتنبؤ بالأسعار
+- عرض السيناريوهات المختلفة
+- تحليل الحساسية للمتغيرات
+
+### 6. شاشة تقارير التسعير
+- قائمة بالتقارير المتاحة
+- خيارات لتخصيص التقارير
+- أدوات لتصدير التقارير
+
+## خطة التنفيذ
+
+### المرحلة 1: تصميم قاعدة البيانات وهيكل الوحدة
+- تصميم جداول قاعدة البيانات
+- تصميم هيكل الكلاسات والوظائف
+- تصميم واجهة المستخدم
+
+### المرحلة 2: تنفيذ إدارة بنود التسعير وحساب التكاليف
+- تنفيذ وظائف إدارة البنود
+- تنفيذ وظائف حساب التكاليف
+- تنفيذ واجهة المستخدم لهذه الوظائف
+
+### المرحلة 3: تنفيذ تحليل الأسعار والتنبؤ
+- تنفيذ وظائف تحليل الأسعار
+- تنفيذ وظائف التنبؤ بالأسعار
+- تنفيذ واجهة المستخدم لهذه الوظائف
+
+### المرحلة 4: تنفيذ التقارير ولوحة المعلومات
+- تنفيذ وظائف إنشاء التقارير
+- تنفيذ لوحة معلومات التسعير
+- تنفيذ واجهة المستخدم لهذه الوظائف
+
+### المرحلة 5: التكامل والاختبار
+- تكامل الوحدة مع النظام الحالي
+- اختبار الوظائف والأداء
+- تصحيح الأخطاء وتحسين الأداء
+
+### المرحلة 6: التوثيق والتسليم
+- توثيق الوحدة وكيفية استخدامها
+- إعداد أمثلة ودروس تعليمية
+- تسليم الوحدة للمستخدم النهائي
diff --git a/fixed_pricing_app.py b/fixed_pricing_app.py
index b1b16e20dc533f3d13c0b5dd7e2cbbf94b4c14a8..ee4cbee24cde87ec7845f33283939f2489887b64 100644
--- a/fixed_pricing_app.py
+++ b/fixed_pricing_app.py
@@ -1,544 +1,544 @@
-"""
-وحدة التسعير - التطبيق الرئيسي
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import plotly.express as px
-import plotly.graph_objects as go
-from datetime import datetime
-import time
-import io
-import os
-import json
-import base64
-from pathlib import Path
-
-class PricingApp:
- """وحدة التسعير"""
-
- def __init__(self):
- """تهيئة وحدة التسعير"""
-
- # تهيئة حالة الجلسة
- if 'bill_of_quantities' not in st.session_state:
- st.session_state.bill_of_quantities = [
- {
- 'id': 1,
- 'code': 'A-001',
- 'description': 'أعمال الحفر والردم',
- 'unit': 'م3',
- 'quantity': 1500,
- 'unit_price': 45,
- 'total_price': 67500,
- 'category': 'أعمال ترابية'
- },
- {
- 'id': 2,
- 'code': 'A-002',
- 'description': 'توريد وصب خرسانة عادية',
- 'unit': 'م3',
- 'quantity': 250,
- 'unit_price': 350,
- 'total_price': 87500,
- 'category': 'أعمال خرسانية'
- },
- {
- 'id': 3,
- 'code': 'A-003',
- 'description': 'توريد وصب خرسانة مسلحة للأساسات',
- 'unit': 'م3',
- 'quantity': 180,
- 'unit_price': 450,
- 'total_price': 81000,
- 'category': 'أعمال خرسانية'
- },
- {
- 'id': 4,
- 'code': 'A-004',
- 'description': 'توريد وصب خرسانة مسلحة للأعمدة',
- 'unit': 'م3',
- 'quantity': 120,
- 'unit_price': 500,
- 'total_price': 60000,
- 'category': 'أعمال خرسانية'
- },
- {
- 'id': 5,
- 'code': 'A-005',
- 'description': 'توريد وتركيب حديد تسليح',
- 'unit': 'طن',
- 'quantity': 45,
- 'unit_price': 3000,
- 'total_price': 135000,
- 'category': 'أعمال حديد'
- },
- {
- 'id': 6,
- 'code': 'A-006',
- 'description': 'توريد وبناء طابوق',
- 'unit': 'م2',
- 'quantity': 1200,
- 'unit_price': 45,
- 'total_price': 54000,
- 'category': 'أعمال بناء'
- },
- {
- 'id': 7,
- 'code': 'A-007',
- 'description': 'أعمال اللياسة والتشطيبات',
- 'unit': 'م2',
- 'quantity': 2400,
- 'unit_price': 35,
- 'total_price': 84000,
- 'category': 'أعمال تشطيبات'
- },
- {
- 'id': 8,
- 'code': 'A-008',
- 'description': 'أعمال الدهانات',
- 'unit': 'م2',
- 'quantity': 2400,
- 'unit_price': 25,
- 'total_price': 60000,
- 'category': 'أعمال تشطيبات'
- },
- {
- 'id': 9,
- 'code': 'A-009',
- 'description': 'توريد وتركيب أبواب خشبية',
- 'unit': 'عدد',
- 'quantity': 24,
- 'unit_price': 750,
- 'total_price': 18000,
- 'category': 'أعمال نجارة'
- },
- {
- 'id': 10,
- 'code': 'A-010',
- 'description': 'توريد وتركيب نوافذ ألمنيوم',
- 'unit': 'م2',
- 'quantity': 120,
- 'unit_price': 350,
- 'total_price': 42000,
- 'category': 'أعمال ألمنيوم'
- }
- ]
-
- if 'cost_analysis' not in st.session_state:
- st.session_state.cost_analysis = [
- {
- 'id': 1,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'مواد',
- 'description': 'خرسانة',
- 'amount': 120000,
- 'percentage': 17.9
- },
- {
- 'id': 2,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'مواد',
- 'description': 'حديد تسليح',
- 'amount': 135000,
- 'percentage': 20.1
- },
- {
- 'id': 3,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'مواد',
- 'description': 'طابوق',
- 'amount': 54000,
- 'percentage': 8.1
- },
- {
- 'id': 4,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'عمالة',
- 'description': 'عمالة تنفيذ',
- 'amount': 120000,
- 'percentage': 17.9
- },
- {
- 'id': 5,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'معدات',
- 'description': 'معدات إنشائية',
- 'amount': 85000,
- 'percentage': 12.7
- },
- {
- 'id': 6,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'إدارة',
- 'description': 'إدارة المشروع',
- 'amount': 45000,
- 'percentage': 6.7
- },
- {
- 'id': 7,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'إدارة',
- 'description': 'إشراف هندسي',
- 'amount': 35000,
- 'percentage': 5.2
- },
- {
- 'id': 8,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'عامة',
- 'description': 'تأمينات وضمانات',
- 'amount': 25000,
- 'percentage': 3.7
- },
- {
- 'id': 9,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'عامة',
- 'description': 'مصاريف إدارية',
- 'amount': 30000,
- 'percentage': 4.5
- },
- {
- 'id': 10,
- 'category': 'أرباح',
- 'subcategory': 'أرباح',
- 'description': 'هامش الربح',
- 'amount': 55000,
- 'percentage': 8.2
- }
- ]
-
- if 'price_scenarios' not in st.session_state:
- st.session_state.price_scenarios = [
- {
- 'id': 1,
- 'name': 'السيناريو الأساسي',
- 'description': 'التسعير الأساسي مع هامش ربح 8%',
- 'total_cost': 615000,
- 'profit_margin': 8.2,
- 'total_price': 670000,
- 'is_active': True
- },
- {
- 'id': 2,
- 'name': 'سيناريو تنافسي',
- 'description': 'تخفيض هامش الربح للمنافسة',
- 'total_cost': 615000,
- 'profit_margin': 5.0,
- 'total_price': 650000,
- 'is_active': False
- },
- {
- 'id': 3,
- 'name': 'سيناريو مرتفع',
- 'description': 'زيادة هامش الربح للمشاريع ذات المخاطر العالية',
- 'total_cost': 615000,
- 'profit_margin': 12.0,
- 'total_price': 700000,
- 'is_active': False
- }
- ]
-
- def run(self):
- """تشغيل وحدة التسعير"""
- # استدعاء دالة العرض
- self.render()
-
- def render(self):
- """عرض واجهة وحدة التسعير"""
-
- st.markdown("وحدة التسعير ", unsafe_allow_html=True)
-
- tabs = st.tabs([
- "لوحة التحكم",
- "جدول الكميات",
- "تحليل التكاليف",
- "سيناريوهات التسعير",
- "المقارنة التنافسية",
- "التقارير"
- ])
-
- with tabs[0]:
- self._render_dashboard_tab()
-
- with tabs[1]:
- self._render_bill_of_quantities_tab()
-
- with tabs[2]:
- self._render_cost_analysis_tab()
-
- with tabs[3]:
- self._render_pricing_scenarios_tab()
-
- with tabs[4]:
- self._render_competitive_analysis_tab()
-
- with tabs[5]:
- self._render_reports_tab()
-
- def _render_dashboard_tab(self):
- """عرض تبويب لوحة التحكم"""
-
- st.markdown("### لوحة تحكم التسعير")
-
- # عرض ملخص التسعير
- col1, col2, col3, col4 = st.columns(4)
-
- # حساب إجمالي التكاليف
- total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
- total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
- total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
- total_cost = total_direct_cost + total_indirect_cost
- total_price = total_cost + total_profit
-
- with col1:
- st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.0f} ريال")
-
- with col2:
- st.metric("إجمالي التكاليف غير المباشرة", f"{total_indirect_cost:,.0f} ريال")
-
- with col3:
- st.metric("إجمالي التكاليف", f"{total_cost:,.0f} ريال")
-
- with col4:
- st.metric("السعر الإجمالي", f"{total_price:,.0f} ريال")
-
- # عرض توزيع التكاليف
- st.markdown("### توزيع التكاليف")
-
- # تجميع البيانات حسب الفئة
- cost_categories = {}
-
- for item in st.session_state.cost_analysis:
- category = item['category']
- if category in cost_categories:
- cost_categories[category] += item['amount']
- else:
- cost_categories[category] = item['amount']
-
- # إنشاء DataFrame للرسم البياني
- cost_df = pd.DataFrame({
- 'الفئة': list(cost_categories.keys()),
- 'المبلغ': list(cost_categories.values())
- })
-
- # إنشاء رسم بياني دائري
- fig = px.pie(
- cost_df,
- values='المبلغ',
- names='الفئة',
- title='توزيع التكاليف حسب الفئة',
- color_discrete_sequence=px.colors.qualitative.Set3
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع التكاليف المباشرة
- st.markdown("### توزيع التكاليف المباشرة")
-
- # تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
- direct_cost_subcategories = {}
-
- for item in st.session_state.cost_analysis:
- if item['category'] == 'تكاليف مباشرة':
- subcategory = item['subcategory']
- if subcategory in direct_cost_subcategories:
- direct_cost_subcategories[subcategory] += item['amount']
- else:
- direct_cost_subcategories[subcategory] = item['amount']
-
- # إنشاء DataFrame للرسم البياني
- direct_cost_df = pd.DataFrame({
- 'الفئة الفرعية': list(direct_cost_subcategories.keys()),
- 'المبلغ': list(direct_cost_subcategories.values())
- })
-
- # إنشاء رسم بياني دائري
- fig = px.pie(
- direct_cost_df,
- values='المبلغ',
- names='الفئة الفرعية',
- title='توزيع التكاليف المباشرة',
- color_discrete_sequence=px.colors.qualitative.Pastel
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع التكاليف غير المباشرة
- st.markdown("### توزيع التكاليف غير المباشرة")
-
- # تجميع البيانات حسب الفئة الفرعية للتكاليف غير المباشرة
- indirect_cost_subcategories = {}
-
- for item in st.session_state.cost_analysis:
- if item['category'] == 'تكاليف غير مباشرة':
- subcategory = item['subcategory']
- if subcategory in indirect_cost_subcategories:
- indirect_cost_subcategories[subcategory] += item['amount']
- else:
- indirect_cost_subcategories[subcategory] = item['amount']
-
- # إنشاء DataFrame للرسم البياني
- indirect_cost_df = pd.DataFrame({
- 'الفئة الفرعية': list(indirect_cost_subcategories.keys()),
- 'المبلغ': list(indirect_cost_subcategories.values())
- })
-
- # إنشاء رسم بياني دائري
- fig = px.pie(
- indirect_cost_df,
- values='المبلغ',
- names='الفئة الفرعية',
- title='توزيع التكاليف غير المباشرة',
- color_discrete_sequence=px.colors.qualitative.Pastel1
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_bill_of_quantities_tab(self):
- """عرض تبويب جدول الكميات"""
-
- st.markdown("### جدول الكميات")
-
- # إنشاء DataFrame من بيانات جدول الكميات
- boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
-
- # عرض جدول الكميات
- st.dataframe(
- boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']],
- column_config={
- 'code': 'الكود',
- 'description': 'الوصف',
- 'unit': 'الوحدة',
- 'quantity': 'الكمية',
- 'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
- 'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
- 'category': 'الفئة'
- },
- hide_index=True,
- use_container_width=True
- )
-
- # إضافة بند جديد
- st.markdown("### إضافة بند جديد")
-
- col1, col2 = st.columns(2)
-
- with col1:
- new_code = st.text_input("الكود", key="new_boq_code")
- new_description = st.text_input("الوصف", key="new_boq_description")
- new_unit = st.selectbox("الوحدة", ["م3", "م2", "طن", "عدد", "متر طولي"], key="new_boq_unit")
-
- with col2:
- new_quantity = st.number_input("الكمية", min_value=0.0, step=1.0, key="new_boq_quantity")
- new_unit_price = st.number_input("سعر الوحدة", min_value=0.0, step=10.0, key="new_boq_unit_price")
- new_category = st.selectbox(
- "الفئة",
- [
- "أعمال ترابية",
- "أعمال خرسانية",
- "أعمال حديد",
- "أعمال بناء",
- "أعمال تشطيبات",
- "أعمال نجارة",
- "أعمال ألمنيوم",
- "أعمال كهربائية",
- "أعمال ميكانيكية",
- "أعمال صحية"
- ],
- key="new_boq_category"
- )
-
- if st.button("إضافة البند", key="add_boq_item"):
- if new_code and new_description and new_quantity > 0 and new_unit_price > 0:
- # حساب السعر الإجمالي
- new_total_price = new_quantity * new_unit_price
-
- # إضافة بند جديد
- new_id = max([item['id'] for item in st.session_state.bill_of_quantities]) + 1
-
- st.session_state.bill_of_quantities.append({
- 'id': new_id,
- 'code': new_code,
- 'description': new_description,
- 'unit': new_unit,
- 'quantity': new_quantity,
- 'unit_price': new_unit_price,
- 'total_price': new_total_price,
- 'category': new_category
- })
-
- st.success(f"تمت إضافة البند بنجاح: {new_description}")
-
- # تحديث الصفحة لعرض البند الجديد
- st.rerun()
- else:
- st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
-
- # عرض ملخص جدول الكميات (إزالة التكرار)
- st.markdown("### ملخص جدول الكميات")
-
- # تجميع البيانات حسب الفئة
- category_totals = {}
- for item in st.session_state.bill_of_quantities:
- category = item['category']
- if category in category_totals:
- category_totals[category] += item['total_price']
- else:
- category_totals[category] = item['total_price']
-
- # إنشاء DataFrame للرسم البياني
- category_df = pd.DataFrame({
- 'الفئة': list(category_totals.keys()),
- 'المبلغ': list(category_totals.values())
- })
-
- # ترتيب البيانات تنازليًا حسب المبلغ
- category_df = category_df.sort_values('المبلغ', ascending=False)
-
- # إنشاء رسم بياني شريطي
- fig = px.bar(
- category_df,
- x='الفئة',
- y='المبلغ',
- title='إجمالي تكلفة البنود حسب الفئة',
- color='الفئة',
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # حساب إجمالي جدول الكميات
- total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities)
-
- # يجب إضافة باقي الدوال المفقودة هنا
- def _render_cost_analysis_tab(self):
- """عرض تبويب تحليل التكاليف"""
- # تنفيذ هذه الدالة حسب متطلبات التطبيق
- st.markdown("### تحليل التكاليف")
- # محتوى مؤقت
- st.info("تبويب تحليل التكاليف قيد التطوير")
-
- def _render_pricing_scenarios_tab(self):
- """عرض تبويب سيناريوهات التسعير"""
- # تنفيذ هذه الدالة حسب متطلبات التطبيق
- st.markdown("### سيناريوهات التسعير")
- # محتوى مؤقت
- st.info("تبويب سيناريوهات التسعير قيد التطوير")
-
- def _render_competitive_analysis_tab(self):
- """عرض تبويب المقارنة التنافسية"""
- # تنفيذ هذه الدالة حسب متطلبات التطبيق
- st.markdown("### المقارنة التنافسية")
- # محتوى مؤقت
- st.info("تبويب المقارنة التنافسية قيد التطوير")
-
- def _render_reports_tab(self):
- """عرض تبويب التقارير"""
- # تنفيذ هذه الدالة حسب متطلبات التطبيق
- st.markdown("### التقارير")
- # محتوى مؤقت
- st.info("تبويب التقارير قيد التطوير")
+"""
+وحدة التسعير - التطبيق الرئيسي
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.express as px
+import plotly.graph_objects as go
+from datetime import datetime
+import time
+import io
+import os
+import json
+import base64
+from pathlib import Path
+
+class PricingApp:
+ """وحدة التسعير"""
+
+ def __init__(self):
+ """تهيئة وحدة التسعير"""
+
+ # تهيئة حالة الجلسة
+ if 'bill_of_quantities' not in st.session_state:
+ st.session_state.bill_of_quantities = [
+ {
+ 'id': 1,
+ 'code': 'A-001',
+ 'description': 'أعمال الحفر والردم',
+ 'unit': 'م3',
+ 'quantity': 1500,
+ 'unit_price': 45,
+ 'total_price': 67500,
+ 'category': 'أعمال ترابية'
+ },
+ {
+ 'id': 2,
+ 'code': 'A-002',
+ 'description': 'توريد وصب خرسانة عادية',
+ 'unit': 'م3',
+ 'quantity': 250,
+ 'unit_price': 350,
+ 'total_price': 87500,
+ 'category': 'أعمال خرسانية'
+ },
+ {
+ 'id': 3,
+ 'code': 'A-003',
+ 'description': 'توريد وصب خرسانة مسلحة للأساسات',
+ 'unit': 'م3',
+ 'quantity': 180,
+ 'unit_price': 450,
+ 'total_price': 81000,
+ 'category': 'أعمال خرسانية'
+ },
+ {
+ 'id': 4,
+ 'code': 'A-004',
+ 'description': 'توريد وصب خرسانة مسلحة للأعمدة',
+ 'unit': 'م3',
+ 'quantity': 120,
+ 'unit_price': 500,
+ 'total_price': 60000,
+ 'category': 'أعمال خرسانية'
+ },
+ {
+ 'id': 5,
+ 'code': 'A-005',
+ 'description': 'توريد وتركيب حديد تسليح',
+ 'unit': 'طن',
+ 'quantity': 45,
+ 'unit_price': 3000,
+ 'total_price': 135000,
+ 'category': 'أعمال حديد'
+ },
+ {
+ 'id': 6,
+ 'code': 'A-006',
+ 'description': 'توريد وبناء طابوق',
+ 'unit': 'م2',
+ 'quantity': 1200,
+ 'unit_price': 45,
+ 'total_price': 54000,
+ 'category': 'أعمال بناء'
+ },
+ {
+ 'id': 7,
+ 'code': 'A-007',
+ 'description': 'أعمال اللياسة والتشطيبات',
+ 'unit': 'م2',
+ 'quantity': 2400,
+ 'unit_price': 35,
+ 'total_price': 84000,
+ 'category': 'أعمال تشطيبات'
+ },
+ {
+ 'id': 8,
+ 'code': 'A-008',
+ 'description': 'أعمال الدهانات',
+ 'unit': 'م2',
+ 'quantity': 2400,
+ 'unit_price': 25,
+ 'total_price': 60000,
+ 'category': 'أعمال تشطيبات'
+ },
+ {
+ 'id': 9,
+ 'code': 'A-009',
+ 'description': 'توريد وتركيب أبواب خشبية',
+ 'unit': 'عدد',
+ 'quantity': 24,
+ 'unit_price': 750,
+ 'total_price': 18000,
+ 'category': 'أعمال نجارة'
+ },
+ {
+ 'id': 10,
+ 'code': 'A-010',
+ 'description': 'توريد وتركيب نوافذ ألمنيوم',
+ 'unit': 'م2',
+ 'quantity': 120,
+ 'unit_price': 350,
+ 'total_price': 42000,
+ 'category': 'أعمال ألمنيوم'
+ }
+ ]
+
+ if 'cost_analysis' not in st.session_state:
+ st.session_state.cost_analysis = [
+ {
+ 'id': 1,
+ 'category': 'تكاليف مباشرة',
+ 'subcategory': 'مواد',
+ 'description': 'خرسانة',
+ 'amount': 120000,
+ 'percentage': 17.9
+ },
+ {
+ 'id': 2,
+ 'category': 'تكاليف مباشرة',
+ 'subcategory': 'مواد',
+ 'description': 'حديد تسليح',
+ 'amount': 135000,
+ 'percentage': 20.1
+ },
+ {
+ 'id': 3,
+ 'category': 'تكاليف مباشرة',
+ 'subcategory': 'مواد',
+ 'description': 'طابوق',
+ 'amount': 54000,
+ 'percentage': 8.1
+ },
+ {
+ 'id': 4,
+ 'category': 'تكاليف مباشرة',
+ 'subcategory': 'عمالة',
+ 'description': 'عمالة تنفيذ',
+ 'amount': 120000,
+ 'percentage': 17.9
+ },
+ {
+ 'id': 5,
+ 'category': 'تكاليف مباشرة',
+ 'subcategory': 'معدات',
+ 'description': 'معدات إنشائية',
+ 'amount': 85000,
+ 'percentage': 12.7
+ },
+ {
+ 'id': 6,
+ 'category': 'تكاليف غير مباشرة',
+ 'subcategory': 'إدارة',
+ 'description': 'إدارة المشروع',
+ 'amount': 45000,
+ 'percentage': 6.7
+ },
+ {
+ 'id': 7,
+ 'category': 'تكاليف غير مباشرة',
+ 'subcategory': 'إدارة',
+ 'description': 'إشراف هندسي',
+ 'amount': 35000,
+ 'percentage': 5.2
+ },
+ {
+ 'id': 8,
+ 'category': 'تكاليف غير مباشرة',
+ 'subcategory': 'عامة',
+ 'description': 'تأمينات وضمانات',
+ 'amount': 25000,
+ 'percentage': 3.7
+ },
+ {
+ 'id': 9,
+ 'category': 'تكاليف غير مباشرة',
+ 'subcategory': 'عامة',
+ 'description': 'مصاريف إدارية',
+ 'amount': 30000,
+ 'percentage': 4.5
+ },
+ {
+ 'id': 10,
+ 'category': 'أرباح',
+ 'subcategory': 'أرباح',
+ 'description': 'هامش الربح',
+ 'amount': 55000,
+ 'percentage': 8.2
+ }
+ ]
+
+ if 'price_scenarios' not in st.session_state:
+ st.session_state.price_scenarios = [
+ {
+ 'id': 1,
+ 'name': 'السيناريو الأساسي',
+ 'description': 'التسعير الأساسي مع هامش ربح 8%',
+ 'total_cost': 615000,
+ 'profit_margin': 8.2,
+ 'total_price': 670000,
+ 'is_active': True
+ },
+ {
+ 'id': 2,
+ 'name': 'سيناريو تنافسي',
+ 'description': 'تخفيض هامش الربح للمنافسة',
+ 'total_cost': 615000,
+ 'profit_margin': 5.0,
+ 'total_price': 650000,
+ 'is_active': False
+ },
+ {
+ 'id': 3,
+ 'name': 'سيناريو مرتفع',
+ 'description': 'زيادة هامش الربح للمشاريع ذات المخاطر العالية',
+ 'total_cost': 615000,
+ 'profit_margin': 12.0,
+ 'total_price': 700000,
+ 'is_active': False
+ }
+ ]
+
+ def run(self):
+ """تشغيل وحدة التسعير"""
+ # استدعاء دالة العرض
+ self.render()
+
+ def render(self):
+ """عرض واجهة وحدة التسعير"""
+
+ st.markdown("وحدة التسعير ", unsafe_allow_html=True)
+
+ tabs = st.tabs([
+ "لوحة التحكم",
+ "جدول الكميات",
+ "تحليل التكاليف",
+ "سيناريوهات التسعير",
+ "المقارنة التنافسية",
+ "التقارير"
+ ])
+
+ with tabs[0]:
+ self._render_dashboard_tab()
+
+ with tabs[1]:
+ self._render_bill_of_quantities_tab()
+
+ with tabs[2]:
+ self._render_cost_analysis_tab()
+
+ with tabs[3]:
+ self._render_pricing_scenarios_tab()
+
+ with tabs[4]:
+ self._render_competitive_analysis_tab()
+
+ with tabs[5]:
+ self._render_reports_tab()
+
+ def _render_dashboard_tab(self):
+ """عرض تبويب لوحة التحكم"""
+
+ st.markdown("### لوحة تحكم التسعير")
+
+ # عرض ملخص التسعير
+ col1, col2, col3, col4 = st.columns(4)
+
+ # حساب إجمالي التكاليف
+ total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
+ total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
+ total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
+ total_cost = total_direct_cost + total_indirect_cost
+ total_price = total_cost + total_profit
+
+ with col1:
+ st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.0f} ريال")
+
+ with col2:
+ st.metric("إجمالي التكاليف غير المباشرة", f"{total_indirect_cost:,.0f} ريال")
+
+ with col3:
+ st.metric("إجمالي التكاليف", f"{total_cost:,.0f} ريال")
+
+ with col4:
+ st.metric("السعر الإجمالي", f"{total_price:,.0f} ريال")
+
+ # عرض توزيع التكاليف
+ st.markdown("### توزيع التكاليف")
+
+ # تجميع البيانات حسب الفئة
+ cost_categories = {}
+
+ for item in st.session_state.cost_analysis:
+ category = item['category']
+ if category in cost_categories:
+ cost_categories[category] += item['amount']
+ else:
+ cost_categories[category] = item['amount']
+
+ # إنشاء DataFrame للرسم البياني
+ cost_df = pd.DataFrame({
+ 'الفئة': list(cost_categories.keys()),
+ 'المبلغ': list(cost_categories.values())
+ })
+
+ # إنشاء رسم بياني دائري
+ fig = px.pie(
+ cost_df,
+ values='المبلغ',
+ names='الفئة',
+ title='توزيع التكاليف حسب الفئة',
+ color_discrete_sequence=px.colors.qualitative.Set3
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض توزيع التكاليف المباشرة
+ st.markdown("### توزيع التكاليف المباشرة")
+
+ # تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
+ direct_cost_subcategories = {}
+
+ for item in st.session_state.cost_analysis:
+ if item['category'] == 'تكاليف مباشرة':
+ subcategory = item['subcategory']
+ if subcategory in direct_cost_subcategories:
+ direct_cost_subcategories[subcategory] += item['amount']
+ else:
+ direct_cost_subcategories[subcategory] = item['amount']
+
+ # إنشاء DataFrame للرسم البياني
+ direct_cost_df = pd.DataFrame({
+ 'الفئة الفرعية': list(direct_cost_subcategories.keys()),
+ 'المبلغ': list(direct_cost_subcategories.values())
+ })
+
+ # إنشاء رسم بياني دائري
+ fig = px.pie(
+ direct_cost_df,
+ values='المبلغ',
+ names='الفئة الفرعية',
+ title='توزيع التكاليف المباشرة',
+ color_discrete_sequence=px.colors.qualitative.Pastel
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض توزيع التكاليف غير المباشرة
+ st.markdown("### توزيع التكاليف غير المباشرة")
+
+ # تجميع البيانات حسب الفئة الفرعية للتكاليف غير المباشرة
+ indirect_cost_subcategories = {}
+
+ for item in st.session_state.cost_analysis:
+ if item['category'] == 'تكاليف غير مباشرة':
+ subcategory = item['subcategory']
+ if subcategory in indirect_cost_subcategories:
+ indirect_cost_subcategories[subcategory] += item['amount']
+ else:
+ indirect_cost_subcategories[subcategory] = item['amount']
+
+ # إنشاء DataFrame للرسم البياني
+ indirect_cost_df = pd.DataFrame({
+ 'الفئة الفرعية': list(indirect_cost_subcategories.keys()),
+ 'المبلغ': list(indirect_cost_subcategories.values())
+ })
+
+ # إنشاء رسم بياني دائري
+ fig = px.pie(
+ indirect_cost_df,
+ values='المبلغ',
+ names='الفئة الفرعية',
+ title='توزيع التكاليف غير المباشرة',
+ color_discrete_sequence=px.colors.qualitative.Pastel1
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _render_bill_of_quantities_tab(self):
+ """عرض تبويب جدول الكميات"""
+
+ st.markdown("### جدول الكميات")
+
+ # إنشاء DataFrame من بيانات جدول الكميات
+ boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
+
+ # عرض جدول الكميات
+ st.dataframe(
+ boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']],
+ column_config={
+ 'code': 'الكود',
+ 'description': 'الوصف',
+ 'unit': 'الوحدة',
+ 'quantity': 'الكمية',
+ 'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
+ 'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
+ 'category': 'الفئة'
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # إضافة بند جديد
+ st.markdown("### إضافة بند جديد")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ new_code = st.text_input("الكود", key="new_boq_code")
+ new_description = st.text_input("الوصف", key="new_boq_description")
+ new_unit = st.selectbox("الوحدة", ["م3", "م2", "طن", "عدد", "متر طولي"], key="new_boq_unit")
+
+ with col2:
+ new_quantity = st.number_input("الكمية", min_value=0.0, step=1.0, key="new_boq_quantity")
+ new_unit_price = st.number_input("سعر الوحدة", min_value=0.0, step=10.0, key="new_boq_unit_price")
+ new_category = st.selectbox(
+ "الفئة",
+ [
+ "أعمال ترابية",
+ "أعمال خرسانية",
+ "أعمال حديد",
+ "أعمال بناء",
+ "أعمال تشطيبات",
+ "أعمال نجارة",
+ "أعمال ألمنيوم",
+ "أعمال كهربائية",
+ "أعمال ميكانيكية",
+ "أعمال صحية"
+ ],
+ key="new_boq_category"
+ )
+
+ if st.button("إضافة البند", key="add_boq_item"):
+ if new_code and new_description and new_quantity > 0 and new_unit_price > 0:
+ # حساب السعر الإجمالي
+ new_total_price = new_quantity * new_unit_price
+
+ # إضافة بند جديد
+ new_id = max([item['id'] for item in st.session_state.bill_of_quantities]) + 1
+
+ st.session_state.bill_of_quantities.append({
+ 'id': new_id,
+ 'code': new_code,
+ 'description': new_description,
+ 'unit': new_unit,
+ 'quantity': new_quantity,
+ 'unit_price': new_unit_price,
+ 'total_price': new_total_price,
+ 'category': new_category
+ })
+
+ st.success(f"تمت إضافة البند بنجاح: {new_description}")
+
+ # تحديث الصفحة لعرض البند الجديد
+ st.rerun()
+ else:
+ st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
+
+ # عرض ملخص جدول الكميات (إزالة التكرار)
+ st.markdown("### ملخص جدول الكميات")
+
+ # تجميع البيانات حسب الفئة
+ category_totals = {}
+ for item in st.session_state.bill_of_quantities:
+ category = item['category']
+ if category in category_totals:
+ category_totals[category] += item['total_price']
+ else:
+ category_totals[category] = item['total_price']
+
+ # إنشاء DataFrame للرسم البياني
+ category_df = pd.DataFrame({
+ 'الفئة': list(category_totals.keys()),
+ 'المبلغ': list(category_totals.values())
+ })
+
+ # ترتيب البيانات تنازليًا حسب المبلغ
+ category_df = category_df.sort_values('المبلغ', ascending=False)
+
+ # إنشاء رسم بياني شريطي
+ fig = px.bar(
+ category_df,
+ x='الفئة',
+ y='المبلغ',
+ title='إجمالي تكلفة البنود حسب الفئة',
+ color='الفئة',
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # حساب إجمالي جدول الكميات
+ total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities)
+
+ # يجب إضافة باقي الدوال المفقودة هنا
+ def _render_cost_analysis_tab(self):
+ """عرض تبويب تحليل التكاليف"""
+ # تنفيذ هذه الدالة حسب متطلبات التطبيق
+ st.markdown("### تحليل التكاليف")
+ # محتوى مؤقت
+ st.info("تبويب تحليل التكاليف قيد التطوير")
+
+ def _render_pricing_scenarios_tab(self):
+ """عرض تبويب سيناريوهات التسعير"""
+ # تنفيذ هذه الدالة حسب متطلبات التطبيق
+ st.markdown("### سيناريوهات التسعير")
+ # محتوى مؤقت
+ st.info("تبويب سيناريوهات التسعير قيد التطوير")
+
+ def _render_competitive_analysis_tab(self):
+ """عرض تبويب المقارنة التنافسية"""
+ # تنفيذ هذه الدالة حسب متطلبات التطبيق
+ st.markdown("### المقارنة التنافسية")
+ # محتوى مؤقت
+ st.info("تبويب المقارنة التنافسية قيد التطوير")
+
+ def _render_reports_tab(self):
+ """عرض تبويب التقارير"""
+ # تنفيذ هذه الدالة حسب متطلبات التطبيق
+ st.markdown("### التقارير")
+ # محتوى مؤقت
+ st.info("تبويب التقارير قيد التطوير")
diff --git a/fixed_pricing_app_complete.py b/fixed_pricing_app_complete.py
index 7a3d62a41dec8662022e47c0c884139a3a29983b..cc705ce6666ee5104e3cac50277714d4f4e66497 100644
--- a/fixed_pricing_app_complete.py
+++ b/fixed_pricing_app_complete.py
@@ -1,1760 +1,1760 @@
-"""
-وحدة التسعير - التطبيق الرئيسي
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import plotly.express as px
-import plotly.graph_objects as go
-from datetime import datetime
-import time
-import io
-import os
-import json
-import base64
-from pathlib import Path
-
-class PricingApp:
- """وحدة التسعير"""
-
- def __init__(self):
- """تهيئة وحدة التسعير"""
-
- # تهيئة حالة الجلسة
- if 'bill_of_quantities' not in st.session_state:
- st.session_state.bill_of_quantities = [
- {
- 'id': 1,
- 'code': 'A-001',
- 'description': 'أعمال الحفر والردم',
- 'unit': 'م3',
- 'quantity': 1500,
- 'unit_price': 45,
- 'total_price': 67500,
- 'category': 'أعمال ترابية'
- },
- {
- 'id': 2,
- 'code': 'A-002',
- 'description': 'توريد وصب خرسانة عادية',
- 'unit': 'م3',
- 'quantity': 250,
- 'unit_price': 350,
- 'total_price': 87500,
- 'category': 'أعمال خرسانية'
- },
- {
- 'id': 3,
- 'code': 'A-003',
- 'description': 'توريد وصب خرسانة مسلحة للأساسات',
- 'unit': 'م3',
- 'quantity': 180,
- 'unit_price': 450,
- 'total_price': 81000,
- 'category': 'أعمال خرسانية'
- },
- {
- 'id': 4,
- 'code': 'A-004',
- 'description': 'توريد وصب خرسانة مسلحة للأعمدة',
- 'unit': 'م3',
- 'quantity': 120,
- 'unit_price': 500,
- 'total_price': 60000,
- 'category': 'أعمال خرسانية'
- },
- {
- 'id': 5,
- 'code': 'A-005',
- 'description': 'توريد وتركيب حديد تسليح',
- 'unit': 'طن',
- 'quantity': 45,
- 'unit_price': 3000,
- 'total_price': 135000,
- 'category': 'أعمال حديد'
- },
- {
- 'id': 6,
- 'code': 'A-006',
- 'description': 'توريد وبناء طابوق',
- 'unit': 'م2',
- 'quantity': 1200,
- 'unit_price': 45,
- 'total_price': 54000,
- 'category': 'أعمال بناء'
- },
- {
- 'id': 7,
- 'code': 'A-007',
- 'description': 'أعمال اللياسة والتشطيبات',
- 'unit': 'م2',
- 'quantity': 2400,
- 'unit_price': 35,
- 'total_price': 84000,
- 'category': 'أعمال تشطيبات'
- },
- {
- 'id': 8,
- 'code': 'A-008',
- 'description': 'أعمال الدهانات',
- 'unit': 'م2',
- 'quantity': 2400,
- 'unit_price': 25,
- 'total_price': 60000,
- 'category': 'أعمال تشطيبات'
- },
- {
- 'id': 9,
- 'code': 'A-009',
- 'description': 'توريد وتركيب أبواب خشبية',
- 'unit': 'عدد',
- 'quantity': 24,
- 'unit_price': 750,
- 'total_price': 18000,
- 'category': 'أعمال نجارة'
- },
- {
- 'id': 10,
- 'code': 'A-010',
- 'description': 'توريد وتركيب نوافذ ألمنيوم',
- 'unit': 'م2',
- 'quantity': 120,
- 'unit_price': 350,
- 'total_price': 42000,
- 'category': 'أعمال ألمنيوم'
- }
- ]
-
- if 'cost_analysis' not in st.session_state:
- st.session_state.cost_analysis = [
- {
- 'id': 1,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'مواد',
- 'description': 'خرسانة',
- 'amount': 120000,
- 'percentage': 17.9
- },
- {
- 'id': 2,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'مواد',
- 'description': 'حديد تسليح',
- 'amount': 135000,
- 'percentage': 20.1
- },
- {
- 'id': 3,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'مواد',
- 'description': 'طابوق',
- 'amount': 54000,
- 'percentage': 8.1
- },
- {
- 'id': 4,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'عمالة',
- 'description': 'عمالة تنفيذ',
- 'amount': 120000,
- 'percentage': 17.9
- },
- {
- 'id': 5,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'معدات',
- 'description': 'معدات إنشائية',
- 'amount': 85000,
- 'percentage': 12.7
- },
- {
- 'id': 6,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'إدارة',
- 'description': 'إدارة المشروع',
- 'amount': 45000,
- 'percentage': 6.7
- },
- {
- 'id': 7,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'إدارة',
- 'description': 'إشراف هندسي',
- 'amount': 35000,
- 'percentage': 5.2
- },
- {
- 'id': 8,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'عامة',
- 'description': 'تأمينات وضمانات',
- 'amount': 25000,
- 'percentage': 3.7
- },
- {
- 'id': 9,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'عامة',
- 'description': 'مصاريف إدارية',
- 'amount': 30000,
- 'percentage': 4.5
- },
- {
- 'id': 10,
- 'category': 'أرباح',
- 'subcategory': 'أرباح',
- 'description': 'هامش الربح',
- 'amount': 55000,
- 'percentage': 8.2
- }
- ]
-
- if 'price_scenarios' not in st.session_state:
- st.session_state.price_scenarios = [
- {
- 'id': 1,
- 'name': 'السيناريو الأساسي',
- 'description': 'التسعير الأساسي مع هامش ربح 8%',
- 'total_cost': 615000,
- 'profit_margin': 8.2,
- 'total_price': 670000,
- 'is_active': True
- },
- {
- 'id': 2,
- 'name': 'سيناريو تنافسي',
- 'description': 'تخفيض هامش الربح للمنافسة',
- 'total_cost': 615000,
- 'profit_margin': 5.0,
- 'total_price': 650000,
- 'is_active': False
- },
- {
- 'id': 3,
- 'name': 'سيناريو مرتفع',
- 'description': 'زيادة هامش الربح للمشاريع ذات المخاطر العالية',
- 'total_cost': 615000,
- 'profit_margin': 12.0,
- 'total_price': 700000,
- 'is_active': False
- }
- ]
-
- # إضافة بيانات المقارنة التنافسية
- if 'competitive_analysis' not in st.session_state:
- st.session_state.competitive_analysis = [
- {
- 'id': 1,
- 'competitor': 'شركة الإنشاءات المتحدة',
- 'project_type': 'مباني سكنية',
- 'price_per_sqm': 1800,
- 'delivery_time': 12,
- 'quality_rating': 4.2,
- 'market_share': 15.5
- },
- {
- 'id': 2,
- 'competitor': 'مجموعة البناء الحديث',
- 'project_type': 'مباني سكنية',
- 'price_per_sqm': 2100,
- 'delivery_time': 10,
- 'quality_rating': 4.5,
- 'market_share': 18.2
- },
- {
- 'id': 3,
- 'competitor': 'شركة الإعمار الدولية',
- 'project_type': 'مباني سكنية',
- 'price_per_sqm': 2300,
- 'delivery_time': 14,
- 'quality_rating': 4.7,
- 'market_share': 22.0
- },
- {
- 'id': 4,
- 'competitor': 'مؤسسة البناء المتكامل',
- 'project_type': 'مباني سكنية',
- 'price_per_sqm': 1750,
- 'delivery_time': 15,
- 'quality_rating': 3.8,
- 'market_share': 12.5
- },
- {
- 'id': 5,
- 'competitor': 'شركتنا',
- 'project_type': 'مباني سكنية',
- 'price_per_sqm': 1950,
- 'delivery_time': 11,
- 'quality_rating': 4.4,
- 'market_share': 14.8
- }
- ]
-
- def run(self):
- """تشغيل وحدة التسعير"""
- # استدعاء دالة العرض
- self.render()
-
- def render(self):
- """عرض واجهة وحدة التسعير"""
-
- st.markdown("وحدة التسعير ", unsafe_allow_html=True)
-
- tabs = st.tabs([
- "لوحة التحكم",
- "جدول الكميات",
- "تحليل التكاليف",
- "سيناريوهات التسعير",
- "المقارنة التنافسية",
- "التقارير"
- ])
-
- with tabs[0]:
- self._render_dashboard_tab()
-
- with tabs[1]:
- self._render_bill_of_quantities_tab()
-
- with tabs[2]:
- self._render_cost_analysis_tab()
-
- with tabs[3]:
- self._render_pricing_scenarios_tab()
-
- with tabs[4]:
- self._render_competitive_analysis_tab()
-
- with tabs[5]:
- self._render_reports_tab()
-
- def _render_dashboard_tab(self):
- """عرض تبويب لوحة التحكم"""
-
- st.markdown("### لوحة تحكم التسعير")
-
- # عرض ملخص التسعير
- col1, col2, col3, col4 = st.columns(4)
-
- # حساب إجمالي التكاليف
- total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
- total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
- total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
- total_cost = total_direct_cost + total_indirect_cost
- total_price = total_cost + total_profit
-
- with col1:
- st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.0f} ريال")
-
- with col2:
- st.metric("إجمالي التكاليف غير المباشرة", f"{total_indirect_cost:,.0f} ريال")
-
- with col3:
- st.metric("إجمالي التكاليف", f"{total_cost:,.0f} ريال")
-
- with col4:
- st.metric("السعر الإجمالي", f"{total_price:,.0f} ريال")
-
- # عرض توزيع التكاليف
- st.markdown("### توزيع التكاليف")
-
- # تجميع البيانات حسب الفئة
- cost_categories = {}
-
- for item in st.session_state.cost_analysis:
- category = item['category']
- if category in cost_categories:
- cost_categories[category] += item['amount']
- else:
- cost_categories[category] = item['amount']
-
- # إنشاء DataFrame للرسم البياني
- cost_df = pd.DataFrame({
- 'الفئة': list(cost_categories.keys()),
- 'المبلغ': list(cost_categories.values())
- })
-
- # إنشاء رسم بياني دائري
- fig = px.pie(
- cost_df,
- values='المبلغ',
- names='الفئة',
- title='توزيع التكاليف حسب الفئة',
- color_discrete_sequence=px.colors.qualitative.Set3
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع التكاليف المباشرة
- st.markdown("### توزيع التكاليف المباشرة")
-
- # تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
- direct_cost_subcategories = {}
-
- for item in st.session_state.cost_analysis:
- if item['category'] == 'تكاليف مباشرة':
- subcategory = item['subcategory']
- if subcategory in direct_cost_subcategories:
- direct_cost_subcategories[subcategory] += item['amount']
- else:
- direct_cost_subcategories[subcategory] = item['amount']
-
- # إنشاء DataFrame للرسم البياني
- direct_cost_df = pd.DataFrame({
- 'الفئة الفرعية': list(direct_cost_subcategories.keys()),
- 'المبلغ': list(direct_cost_subcategories.values())
- })
-
- # إنشاء رسم بياني دائري
- fig = px.pie(
- direct_cost_df,
- values='المبلغ',
- names='الفئة الفرعية',
- title='توزيع التكاليف المباشرة',
- color_discrete_sequence=px.colors.qualitative.Pastel
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع التكاليف غير المباشرة
- st.markdown("### توزيع التكاليف غير المباشرة")
-
- # تجميع البيانات حسب الفئة الفرعية للتكاليف غير المباشرة
- indirect_cost_subcategories = {}
-
- for item in st.session_state.cost_analysis:
- if item['category'] == 'تكاليف غير مباشرة':
- subcategory = item['subcategory']
- if subcategory in indirect_cost_subcategories:
- indirect_cost_subcategories[subcategory] += item['amount']
- else:
- indirect_cost_subcategories[subcategory] = item['amount']
-
- # إنشاء DataFrame للرسم البياني
- indirect_cost_df = pd.DataFrame({
- 'الفئة الفرعية': list(indirect_cost_subcategories.keys()),
- 'المبلغ': list(indirect_cost_subcategories.values())
- })
-
- # إنشاء رسم بياني دائري
- fig = px.pie(
- indirect_cost_df,
- values='المبلغ',
- names='الفئة الفرعية',
- title='توزيع التكاليف غير المباشرة',
- color_discrete_sequence=px.colors.qualitative.Pastel1
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_bill_of_quantities_tab(self):
- """عرض تبويب جدول الكميات"""
-
- st.markdown("### جدول الكميات")
-
- # إنشاء DataFrame من بيانات جدول الكميات
- boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
-
- # عرض جدول الكميات
- st.dataframe(
- boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']],
- column_config={
- 'code': 'الكود',
- 'description': 'الوصف',
- 'unit': 'الوحدة',
- 'quantity': 'الكمية',
- 'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
- 'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
- 'category': 'الفئة'
- },
- hide_index=True,
- use_container_width=True
- )
-
- # إضافة بند جديد
- st.markdown("### إضافة بند جديد")
-
- col1, col2 = st.columns(2)
-
- with col1:
- new_code = st.text_input("الكود", key="new_boq_code")
- new_description = st.text_input("الوصف", key="new_boq_description")
- new_unit = st.selectbox("الوحدة", ["م3", "م2", "طن", "عدد", "متر طولي"], key="new_boq_unit")
-
- with col2:
- new_quantity = st.number_input("الكمية", min_value=0.0, step=1.0, key="new_boq_quantity")
- new_unit_price = st.number_input("سعر الوحدة", min_value=0.0, step=10.0, key="new_boq_unit_price")
- new_category = st.selectbox(
- "الفئة",
- [
- "أعمال ترابية",
- "أعمال خرسانية",
- "أعمال حديد",
- "أعمال بناء",
- "أعمال تشطيبات",
- "أعمال نجارة",
- "أعمال ألمنيوم",
- "أعمال كهربائية",
- "أعمال ميكانيكية",
- "أعمال صحية"
- ],
- key="new_boq_category"
- )
-
- if st.button("إضافة البند", key="add_boq_item"):
- if new_code and new_description and new_quantity > 0 and new_unit_price > 0:
- # حساب السعر الإجمالي
- new_total_price = new_quantity * new_unit_price
-
- # إضافة بند جديد
- new_id = max([item['id'] for item in st.session_state.bill_of_quantities]) + 1
-
- st.session_state.bill_of_quantities.append({
- 'id': new_id,
- 'code': new_code,
- 'description': new_description,
- 'unit': new_unit,
- 'quantity': new_quantity,
- 'unit_price': new_unit_price,
- 'total_price': new_total_price,
- 'category': new_category
- })
-
- st.success(f"تمت إضافة البند بنجاح: {new_description}")
-
- # تحديث الصفحة لعرض البند الجديد
- st.rerun()
- else:
- st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
-
- # عرض ملخص جدول الكميات (إزالة التكرار)
- st.markdown("### ملخص جدول الكميات")
-
- # تجميع البيانات حسب الفئة
- category_totals = {}
- for item in st.session_state.bill_of_quantities:
- category = item['category']
- if category in category_totals:
- category_totals[category] += item['total_price']
- else:
- category_totals[category] = item['total_price']
-
- # إنشاء DataFrame للرسم البياني
- category_df = pd.DataFrame({
- 'الفئة': list(category_totals.keys()),
- 'المبلغ': list(category_totals.values())
- })
-
- # ترتيب البيانات تنازليًا حسب المبلغ
- category_df = category_df.sort_values('المبلغ', ascending=False)
-
- # إنشاء رسم بياني شريطي
- fig = px.bar(
- category_df,
- x='الفئة',
- y='المبلغ',
- title='إجمالي تكلفة البنود حسب الفئة',
- color='الفئة',
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # حساب إجمالي جدول الكميات
- total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities)
-
- def _render_cost_analysis_tab(self):
- """عرض تبويب تحليل التكاليف"""
-
- st.markdown("### تحليل التكاليف")
-
- # عرض جدول تحليل التكاليف
- cost_df = pd.DataFrame(st.session_state.cost_analysis)
-
- st.dataframe(
- cost_df[['category', 'subcategory', 'description', 'amount', 'percentage']],
- column_config={
- 'category': 'الفئة',
- 'subcategory': 'الفئة الفرعية',
- 'description': 'الوصف',
- 'amount': st.column_config.NumberColumn('المبلغ', format='%d ريال'),
- 'percentage': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
- },
- hide_index=True,
- use_container_width=True
- )
-
- # إضافة بند تكلفة جديد
- st.markdown("### إضافة بند تكلفة جديد")
-
- col1, col2 = st.columns(2)
-
- with col1:
- new_category = st.selectbox(
- "الفئة",
- ["تكاليف مباشرة", "تكاليف غير مباشرة", "أرباح"],
- key="new_cost_category"
- )
-
- # تحديد الفئات الفرعية بناءً على الفئة المختارة
- subcategory_options = []
- if new_category == "تكاليف مباشرة":
- subcategory_options = ["مواد", "عمالة", "معدات"]
- elif new_category == "تكاليف غير مباشرة":
- subcategory_options = ["إدارة", "عامة", "تمويل"]
- else:
- subcategory_options = ["أرباح"]
-
- new_subcategory = st.selectbox(
- "الفئة الفرعية",
- subcategory_options,
- key="new_cost_subcategory"
- )
-
- new_description = st.text_input("الوصف", key="new_cost_description")
-
- with col2:
- new_amount = st.number_input("المبلغ", min_value=0.0, step=1000.0, key="new_cost_amount")
-
- # حساب إجمالي التكاليف الحالية
- total_cost = sum(item['amount'] for item in st.session_state.cost_analysis)
-
- # حساب النسبة المئوية التقريبية
- if total_cost > 0:
- estimated_percentage = (new_amount / total_cost) * 100
- else:
- estimated_percentage = 0
-
- st.metric("النسبة المئوية التقديرية", f"{estimated_percentage:.1f}%")
-
- if st.button("إضافة بند التكلفة", key="add_cost_item"):
- if new_description and new_amount > 0:
- # إضافة بند جديد
- new_id = max([item['id'] for item in st.session_state.cost_analysis]) + 1
-
- # حساب النسبة المئوية الفعلية بعد إضافة البند الجديد
- new_total = total_cost + new_amount
-
- # إعادة حساب النسب المئوية لجميع البنود
- for item in st.session_state.cost_analysis:
- item['percentage'] = (item['amount'] / new_total) * 100
-
- # إضافة البند الجديد
- st.session_state.cost_analysis.append({
- 'id': new_id,
- 'category': new_category,
- 'subcategory': new_subcategory,
- 'description': new_description,
- 'amount': new_amount,
- 'percentage': (new_amount / new_total) * 100
- })
-
- st.success(f"تمت إضافة بند التكلفة بنجاح: {new_description}")
-
- # تحديث الصفحة لعرض البند الجديد
- st.rerun()
- else:
- st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
-
- # تحليل التكاليف حسب الفئة والفئة الفرعية
- st.markdown("### تحليل التكاليف حسب الفئة والفئة الفرعية")
-
- # تجميع البيانات حسب الفئة والفئة الفرعية
- cost_by_category_subcategory = {}
-
- for item in st.session_state.cost_analysis:
- category = item['category']
- subcategory = item['subcategory']
- key = f"{category} - {subcategory}"
-
- if key in cost_by_category_subcategory:
- cost_by_category_subcategory[key] += item['amount']
- else:
- cost_by_category_subcategory[key] = item['amount']
-
- # إنشاء DataFrame للرسم البياني
- cost_category_subcategory_df = pd.DataFrame({
- 'الفئة والفئة الفرعية': list(cost_by_category_subcategory.keys()),
- 'المبلغ': list(cost_by_category_subcategory.values())
- })
-
- # ترتيب البيانات تنازليًا حسب المبلغ
- cost_category_subcategory_df = cost_category_subcategory_df.sort_values('المبلغ', ascending=False)
-
- # إنشاء رسم بياني شريطي
- fig = px.bar(
- cost_category_subcategory_df,
- x='الفئة والفئة الفرعية',
- y='المبلغ',
- title='تحليل التكاليف حسب الفئة والفئة الفرعية',
- color='الفئة والفئة الفرعية',
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل نسب التكاليف
- st.markdown("### تحليل نسب التكاليف")
-
- # إنشاء رسم بياني للنسب المئوية
- percentage_df = pd.DataFrame(st.session_state.cost_analysis)
-
- fig = px.treemap(
- percentage_df,
- path=['category', 'subcategory', 'description'],
- values='amount',
- title='تحليل هيكل التكاليف',
- color='percentage',
- color_continuous_scale='RdBu',
- color_continuous_midpoint=np.average(percentage_df['percentage'])
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل اتجاهات التكاليف (بيانات افتراضية)
- st.markdown("### تحليل اتجاهات التكاليف")
-
- # إنشاء بيانات افتراضية للاتجاهات
- months = ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو']
- direct_costs = [510000, 520000, 515000, 525000, 530000, 514000]
- indirect_costs = [130000, 135000, 132000, 138000, 140000, 135000]
-
- # إنشاء DataFrame للرسم البياني
- trends_df = pd.DataFrame({
- 'الشهر': months * 2,
- 'نوع التكلفة': ['تكاليف مباشرة'] * 6 + ['تكاليف غير مباشرة'] * 6,
- 'المبلغ': direct_costs + indirect_costs
- })
-
- # إنشاء رسم بياني خطي
- fig = px.line(
- trends_df,
- x='الشهر',
- y='المبلغ',
- color='نوع التكلفة',
- title='اتجاهات التكاليف على مدار الأشهر الستة الماضية',
- markers=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_pricing_scenarios_tab(self):
- """عرض تبويب سيناريوهات التسعير"""
-
- st.markdown("### سيناريوهات التسعير")
-
- # عرض جدول سيناريوهات التسعير
- scenarios_df = pd.DataFrame(st.session_state.price_scenarios)
-
- st.dataframe(
- scenarios_df[['name', 'description', 'total_cost', 'profit_margin', 'total_price', 'is_active']],
- column_config={
- 'name': 'اسم السيناريو',
- 'description': 'الوصف',
- 'total_cost': st.column_config.NumberColumn('إجمالي التكلفة', format='%d ريال'),
- 'profit_margin': st.column_config.NumberColumn('هامش الربح', format='%.1f%%'),
- 'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
- 'is_active': st.column_config.CheckboxColumn('نشط')
- },
- hide_index=True,
- use_container_width=True
- )
-
- # إنشاء سيناريو جديد
- st.markdown("### إنشاء سيناريو جديد")
-
- col1, col2 = st.columns(2)
-
- # حساب إجمالي التكاليف
- total_cost = sum(item['amount'] for item in st.session_state.cost_analysis
- if item['category'] != 'أرباح')
-
- with col1:
- new_name = st.text_input("اسم السيناريو", key="new_scenario_name")
- new_description = st.text_input("وصف السيناريو", key="new_scenario_description")
-
- with col2:
- new_profit_margin = st.slider(
- "هامش الربح (%)",
- min_value=0.0,
- max_value=30.0,
- value=10.0,
- step=0.5,
- key="new_scenario_profit_margin"
- )
-
- # حساب السعر الإجمالي بناءً على هامش الربح
- profit_amount = total_cost * (new_profit_margin / 100)
- new_total_price = total_cost + profit_amount
-
- st.metric("إجمالي التكلفة", f"{total_cost:,.0f} ريال")
- st.metric("السعر الإجمالي المقترح", f"{new_total_price:,.0f} ريال")
-
- if st.button("إضافة السيناريو", key="add_scenario"):
- if new_name and new_description:
- # إضافة سيناريو جديد
- new_id = max([item['id'] for item in st.session_state.price_scenarios]) + 1
-
- st.session_state.price_scenarios.append({
- 'id': new_id,
- 'name': new_name,
- 'description': new_description,
- 'total_cost': total_cost,
- 'profit_margin': new_profit_margin,
- 'total_price': new_total_price,
- 'is_active': False
- })
-
- st.success(f"تمت إضافة السيناريو بنجاح: {new_name}")
-
- # تحديث الصفحة لعرض السيناريو الجديد
- st.rerun()
- else:
- st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
-
- # مقارنة السيناريوهات
- st.markdown("### مقارنة السيناريوهات")
-
- # إنشاء رسم بياني للمقارنة
- fig = go.Figure()
-
- for scenario in st.session_state.price_scenarios:
- fig.add_trace(go.Bar(
- name=scenario['name'],
- x=['التكلفة', 'الربح', 'السعر الإجمالي'],
- y=[
- scenario['total_cost'],
- scenario['total_price'] - scenario['total_cost'],
- scenario['total_price']
- ],
- text=[
- f"{scenario['total_cost']:,.0f}",
- f"{scenario['total_price'] - scenario['total_cost']:,.0f}",
- f"{scenario['total_price']:,.0f}"
- ],
- textposition='auto'
- ))
-
- fig.update_layout(
- title='مقارنة السيناريوهات',
- barmode='group',
- xaxis_title='العنصر',
- yaxis_title='المبلغ (ريال)'
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل حساسية هامش الربح
- st.markdown("### تحليل حساسية هامش الربح")
-
- # إنشاء بيانات لتحليل الحساسية
- profit_margins = list(range(5, 26, 1)) # من 5% إلى 25%
- total_prices = [total_cost * (1 + margin/100) for margin in profit_margins]
-
- # إنشاء DataFrame للرسم البياني
- sensitivity_df = pd.DataFrame({
- 'هامش الربح (%)': profit_margins,
- 'السعر الإجمالي': total_prices
- })
-
- # إنشاء رسم بياني خطي
- fig = px.line(
- sensitivity_df,
- x='هامش الربح (%)',
- y='السعر الإجمالي',
- title='تحليل حساسية هامش الربح',
- markers=True
- )
-
- # إضافة خط أفقي يمثل السعر التنافسي (افتراضي)
- competitive_price = 650000
- fig.add_hline(
- y=competitive_price,
- line_dash="dash",
- line_color="red",
- annotation_text="السعر التنافسي",
- annotation_position="bottom right"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تفعيل/تعطيل السيناريوهات
- st.markdown("### تفعيل/تعطيل السيناريوهات")
-
- for i, scenario in enumerate(st.session_state.price_scenarios):
- col1, col2 = st.columns([4, 1])
-
- with col1:
- st.write(f"**{scenario['name']}**: {scenario['description']}")
-
- with col2:
- is_active = st.checkbox(
- "تفعيل",
- value=scenario['is_active'],
- key=f"activate_scenario_{scenario['id']}"
- )
-
- # تحديث حالة التفعيل
- if is_active != scenario['is_active']:
- # إذا تم تفعيل سيناريو، قم بتعطيل جميع السيناريوهات الأخرى
- if is_active:
- for j, other_scenario in enumerate(st.session_state.price_scenarios):
- if j != i:
- other_scenario['is_active'] = False
-
- # تحديث حالة السيناريو الحالي
- scenario['is_active'] = is_active
-
- # تحديث الصفحة
- st.rerun()
-
- def _render_competitive_analysis_tab(self):
- """عرض تبويب المقارنة التنافسية"""
-
- st.markdown("### المقارنة التنافسية")
-
- # عرض جدول المقارنة التنافسية
- competitive_df = pd.DataFrame(st.session_state.competitive_analysis)
-
- st.dataframe(
- competitive_df[['competitor', 'project_type', 'price_per_sqm', 'delivery_time', 'quality_rating', 'market_share']],
- column_config={
- 'competitor': 'المنافس',
- 'project_type': 'نوع المشروع',
- 'price_per_sqm': st.column_config.NumberColumn('السعر لكل متر مربع', format='%d ريال'),
- 'delivery_time': st.column_config.NumberColumn('مدة التسليم (شهر)', format='%d'),
- 'quality_rating': st.column_config.NumberColumn('تقييم الجودة', format='%.1f/5.0'),
- 'market_share': st.column_config.NumberColumn('الحصة السوقية', format='%.1f%%')
- },
- hide_index=True,
- use_container_width=True
- )
-
- # إضافة منافس جديد
- st.markdown("### إضافة منافس جديد")
-
- col1, col2 = st.columns(2)
-
- with col1:
- new_competitor = st.text_input("اسم المنافس", key="new_competitor_name")
- new_project_type = st.selectbox(
- "نوع المشروع",
- ["مباني سكنية", "مباني تجارية", "مباني صناعية", "بنية تحتية"],
- key="new_competitor_project_type"
- )
- new_price_per_sqm = st.number_input(
- "السعر لكل متر مربع (ريال)",
- min_value=0,
- step=50,
- key="new_competitor_price"
- )
-
- with col2:
- new_delivery_time = st.number_input(
- "مدة التسليم (شهر)",
- min_value=1,
- max_value=36,
- step=1,
- key="new_competitor_delivery"
- )
- new_quality_rating = st.slider(
- "تقييم الجودة",
- min_value=1.0,
- max_value=5.0,
- value=3.5,
- step=0.1,
- key="new_competitor_quality"
- )
- new_market_share = st.number_input(
- "الحصة السوقية (%)",
- min_value=0.0,
- max_value=100.0,
- step=0.5,
- key="new_competitor_market_share"
- )
-
- if st.button("إضافة منافس", key="add_competitor"):
- if new_competitor and new_price_per_sqm > 0:
- # إضافة منافس جديد
- new_id = max([item['id'] for item in st.session_state.competitive_analysis]) + 1
-
- st.session_state.competitive_analysis.append({
- 'id': new_id,
- 'competitor': new_competitor,
- 'project_type': new_project_type,
- 'price_per_sqm': new_price_per_sqm,
- 'delivery_time': new_delivery_time,
- 'quality_rating': new_quality_rating,
- 'market_share': new_market_share
- })
-
- st.success(f"تمت إضافة المنافس بنجاح: {new_competitor}")
-
- # تحديث الصفحة لعرض المنافس الجديد
- st.rerun()
- else:
- st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
-
- # تحليل مقارنة الأسعار
- st.markdown("### مقارنة الأسعار")
-
- # إنشاء DataFrame للرسم البياني
- price_comparison_df = pd.DataFrame(st.session_state.competitive_analysis)
-
- # ترتيب البيانات تصاعديًا حسب السعر
- price_comparison_df = price_comparison_df.sort_values('price_per_sqm')
-
- # إنشاء رسم بياني شريطي
- fig = px.bar(
- price_comparison_df,
- x='competitor',
- y='price_per_sqm',
- title='مقارنة الأسعار لكل متر مربع',
- color='competitor',
- text_auto=True
- )
-
- # تمييز شركتنا
- for i, competitor in enumerate(price_comparison_df['competitor']):
- if competitor == 'شركتنا':
- fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(price_comparison_df))]
- break
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل مقارنة الجودة والسعر
- st.markdown("### مقارنة الجودة والسعر")
-
- # إنشاء رسم بياني للعلاقة بين السعر والجودة
- fig = px.scatter(
- price_comparison_df,
- x='price_per_sqm',
- y='quality_rating',
- size='market_share',
- color='competitor',
- title='العلاقة بين السعر والجودة والحصة السوقية',
- labels={
- 'price_per_sqm': 'السعر لكل متر مربع (ريال)',
- 'quality_rating': 'تقييم الجودة',
- 'market_share': 'الحصة السوقية (%)'
- },
- text='competitor'
- )
-
- fig.update_traces(textposition='top center')
-
- # إضافة خط اتجاه
- fig.update_layout(
- shapes=[
- dict(
- type='line',
- x0=min(price_comparison_df['price_per_sqm']),
- y0=min(price_comparison_df['quality_rating']),
- x1=max(price_comparison_df['price_per_sqm']),
- y1=max(price_comparison_df['quality_rating']),
- line=dict(color='gray', dash='dash')
- )
- ]
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل مقارنة مدة التسليم
- st.markdown("### مقارنة مدة التسليم")
-
- # ترتيب البيانات تصاعديًا حسب مدة التسليم
- delivery_comparison_df = price_comparison_df.sort_values('delivery_time')
-
- # إنشاء رسم بياني شريطي
- fig = px.bar(
- delivery_comparison_df,
- x='competitor',
- y='delivery_time',
- title='مقارنة مدة التسليم (شهر)',
- color='competitor',
- text_auto=True
- )
-
- # تمييز شركتنا
- for i, competitor in enumerate(delivery_comparison_df['competitor']):
- if competitor == 'شركتنا':
- fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(delivery_comparison_df))]
- break
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل الحصة السوقية
- st.markdown("### تحليل الحصة السوقية")
-
- # إنشاء رسم بياني دائري للحصة السوقية
- fig = px.pie(
- price_comparison_df,
- values='market_share',
- names='competitor',
- title='توزيع الحصة السوقية',
- hole=0.4
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_reports_tab(self):
- """عرض تبويب التقارير"""
-
- st.markdown("### تقارير التسعير")
-
- # اختيار نوع التقرير
- report_type = st.selectbox(
- "اختر نوع التقرير",
- [
- "ملخص التسعير",
- "تقرير جدول الكميات",
- "تقرير تحليل التكاليف",
- "تقرير سيناريوهات التسعير",
- "تقرير المقارنة التنافسية",
- "التقرير الشامل"
- ]
- )
-
- # عرض معلومات المشروع
- col1, col2 = st.columns(2)
-
- with col1:
- project_name = st.text_input("اسم المشروع", "مشروع إنشاء مبنى سكني")
- client_name = st.text_input("اسم العميل", "شركة التطوير العقاري")
-
- with col2:
- project_location = st.text_input("موقع المشروع", "الرياض، المملكة العربية السعودية")
- report_date = st.date_input("تاريخ التقرير", datetime.now())
-
- # إنشاء التقرير
- if st.button("إنشاء التقرير"):
- st.markdown("### معاينة التقرير")
-
- # عرض ترويسة التقرير
- st.markdown(f"""
- ## {report_type}
- **اسم المشروع:** {project_name}
- **اسم العميل:** {client_name}
- **موقع المشروع:** {project_location}
- **تاريخ التقرير:** {report_date.strftime('%Y-%m-%d')}
- """)
-
- # عرض محتوى التقرير حسب النوع المختار
- if report_type == "ملخص التسعير" or report_type == "التقرير الشامل":
- self._render_pricing_summary_report()
-
- if report_type == "تقرير جدول الكميات" or report_type == "التقرير الشامل":
- self._render_boq_report()
-
- if report_type == "تقرير تحليل التكاليف" or report_type == "التقرير الشامل":
- self._render_cost_analysis_report()
-
- if report_type == "تقرير سيناريوهات التسعير" or report_type == "التقرير الشامل":
- self._render_pricing_scenarios_report()
-
- if report_type == "تقرير المقارنة التنافسية" or report_type == "التقرير الشامل":
- self._render_competitive_analysis_report()
-
- # خيارات تصدير التقرير
- st.markdown("### تصدير التقرير")
-
- export_format = st.radio(
- "اختر صيغة التصدير",
- ["PDF", "Excel", "Word"],
- horizontal=True
- )
-
- if st.button("تصدير التقرير"):
- st.success(f"تم تصدير التقرير بصيغة {export_format} بنجاح!")
-
- def _render_pricing_summary_report(self):
- """عرض تقرير ملخص التسعير"""
-
- st.markdown("## ملخص التسعير")
-
- # حساب إجمالي التكاليف
- total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
- total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
- total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
- total_cost = total_direct_cost + total_indirect_cost
- total_price = total_cost + total_profit
-
- # عرض ملخص التكاليف
- st.markdown("### ملخص التكاليف")
-
- summary_data = {
- 'البند': ['التكاليف المباشرة', 'التكاليف غير المباشرة', 'إجمالي التكاليف', 'هامش الربح', 'السعر الإجمالي'],
- 'المبلغ (ريال)': [total_direct_cost, total_indirect_cost, total_cost, total_profit, total_price],
- 'النسبة المئوية': [
- total_direct_cost / total_price * 100,
- total_indirect_cost / total_price * 100,
- total_cost / total_price * 100,
- total_profit / total_price * 100,
- 100.0
- ]
- }
-
- summary_df = pd.DataFrame(summary_data)
-
- st.dataframe(
- summary_df,
- column_config={
- 'البند': st.column_config.TextColumn('البند'),
- 'المبلغ (ريال)': st.column_config.NumberColumn('المبلغ (ريال)', format='%d'),
- 'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
- },
- hide_index=True,
- use_container_width=True
- )
-
- # عرض رسم بياني للملخص
- fig = px.pie(
- summary_df.iloc[0:3], # استخدام أول 3 صفوف فقط (التكاليف)
- values='المبلغ (ريال)',
- names='البند',
- title='توزيع التكاليف',
- color_discrete_sequence=px.colors.qualitative.Set3
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض رسم بياني للسعر الإجمالي
- fig = px.pie(
- summary_df.iloc[[2, 3]], # استخدام صفوف التكاليف والربح
- values='المبلغ (ريال)',
- names='البند',
- title='تركيبة السعر الإجمالي',
- color_discrete_sequence=px.colors.qualitative.Pastel
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_boq_report(self):
- """عرض تقرير جدول الكميات"""
-
- st.markdown("## تقرير جدول الكميات")
-
- # عرض جدول الكميات
- boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
-
- st.dataframe(
- boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']],
- column_config={
- 'code': 'الكود',
- 'description': 'الوصف',
- 'unit': 'الوحدة',
- 'quantity': 'الكمية',
- 'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
- 'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
- 'category': 'الفئة'
- },
- hide_index=True,
- use_container_width=True
- )
-
- # عرض ملخص جدول الكميات حسب الفئة
- st.markdown("### ملخص جدول الكميات حسب الفئة")
-
- # تجميع البيانات حسب الفئة
- category_totals = {}
- for item in st.session_state.bill_of_quantities:
- category = item['category']
- if category in category_totals:
- category_totals[category] += item['total_price']
- else:
- category_totals[category] = item['total_price']
-
- # إنشاء DataFrame للملخص
- category_summary_df = pd.DataFrame({
- 'الفئة': list(category_totals.keys()),
- 'المبلغ الإجمالي': list(category_totals.values())
- })
-
- # حساب النسبة المئوية
- total_boq = sum(category_totals.values())
- category_summary_df['النسبة المئوية'] = category_summary_df['المبلغ الإجمالي'] / total_boq * 100
-
- # ترتيب البيانات تنازليًا حسب المبلغ
- category_summary_df = category_summary_df.sort_values('المبلغ الإجمالي', ascending=False)
-
- st.dataframe(
- category_summary_df,
- column_config={
- 'الفئة': st.column_config.TextColumn('الفئة'),
- 'المبلغ الإجمالي': st.column_config.NumberColumn('المبلغ الإجمالي', format='%d ريال'),
- 'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
- },
- hide_index=True,
- use_container_width=True
- )
-
- # عرض رسم بياني للملخص
- fig = px.pie(
- category_summary_df,
- values='المبلغ الإجمالي',
- names='الفئة',
- title='توزيع تكاليف البنود حسب الفئة',
- color_discrete_sequence=px.colors.qualitative.Set3
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض رسم بياني شريطي للملخص
- fig = px.bar(
- category_summary_df,
- x='الفئة',
- y='المبلغ الإجمالي',
- title='إجمالي تكلفة البنود حسب الفئة',
- color='الفئة',
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_cost_analysis_report(self):
- """عرض تقرير تحليل التكاليف"""
-
- st.markdown("## تقرير تحليل التكاليف")
-
- # عرض جدول تحليل التكاليف
- cost_df = pd.DataFrame(st.session_state.cost_analysis)
-
- st.dataframe(
- cost_df[['category', 'subcategory', 'description', 'amount', 'percentage']],
- column_config={
- 'category': 'الفئة',
- 'subcategory': 'الفئة الفرعية',
- 'description': 'الوصف',
- 'amount': st.column_config.NumberColumn('المبلغ', format='%d ريال'),
- 'percentage': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
- },
- hide_index=True,
- use_container_width=True
- )
-
- # عرض ملخص تحليل التكاليف حسب الفئة
- st.markdown("### ملخص تحليل التكاليف حسب الفئة")
-
- # تجميع البيانات حسب الفئة
- category_totals = {}
- for item in st.session_state.cost_analysis:
- category = item['category']
- if category in category_totals:
- category_totals[category] += item['amount']
- else:
- category_totals[category] = item['amount']
-
- # إنشاء DataFrame للملخص
- category_summary_df = pd.DataFrame({
- 'الفئة': list(category_totals.keys()),
- 'المبلغ الإجمالي': list(category_totals.values())
- })
-
- # حساب النسبة المئوية
- total_cost = sum(category_totals.values())
- category_summary_df['النسبة المئوية'] = category_summary_df['المبلغ الإجمالي'] / total_cost * 100
-
- # ترتيب البيانات تنازليًا حسب المبلغ
- category_summary_df = category_summary_df.sort_values('المبلغ الإجمالي', ascending=False)
-
- st.dataframe(
- category_summary_df,
- column_config={
- 'الفئة': st.column_config.TextColumn('الفئة'),
- 'المبلغ الإجمالي': st.column_config.NumberColumn('المبلغ الإجمالي', format='%d ريال'),
- 'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
- },
- hide_index=True,
- use_container_width=True
- )
-
- # عرض رسم بياني للملخص
- fig = px.pie(
- category_summary_df,
- values='المبلغ الإجمالي',
- names='الفئة',
- title='توزيع التكاليف حسب الفئة',
- color_discrete_sequence=px.colors.qualitative.Set3
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض ملخص تحليل التكاليف حسب الفئة والفئة الفرعية
- st.markdown("### ملخص تحليل التكاليف حسب الفئة والفئة الفرعية")
-
- # تجميع البيانات حسب الفئة والفئة الفرعية
- subcategory_totals = {}
- for item in st.session_state.cost_analysis:
- category = item['category']
- subcategory = item['subcategory']
- key = f"{category} - {subcategory}"
- if key in subcategory_totals:
- subcategory_totals[key] += item['amount']
- else:
- subcategory_totals[key] = item['amount']
-
- # إنشاء DataFrame للملخص
- subcategory_summary_df = pd.DataFrame({
- 'الفئة والفئة الفرعية': list(subcategory_totals.keys()),
- 'المبلغ الإجمالي': list(subcategory_totals.values())
- })
-
- # حساب النسبة المئوية
- subcategory_summary_df['النسبة المئوية'] = subcategory_summary_df['المبلغ الإجمالي'] / total_cost * 100
-
- # ترتيب البيانات تنازليًا حسب المبلغ
- subcategory_summary_df = subcategory_summary_df.sort_values('المبلغ الإجمالي', ascending=False)
-
- st.dataframe(
- subcategory_summary_df,
- column_config={
- 'الفئة والفئة الفرعية': st.column_config.TextColumn('الفئة والفئة الفرعية'),
- 'المبلغ الإجمالي': st.column_config.NumberColumn('المبلغ الإجمالي', format='%d ريال'),
- 'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
- },
- hide_index=True,
- use_container_width=True
- )
-
- # عرض رسم بياني للملخص
- fig = px.bar(
- subcategory_summary_df,
- x='الفئة والفئة الفرعية',
- y='المبلغ الإجمالي',
- title='توزيع التكاليف حسب الفئة والفئة الفرعية',
- color='الفئة والفئة الفرعية',
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض تحليل هيكل التكاليف
- st.markdown("### تحليل هيكل التكاليف")
-
- # إنشاء رسم بياني للنسب المئوية
- fig = px.treemap(
- cost_df,
- path=['category', 'subcategory', 'description'],
- values='amount',
- title='تحليل هيكل التكاليف',
- color='percentage',
- color_continuous_scale='RdBu',
- color_continuous_midpoint=np.average(cost_df['percentage'])
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_pricing_scenarios_report(self):
- """عرض تقرير سيناريوهات التسعير"""
-
- st.markdown("## تقرير سيناريوهات التسعير")
-
- # عرض جدول سيناريوهات التسعير
- scenarios_df = pd.DataFrame(st.session_state.price_scenarios)
-
- st.dataframe(
- scenarios_df[['name', 'description', 'total_cost', 'profit_margin', 'total_price', 'is_active']],
- column_config={
- 'name': 'اسم السيناريو',
- 'description': 'الوصف',
- 'total_cost': st.column_config.NumberColumn('إجمالي التكلفة', format='%d ريال'),
- 'profit_margin': st.column_config.NumberColumn('هامش الربح', format='%.1f%%'),
- 'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
- 'is_active': st.column_config.CheckboxColumn('نشط')
- },
- hide_index=True,
- use_container_width=True
- )
-
- # عرض مقارنة السيناريوهات
- st.markdown("### مقارنة السيناريوهات")
-
- # إنشاء رسم بياني للمقارنة
- fig = go.Figure()
-
- for scenario in st.session_state.price_scenarios:
- fig.add_trace(go.Bar(
- name=scenario['name'],
- x=['التكلفة', 'الربح', 'السعر الإجمالي'],
- y=[
- scenario['total_cost'],
- scenario['total_price'] - scenario['total_cost'],
- scenario['total_price']
- ],
- text=[
- f"{scenario['total_cost']:,.0f}",
- f"{scenario['total_price'] - scenario['total_cost']:,.0f}",
- f"{scenario['total_price']:,.0f}"
- ],
- textposition='auto'
- ))
-
- fig.update_layout(
- title='مقارنة السيناريوهات',
- barmode='group',
- xaxis_title='العنصر',
- yaxis_title='المبلغ (ريال)'
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض مقارنة هوامش الربح
- st.markdown("### مقارنة هوامش الربح")
-
- # إنشاء DataFrame للمقارنة
- profit_comparison_df = pd.DataFrame({
- 'السيناريو': [scenario['name'] for scenario in st.session_state.price_scenarios],
- 'هامش الربح (%)': [scenario['profit_margin'] for scenario in st.session_state.price_scenarios],
- 'مبلغ الربح (ريال)': [scenario['total_price'] - scenario['total_cost'] for scenario in st.session_state.price_scenarios]
- })
-
- # ترتيب البيانات تنازليًا حسب هامش الربح
- profit_comparison_df = profit_comparison_df.sort_values('هامش الربح (%)', ascending=False)
-
- # إنشاء رسم بياني شريطي
- fig = px.bar(
- profit_comparison_df,
- x='السيناريو',
- y='هامش الربح (%)',
- title='مقارنة هوامش الربح',
- color='السيناريو',
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض تحليل حساسية هامش الربح
- st.markdown("### تحليل حساسية هامش الربح")
-
- # الحصول على التكلفة الإجمالية من السيناريو النشط أو الأول
- active_scenario = next((s for s in st.session_state.price_scenarios if s['is_active']), st.session_state.price_scenarios[0])
- total_cost = active_scenario['total_cost']
-
- # إنشاء بيانات لتحليل الحساسية
- profit_margins = list(range(5, 26, 1)) # من 5% إلى 25%
- total_prices = [total_cost * (1 + margin/100) for margin in profit_margins]
-
- # إنشاء DataFrame للرسم البياني
- sensitivity_df = pd.DataFrame({
- 'هامش الربح (%)': profit_margins,
- 'السعر الإجمالي': total_prices
- })
-
- # إنشاء رسم بياني خطي
- fig = px.line(
- sensitivity_df,
- x='هامش الربح (%)',
- y='السعر الإجمالي',
- title='تحليل حساسية هامش الربح',
- markers=True
- )
-
- # إضافة خط أفقي يمثل السعر التنافسي (افتراضي)
- competitive_price = 650000
- fig.add_hline(
- y=competitive_price,
- line_dash="dash",
- line_color="red",
- annotation_text="السعر التنافسي",
- annotation_position="bottom right"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_competitive_analysis_report(self):
- """عرض تقرير المقارنة التنافسية"""
-
- st.markdown("## تقرير المقارنة التنافسية")
-
- # عرض جدول المقارنة التنافسية
- competitive_df = pd.DataFrame(st.session_state.competitive_analysis)
-
- st.dataframe(
- competitive_df[['competitor', 'project_type', 'price_per_sqm', 'delivery_time', 'quality_rating', 'market_share']],
- column_config={
- 'competitor': 'المنافس',
- 'project_type': 'نوع المشروع',
- 'price_per_sqm': st.column_config.NumberColumn('السعر لكل متر مربع', format='%d ريال'),
- 'delivery_time': st.column_config.NumberColumn('مدة التسليم (شهر)', format='%d'),
- 'quality_rating': st.column_config.NumberColumn('تقييم الجودة', format='%.1f/5.0'),
- 'market_share': st.column_config.NumberColumn('الحصة السوقية', format='%.1f%%')
- },
- hide_index=True,
- use_container_width=True
- )
-
- # عرض مقارنة الأسعار
- st.markdown("### مقارنة الأسعار")
-
- # ترتيب البيانات تصاعديًا حسب السعر
- price_comparison_df = competitive_df.sort_values('price_per_sqm')
-
- # إنشاء رسم بياني شريطي
- fig = px.bar(
- price_comparison_df,
- x='competitor',
- y='price_per_sqm',
- title='مقارنة الأسعار لكل متر مربع',
- color='competitor',
- text_auto=True
- )
-
- # تمييز شركتنا
- for i, competitor in enumerate(price_comparison_df['competitor']):
- if competitor == 'شركتنا':
- fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(price_comparison_df))]
- break
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض مقارنة الجودة والسعر
- st.markdown("### مقارنة الجودة والسعر")
-
- # إنشاء رسم بياني للعلاقة بين السعر والجودة
- fig = px.scatter(
- price_comparison_df,
- x='price_per_sqm',
- y='quality_rating',
- size='market_share',
- color='competitor',
- title='العلاقة بين السعر والجودة والحصة السوقية',
- labels={
- 'price_per_sqm': 'السعر لكل متر مربع (ريال)',
- 'quality_rating': 'تقييم الجودة',
- 'market_share': 'الحصة السوقية (%)'
- },
- text='competitor'
- )
-
- fig.update_traces(textposition='top center')
-
- # إضافة خط اتجاه
- fig.update_layout(
- shapes=[
- dict(
- type='line',
- x0=min(price_comparison_df['price_per_sqm']),
- y0=min(price_comparison_df['quality_rating']),
- x1=max(price_comparison_df['price_per_sqm']),
- y1=max(price_comparison_df['quality_rating']),
- line=dict(color='gray', dash='dash')
- )
- ]
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض مقارنة مدة التسليم
- st.markdown("### مقارنة مدة التسليم")
-
- # ترتيب البيانات تصاعديًا حسب مدة التسليم
- delivery_comparison_df = competitive_df.sort_values('delivery_time')
-
- # إنشاء رسم بياني شريطي
- fig = px.bar(
- delivery_comparison_df,
- x='competitor',
- y='delivery_time',
- title='مقارنة مدة التسليم (شهر)',
- color='competitor',
- text_auto=True
- )
-
- # تمييز شركتنا
- for i, competitor in enumerate(delivery_comparison_df['competitor']):
- if competitor == 'شركتنا':
- fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(delivery_comparison_df))]
- break
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض تحليل الحصة السوقية
- st.markdown("### تحليل الحصة السوقية")
-
- # إنشاء رسم بياني دائري للحصة السوقية
- fig = px.pie(
- competitive_df,
- values='market_share',
- names='competitor',
- title='توزيع الحصة السوقية',
- hole=0.4
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض تحليل الموقع التنافسي
- st.markdown("### تحليل الموقع التنافسي")
-
- # إيجاد بيانات شركتنا
- our_company = next((item for item in st.session_state.competitive_analysis if item['competitor'] == 'شركتنا'), None)
-
- if our_company:
- # حساب متوسطات السوق
- avg_price = competitive_df['price_per_sqm'].mean()
- avg_delivery = competitive_df['delivery_time'].mean()
- avg_quality = competitive_df['quality_rating'].mean()
-
- # إنشاء بيانات المقارنة
- comparison_data = {
- 'المؤشر': ['السعر لكل متر مربع', 'مدة التسليم', 'تقييم الجودة'],
- 'قيمة شركتنا': [our_company['price_per_sqm'], our_company['delivery_time'], our_company['quality_rating']],
- 'متوسط السوق': [avg_price, avg_delivery, avg_quality],
- 'الفرق (%)': [
- (our_company['price_per_sqm'] - avg_price) / avg_price * 100,
- (our_company['delivery_time'] - avg_delivery) / avg_delivery * 100,
- (our_company['quality_rating'] - avg_quality) / avg_quality * 100
- ]
- }
-
- comparison_df = pd.DataFrame(comparison_data)
-
- st.dataframe(
- comparison_df,
- column_config={
- 'المؤشر': st.column_config.TextColumn('المؤشر'),
- 'قيمة شركتنا': st.column_config.NumberColumn('قيمة شركتنا'),
- 'متوسط السوق': st.column_config.NumberColumn('متوسط السوق'),
- 'الفرق (%)': st.column_config.NumberColumn('الفرق (%)', format='%+.1f%%')
- },
- hide_index=True,
- use_container_width=True
- )
-
- # إنشاء رسم بياني راداري للموقع التنافسي
- # تحويل البيانات إلى نسب مئوية للمقارنة
- max_price = competitive_df['price_per_sqm'].max()
- min_price = competitive_df['price_per_sqm'].min()
- price_range = max_price - min_price
-
- max_delivery = competitive_df['delivery_time'].max()
- min_delivery = competitive_df['delivery_time'].min()
- delivery_range = max_delivery - min_delivery
-
- # ملاحظة: نقوم بعكس مقياس السعر ومدة التسليم لأن القيم الأقل أفضل
- normalized_price = 100 - ((our_company['price_per_sqm'] - min_price) / price_range * 100) if price_range > 0 else 50
- normalized_delivery = 100 - ((our_company['delivery_time'] - min_delivery) / delivery_range * 100) if delivery_range > 0 else 50
- normalized_quality = (our_company['quality_rating'] / 5) * 100
- normalized_market_share = (our_company['market_share'] / competitive_df['market_share'].max()) * 100
-
- # إنشاء رسم بياني راداري
- fig = go.Figure()
-
- fig.add_trace(go.Scatterpolar(
- r=[normalized_price, normalized_delivery, normalized_quality, normalized_market_share],
- theta=['السعر التنافسي', 'سرعة التسليم', 'الجودة', 'الحصة السوقية'],
- fill='toself',
- name='شركتنا'
- ))
-
- fig.update_layout(
- polar=dict(
- radialaxis=dict(
- visible=True,
- range=[0, 100]
- )
- ),
- title='تحليل الموقع التنافسي لشركتنا'
- )
-
- st.plotly_chart(fig, use_container_width=True)
+"""
+وحدة التسعير - التطبيق الرئيسي
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.express as px
+import plotly.graph_objects as go
+from datetime import datetime
+import time
+import io
+import os
+import json
+import base64
+from pathlib import Path
+
+class PricingApp:
+ """وحدة التسعير"""
+
+ def __init__(self):
+ """تهيئة وحدة التسعير"""
+
+ # تهيئة حالة الجلسة
+ if 'bill_of_quantities' not in st.session_state:
+ st.session_state.bill_of_quantities = [
+ {
+ 'id': 1,
+ 'code': 'A-001',
+ 'description': 'أعمال الحفر والردم',
+ 'unit': 'م3',
+ 'quantity': 1500,
+ 'unit_price': 45,
+ 'total_price': 67500,
+ 'category': 'أعمال ترابية'
+ },
+ {
+ 'id': 2,
+ 'code': 'A-002',
+ 'description': 'توريد وصب خرسانة عادية',
+ 'unit': 'م3',
+ 'quantity': 250,
+ 'unit_price': 350,
+ 'total_price': 87500,
+ 'category': 'أعمال خرسانية'
+ },
+ {
+ 'id': 3,
+ 'code': 'A-003',
+ 'description': 'توريد وصب خرسانة مسلحة للأساسات',
+ 'unit': 'م3',
+ 'quantity': 180,
+ 'unit_price': 450,
+ 'total_price': 81000,
+ 'category': 'أعمال خرسانية'
+ },
+ {
+ 'id': 4,
+ 'code': 'A-004',
+ 'description': 'توريد وصب خرسانة مسلحة للأعمدة',
+ 'unit': 'م3',
+ 'quantity': 120,
+ 'unit_price': 500,
+ 'total_price': 60000,
+ 'category': 'أعمال خرسانية'
+ },
+ {
+ 'id': 5,
+ 'code': 'A-005',
+ 'description': 'توريد وتركيب حديد تسليح',
+ 'unit': 'طن',
+ 'quantity': 45,
+ 'unit_price': 3000,
+ 'total_price': 135000,
+ 'category': 'أعمال حديد'
+ },
+ {
+ 'id': 6,
+ 'code': 'A-006',
+ 'description': 'توريد وبناء طابوق',
+ 'unit': 'م2',
+ 'quantity': 1200,
+ 'unit_price': 45,
+ 'total_price': 54000,
+ 'category': 'أعمال بناء'
+ },
+ {
+ 'id': 7,
+ 'code': 'A-007',
+ 'description': 'أعمال اللياسة والتشطيبات',
+ 'unit': 'م2',
+ 'quantity': 2400,
+ 'unit_price': 35,
+ 'total_price': 84000,
+ 'category': 'أعمال تشطيبات'
+ },
+ {
+ 'id': 8,
+ 'code': 'A-008',
+ 'description': 'أعمال الدهانات',
+ 'unit': 'م2',
+ 'quantity': 2400,
+ 'unit_price': 25,
+ 'total_price': 60000,
+ 'category': 'أعمال تشطيبات'
+ },
+ {
+ 'id': 9,
+ 'code': 'A-009',
+ 'description': 'توريد وتركيب أبواب خشبية',
+ 'unit': 'عدد',
+ 'quantity': 24,
+ 'unit_price': 750,
+ 'total_price': 18000,
+ 'category': 'أعمال نجارة'
+ },
+ {
+ 'id': 10,
+ 'code': 'A-010',
+ 'description': 'توريد وتركيب نوافذ ألمنيوم',
+ 'unit': 'م2',
+ 'quantity': 120,
+ 'unit_price': 350,
+ 'total_price': 42000,
+ 'category': 'أعمال ألمنيوم'
+ }
+ ]
+
+ if 'cost_analysis' not in st.session_state:
+ st.session_state.cost_analysis = [
+ {
+ 'id': 1,
+ 'category': 'تكاليف مباشرة',
+ 'subcategory': 'مواد',
+ 'description': 'خرسانة',
+ 'amount': 120000,
+ 'percentage': 17.9
+ },
+ {
+ 'id': 2,
+ 'category': 'تكاليف مباشرة',
+ 'subcategory': 'مواد',
+ 'description': 'حديد تسليح',
+ 'amount': 135000,
+ 'percentage': 20.1
+ },
+ {
+ 'id': 3,
+ 'category': 'تكاليف مباشرة',
+ 'subcategory': 'مواد',
+ 'description': 'طابوق',
+ 'amount': 54000,
+ 'percentage': 8.1
+ },
+ {
+ 'id': 4,
+ 'category': 'تكاليف مباشرة',
+ 'subcategory': 'عمالة',
+ 'description': 'عمالة تنفيذ',
+ 'amount': 120000,
+ 'percentage': 17.9
+ },
+ {
+ 'id': 5,
+ 'category': 'تكاليف مباشرة',
+ 'subcategory': 'معدات',
+ 'description': 'معدات إنشائية',
+ 'amount': 85000,
+ 'percentage': 12.7
+ },
+ {
+ 'id': 6,
+ 'category': 'تكاليف غير مباشرة',
+ 'subcategory': 'إدارة',
+ 'description': 'إدارة المشروع',
+ 'amount': 45000,
+ 'percentage': 6.7
+ },
+ {
+ 'id': 7,
+ 'category': 'تكاليف غير مباشرة',
+ 'subcategory': 'إدارة',
+ 'description': 'إشراف هندسي',
+ 'amount': 35000,
+ 'percentage': 5.2
+ },
+ {
+ 'id': 8,
+ 'category': 'تكاليف غير مباشرة',
+ 'subcategory': 'عامة',
+ 'description': 'تأمينات وضمانات',
+ 'amount': 25000,
+ 'percentage': 3.7
+ },
+ {
+ 'id': 9,
+ 'category': 'تكاليف غير مباشرة',
+ 'subcategory': 'عامة',
+ 'description': 'مصاريف إدارية',
+ 'amount': 30000,
+ 'percentage': 4.5
+ },
+ {
+ 'id': 10,
+ 'category': 'أرباح',
+ 'subcategory': 'أرباح',
+ 'description': 'هامش الربح',
+ 'amount': 55000,
+ 'percentage': 8.2
+ }
+ ]
+
+ if 'price_scenarios' not in st.session_state:
+ st.session_state.price_scenarios = [
+ {
+ 'id': 1,
+ 'name': 'السيناريو الأساسي',
+ 'description': 'التسعير الأساسي مع هامش ربح 8%',
+ 'total_cost': 615000,
+ 'profit_margin': 8.2,
+ 'total_price': 670000,
+ 'is_active': True
+ },
+ {
+ 'id': 2,
+ 'name': 'سيناريو تنافسي',
+ 'description': 'تخفيض هامش الربح للمنافسة',
+ 'total_cost': 615000,
+ 'profit_margin': 5.0,
+ 'total_price': 650000,
+ 'is_active': False
+ },
+ {
+ 'id': 3,
+ 'name': 'سيناريو مرتفع',
+ 'description': 'زيادة هامش الربح للمشاريع ذات المخاطر العالية',
+ 'total_cost': 615000,
+ 'profit_margin': 12.0,
+ 'total_price': 700000,
+ 'is_active': False
+ }
+ ]
+
+ # إضافة بيانات المقارنة التنافسية
+ if 'competitive_analysis' not in st.session_state:
+ st.session_state.competitive_analysis = [
+ {
+ 'id': 1,
+ 'competitor': 'شركة الإنشاءات المتحدة',
+ 'project_type': 'مباني سكنية',
+ 'price_per_sqm': 1800,
+ 'delivery_time': 12,
+ 'quality_rating': 4.2,
+ 'market_share': 15.5
+ },
+ {
+ 'id': 2,
+ 'competitor': 'مجموعة البناء الحديث',
+ 'project_type': 'مباني سكنية',
+ 'price_per_sqm': 2100,
+ 'delivery_time': 10,
+ 'quality_rating': 4.5,
+ 'market_share': 18.2
+ },
+ {
+ 'id': 3,
+ 'competitor': 'شركة الإعمار الدولية',
+ 'project_type': 'مباني سكنية',
+ 'price_per_sqm': 2300,
+ 'delivery_time': 14,
+ 'quality_rating': 4.7,
+ 'market_share': 22.0
+ },
+ {
+ 'id': 4,
+ 'competitor': 'مؤسسة البناء المتكامل',
+ 'project_type': 'مباني سكنية',
+ 'price_per_sqm': 1750,
+ 'delivery_time': 15,
+ 'quality_rating': 3.8,
+ 'market_share': 12.5
+ },
+ {
+ 'id': 5,
+ 'competitor': 'شركتنا',
+ 'project_type': 'مباني سكنية',
+ 'price_per_sqm': 1950,
+ 'delivery_time': 11,
+ 'quality_rating': 4.4,
+ 'market_share': 14.8
+ }
+ ]
+
+ def run(self):
+ """تشغيل وحدة التسعير"""
+ # استدعاء دالة العرض
+ self.render()
+
+ def render(self):
+ """عرض واجهة وحدة التسعير"""
+
+ st.markdown("وحدة التسعير ", unsafe_allow_html=True)
+
+ tabs = st.tabs([
+ "لوحة التحكم",
+ "جدول الكميات",
+ "تحليل التكاليف",
+ "سيناريوهات التسعير",
+ "المقارنة التنافسية",
+ "التقارير"
+ ])
+
+ with tabs[0]:
+ self._render_dashboard_tab()
+
+ with tabs[1]:
+ self._render_bill_of_quantities_tab()
+
+ with tabs[2]:
+ self._render_cost_analysis_tab()
+
+ with tabs[3]:
+ self._render_pricing_scenarios_tab()
+
+ with tabs[4]:
+ self._render_competitive_analysis_tab()
+
+ with tabs[5]:
+ self._render_reports_tab()
+
+ def _render_dashboard_tab(self):
+ """عرض تبويب لوحة التحكم"""
+
+ st.markdown("### لوحة تحكم التسعير")
+
+ # عرض ملخص التسعير
+ col1, col2, col3, col4 = st.columns(4)
+
+ # حساب إجمالي التكاليف
+ total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
+ total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
+ total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
+ total_cost = total_direct_cost + total_indirect_cost
+ total_price = total_cost + total_profit
+
+ with col1:
+ st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.0f} ريال")
+
+ with col2:
+ st.metric("إجمالي التكاليف غير المباشرة", f"{total_indirect_cost:,.0f} ريال")
+
+ with col3:
+ st.metric("إجمالي التكاليف", f"{total_cost:,.0f} ريال")
+
+ with col4:
+ st.metric("السعر الإجمالي", f"{total_price:,.0f} ريال")
+
+ # عرض توزيع التكاليف
+ st.markdown("### توزيع التكاليف")
+
+ # تجميع البيانات حسب الفئة
+ cost_categories = {}
+
+ for item in st.session_state.cost_analysis:
+ category = item['category']
+ if category in cost_categories:
+ cost_categories[category] += item['amount']
+ else:
+ cost_categories[category] = item['amount']
+
+ # إنشاء DataFrame للرسم البياني
+ cost_df = pd.DataFrame({
+ 'الفئة': list(cost_categories.keys()),
+ 'المبلغ': list(cost_categories.values())
+ })
+
+ # إنشاء رسم بياني دائري
+ fig = px.pie(
+ cost_df,
+ values='المبلغ',
+ names='الفئة',
+ title='توزيع التكاليف حسب الفئة',
+ color_discrete_sequence=px.colors.qualitative.Set3
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض توزيع التكاليف المباشرة
+ st.markdown("### توزيع التكاليف المباشرة")
+
+ # تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
+ direct_cost_subcategories = {}
+
+ for item in st.session_state.cost_analysis:
+ if item['category'] == 'تكاليف مباشرة':
+ subcategory = item['subcategory']
+ if subcategory in direct_cost_subcategories:
+ direct_cost_subcategories[subcategory] += item['amount']
+ else:
+ direct_cost_subcategories[subcategory] = item['amount']
+
+ # إنشاء DataFrame للرسم البياني
+ direct_cost_df = pd.DataFrame({
+ 'الفئة الفرعية': list(direct_cost_subcategories.keys()),
+ 'المبلغ': list(direct_cost_subcategories.values())
+ })
+
+ # إنشاء رسم بياني دائري
+ fig = px.pie(
+ direct_cost_df,
+ values='المبلغ',
+ names='الفئة الفرعية',
+ title='توزيع التكاليف المباشرة',
+ color_discrete_sequence=px.colors.qualitative.Pastel
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض توزيع التكاليف غير المباشرة
+ st.markdown("### توزيع التكاليف غير المباشرة")
+
+ # تجميع البيانات حسب الفئة الفرعية للتكاليف غير المباشرة
+ indirect_cost_subcategories = {}
+
+ for item in st.session_state.cost_analysis:
+ if item['category'] == 'تكاليف غير مباشرة':
+ subcategory = item['subcategory']
+ if subcategory in indirect_cost_subcategories:
+ indirect_cost_subcategories[subcategory] += item['amount']
+ else:
+ indirect_cost_subcategories[subcategory] = item['amount']
+
+ # إنشاء DataFrame للرسم البياني
+ indirect_cost_df = pd.DataFrame({
+ 'الفئة الفرعية': list(indirect_cost_subcategories.keys()),
+ 'المبلغ': list(indirect_cost_subcategories.values())
+ })
+
+ # إنشاء رسم بياني دائري
+ fig = px.pie(
+ indirect_cost_df,
+ values='المبلغ',
+ names='الفئة الفرعية',
+ title='توزيع التكاليف غير المباشرة',
+ color_discrete_sequence=px.colors.qualitative.Pastel1
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _render_bill_of_quantities_tab(self):
+ """عرض تبويب جدول الكميات"""
+
+ st.markdown("### جدول الكميات")
+
+ # إنشاء DataFrame من بيانات جدول الكميات
+ boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
+
+ # عرض جدول الكميات
+ st.dataframe(
+ boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']],
+ column_config={
+ 'code': 'الكود',
+ 'description': 'الوصف',
+ 'unit': 'الوحدة',
+ 'quantity': 'الكمية',
+ 'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
+ 'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
+ 'category': 'الفئة'
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # إضافة بند جديد
+ st.markdown("### إضافة بند جديد")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ new_code = st.text_input("الكود", key="new_boq_code")
+ new_description = st.text_input("الوصف", key="new_boq_description")
+ new_unit = st.selectbox("الوحدة", ["م3", "م2", "طن", "عدد", "متر طولي"], key="new_boq_unit")
+
+ with col2:
+ new_quantity = st.number_input("الكمية", min_value=0.0, step=1.0, key="new_boq_quantity")
+ new_unit_price = st.number_input("سعر الوحدة", min_value=0.0, step=10.0, key="new_boq_unit_price")
+ new_category = st.selectbox(
+ "الفئة",
+ [
+ "أعمال ترابية",
+ "أعمال خرسانية",
+ "أعمال حديد",
+ "أعمال بناء",
+ "أعمال تشطيبات",
+ "أعمال نجارة",
+ "أعمال ألمنيوم",
+ "أعمال كهربائية",
+ "أعمال ميكانيكية",
+ "أعمال صحية"
+ ],
+ key="new_boq_category"
+ )
+
+ if st.button("إضافة البند", key="add_boq_item"):
+ if new_code and new_description and new_quantity > 0 and new_unit_price > 0:
+ # حساب السعر الإجمالي
+ new_total_price = new_quantity * new_unit_price
+
+ # إضافة بند جديد
+ new_id = max([item['id'] for item in st.session_state.bill_of_quantities]) + 1
+
+ st.session_state.bill_of_quantities.append({
+ 'id': new_id,
+ 'code': new_code,
+ 'description': new_description,
+ 'unit': new_unit,
+ 'quantity': new_quantity,
+ 'unit_price': new_unit_price,
+ 'total_price': new_total_price,
+ 'category': new_category
+ })
+
+ st.success(f"تمت إضافة البند بنجاح: {new_description}")
+
+ # تحديث الصفحة لعرض البند الجديد
+ st.rerun()
+ else:
+ st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
+
+ # عرض ملخص جدول الكميات (إزالة التكرار)
+ st.markdown("### ملخص جدول الكميات")
+
+ # تجميع البيانات حسب الفئة
+ category_totals = {}
+ for item in st.session_state.bill_of_quantities:
+ category = item['category']
+ if category in category_totals:
+ category_totals[category] += item['total_price']
+ else:
+ category_totals[category] = item['total_price']
+
+ # إنشاء DataFrame للرسم البياني
+ category_df = pd.DataFrame({
+ 'الفئة': list(category_totals.keys()),
+ 'المبلغ': list(category_totals.values())
+ })
+
+ # ترتيب البيانات تنازليًا حسب المبلغ
+ category_df = category_df.sort_values('المبلغ', ascending=False)
+
+ # إنشاء رسم بياني شريطي
+ fig = px.bar(
+ category_df,
+ x='الفئة',
+ y='المبلغ',
+ title='إجمالي تكلفة البنود حسب الفئة',
+ color='الفئة',
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # حساب إجمالي جدول الكميات
+ total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities)
+
+ def _render_cost_analysis_tab(self):
+ """عرض تبويب تحليل التكاليف"""
+
+ st.markdown("### تحليل التكاليف")
+
+ # عرض جدول تحليل التكاليف
+ cost_df = pd.DataFrame(st.session_state.cost_analysis)
+
+ st.dataframe(
+ cost_df[['category', 'subcategory', 'description', 'amount', 'percentage']],
+ column_config={
+ 'category': 'الفئة',
+ 'subcategory': 'الفئة الفرعية',
+ 'description': 'الوصف',
+ 'amount': st.column_config.NumberColumn('المبلغ', format='%d ريال'),
+ 'percentage': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # إضافة بند تكلفة جديد
+ st.markdown("### إضافة بند تكلفة جديد")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ new_category = st.selectbox(
+ "الفئة",
+ ["تكاليف مباشرة", "تكاليف غير مباشرة", "أرباح"],
+ key="new_cost_category"
+ )
+
+ # تحديد الفئات الفرعية بناءً على الفئة المختارة
+ subcategory_options = []
+ if new_category == "تكاليف مباشرة":
+ subcategory_options = ["مواد", "عمالة", "معدات"]
+ elif new_category == "تكاليف غير مباشرة":
+ subcategory_options = ["إدارة", "عامة", "تمويل"]
+ else:
+ subcategory_options = ["أرباح"]
+
+ new_subcategory = st.selectbox(
+ "الفئة الفرعية",
+ subcategory_options,
+ key="new_cost_subcategory"
+ )
+
+ new_description = st.text_input("الوصف", key="new_cost_description")
+
+ with col2:
+ new_amount = st.number_input("المبلغ", min_value=0.0, step=1000.0, key="new_cost_amount")
+
+ # حساب إجمالي التكاليف الحالية
+ total_cost = sum(item['amount'] for item in st.session_state.cost_analysis)
+
+ # حساب النسبة المئوية التقريبية
+ if total_cost > 0:
+ estimated_percentage = (new_amount / total_cost) * 100
+ else:
+ estimated_percentage = 0
+
+ st.metric("النسبة المئوية التقديرية", f"{estimated_percentage:.1f}%")
+
+ if st.button("إضافة بند التكلفة", key="add_cost_item"):
+ if new_description and new_amount > 0:
+ # إضافة بند جديد
+ new_id = max([item['id'] for item in st.session_state.cost_analysis]) + 1
+
+ # حساب النسبة المئوية الفعلية بعد إضافة البند الجديد
+ new_total = total_cost + new_amount
+
+ # إعادة حساب النسب المئوية لجميع البنود
+ for item in st.session_state.cost_analysis:
+ item['percentage'] = (item['amount'] / new_total) * 100
+
+ # إضافة البند الجديد
+ st.session_state.cost_analysis.append({
+ 'id': new_id,
+ 'category': new_category,
+ 'subcategory': new_subcategory,
+ 'description': new_description,
+ 'amount': new_amount,
+ 'percentage': (new_amount / new_total) * 100
+ })
+
+ st.success(f"تمت إضافة بند التكلفة بنجاح: {new_description}")
+
+ # تحديث الصفحة لعرض البند الجديد
+ st.rerun()
+ else:
+ st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
+
+ # تحليل التكاليف حسب الفئة والفئة الفرعية
+ st.markdown("### تحليل التكاليف حسب الفئة والفئة الفرعية")
+
+ # تجميع البيانات حسب الفئة والفئة الفرعية
+ cost_by_category_subcategory = {}
+
+ for item in st.session_state.cost_analysis:
+ category = item['category']
+ subcategory = item['subcategory']
+ key = f"{category} - {subcategory}"
+
+ if key in cost_by_category_subcategory:
+ cost_by_category_subcategory[key] += item['amount']
+ else:
+ cost_by_category_subcategory[key] = item['amount']
+
+ # إنشاء DataFrame للرسم البياني
+ cost_category_subcategory_df = pd.DataFrame({
+ 'الفئة والفئة الفرعية': list(cost_by_category_subcategory.keys()),
+ 'المبلغ': list(cost_by_category_subcategory.values())
+ })
+
+ # ترتيب البيانات تنازليًا حسب المبلغ
+ cost_category_subcategory_df = cost_category_subcategory_df.sort_values('المبلغ', ascending=False)
+
+ # إنشاء رسم بياني شريطي
+ fig = px.bar(
+ cost_category_subcategory_df,
+ x='الفئة والفئة الفرعية',
+ y='المبلغ',
+ title='تحليل التكاليف حسب الفئة والفئة الفرعية',
+ color='الفئة والفئة الفرعية',
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل نسب التكاليف
+ st.markdown("### تحليل نسب التكاليف")
+
+ # إنشاء رسم بياني للنسب المئوية
+ percentage_df = pd.DataFrame(st.session_state.cost_analysis)
+
+ fig = px.treemap(
+ percentage_df,
+ path=['category', 'subcategory', 'description'],
+ values='amount',
+ title='تحليل هيكل التكاليف',
+ color='percentage',
+ color_continuous_scale='RdBu',
+ color_continuous_midpoint=np.average(percentage_df['percentage'])
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل اتجاهات التكاليف (بيانات افتراضية)
+ st.markdown("### تحليل اتجاهات التكاليف")
+
+ # إنشاء بيانات افتراضية للاتجاهات
+ months = ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو']
+ direct_costs = [510000, 520000, 515000, 525000, 530000, 514000]
+ indirect_costs = [130000, 135000, 132000, 138000, 140000, 135000]
+
+ # إنشاء DataFrame للرسم البياني
+ trends_df = pd.DataFrame({
+ 'الشهر': months * 2,
+ 'نوع التكلفة': ['تكاليف مباشرة'] * 6 + ['تكاليف غير مباشرة'] * 6,
+ 'المبلغ': direct_costs + indirect_costs
+ })
+
+ # إنشاء رسم بياني خطي
+ fig = px.line(
+ trends_df,
+ x='الشهر',
+ y='المبلغ',
+ color='نوع التكلفة',
+ title='اتجاهات التكاليف على مدار الأشهر الستة الماضية',
+ markers=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _render_pricing_scenarios_tab(self):
+ """عرض تبويب سيناريوهات التسعير"""
+
+ st.markdown("### سيناريوهات التسعير")
+
+ # عرض جدول سيناريوهات التسعير
+ scenarios_df = pd.DataFrame(st.session_state.price_scenarios)
+
+ st.dataframe(
+ scenarios_df[['name', 'description', 'total_cost', 'profit_margin', 'total_price', 'is_active']],
+ column_config={
+ 'name': 'اسم السيناريو',
+ 'description': 'الوصف',
+ 'total_cost': st.column_config.NumberColumn('إجمالي التكلفة', format='%d ريال'),
+ 'profit_margin': st.column_config.NumberColumn('هامش الربح', format='%.1f%%'),
+ 'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
+ 'is_active': st.column_config.CheckboxColumn('نشط')
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # إنشاء سيناريو جديد
+ st.markdown("### إنشاء سيناريو جديد")
+
+ col1, col2 = st.columns(2)
+
+ # حساب إجمالي التكاليف
+ total_cost = sum(item['amount'] for item in st.session_state.cost_analysis
+ if item['category'] != 'أرباح')
+
+ with col1:
+ new_name = st.text_input("اسم السيناريو", key="new_scenario_name")
+ new_description = st.text_input("وصف السيناريو", key="new_scenario_description")
+
+ with col2:
+ new_profit_margin = st.slider(
+ "هامش الربح (%)",
+ min_value=0.0,
+ max_value=30.0,
+ value=10.0,
+ step=0.5,
+ key="new_scenario_profit_margin"
+ )
+
+ # حساب السعر الإجمالي بناءً على هامش الربح
+ profit_amount = total_cost * (new_profit_margin / 100)
+ new_total_price = total_cost + profit_amount
+
+ st.metric("إجمالي التكلفة", f"{total_cost:,.0f} ريال")
+ st.metric("السعر الإجمالي المقترح", f"{new_total_price:,.0f} ريال")
+
+ if st.button("إضافة السيناريو", key="add_scenario"):
+ if new_name and new_description:
+ # إضافة سيناريو جديد
+ new_id = max([item['id'] for item in st.session_state.price_scenarios]) + 1
+
+ st.session_state.price_scenarios.append({
+ 'id': new_id,
+ 'name': new_name,
+ 'description': new_description,
+ 'total_cost': total_cost,
+ 'profit_margin': new_profit_margin,
+ 'total_price': new_total_price,
+ 'is_active': False
+ })
+
+ st.success(f"تمت إضافة السيناريو بنجاح: {new_name}")
+
+ # تحديث الصفحة لعرض السيناريو الجديد
+ st.rerun()
+ else:
+ st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
+
+ # مقارنة السيناريوهات
+ st.markdown("### مقارنة السيناريوهات")
+
+ # إنشاء رسم بياني للمقارنة
+ fig = go.Figure()
+
+ for scenario in st.session_state.price_scenarios:
+ fig.add_trace(go.Bar(
+ name=scenario['name'],
+ x=['التكلفة', 'الربح', 'السعر الإجمالي'],
+ y=[
+ scenario['total_cost'],
+ scenario['total_price'] - scenario['total_cost'],
+ scenario['total_price']
+ ],
+ text=[
+ f"{scenario['total_cost']:,.0f}",
+ f"{scenario['total_price'] - scenario['total_cost']:,.0f}",
+ f"{scenario['total_price']:,.0f}"
+ ],
+ textposition='auto'
+ ))
+
+ fig.update_layout(
+ title='مقارنة السيناريوهات',
+ barmode='group',
+ xaxis_title='العنصر',
+ yaxis_title='المبلغ (ريال)'
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل حساسية هامش الربح
+ st.markdown("### تحليل حساسية هامش الربح")
+
+ # إنشاء بيانات لتحليل الحساسية
+ profit_margins = list(range(5, 26, 1)) # من 5% إلى 25%
+ total_prices = [total_cost * (1 + margin/100) for margin in profit_margins]
+
+ # إنشاء DataFrame للرسم البياني
+ sensitivity_df = pd.DataFrame({
+ 'هامش الربح (%)': profit_margins,
+ 'السعر الإجمالي': total_prices
+ })
+
+ # إنشاء رسم بياني خطي
+ fig = px.line(
+ sensitivity_df,
+ x='هامش الربح (%)',
+ y='السعر الإجمالي',
+ title='تحليل حساسية هامش الربح',
+ markers=True
+ )
+
+ # إضافة خط أفقي يمثل السعر التنافسي (افتراضي)
+ competitive_price = 650000
+ fig.add_hline(
+ y=competitive_price,
+ line_dash="dash",
+ line_color="red",
+ annotation_text="السعر التنافسي",
+ annotation_position="bottom right"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تفعيل/تعطيل السيناريوهات
+ st.markdown("### تفعيل/تعطيل السيناريوهات")
+
+ for i, scenario in enumerate(st.session_state.price_scenarios):
+ col1, col2 = st.columns([4, 1])
+
+ with col1:
+ st.write(f"**{scenario['name']}**: {scenario['description']}")
+
+ with col2:
+ is_active = st.checkbox(
+ "تفعيل",
+ value=scenario['is_active'],
+ key=f"activate_scenario_{scenario['id']}"
+ )
+
+ # تحديث حالة التفعيل
+ if is_active != scenario['is_active']:
+ # إذا تم تفعيل سيناريو، قم بتعطيل جميع السيناريوهات الأخرى
+ if is_active:
+ for j, other_scenario in enumerate(st.session_state.price_scenarios):
+ if j != i:
+ other_scenario['is_active'] = False
+
+ # تحديث حالة السيناريو الحالي
+ scenario['is_active'] = is_active
+
+ # تحديث الصفحة
+ st.rerun()
+
+ def _render_competitive_analysis_tab(self):
+ """عرض تبويب المقارنة التنافسية"""
+
+ st.markdown("### المقارنة التنافسية")
+
+ # عرض جدول المقارنة التنافسية
+ competitive_df = pd.DataFrame(st.session_state.competitive_analysis)
+
+ st.dataframe(
+ competitive_df[['competitor', 'project_type', 'price_per_sqm', 'delivery_time', 'quality_rating', 'market_share']],
+ column_config={
+ 'competitor': 'المنافس',
+ 'project_type': 'نوع المشروع',
+ 'price_per_sqm': st.column_config.NumberColumn('السعر لكل متر مربع', format='%d ريال'),
+ 'delivery_time': st.column_config.NumberColumn('مدة التسليم (شهر)', format='%d'),
+ 'quality_rating': st.column_config.NumberColumn('تقييم الجودة', format='%.1f/5.0'),
+ 'market_share': st.column_config.NumberColumn('الحصة السوقية', format='%.1f%%')
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # إضافة منافس جديد
+ st.markdown("### إضافة منافس جديد")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ new_competitor = st.text_input("اسم المنافس", key="new_competitor_name")
+ new_project_type = st.selectbox(
+ "نوع المشروع",
+ ["مباني سكنية", "مباني تجارية", "مباني صناعية", "بنية تحتية"],
+ key="new_competitor_project_type"
+ )
+ new_price_per_sqm = st.number_input(
+ "السعر لكل متر مربع (ريال)",
+ min_value=0,
+ step=50,
+ key="new_competitor_price"
+ )
+
+ with col2:
+ new_delivery_time = st.number_input(
+ "مدة التسليم (شهر)",
+ min_value=1,
+ max_value=36,
+ step=1,
+ key="new_competitor_delivery"
+ )
+ new_quality_rating = st.slider(
+ "تقييم الجودة",
+ min_value=1.0,
+ max_value=5.0,
+ value=3.5,
+ step=0.1,
+ key="new_competitor_quality"
+ )
+ new_market_share = st.number_input(
+ "الحصة السوقية (%)",
+ min_value=0.0,
+ max_value=100.0,
+ step=0.5,
+ key="new_competitor_market_share"
+ )
+
+ if st.button("إضافة منافس", key="add_competitor"):
+ if new_competitor and new_price_per_sqm > 0:
+ # إضافة منافس جديد
+ new_id = max([item['id'] for item in st.session_state.competitive_analysis]) + 1
+
+ st.session_state.competitive_analysis.append({
+ 'id': new_id,
+ 'competitor': new_competitor,
+ 'project_type': new_project_type,
+ 'price_per_sqm': new_price_per_sqm,
+ 'delivery_time': new_delivery_time,
+ 'quality_rating': new_quality_rating,
+ 'market_share': new_market_share
+ })
+
+ st.success(f"تمت إضافة المنافس بنجاح: {new_competitor}")
+
+ # تحديث الصفحة لعرض المنافس الجديد
+ st.rerun()
+ else:
+ st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
+
+ # تحليل مقارنة الأسعار
+ st.markdown("### مقارنة الأسعار")
+
+ # إنشاء DataFrame للرسم البياني
+ price_comparison_df = pd.DataFrame(st.session_state.competitive_analysis)
+
+ # ترتيب البيانات تصاعديًا حسب السعر
+ price_comparison_df = price_comparison_df.sort_values('price_per_sqm')
+
+ # إنشاء رسم بياني شريطي
+ fig = px.bar(
+ price_comparison_df,
+ x='competitor',
+ y='price_per_sqm',
+ title='مقارنة الأسعار لكل متر مربع',
+ color='competitor',
+ text_auto=True
+ )
+
+ # تمييز شركتنا
+ for i, competitor in enumerate(price_comparison_df['competitor']):
+ if competitor == 'شركتنا':
+ fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(price_comparison_df))]
+ break
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل مقارنة الجودة والسعر
+ st.markdown("### مقارنة الجودة والسعر")
+
+ # إنشاء رسم بياني للعلاقة بين السعر والجودة
+ fig = px.scatter(
+ price_comparison_df,
+ x='price_per_sqm',
+ y='quality_rating',
+ size='market_share',
+ color='competitor',
+ title='العلاقة بين السعر والجودة والحصة السوقية',
+ labels={
+ 'price_per_sqm': 'السعر لكل متر مربع (ريال)',
+ 'quality_rating': 'تقييم الجودة',
+ 'market_share': 'الحصة السوقية (%)'
+ },
+ text='competitor'
+ )
+
+ fig.update_traces(textposition='top center')
+
+ # إضافة خط اتجاه
+ fig.update_layout(
+ shapes=[
+ dict(
+ type='line',
+ x0=min(price_comparison_df['price_per_sqm']),
+ y0=min(price_comparison_df['quality_rating']),
+ x1=max(price_comparison_df['price_per_sqm']),
+ y1=max(price_comparison_df['quality_rating']),
+ line=dict(color='gray', dash='dash')
+ )
+ ]
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل مقارنة مدة التسليم
+ st.markdown("### مقارنة مدة التسليم")
+
+ # ترتيب البيانات تصاعديًا حسب مدة التسليم
+ delivery_comparison_df = price_comparison_df.sort_values('delivery_time')
+
+ # إنشاء رسم بياني شريطي
+ fig = px.bar(
+ delivery_comparison_df,
+ x='competitor',
+ y='delivery_time',
+ title='مقارنة مدة التسليم (شهر)',
+ color='competitor',
+ text_auto=True
+ )
+
+ # تمييز شركتنا
+ for i, competitor in enumerate(delivery_comparison_df['competitor']):
+ if competitor == 'شركتنا':
+ fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(delivery_comparison_df))]
+ break
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل الحصة السوقية
+ st.markdown("### تحليل الحصة السوقية")
+
+ # إنشاء رسم بياني دائري للحصة السوقية
+ fig = px.pie(
+ price_comparison_df,
+ values='market_share',
+ names='competitor',
+ title='توزيع الحصة السوقية',
+ hole=0.4
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _render_reports_tab(self):
+ """عرض تبويب التقارير"""
+
+ st.markdown("### تقارير التسعير")
+
+ # اختيار نوع التقرير
+ report_type = st.selectbox(
+ "اختر نوع التقرير",
+ [
+ "ملخص التسعير",
+ "تقرير جدول الكميات",
+ "تقرير تحليل التكاليف",
+ "تقرير سيناريوهات التسعير",
+ "تقرير المقارنة التنافسية",
+ "التقرير الشامل"
+ ]
+ )
+
+ # عرض معلومات المشروع
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_name = st.text_input("اسم المشروع", "مشروع إنشاء مبنى سكني")
+ client_name = st.text_input("اسم العميل", "شركة التطوير العقاري")
+
+ with col2:
+ project_location = st.text_input("موقع المشروع", "الرياض، المملكة العربية السعودية")
+ report_date = st.date_input("تاريخ التقرير", datetime.now())
+
+ # إنشاء التقرير
+ if st.button("إنشاء التقرير"):
+ st.markdown("### معاينة التقرير")
+
+ # عرض ترويسة التقرير
+ st.markdown(f"""
+ ## {report_type}
+ **اسم المشروع:** {project_name}
+ **اسم العميل:** {client_name}
+ **موقع المشروع:** {project_location}
+ **تاريخ التقرير:** {report_date.strftime('%Y-%m-%d')}
+ """)
+
+ # عرض محتوى التقرير حسب النوع المختار
+ if report_type == "ملخص التسعير" or report_type == "التقرير الشامل":
+ self._render_pricing_summary_report()
+
+ if report_type == "تقرير جدول الكميات" or report_type == "التقرير الشامل":
+ self._render_boq_report()
+
+ if report_type == "تقرير تحليل التكاليف" or report_type == "التقرير الشامل":
+ self._render_cost_analysis_report()
+
+ if report_type == "تقرير سيناريوهات التسعير" or report_type == "التقرير الشامل":
+ self._render_pricing_scenarios_report()
+
+ if report_type == "تقرير المقارنة التنافسية" or report_type == "التقرير الشامل":
+ self._render_competitive_analysis_report()
+
+ # خيارات تصدير التقرير
+ st.markdown("### تصدير التقرير")
+
+ export_format = st.radio(
+ "اختر صيغة التصدير",
+ ["PDF", "Excel", "Word"],
+ horizontal=True
+ )
+
+ if st.button("تصدير التقرير"):
+ st.success(f"تم تصدير التقرير بصيغة {export_format} بنجاح!")
+
+ def _render_pricing_summary_report(self):
+ """عرض تقرير ملخص التسعير"""
+
+ st.markdown("## ملخص التسعير")
+
+ # حساب إجمالي التكاليف
+ total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
+ total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
+ total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
+ total_cost = total_direct_cost + total_indirect_cost
+ total_price = total_cost + total_profit
+
+ # عرض ملخص التكاليف
+ st.markdown("### ملخص التكاليف")
+
+ summary_data = {
+ 'البند': ['التكاليف المباشرة', 'التكاليف غير المباشرة', 'إجمالي التكاليف', 'هامش الربح', 'السعر الإجمالي'],
+ 'المبلغ (ريال)': [total_direct_cost, total_indirect_cost, total_cost, total_profit, total_price],
+ 'النسبة المئوية': [
+ total_direct_cost / total_price * 100,
+ total_indirect_cost / total_price * 100,
+ total_cost / total_price * 100,
+ total_profit / total_price * 100,
+ 100.0
+ ]
+ }
+
+ summary_df = pd.DataFrame(summary_data)
+
+ st.dataframe(
+ summary_df,
+ column_config={
+ 'البند': st.column_config.TextColumn('البند'),
+ 'المبلغ (ريال)': st.column_config.NumberColumn('المبلغ (ريال)', format='%d'),
+ 'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # عرض رسم بياني للملخص
+ fig = px.pie(
+ summary_df.iloc[0:3], # استخدام أول 3 صفوف فقط (التكاليف)
+ values='المبلغ (ريال)',
+ names='البند',
+ title='توزيع التكاليف',
+ color_discrete_sequence=px.colors.qualitative.Set3
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض رسم بياني للسعر الإجمالي
+ fig = px.pie(
+ summary_df.iloc[[2, 3]], # استخدام صفوف التكاليف والربح
+ values='المبلغ (ريال)',
+ names='البند',
+ title='تركيبة السعر الإجمالي',
+ color_discrete_sequence=px.colors.qualitative.Pastel
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _render_boq_report(self):
+ """عرض تقرير جدول الكميات"""
+
+ st.markdown("## تقرير جدول الكميات")
+
+ # عرض جدول الكميات
+ boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
+
+ st.dataframe(
+ boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']],
+ column_config={
+ 'code': 'الكود',
+ 'description': 'الوصف',
+ 'unit': 'الوحدة',
+ 'quantity': 'الكمية',
+ 'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
+ 'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
+ 'category': 'الفئة'
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # عرض ملخص جدول الكميات حسب الفئة
+ st.markdown("### ملخص جدول الكميات حسب الفئة")
+
+ # تجميع البيانات حسب الفئة
+ category_totals = {}
+ for item in st.session_state.bill_of_quantities:
+ category = item['category']
+ if category in category_totals:
+ category_totals[category] += item['total_price']
+ else:
+ category_totals[category] = item['total_price']
+
+ # إنشاء DataFrame للملخص
+ category_summary_df = pd.DataFrame({
+ 'الفئة': list(category_totals.keys()),
+ 'المبلغ الإجمالي': list(category_totals.values())
+ })
+
+ # حساب النسبة المئوية
+ total_boq = sum(category_totals.values())
+ category_summary_df['النسبة المئوية'] = category_summary_df['المبلغ الإجمالي'] / total_boq * 100
+
+ # ترتيب البيانات تنازليًا حسب المبلغ
+ category_summary_df = category_summary_df.sort_values('المبلغ الإجمالي', ascending=False)
+
+ st.dataframe(
+ category_summary_df,
+ column_config={
+ 'الفئة': st.column_config.TextColumn('الفئة'),
+ 'المبلغ الإجمالي': st.column_config.NumberColumn('المبلغ الإجمالي', format='%d ريال'),
+ 'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # عرض رسم بياني للملخص
+ fig = px.pie(
+ category_summary_df,
+ values='المبلغ الإجمالي',
+ names='الفئة',
+ title='توزيع تكاليف البنود حسب الفئة',
+ color_discrete_sequence=px.colors.qualitative.Set3
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض رسم بياني شريطي للملخص
+ fig = px.bar(
+ category_summary_df,
+ x='الفئة',
+ y='المبلغ الإجمالي',
+ title='إجمالي تكلفة البنود حسب الفئة',
+ color='الفئة',
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _render_cost_analysis_report(self):
+ """عرض تقرير تحليل التكاليف"""
+
+ st.markdown("## تقرير تحليل التكاليف")
+
+ # عرض جدول تحليل التكاليف
+ cost_df = pd.DataFrame(st.session_state.cost_analysis)
+
+ st.dataframe(
+ cost_df[['category', 'subcategory', 'description', 'amount', 'percentage']],
+ column_config={
+ 'category': 'الفئة',
+ 'subcategory': 'الفئة الفرعية',
+ 'description': 'الوصف',
+ 'amount': st.column_config.NumberColumn('المبلغ', format='%d ريال'),
+ 'percentage': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # عرض ملخص تحليل التكاليف حسب الفئة
+ st.markdown("### ملخص تحليل التكاليف حسب الفئة")
+
+ # تجميع البيانات حسب الفئة
+ category_totals = {}
+ for item in st.session_state.cost_analysis:
+ category = item['category']
+ if category in category_totals:
+ category_totals[category] += item['amount']
+ else:
+ category_totals[category] = item['amount']
+
+ # إنشاء DataFrame للملخص
+ category_summary_df = pd.DataFrame({
+ 'الفئة': list(category_totals.keys()),
+ 'المبلغ الإجمالي': list(category_totals.values())
+ })
+
+ # حساب النسبة المئوية
+ total_cost = sum(category_totals.values())
+ category_summary_df['النسبة المئوية'] = category_summary_df['المبلغ الإجمالي'] / total_cost * 100
+
+ # ترتيب البيانات تنازليًا حسب المبلغ
+ category_summary_df = category_summary_df.sort_values('المبلغ الإجمالي', ascending=False)
+
+ st.dataframe(
+ category_summary_df,
+ column_config={
+ 'الفئة': st.column_config.TextColumn('الفئة'),
+ 'المبلغ الإجمالي': st.column_config.NumberColumn('المبلغ الإجمالي', format='%d ريال'),
+ 'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # عرض رسم بياني للملخص
+ fig = px.pie(
+ category_summary_df,
+ values='المبلغ الإجمالي',
+ names='الفئة',
+ title='توزيع التكاليف حسب الفئة',
+ color_discrete_sequence=px.colors.qualitative.Set3
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض ملخص تحليل التكاليف حسب الفئة والفئة الفرعية
+ st.markdown("### ملخص تحليل التكاليف حسب الفئة والفئة الفرعية")
+
+ # تجميع البيانات حسب الفئة والفئة الفرعية
+ subcategory_totals = {}
+ for item in st.session_state.cost_analysis:
+ category = item['category']
+ subcategory = item['subcategory']
+ key = f"{category} - {subcategory}"
+ if key in subcategory_totals:
+ subcategory_totals[key] += item['amount']
+ else:
+ subcategory_totals[key] = item['amount']
+
+ # إنشاء DataFrame للملخص
+ subcategory_summary_df = pd.DataFrame({
+ 'الفئة والفئة الفرعية': list(subcategory_totals.keys()),
+ 'المبلغ الإجمالي': list(subcategory_totals.values())
+ })
+
+ # حساب النسبة المئوية
+ subcategory_summary_df['النسبة المئوية'] = subcategory_summary_df['المبلغ الإجمالي'] / total_cost * 100
+
+ # ترتيب البيانات تنازليًا حسب المبلغ
+ subcategory_summary_df = subcategory_summary_df.sort_values('المبلغ الإجمالي', ascending=False)
+
+ st.dataframe(
+ subcategory_summary_df,
+ column_config={
+ 'الفئة والفئة الفرعية': st.column_config.TextColumn('الفئة والفئة الفرعية'),
+ 'المبلغ الإجمالي': st.column_config.NumberColumn('المبلغ الإجمالي', format='%d ريال'),
+ 'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # عرض رسم بياني للملخص
+ fig = px.bar(
+ subcategory_summary_df,
+ x='الفئة والفئة الفرعية',
+ y='المبلغ الإجمالي',
+ title='توزيع التكاليف حسب الفئة والفئة الفرعية',
+ color='الفئة والفئة الفرعية',
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض تحليل هيكل التكاليف
+ st.markdown("### تحليل هيكل التكاليف")
+
+ # إنشاء رسم بياني للنسب المئوية
+ fig = px.treemap(
+ cost_df,
+ path=['category', 'subcategory', 'description'],
+ values='amount',
+ title='تحليل هيكل التكاليف',
+ color='percentage',
+ color_continuous_scale='RdBu',
+ color_continuous_midpoint=np.average(cost_df['percentage'])
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _render_pricing_scenarios_report(self):
+ """عرض تقرير سيناريوهات التسعير"""
+
+ st.markdown("## تقرير سيناريوهات التسعير")
+
+ # عرض جدول سيناريوهات التسعير
+ scenarios_df = pd.DataFrame(st.session_state.price_scenarios)
+
+ st.dataframe(
+ scenarios_df[['name', 'description', 'total_cost', 'profit_margin', 'total_price', 'is_active']],
+ column_config={
+ 'name': 'اسم السيناريو',
+ 'description': 'الوصف',
+ 'total_cost': st.column_config.NumberColumn('إجمالي التكلفة', format='%d ريال'),
+ 'profit_margin': st.column_config.NumberColumn('هامش الربح', format='%.1f%%'),
+ 'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
+ 'is_active': st.column_config.CheckboxColumn('نشط')
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # عرض مقارنة السيناريوهات
+ st.markdown("### مقارنة السيناريوهات")
+
+ # إنشاء رسم بياني للمقارنة
+ fig = go.Figure()
+
+ for scenario in st.session_state.price_scenarios:
+ fig.add_trace(go.Bar(
+ name=scenario['name'],
+ x=['التكلفة', 'الربح', 'السعر الإجمالي'],
+ y=[
+ scenario['total_cost'],
+ scenario['total_price'] - scenario['total_cost'],
+ scenario['total_price']
+ ],
+ text=[
+ f"{scenario['total_cost']:,.0f}",
+ f"{scenario['total_price'] - scenario['total_cost']:,.0f}",
+ f"{scenario['total_price']:,.0f}"
+ ],
+ textposition='auto'
+ ))
+
+ fig.update_layout(
+ title='مقارنة السيناريوهات',
+ barmode='group',
+ xaxis_title='العنصر',
+ yaxis_title='المبلغ (ريال)'
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض مقارنة هوامش الربح
+ st.markdown("### مقارنة هوامش الربح")
+
+ # إنشاء DataFrame للمقارنة
+ profit_comparison_df = pd.DataFrame({
+ 'السيناريو': [scenario['name'] for scenario in st.session_state.price_scenarios],
+ 'هامش الربح (%)': [scenario['profit_margin'] for scenario in st.session_state.price_scenarios],
+ 'مبلغ الربح (ريال)': [scenario['total_price'] - scenario['total_cost'] for scenario in st.session_state.price_scenarios]
+ })
+
+ # ترتيب البيانات تنازليًا حسب هامش الربح
+ profit_comparison_df = profit_comparison_df.sort_values('هامش الربح (%)', ascending=False)
+
+ # إنشاء رسم بياني شريطي
+ fig = px.bar(
+ profit_comparison_df,
+ x='السيناريو',
+ y='هامش الربح (%)',
+ title='مقارنة هوامش الربح',
+ color='السيناريو',
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض تحليل حساسية هامش الربح
+ st.markdown("### تحليل حساسية هامش الربح")
+
+ # الحصول على التكلفة الإجمالية من السيناريو النشط أو الأول
+ active_scenario = next((s for s in st.session_state.price_scenarios if s['is_active']), st.session_state.price_scenarios[0])
+ total_cost = active_scenario['total_cost']
+
+ # إنشاء بيانات لتحليل الحساسية
+ profit_margins = list(range(5, 26, 1)) # من 5% إلى 25%
+ total_prices = [total_cost * (1 + margin/100) for margin in profit_margins]
+
+ # إنشاء DataFrame للرسم البياني
+ sensitivity_df = pd.DataFrame({
+ 'هامش الربح (%)': profit_margins,
+ 'السعر الإجمالي': total_prices
+ })
+
+ # إنشاء رسم بياني خطي
+ fig = px.line(
+ sensitivity_df,
+ x='هامش الربح (%)',
+ y='السعر الإجمالي',
+ title='تحليل حساسية هامش الربح',
+ markers=True
+ )
+
+ # إضافة خط أفقي يمثل السعر التنافسي (افتراضي)
+ competitive_price = 650000
+ fig.add_hline(
+ y=competitive_price,
+ line_dash="dash",
+ line_color="red",
+ annotation_text="السعر التنافسي",
+ annotation_position="bottom right"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _render_competitive_analysis_report(self):
+ """عرض تقرير المقارنة التنافسية"""
+
+ st.markdown("## تقرير المقارنة التنافسية")
+
+ # عرض جدول المقارنة التنافسية
+ competitive_df = pd.DataFrame(st.session_state.competitive_analysis)
+
+ st.dataframe(
+ competitive_df[['competitor', 'project_type', 'price_per_sqm', 'delivery_time', 'quality_rating', 'market_share']],
+ column_config={
+ 'competitor': 'المنافس',
+ 'project_type': 'نوع المشروع',
+ 'price_per_sqm': st.column_config.NumberColumn('السعر لكل متر مربع', format='%d ريال'),
+ 'delivery_time': st.column_config.NumberColumn('مدة التسليم (شهر)', format='%d'),
+ 'quality_rating': st.column_config.NumberColumn('تقييم الجودة', format='%.1f/5.0'),
+ 'market_share': st.column_config.NumberColumn('الحصة السوقية', format='%.1f%%')
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # عرض مقارنة الأسعار
+ st.markdown("### مقارنة الأسعار")
+
+ # ترتيب البيانات تصاعديًا حسب السعر
+ price_comparison_df = competitive_df.sort_values('price_per_sqm')
+
+ # إنشاء رسم بياني شريطي
+ fig = px.bar(
+ price_comparison_df,
+ x='competitor',
+ y='price_per_sqm',
+ title='مقارنة الأسعار لكل متر مربع',
+ color='competitor',
+ text_auto=True
+ )
+
+ # تمييز شركتنا
+ for i, competitor in enumerate(price_comparison_df['competitor']):
+ if competitor == 'شركتنا':
+ fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(price_comparison_df))]
+ break
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض مقارنة الجودة والسعر
+ st.markdown("### مقارنة الجودة والسعر")
+
+ # إنشاء رسم بياني للعلاقة بين السعر والجودة
+ fig = px.scatter(
+ price_comparison_df,
+ x='price_per_sqm',
+ y='quality_rating',
+ size='market_share',
+ color='competitor',
+ title='العلاقة بين السعر والجودة والحصة السوقية',
+ labels={
+ 'price_per_sqm': 'السعر لكل متر مربع (ريال)',
+ 'quality_rating': 'تقييم الجودة',
+ 'market_share': 'الحصة السوقية (%)'
+ },
+ text='competitor'
+ )
+
+ fig.update_traces(textposition='top center')
+
+ # إضافة خط اتجاه
+ fig.update_layout(
+ shapes=[
+ dict(
+ type='line',
+ x0=min(price_comparison_df['price_per_sqm']),
+ y0=min(price_comparison_df['quality_rating']),
+ x1=max(price_comparison_df['price_per_sqm']),
+ y1=max(price_comparison_df['quality_rating']),
+ line=dict(color='gray', dash='dash')
+ )
+ ]
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض مقارنة مدة التسليم
+ st.markdown("### مقارنة مدة التسليم")
+
+ # ترتيب البيانات تصاعديًا حسب مدة التسليم
+ delivery_comparison_df = competitive_df.sort_values('delivery_time')
+
+ # إنشاء رسم بياني شريطي
+ fig = px.bar(
+ delivery_comparison_df,
+ x='competitor',
+ y='delivery_time',
+ title='مقارنة مدة التسليم (شهر)',
+ color='competitor',
+ text_auto=True
+ )
+
+ # تمييز شركتنا
+ for i, competitor in enumerate(delivery_comparison_df['competitor']):
+ if competitor == 'شركتنا':
+ fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(delivery_comparison_df))]
+ break
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض تحليل الحصة السوقية
+ st.markdown("### تحليل الحصة السوقية")
+
+ # إنشاء رسم بياني دائري للحصة السوقية
+ fig = px.pie(
+ competitive_df,
+ values='market_share',
+ names='competitor',
+ title='توزيع الحصة السوقية',
+ hole=0.4
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض تحليل الموقع التنافسي
+ st.markdown("### تحليل الموقع التنافسي")
+
+ # إيجاد بيانات شركتنا
+ our_company = next((item for item in st.session_state.competitive_analysis if item['competitor'] == 'شركتنا'), None)
+
+ if our_company:
+ # حساب متوسطات السوق
+ avg_price = competitive_df['price_per_sqm'].mean()
+ avg_delivery = competitive_df['delivery_time'].mean()
+ avg_quality = competitive_df['quality_rating'].mean()
+
+ # إنشاء بيانات المقارنة
+ comparison_data = {
+ 'المؤشر': ['السعر لكل متر مربع', 'مدة التسليم', 'تقييم الجودة'],
+ 'قيمة شركتنا': [our_company['price_per_sqm'], our_company['delivery_time'], our_company['quality_rating']],
+ 'متوسط السوق': [avg_price, avg_delivery, avg_quality],
+ 'الفرق (%)': [
+ (our_company['price_per_sqm'] - avg_price) / avg_price * 100,
+ (our_company['delivery_time'] - avg_delivery) / avg_delivery * 100,
+ (our_company['quality_rating'] - avg_quality) / avg_quality * 100
+ ]
+ }
+
+ comparison_df = pd.DataFrame(comparison_data)
+
+ st.dataframe(
+ comparison_df,
+ column_config={
+ 'المؤشر': st.column_config.TextColumn('المؤشر'),
+ 'قيمة شركتنا': st.column_config.NumberColumn('قيمة شركتنا'),
+ 'متوسط السوق': st.column_config.NumberColumn('متوسط السوق'),
+ 'الفرق (%)': st.column_config.NumberColumn('الفرق (%)', format='%+.1f%%')
+ },
+ hide_index=True,
+ use_container_width=True
+ )
+
+ # إنشاء رسم بياني راداري للموقع التنافسي
+ # تحويل البيانات إلى نسب مئوية للمقارنة
+ max_price = competitive_df['price_per_sqm'].max()
+ min_price = competitive_df['price_per_sqm'].min()
+ price_range = max_price - min_price
+
+ max_delivery = competitive_df['delivery_time'].max()
+ min_delivery = competitive_df['delivery_time'].min()
+ delivery_range = max_delivery - min_delivery
+
+ # ملاحظة: نقوم بعكس مقياس السعر ومدة التسليم لأن القيم الأقل أفضل
+ normalized_price = 100 - ((our_company['price_per_sqm'] - min_price) / price_range * 100) if price_range > 0 else 50
+ normalized_delivery = 100 - ((our_company['delivery_time'] - min_delivery) / delivery_range * 100) if delivery_range > 0 else 50
+ normalized_quality = (our_company['quality_rating'] / 5) * 100
+ normalized_market_share = (our_company['market_share'] / competitive_df['market_share'].max()) * 100
+
+ # إنشاء رسم بياني راداري
+ fig = go.Figure()
+
+ fig.add_trace(go.Scatterpolar(
+ r=[normalized_price, normalized_delivery, normalized_quality, normalized_market_share],
+ theta=['السعر التنافسي', 'سرعة التسليم', 'الجودة', 'الحصة السوقية'],
+ fill='toself',
+ name='شركتنا'
+ ))
+
+ fig.update_layout(
+ polar=dict(
+ radialaxis=dict(
+ visible=True,
+ range=[0, 100]
+ )
+ ),
+ title='تحليل الموقع التنافسي لشركتنا'
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
diff --git a/modules/ai_assistant/ai_app.py b/modules/ai_assistant/ai_app.py
index c6a9a4f01123b0c3020ce5c18138f5582e154f4e..6881311c3e93e73f66de4e59e04b4b3d49e2dbee 100644
--- a/modules/ai_assistant/ai_app.py
+++ b/modules/ai_assistant/ai_app.py
@@ -1,1361 +1,1361 @@
-# -*- coding: utf-8 -*-
-"""
-وحدة مساعد الذكاء الاصطناعي
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import plotly.express as px
-import plotly.graph_objects as go
-import seaborn as sns
-from datetime import datetime
-import time
-import os
-import sys
-import json
-import requests
-from pathlib import Path
-import io
-import base64
-import re
-from PIL import Image
-import PyPDF2
-import docx
-import anthropic
-import tempfile
-
-# إضافة المسار للوصول إلى الوحدات الأخرى
-current_dir = os.path.dirname(os.path.abspath(__file__))
-parent_dir = os.path.dirname(os.path.dirname(current_dir))
-if parent_dir not in sys.path:
- sys.path.append(parent_dir)
-
-class AIAssistantApp:
- """تطبيق مساعد الذكاء الاصطناعي"""
-
- def __init__(self):
- """تهيئة تطبيق مساعد الذكاء الاصطناعي"""
- self.uploaded_files = {}
- self.analysis_results = {}
-
- # تهيئة مفاتيح API لنماذج هجين فيس
- # تحميل المفاتيح من متغيرات البيئة أو من الإعدادات
- if 'ai_api_key' not in st.session_state:
- # محاولة الحصول على المفتاح من متغيرات البيئة أولاً
- ai_key = os.environ.get('AI_API_KEY', '')
- # إذا لم يكن موجودًا، حاول الحصول عليه من أسرار هجين فيس
- if not ai_key and os.path.exists('/home/user/.huggingface/token'):
- with open('/home/user/.huggingface/token', 'r') as f:
- ai_key = f.read().strip()
- # إذا لم يكن موجودًا، استخدم المفتاح المقدم في وحدة المعرفة
- if not ai_key:
- ai_key = ""
- st.session_state.ai_api_key = ai_key
-
- if 'anthropic_api_key' not in st.session_state:
- # محاولة الحصول على المفتاح من متغيرات البيئة أولاً
- anthropic_key = os.environ.get('ANTHROPIC_API_KEY', '')
- # إذا لم يكن موجودًا، حاول الحصول عليه من أسرار هجين فيس
- if not anthropic_key:
- # استخدم نفس المفتاح لـ anthropic مؤقتًا
- anthropic_key = st.session_state.ai_api_key
- st.session_state.anthropic_api_key = anthropic_key
-
- # تهيئة محادثة الذكاء الاصطناعي
- if 'messages' not in st.session_state:
- st.session_state.messages = []
-
- if 'selected_model' not in st.session_state:
- st.session_state.selected_model = "anthropic"
-
- def run(self):
- """تشغيل تطبيق مساعد الذكاء الاصطناعي"""
- # عرض عنوان التطبيق
- st.title("مساعد الذكاء الاصطناعي")
-
- # إنشاء تبويبات التطبيق
- tabs = st.tabs([
- "المحادثة",
- "تحليل المستندات",
- "تحليل العقود",
- "تقدير التكاليف",
- "تحليل المخاطر",
- "الإعدادات"
- ])
-
- # عرض محتوى كل تبويب
- with tabs[0]:
- self._render_chat_tab()
-
- with tabs[1]:
- self._render_document_analysis_tab()
-
- with tabs[2]:
- self._render_contract_analysis_tab()
-
- with tabs[3]:
- self._render_cost_estimation_tab()
-
- with tabs[4]:
- self._render_risk_analysis_tab()
-
- with tabs[5]:
- self._render_settings_tab()
-
- def _render_chat_tab(self):
- """عرض تبويب المحادثة مع الذكاء الاصطناعي"""
- st.markdown("### المحادثة مع الذكاء الاصطناعي")
-
- # اختيار نموذج الذكاء الاصطناعي
- selected_model = st.selectbox(
- "اختر نموذج الذكاء الاصطناعي:",
- ["anthropic", "ai"],
- index=0 if st.session_state.selected_model == "anthropic" else 1
- )
-
- # تحديث النموذج المختار إذا تغير
- if selected_model != st.session_state.selected_model:
- st.session_state.selected_model = selected_model
- st.rerun() # تم تعديل هذا من st.experimental_rerun()
-
- # التحقق من وجود مفتاح API للنموذج المختار
- api_key_available = False
- if selected_model == "anthropic" and st.session_state.anthropic_api_key:
- api_key_available = True
- elif selected_model == "ai" and st.session_state.ai_api_key:
- api_key_available = True
-
- # عرض رسائل المحادثة
- for message in st.session_state.messages:
- with st.chat_message(message["role"]):
- st.markdown(message["content"])
-
- # إذا لم يكن مفتاح API متاحًا، عرض رسالة خطأ
- if not api_key_available:
- st.error(f"لم يتم تكوين مفتاح API لنموذج {selected_model}. يرجى تكوين المفتاح في الإعدادات.")
- else:
- # مربع إدخال الرسالة
- if prompt := st.chat_input("اكتب رسالتك هنا:"):
- # إضافة رسالة المستخدم إلى المحادثة
- st.session_state.messages.append({"role": "user", "content": prompt})
-
- # عرض رسالة المستخدم
- with st.chat_message("user"):
- st.markdown(prompt)
-
- # عرض مؤشر التحميل أثناء معالجة الرسالة
- with st.chat_message("assistant"):
- message_placeholder = st.empty()
- message_placeholder.markdown("جاري التفكير...")
-
- try:
- # الحصول على رد من النموذج المختار
- if selected_model == "anthropic":
- response = self._get_anthropic_response(prompt)
- else:
- response = self._get_ai_response(prompt)
-
- # عرض الرد
- message_placeholder.markdown(response)
-
- # إضافة رد المساعد إلى المحادثة
- st.session_state.messages.append({"role": "assistant", "content": response})
- except Exception as e:
- message_placeholder.markdown(f"حدث خطأ: {str(e)}")
-
- # زر لمسح المحادثة
- if st.button("مسح المحادثة"):
- st.session_state.messages = []
- st.rerun() # تم تعديل هذا من st.experimental_rerun()
-
- def _render_document_analysis_tab(self):
- """عرض تبويب تحليل المستندات"""
- st.markdown("### تحليل المستندات")
-
- st.markdown("""
- يمكنك استخدام هذه الأداة لتحليل المستندات والتقارير باستخدام الذكاء الاصطناعي.
- الأداة تدعم تحليل الملفات التالية:
- - ملفات PDF
- - ملفات Word
- - ملفات النصوص TXT
- """)
-
- # إنشاء تبويبات فرعية
- doc_tabs = st.tabs([
- "تحميل المستندات",
- "استخراج النص",
- "تحليل المحتوى",
- "الملخص والتوصيات"
- ])
-
- # تبويب تحميل المستندات
- with doc_tabs[0]:
- st.markdown("#### تحميل المستندات")
-
- uploaded_file = st.file_uploader(
- "اختر ملفًا للتحليل (PDF, DOCX, TXT)",
- type=["pdf", "docx", "txt"],
- key="document_file_uploader"
- )
-
- if uploaded_file is not None:
- # حفظ الملف المرفوع
- file_details = {
- "filename": uploaded_file.name,
- "filetype": uploaded_file.type,
- "filesize": uploaded_file.size
- }
-
- st.write("### تفاصيل الملف")
- st.write(f"اسم الملف: {file_details['filename']}")
- st.write(f"نوع الملف: {file_details['filetype']}")
- st.write(f"حجم الملف: {file_details['filesize']} بايت")
-
- # حفظ الملف في الذاكرة المؤقتة
- self.uploaded_files["document"] = uploaded_file
-
- st.success(f"تم تحميل الملف {uploaded_file.name} بنجاح!")
-
- # تبويب استخراج النص
- with doc_tabs[1]:
- st.markdown("#### استخراج النص")
-
- if "document" not in self.uploaded_files:
- st.info("الرجاء تحميل مستند أولاً من تبويب 'تحميل المستندات'")
- else:
- if st.button("استخراج النص من المستند"):
- with st.spinner("جاري استخراج النص..."):
- # استخراج النص من الملف
- extracted_text = self._extract_text_from_file(self.uploaded_files["document"])
-
- # حفظ النص المستخرج
- self.analysis_results["extracted_text"] = extracted_text
-
- # عرض النص المستخرج
- st.markdown("### النص المستخرج")
- st.text_area("النص:", value=extracted_text, height=400, disabled=True)
-
- st.success("تم استخراج النص بنجاح!")
-
- # إذا كان النص قد تم استخراجه بالفعل، عرضه
- if "extracted_text" in self.analysis_results:
- st.markdown("### النص المستخرج")
- st.text_area("النص:", value=self.analysis_results["extracted_text"], height=400, disabled=True)
-
- # تبويب تحليل المحتوى
- with doc_tabs[2]:
- st.markdown("#### تحليل المحتوى")
-
- if "extracted_text" not in self.analysis_results:
- st.info("الرجاء استخراج النص أولاً من تبويب 'استخراج النص'")
- else:
- if st.button("تحليل المحتوى"):
- with st.spinner("جاري تحليل المحتوى..."):
- # تحليل المحتوى باستخدام الذكاء الاصطناعي
- analysis_prompt = f"""
- قم بتحليل النص التالي وتقديم:
- 1. الموضوعات الرئيسية
- 2. الكلمات المفتاحية
- 3. الأفكار الرئيسية
- 4. أي معلومات مهمة أخرى
-
- النص:
- {self.analysis_results["extracted_text"][:4000]}
- """
-
- # الحصول على التحليل من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- analysis_result = self._get_anthropic_response(analysis_prompt)
- else:
- analysis_result = self._get_ai_response(analysis_prompt)
-
- # حفظ نتيجة التحليل
- self.analysis_results["content_analysis"] = analysis_result
-
- # عرض نتيجة التحليل
- st.markdown("### نتيجة التحليل")
- st.markdown(analysis_result)
-
- st.success("تم تحليل المحتوى بنجاح!")
-
- # إذا كان التحليل قد تم بالفعل، عرضه
- if "content_analysis" in self.analysis_results:
- st.markdown("### نتيجة التحليل")
- st.markdown(self.analysis_results["content_analysis"])
-
- # تبويب الملخص والتوصيات
- with doc_tabs[3]:
- st.markdown("#### الملخص والتوصيات")
-
- if "content_analysis" not in self.analysis_results:
- st.info("الرجاء تحليل المحتوى أولاً من تبويب 'تحليل المحتوى'")
- else:
- if st.button("إنشاء ملخص وتوصيات"):
- with st.spinner("جاري إنشاء الملخص والتوصيات..."):
- # إنشاء ملخص وتوصيات باستخدام الذكاء الاصطناعي
- summary_prompt = f"""
- بناءً على التحليل التالي، قم بإنشاء:
- 1. ملخص موجز (لا يزيد عن 300 كلمة)
- 2. توصيات عملية (3-5 توصيات)
-
- التحليل:
- {self.analysis_results["content_analysis"]}
- """
-
- # الحصول على الملخص والتوصيات من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- summary_result = self._get_anthropic_response(summary_prompt)
- else:
- summary_result = self._get_ai_response(summary_prompt)
-
- # حفظ الملخص والتوصيات
- self.analysis_results["summary_recommendations"] = summary_result
-
- # عرض الملخص والتوصيات
- st.markdown("### الملخص والتوصيات")
- st.markdown(summary_result)
-
- st.success("تم إنشاء الملخص والتوصيات بنجاح!")
-
- # إذا كان الملخص والتوصيات قد تم إنشاؤهما بالفعل، عرضهما
- if "summary_recommendations" in self.analysis_results:
- st.markdown("### الملخص والتوصيات")
- st.markdown(self.analysis_results["summary_recommendations"])
-
- # زر لتصدير التقرير
- report_content = f"""
- # تقرير تحليل المستند
-
- ## معلومات الملف
- - اسم الملف: {self.uploaded_files["document"].name}
- - نوع الملف: {self.uploaded_files["document"].type}
- - حجم الملف: {self.uploaded_files["document"].size} بايت
-
- ## تحليل المحتوى
- {self.analysis_results["content_analysis"]}
-
- ## الملخص والتوصيات
- {self.analysis_results["summary_recommendations"]}
-
- ## تم إنشاء هذا التقرير بواسطة مساعد الذكاء الاصطناعي
- تاريخ الإنشاء: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
- """
-
- st.download_button(
- label="تصدير التقرير (PDF)",
- data=report_content.encode('utf-8'),
- file_name=f"تقرير_تحليل_المستند_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf",
- mime="application/pdf",
- key="export_document_report_pdf"
- )
-
- def _render_contract_analysis_tab(self):
- """عرض تبويب تحليل العقود"""
- st.markdown("### تحليل العقود")
-
- st.markdown("""
- يمكنك استخدام هذه الأداة لتحليل العقود واستخراج البنود المهمة باستخدام الذكاء الاصطناعي.
- الأداة تدعم تحليل الملفات التالية:
- - ملفات PDF
- - ملفات Word
- - ملفات النصوص TXT
- """)
-
- # إنشاء تبويبات فرعية
- contract_tabs = st.tabs([
- "تحميل العقد",
- "استخراج البنود",
- "تحليل المخاطر",
- "التقرير النهائي"
- ])
-
- # تبويب تحميل العقد
- with contract_tabs[0]:
- st.markdown("#### تحميل العقد")
-
- uploaded_file = st.file_uploader(
- "اختر ملف العقد للتحليل (PDF, DOCX, TXT)",
- type=["pdf", "docx", "txt"],
- key="contract_file_uploader"
- )
-
- if uploaded_file is not None:
- # حفظ الملف المرفوع
- file_details = {
- "filename": uploaded_file.name,
- "filetype": uploaded_file.type,
- "filesize": uploaded_file.size
- }
-
- st.write("### تفاصيل الملف")
- st.write(f"اسم الملف: {file_details['filename']}")
- st.write(f"نوع الملف: {file_details['filetype']}")
- st.write(f"حجم الملف: {file_details['filesize']} بايت")
-
- # حفظ الملف في الذاكرة المؤقتة
- self.uploaded_files["contract"] = uploaded_file
-
- st.success(f"تم تحميل الملف {uploaded_file.name} بنجاح!")
-
- # تبويب استخراج البنود
- with contract_tabs[1]:
- st.markdown("#### استخراج البنود")
-
- if "contract" not in self.uploaded_files:
- st.info("الرجاء تحميل ملف العقد أولاً من تبويب 'تحميل العقد'")
- else:
- if st.button("استخراج البنود من العقد"):
- with st.spinner("جاري استخراج البنود..."):
- # استخراج النص من الملف
- extracted_text = self._extract_text_from_file(self.uploaded_files["contract"])
-
- # حفظ النص المستخرج
- self.analysis_results["contract_text"] = extracted_text
-
- # استخراج البنود باستخدام الذكاء الاصطناعي
- clauses_prompt = f"""
- قم بتحليل نص العقد التالي واستخراج البنود المهمة التالية:
- 1. الأطراف المتعاقدة
- 2. موضوع العقد
- 3. مدة العقد
- 4. قيمة العقد
- 5. شروط الدفع
- 6. الالتزامات والمسؤوليات
- 7. شروط الإنهاء
- 8. تسوية النزاعات
- 9. القانون الحاكم
- 10. أي بنود أخرى مهمة
-
- نص العقد:
- {extracted_text[:4000]}
- """
-
- # الحصول على البنود من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- clauses_result = self._get_anthropic_response(clauses_prompt)
- else:
- clauses_result = self._get_ai_response(clauses_prompt)
-
- # حفظ البنود المستخرجة
- self.analysis_results["contract_clauses"] = clauses_result
-
- # عرض البنود المستخرجة
- st.markdown("### البنود المستخرجة")
- st.markdown(clauses_result)
-
- st.success("تم استخراج البنود بنجاح!")
-
- # إذا كانت البنود قد تم استخراجها بالفعل، عرضها
- if "contract_clauses" in self.analysis_results:
- st.markdown("### البنود المستخرجة")
- st.markdown(self.analysis_results["contract_clauses"])
-
- # تبويب تحليل المخاطر
- with contract_tabs[2]:
- st.markdown("#### تحليل المخاطر")
-
- if "contract_clauses" not in self.analysis_results:
- st.info("الرجاء استخراج البنود أولاً من تبويب 'استخراج البنود'")
- else:
- if st.button("تحليل المخاطر في العقد"):
- with st.spinner("جاري تحليل المخاطر..."):
- # تحليل المخاطر باستخدام الذكاء الاصطناعي
- risks_prompt = f"""
- بناءً على البنود المستخرجة من العقد، قم بتحليل المخاطر المحتملة:
- 1. المخاطر القانونية
- 2. المخاطر المالية
- 3. مخاطر التنفيذ
- 4. مخاطر الجدول الزمني
- 5. أي مخاطر أخرى
-
- لكل مخاطرة، قدم:
- - وصف المخاطرة
- - احتمالية حدوثها (منخفضة، متوسطة، عالية)
- - تأثيرها (منخفض، متوسط، عالي)
- - توصيات للتخفيف من المخاطرة
-
- البنود المستخرجة:
- {self.analysis_results["contract_clauses"]}
- """
-
- # الحصول على تحليل المخاطر من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- risks_result = self._get_anthropic_response(risks_prompt)
- else:
- risks_result = self._get_ai_response(risks_prompt)
-
- # حفظ تحليل المخاطر
- self.analysis_results["contract_risks"] = risks_result
-
- # عرض تحليل المخاطر
- st.markdown("### تحليل المخاطر")
- st.markdown(risks_result)
-
- st.success("تم تحليل المخاطر بنجاح!")
-
- # إذا كان تحليل المخاطر قد تم بالفعل، عرضه
- if "contract_risks" in self.analysis_results:
- st.markdown("### تحليل المخاطر")
- st.markdown(self.analysis_results["contract_risks"])
-
- # تبويب التقرير النهائي
- with contract_tabs[3]:
- st.markdown("#### التقرير النهائي")
-
- if "contract_risks" not in self.analysis_results:
- st.info("الرجاء تحليل المخاطر أولاً من تبويب 'تحليل المخاطر'")
- else:
- if st.button("إنشاء التقرير النهائي"):
- with st.spinner("جاري إنشاء التقرير النهائي..."):
- # إنشاء التقرير النهائي باستخدام الذكاء الاصطناعي
- report_prompt = f"""
- بناءً على البنود المستخرجة وتحليل المخاطر، قم بإنشاء تقرير نهائي يتضمن:
- 1. ملخص تنفيذي
- 2. تحليل البنود الرئيسية
- 3. تحليل المخاطر
- 4. التوصيات
- 5. الخلاصة
-
- البنود المستخرجة:
- {self.analysis_results["contract_clauses"]}
-
- تحليل المخاطر:
- {self.analysis_results["contract_risks"]}
- """
-
- # الحصول على التقرير النهائي من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- report_result = self._get_anthropic_response(report_prompt)
- else:
- report_result = self._get_ai_response(report_prompt)
-
- # حفظ التقرير النهائي
- self.analysis_results["contract_report"] = report_result
-
- # عرض التقرير النهائي
- st.markdown("### التقرير النهائي")
- st.markdown(report_result)
-
- st.success("تم إنشاء التقرير النهائي بنجاح!")
-
- # إذا كان التقرير النهائي قد تم إنشاؤه بالفعل، عرضه
- if "contract_report" in self.analysis_results:
- st.markdown("### التقرير النهائي")
- st.markdown(self.analysis_results["contract_report"])
-
- # زر لتصدير التقرير
- report_content = f"""
- # تقرير تحليل العقد
-
- ## معلومات الملف
- - اسم الملف: {self.uploaded_files["contract"].name}
- - نوع الملف: {self.uploaded_files["contract"].type}
- - حجم الملف: {self.uploaded_files["contract"].size} بايت
-
- ## البنود المستخرجة
- {self.analysis_results["contract_clauses"]}
-
- ## تحليل المخاطر
- {self.analysis_results["contract_risks"]}
-
- ## التقرير النهائي
- {self.analysis_results["contract_report"]}
-
- ## تم إنشاء هذا التقرير بواسطة مساعد الذكاء الاصطناعي
- تاريخ الإنشاء: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
- """
-
- st.download_button(
- label="تصدير التقرير (PDF)",
- data=report_content.encode('utf-8'),
- file_name=f"تقرير_تحليل_العقد_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf",
- mime="application/pdf",
- key="export_contract_report_pdf"
- )
-
- def _render_cost_estimation_tab(self):
- """عرض تبويب تقدير التكاليف باستخدام نماذج هجين فيس"""
-
- st.markdown("### تقدير التكاليف")
-
- st.markdown("""
- يمكنك استخدام هذه الأداة لتقدير تكاليف المشاريع باستخدام نماذج الذكاء الاصطناعي المتقدمة من بيئة هجين فيس.
- الأداة تدعم تحليل الملفات التالية:
- - ملفات PDF (كراسات الشروط، المواصفات الفنية)
- - ملفات DWG (المخططات الهندسية)
- - ملفات Excel (جداول الكميات، التكاليف)
- - ملفات Word (العقود، المستندات)
- - ملفات النصوص TXT
- - ملفات الصور (PNG, JPG) للمخططات والرسومات
- """)
-
- # إنشاء تبويبات فرعية
- cost_tabs = st.tabs([
- "تحميل الملفات",
- "تقدير التكاليف",
- "تحليل البنود",
- "المقارنة مع السوق",
- "التقارير"
- ])
-
- # تبويب تحميل الملفات
- with cost_tabs[0]:
- st.markdown("#### تحميل ملفات المشروع")
-
- uploaded_files = st.file_uploader(
- "اختر ملفات المشروع للتحليل",
- type=["pdf", "dwg", "xlsx", "docx", "txt", "png", "jpg"],
- accept_multiple_files=True,
- key="cost_files_uploader"
- )
-
- if uploaded_files:
- st.write("### الملفات المرفوعة")
-
- for uploaded_file in uploaded_files:
- file_details = {
- "filename": uploaded_file.name,
- "filetype": uploaded_file.type,
- "filesize": uploaded_file.size
- }
-
- # حفظ الملف في الذاكرة المؤقتة
- self.uploaded_files[uploaded_file.name] = uploaded_file
-
- # عرض تفاصيل الملف
- col1, col2, col3, col4 = st.columns([3, 2, 2, 1])
- with col1:
- st.write(f"**{file_details['filename']}**")
- with col2:
- st.write(f"النوع: {self._detect_file_type(file_details['filename'])}")
- with col3:
- st.write(f"الحجم: {file_details['filesize']} بايت")
- with col4:
- if st.button("حذف", key=f"delete_{file_details['filename']}"):
- del self.uploaded_files[file_details['filename']]
- st.rerun() # تم تعديل هذا من st.experimental_rerun()
-
- st.success(f"تم تحميل {len(uploaded_files)} ملف بنجاح!")
-
- # تبويب تقدير التكاليف
- with cost_tabs[1]:
- st.markdown("#### تقدير التكاليف")
-
- if not self.uploaded_files:
- st.info("الرجاء تحميل ملفات المشروع أولاً من تبويب 'تحميل الملفات'")
- else:
- # نموذج إدخال معلومات المشروع
- st.markdown("### معلومات المشروع")
-
- col1, col2 = st.columns(2)
- with col1:
- project_name = st.text_input("اسم المشروع")
- project_location = st.text_input("موقع المشروع")
- project_type = st.selectbox(
- "نوع المشروع",
- ["سكني", "تجاري", "صناعي", "بنية تحتية", "آخر"]
- )
-
- with col2:
- project_area = st.number_input("مساحة المشروع (م²)", min_value=0.0, step=100.0)
- project_duration = st.number_input("مدة المشروع (بالأشهر)", min_value=1, step=1)
- project_quality = st.select_slider(
- "مستوى الجودة",
- options=["اقتصادي", "متوسط", "عالي", "فاخر"]
- )
-
- # زر لبدء تقدير التكاليف
- if st.button("تقدير التكاليف"):
- with st.spinner("جاري تقدير التكاليف..."):
- # إنشاء تقدير التكاليف باستخدام الذكاء الاصطناعي
-
- # تجميع معلومات المشروع
- project_info = f"""
- اسم المشروع: {project_name}
- موقع المشروع: {project_location}
- نوع المشروع: {project_type}
- مساحة المشروع: {project_area} م²
- مدة المشروع: {project_duration} أشهر
- مستوى الجودة: {project_quality}
-
- الملفات المرفوعة:
- {', '.join(self.uploaded_files.keys())}
- """
-
- # إنشاء تقدير التكاليف
- estimation_prompt = f"""
- أنت خبير في تقدير تكاليف مشاريع البناء في المملكة العربية السعودية.
- قم بتقدير تكاليف المشروع التالي بناءً على المعلومات المقدمة:
-
- {project_info}
-
- قدم تقديرًا مفصلاً للتكاليف يتضمن:
- 1. تكلفة المواد الرئيسية (الخرسانة، حديد التسليح، الطوب، إلخ)
- 2. تكلفة العمالة
- 3. تكلفة المعدات
- 4. التكاليف غير المباشرة
- 5. هامش الربح المقترح
- 6. إجمالي التكلفة المقدرة
- 7. تكلفة المتر المربع
-
- استخدم أسعار السوق الحالية في المملكة العربية السعودية لعام 2025.
- """
-
- # الحصول على تقدير التكاليف من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- estimation_result = self._get_anthropic_response(estimation_prompt)
- else:
- estimation_result = self._get_ai_response(estimation_prompt)
-
- # حفظ تقدير التكاليف
- self.analysis_results["cost_estimation"] = estimation_result
-
- # عرض تقدير التكاليف
- st.markdown("### تقدير التكاليف")
- st.markdown(estimation_result)
-
- st.success("تم تقدير التكاليف بنجاح!")
-
- # إذا كان تقدير التكاليف قد تم بالفعل، عرضه
- if "cost_estimation" in self.analysis_results:
- st.markdown("### تقدير التكاليف")
- st.markdown(self.analysis_results["cost_estimation"])
-
- # زر لتصدير التقرير
- st.download_button(
- label="تصدير التقرير (PDF)",
- data="تقرير تقدير التكاليف".encode('utf-8'), # تم تعديل هذا من b"تقرير تقدير التكاليف"
- file_name=f"تقرير_تقدير_التكاليف_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf",
- mime="application/pdf",
- key="export_cost_report_pdf"
- )
-
- st.download_button(
- label="تصدير البيانات (Excel)",
- data="بيانات تقدير التكاليف".encode('utf-8'), # تم تعديل هذا من b"بيانات تقدير التكاليف"
- file_name=f"بيانات_تقدير_التكاليف_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- key="export_cost_data_excel"
- )
-
- # تبويب تحليل البنود
- with cost_tabs[2]:
- st.markdown("#### تحليل البنود")
-
- if "cost_estimation" not in self.analysis_results:
- st.info("الرجاء تقدير التكاليف أولاً من تبويب 'تقدير التكاليف'")
- else:
- # إنشاء تحليل البنود باستخدام الذكاء الاصطناعي
- if st.button("تحليل البنود"):
- with st.spinner("جاري تحليل البنود..."):
- # تحليل البنود
- items_prompt = f"""
- بناءً على تقدير التكاليف التالي، قم بتحليل البنود الرئيسية:
-
- {self.analysis_results["cost_estimation"]}
-
- قدم تحليلاً مفصلاً للبنود يتضمن:
- 1. البنود ذات التكلفة الأعلى
- 2. البنود التي يمكن تقليل تكلفتها
- 3. البنود التي قد تتغير أسعارها بشكل كبير
- 4. توصيات لتحسين التكلفة الإجمالية
-
- قدم البيانات في شكل جدول حيثما أمكن.
- """
-
- # الحصول على تحليل البنود من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- items_result = self._get_anthropic_response(items_prompt)
- else:
- items_result = self._get_ai_response(items_prompt)
-
- # حفظ تحليل البنود
- self.analysis_results["items_analysis"] = items_result
-
- # عرض تحليل البنود
- st.markdown("### تحليل البنود")
- st.markdown(items_result)
-
- st.success("تم تحليل البنود بنجاح!")
-
- # إذا كان تحليل البنود قد تم بالفعل، عرضه
- if "items_analysis" in self.analysis_results:
- st.markdown("### تحليل البنود")
- st.markdown(self.analysis_results["items_analysis"])
-
- # تبويب المقارنة مع السوق
- with cost_tabs[3]:
- st.markdown("#### المقارنة مع السوق")
-
- if "items_analysis" not in self.analysis_results:
- st.info("الرجاء تحليل البنود أولاً من تبويب 'تحليل البنود'")
- else:
- # إنشاء مقارنة مع السوق باستخدام الذكاء الاصطناعي
- if st.button("مقارنة مع السوق"):
- with st.spinner("جاري إجراء المقارنة مع السوق..."):
- # مقارنة مع السوق
- market_prompt = f"""
- بناءً على تقدير التكاليف وتحليل البنود التاليين، قم بإجراء مقارنة مع أسعار السوق الحالية في المملكة العربية السعودية:
-
- تقدير التكاليف:
- {self.analysis_results["cost_estimation"]}
-
- تحليل البنود:
- {self.analysis_results["items_analysis"]}
-
- قدم مقارنة مفصلة تتضمن:
- 1. مقارنة أسعار المواد الرئيسية مع متوسط أسعار السوق
- 2. مقارنة تكلفة المتر المربع مع المشاريع المماثلة
- 3. تحليل الفروقات وأسبابها
- 4. توصيات للحصول على أفضل الأسعار
-
- استخدم أسعار السوق الحالية في المملكة العربية السعودية لعام 2025.
- """
-
- # الحصول على مقارنة مع السوق من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- market_result = self._get_anthropic_response(market_prompt)
- else:
- market_result = self._get_ai_response(market_prompt)
-
- # حفظ مقارنة مع السوق
- self.analysis_results["market_comparison"] = market_result
-
- # عرض مقارنة مع السوق
- st.markdown("### المقارنة مع السوق")
- st.markdown(market_result)
-
- st.success("تمت المقارنة مع السوق بنجاح!")
-
- # إذا كانت المقارنة مع السوق قد تمت بالفعل، عرضها
- if "market_comparison" in self.analysis_results:
- st.markdown("### المقارنة مع السوق")
- st.markdown(self.analysis_results["market_comparison"])
-
- # تبويب التقارير
- with cost_tabs[4]:
- st.markdown("#### التقارير")
-
- if "market_comparison" not in self.analysis_results:
- st.info("الرجاء إجراء المقارنة مع السوق أولاً من تبويب 'المقارنة مع السوق'")
- else:
- # إنشاء التقرير النهائي
- if st.button("إنشاء التقرير النهائي"):
- with st.spinner("جاري إنشاء التقرير النهائي..."):
- # إنشاء التقرير النهائي
- report_prompt = f"""
- بناءً على المعلومات التالية، قم بإنشاء تقرير نهائي شامل لتقدير تكاليف المشروع:
-
- تقدير التكاليف:
- {self.analysis_results["cost_estimation"]}
-
- تحليل البنود:
- {self.analysis_results["items_analysis"]}
-
- المقارنة مع السوق:
- {self.analysis_results["market_comparison"]}
-
- قدم تقريرًا شاملاً يتضمن:
- 1. ملخص تنفيذي
- 2. معلومات المشروع
- 3. منهجية تقدير التكاليف
- 4. تقدير التكاليف المفصل
- 5. تحليل البنود
- 6. المقارنة مع السوق
- 7. التوصيات
- 8. الخلاصة
-
- نسق التقرير بشكل احترافي وقابل للطباعة.
- """
-
- # الحصول على التقرير النهائي من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- report_result = self._get_anthropic_response(report_prompt)
- else:
- report_result = self._get_ai_response(report_prompt)
-
- # حفظ التقرير النهائي
- self.analysis_results["final_report"] = report_result
-
- # عرض التقرير النهائي
- st.markdown("### التقرير النهائي")
- st.markdown(report_result)
-
- st.success("تم إنشاء التقرير النهائي بنجاح!")
-
- # إذا كان التقرير النهائي قد تم إنشاؤه بالفعل، عرضه
- if "final_report" in self.analysis_results:
- st.markdown("### التقرير النهائي")
- st.markdown(self.analysis_results["final_report"])
-
- # اسم ملف التقرير
- report_file_name = f"تقرير_تقدير_تكاليف_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
-
- # أزرار لتنزيل التقرير بصيغ مختلفة
- st.download_button(
- label="تنزيل التقرير (PDF)",
- data="تقرير تقدير التكاليف".encode('utf-8'), # تم تعديل هذا من b"تقرير تقدير التكاليف"
- file_name=f"{report_file_name}.pdf",
- mime="application/pdf",
- key="download_report_pdf"
- )
-
- st.download_button(
- label="تنزيل التقرير (Excel)",
- data="بيانات تقدير التكاليف".encode('utf-8'), # تم تعديل هذا من b"بيانات تقدير التكاليف"
- file_name=f"{report_file_name}.xlsx",
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- key="download_report_excel"
- )
-
- st.download_button(
- label="تنزيل التقرير (Word)",
- data="تقرير تقدير التكاليف".encode('utf-8'), # تم تعديل هذا من b"تقرير تقدير التكاليف"
- file_name=f"{report_file_name}.docx",
- mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- key="download_report_word"
- )
-
- def _render_risk_analysis_tab(self):
- """عرض تبويب تحليل المخاطر"""
- st.markdown("### تحليل المخاطر")
-
- st.markdown("""
- يمكنك استخدام هذه الأداة لتحليل المخاطر المحتملة في المشاريع باستخدام الذكاء الاصطناعي.
- """)
-
- # إنشاء تبويبات فرعية
- risk_tabs = st.tabs([
- "تحديد المخاطر",
- "تقييم المخاطر",
- "خطة الاستجابة",
- "التقرير النهائي"
- ])
-
- # تبويب تحديد المخاطر
- with risk_tabs[0]:
- st.markdown("#### تحديد المخاطر")
-
- # نموذج إدخال معلومات المشروع
- st.markdown("### معلومات المشروع")
-
- col1, col2 = st.columns(2)
- with col1:
- project_name = st.text_input("اسم المشروع", key="risk_project_name")
- project_location = st.text_input("موقع المشروع", key="risk_project_location")
- project_type = st.selectbox(
- "نوع المشروع",
- ["سكني", "تجاري", "صناعي", "بنية تحتية", "آخر"],
- key="risk_project_type"
- )
-
- with col2:
- project_budget = st.number_input("ميزانية المشروع (ريال سعودي)", min_value=0.0, step=100000.0)
- project_duration = st.number_input("مدة المشروع (بالأشهر)", min_value=1, step=1, key="risk_project_duration")
- project_complexity = st.select_slider(
- "مستوى تعقيد المشروع",
- options=["بسيط", "متوسط", "معقد", "معقد جدًا"]
- )
-
- # وصف المشروع
- project_description = st.text_area("وصف المشروع", height=150)
-
- # زر لتحديد المخاطر
- if st.button("تحديد المخاطر"):
- with st.spinner("جاري تحديد المخاطر..."):
- # تجميع معلومات المشروع
- project_info = f"""
- اسم المشروع: {project_name}
- موقع المشروع: {project_location}
- نوع المشروع: {project_type}
- ميزانية المشروع: {project_budget} ريال سعودي
- مدة المشروع: {project_duration} أشهر
- مستوى تعقيد المشروع: {project_complexity}
-
- وصف المشروع:
- {project_description}
- """
-
- # تحديد المخاطر باستخدام الذكاء الاصطناعي
- risks_prompt = f"""
- أنت خبير في إدارة المخاطر في مشاريع البناء في المملكة العربية السعودية.
- قم بتحديد المخاطر المحتملة للمشروع التالي:
-
- {project_info}
-
- حدد المخاطر في الفئات التالية:
- 1. المخاطر الفنية
- 2. المخاطر الإدارية
- 3. المخاطر المالية
- 4. المخاطر التعاقدية
- 5. المخاطر البيئية
- 6. مخاطر الجدول الزمني
- 7. مخاطر الموارد البشرية
- 8. مخاطر الصحة والسلامة
- 9. المخاطر القانونية والتنظيمية
- 10. مخاطر أخرى
-
- لكل فئة، قدم قائمة بالمخاطر المحتملة مع وصف موجز لكل مخاطرة.
- """
-
- # الحصول على تحديد المخاطر من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- risks_result = self._get_anthropic_response(risks_prompt)
- else:
- risks_result = self._get_ai_response(risks_prompt)
-
- # حفظ تحديد المخاطر
- self.analysis_results["identified_risks"] = risks_result
-
- # حفظ معلومات المشروع
- self.analysis_results["risk_project_info"] = project_info
-
- # عرض تحديد المخاطر
- st.markdown("### المخاطر المحددة")
- st.markdown(risks_result)
-
- st.success("تم تحديد المخاطر بنجاح!")
-
- # إذا كان تحديد المخاطر قد تم بالفعل، عرضه
- if "identified_risks" in self.analysis_results:
- st.markdown("### المخاطر المحددة")
- st.markdown(self.analysis_results["identified_risks"])
-
- # تبويب تقييم المخاطر
- with risk_tabs[1]:
- st.markdown("#### تقييم المخاطر")
-
- if "identified_risks" not in self.analysis_results:
- st.info("الرجاء تحديد المخاطر أولاً من تبويب 'تحديد المخاطر'")
- else:
- # زر لتقييم المخاطر
- if st.button("تقييم المخاطر"):
- with st.spinner("جاري تقييم المخاطر..."):
- # تقييم المخاطر باستخدام الذكاء الاصطناعي
- assessment_prompt = f"""
- بناءً على المخاطر المحددة للمشروع، قم بتقييم كل مخاطرة من حيث:
-
- 1. احتمالية الحدوث (منخفضة، متوسطة، عالية)
- 2. التأثير (منخفض، متوسط، عالي)
- 3. درجة المخاطرة (منخفضة، متوسطة، عالية، حرجة)
-
- معلومات المشروع:
- {self.analysis_results["risk_project_info"]}
-
- المخاطر المحددة:
- {self.analysis_results["identified_risks"]}
-
- قدم تقييمًا مفصلاً لكل مخاطرة في شكل جدول يتضمن:
- - وصف المخاطرة
- - الفئة
- - احتمالية الحدوث
- - التأثير
- - درجة المخاطرة
-
- ثم قم بترتيب المخاطر حسب درجة المخاطرة من الأعلى إلى الأدنى.
- """
-
- # الحصول على تقييم المخاطر من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- assessment_result = self._get_anthropic_response(assessment_prompt)
- else:
- assessment_result = self._get_ai_response(assessment_prompt)
-
- # حفظ تقييم المخاطر
- self.analysis_results["risk_assessment"] = assessment_result
-
- # عرض تقييم المخاطر
- st.markdown("### تقييم المخاطر")
- st.markdown(assessment_result)
-
- st.success("تم تقييم المخاطر بنجاح!")
-
- # إذا كان تقييم المخاطر قد تم بالفعل، عرضه
- if "risk_assessment" in self.analysis_results:
- st.markdown("### تقييم المخاطر")
- st.markdown(self.analysis_results["risk_assessment"])
-
- # تبويب خطة الاستجابة
- with risk_tabs[2]:
- st.markdown("#### خطة الاستجابة")
-
- if "risk_assessment" not in self.analysis_results:
- st.info("الرجاء تقييم المخاطر أولاً من تبويب 'تقييم المخاطر'")
- else:
- # زر لإنشاء خطة الاستجابة
- if st.button("إنشاء خطة الاستجابة"):
- with st.spinner("جاري إنشاء خطة الاستجابة..."):
- # إنشاء خطة الاستجابة باستخدام الذكاء الاصطناعي
- response_prompt = f"""
- بناءً على تقييم المخاطر للمشروع، قم بإنشاء خطة استجابة لكل مخاطرة تتضمن:
-
- 1. استراتيجية الاستجابة (تجنب، تخفيف، نقل، قبول)
- 2. الإجراءات المحددة
- 3. المسؤول عن التنفيذ
- 4. الموارد المطلوبة
- 5. الجدول الزمني
-
- معلومات المشروع:
- {self.analysis_results["risk_project_info"]}
-
- تقييم المخاطر:
- {self.analysis_results["risk_assessment"]}
-
- قدم خطة استجابة مفصلة لكل مخاطرة، مع التركيز على المخاطر ذات الدرجة العالية والحرجة.
- """
-
- # الحصول على خطة الاستجابة من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- response_result = self._get_anthropic_response(response_prompt)
- else:
- response_result = self._get_ai_response(response_prompt)
-
- # حفظ خطة الاستجابة
- self.analysis_results["risk_response"] = response_result
-
- # عرض خطة الاستجابة
- st.markdown("### خطة الاستجابة")
- st.markdown(response_result)
-
- st.success("تم إنشاء خطة الاستجابة بنجاح!")
-
- # إذا كانت خطة الاستجابة قد تم إنشاؤها بالفعل، عرضها
- if "risk_response" in self.analysis_results:
- st.markdown("### خطة الاستجابة")
- st.markdown(self.analysis_results["risk_response"])
-
- # تبويب التقرير النهائي
- with risk_tabs[3]:
- st.markdown("#### التقرير النهائي")
-
- if "risk_response" not in self.analysis_results:
- st.info("الرجاء إنشاء خطة الاستجابة أولاً من تبويب 'خطة الاستجابة'")
- else:
- # زر لإنشاء التقرير النهائي
- if st.button("إنشاء التقرير النهائي"):
- with st.spinner("جاري إنشاء التقرير النهائي..."):
- # إنشاء التقرير النهائي باستخدام الذكاء الاصطناعي
- report_prompt = f"""
- بناءً على المعلومات التالية، قم بإنشاء تقرير نهائي شامل لتحليل المخاطر في المشروع:
-
- معلومات المشروع:
- {self.analysis_results["risk_project_info"]}
-
- المخاطر المحددة:
- {self.analysis_results["identified_risks"]}
-
- تقييم المخاطر:
- {self.analysis_results["risk_assessment"]}
-
- خطة الاستجابة:
- {self.analysis_results["risk_response"]}
-
- قدم تقريرًا شاملاً يتضمن:
- 1. ملخص تنفيذي
- 2. معلومات المشروع
- 3. منهجية تحليل المخاطر
- 4. المخاطر المحددة
- 5. تقييم المخاطر
- 6. خطة الاستجابة
- 7. خطة المراقبة والتحكم
- 8. التوصيات
- 9. الخلاصة
-
- نسق التقرير بشكل احترافي وقابل للطباعة.
- """
-
- # الحصول على التقرير النهائي من النموذج المختار
- if st.session_state.selected_model == "anthropic":
- report_result = self._get_anthropic_response(report_prompt)
- else:
- report_result = self._get_ai_response(report_prompt)
-
- # حفظ التقرير النهائي
- self.analysis_results["risk_report"] = report_result
-
- # عرض التقرير النهائي
- st.markdown("### التقرير النهائي")
- st.markdown(report_result)
-
- st.success("تم إنشاء التقرير النهائي بنجاح!")
-
- # إذا كان التقرير النهائي قد تم إنشاؤه بالفعل، عرضه
- if "risk_report" in self.analysis_results:
- st.markdown("### التقرير النهائي")
- st.markdown(self.analysis_results["risk_report"])
-
- # اسم ملف التقرير
- report_file_name = f"تقرير_تحليل_المخاطر_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
-
- # أزرار لتنزيل التقرير بصيغ مختلفة
- st.download_button(
- label="تنزيل التقرير (PDF)",
- data="تقرير تحليل المخاطر".encode('utf-8'), # تم تعديل هذا من b"تقرير تحليل المخاطر"
- file_name=f"{report_file_name}.pdf",
- mime="application/pdf",
- key="download_risk_report_pdf"
- )
-
- st.download_button(
- label="تنزيل التقرير (Word)",
- data="تقرير تحليل المخاطر".encode('utf-8'), # تم تعديل هذا من b"تقرير تحليل المخاطر"
- file_name=f"{report_file_name}.docx",
- mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- key="download_risk_report_word"
- )
-
- def _render_settings_tab(self):
- """عرض تبويب الإعدادات"""
- st.markdown("### الإعدادات")
-
- st.markdown("#### إعدادات نماذج الذكاء الاصطناعي")
-
- # إعدادات مفاتيح API
- st.markdown("##### مفاتيح API")
-
- # مفتاح API لنموذج ai
- ai_api_key = st.text_input(
- "مفتاح API لنموذج ai",
- value=st.session_state.ai_api_key,
- type="password"
- )
-
- # مفتاح API لنموذج anthropic
- anthropic_api_key = st.text_input(
- "مفتاح API لنموذج anthropic",
- value=st.session_state.anthropic_api_key,
- type="password"
- )
-
- # زر لحفظ الإعدادات
- if st.button("حفظ الإعدادات"):
- # تحديث مفاتيح API
- st.session_state.ai_api_key = ai_api_key
- st.session_state.anthropic_api_key = anthropic_api_key
-
- st.success("تم حفظ الإعدادات بنجاح!")
-
- def _get_anthropic_response(self, prompt):
- """الحصول على رد من نموذج anthropic"""
- try:
- # التحقق من وجود مفتاح API
- if not st.session_state.anthropic_api_key:
- return "لم يتم تكوين مفتاح API لنموذج anthropic. يرجى تكوين المفتاح في الإعدادات."
-
- # إنشاء عميل anthropic
- client = anthropic.Anthropic(api_key=st.session_state.anthropic_api_key)
-
- # إرسال الطلب إلى النموذج
- response = client.messages.create(
- model="claude-3-opus-20240229",
- max_tokens=4000,
- temperature=0.7,
- system="أنت مساعد ذكي متخصص في تحليل مشاريع البناء والمقاولات في المملكة العربية السعودية. تقدم تحليلات دقيقة وتوصيات عملية بناءً على البيانات المقدمة.",
- messages=[
- {"role": "user", "content": prompt}
- ]
- )
-
- # إرجاع الرد
- return response.content[0].text
- except Exception as e:
- return f"حدث خطأ أثناء الاتصال بنموذج anthropic: {str(e)}"
-
- def _get_ai_response(self, prompt):
- """الحصول على رد من نموذج ai"""
- try:
- # التحقق من وجود مفتاح API
- if not st.session_state.ai_api_key:
- return "لم يتم تكوين مفتاح API لنموذج ai. يرجى تكوين المفتاح في الإعدادات."
-
- # إعداد رأس الطلب
- headers = {
- "Authorization": f"Bearer {st.session_state.ai_api_key}",
- "Content-Type": "application/json"
- }
-
- # إعداد بيانات الطلب
- data = {
- "model": "gpt-4",
- "messages": [
- {
- "role": "system",
- "content": "أنت مساعد ذكي متخصص في تحليل مشاريع البناء والمقاولات في المملكة العربية السعودية. تقدم تحليلات دقيقة وتوصيات عملية بناءً على البيانات المقدمة."
- },
- {
- "role": "user",
- "content": prompt
- }
- ],
- "temperature": 0.7,
- "max_tokens": 4000
- }
-
- # إرسال الطلب إلى النموذج
- response = requests.post(
- "https://api.openai.com/v1/chat/completions",
- headers=headers,
- json=data
- )
-
- # التحقق من نجاح الطلب
- if response.status_code == 200:
- # إرجاع الرد
- return response.json()["choices"][0]["message"]["content"]
- else:
- return f"حدث خطأ أثناء الاتصال بنموذج ai: {response.text}"
- except Exception as e:
- return f"حدث خطأ أثناء الاتصال بنموذج ai: {str(e)}"
-
- def _extract_text_from_file(self, file):
- """استخراج النص من الملف"""
- try:
- # تحديد نوع الملف
- file_name = file.name
- file_extension = file_name.split('.')[-1].lower()
-
- # استخراج النص حسب نوع الملف
- if file_extension == 'pdf':
- # استخراج النص من ملف PDF
- pdf_reader = PyPDF2.PdfReader(file)
- text = ""
- for page in pdf_reader.pages:
- text += page.extract_text() + "\n"
- return text
- elif file_extension in ['docx', 'doc']:
- # استخراج النص من ملف Word
- doc = docx.Document(file)
- text = ""
- for para in doc.paragraphs:
- text += para.text + "\n"
- return text
- elif file_extension == 'txt':
- # استخراج النص من ملف نصي
- return file.getvalue().decode('utf-8')
- else:
- return f"نوع الملف {file_extension} غير مدعوم لاستخراج النص."
- except Exception as e:
- return f"حدث خطأ أثناء استخراج النص من الملف: {str(e)}"
-
- def _detect_file_type(self, file_name):
- """تحديد نوع الملف بناءً على الامتداد"""
- extension = file_name.split('.')[-1].lower()
-
- if extension in ['pdf']:
- return 'application/pdf'
- elif extension in ['dwg']:
- return 'application/acad'
- elif extension in ['xlsx', 'xls']:
- return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
- elif extension in ['docx', 'doc']:
- return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
- elif extension in ['txt']:
- return 'text/plain'
- elif extension in ['png']:
- return 'image/png'
- elif extension in ['jpg', 'jpeg']:
- return 'image/jpeg'
- else:
- return 'application/octet-stream'
+# -*- coding: utf-8 -*-
+"""
+وحدة مساعد الذكاء الاصطناعي
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.express as px
+import plotly.graph_objects as go
+import seaborn as sns
+from datetime import datetime
+import time
+import os
+import sys
+import json
+import requests
+from pathlib import Path
+import io
+import base64
+import re
+from PIL import Image
+import PyPDF2
+import docx
+import anthropic
+import tempfile
+
+# إضافة المسار للوصول إلى الوحدات الأخرى
+current_dir = os.path.dirname(os.path.abspath(__file__))
+parent_dir = os.path.dirname(os.path.dirname(current_dir))
+if parent_dir not in sys.path:
+ sys.path.append(parent_dir)
+
+class AIAssistantApp:
+ """تطبيق مساعد الذكاء الاصطناعي"""
+
+ def __init__(self):
+ """تهيئة تطبيق مساعد الذكاء الاصطناعي"""
+ self.uploaded_files = {}
+ self.analysis_results = {}
+
+ # تهيئة مفاتيح API لنماذج هجين فيس
+ # تحميل المفاتيح من متغيرات البيئة أو من الإعدادات
+ if 'ai_api_key' not in st.session_state:
+ # محاولة الحصول على المفتاح من متغيرات البيئة أولاً
+ ai_key = os.environ.get('AI_API_KEY', '')
+ # إذا لم يكن موجودًا، حاول الحصول عليه من أسرار هجين فيس
+ if not ai_key and os.path.exists('/home/user/.huggingface/token'):
+ with open('/home/user/.huggingface/token', 'r') as f:
+ ai_key = f.read().strip()
+ # إذا لم يكن موجودًا، استخدم المفتاح المقدم في وحدة المعرفة
+ if not ai_key:
+ ai_key = ""
+ st.session_state.ai_api_key = ai_key
+
+ if 'anthropic_api_key' not in st.session_state:
+ # محاولة الحصول على المفتاح من متغيرات البيئة أولاً
+ anthropic_key = os.environ.get('ANTHROPIC_API_KEY', '')
+ # إذا لم يكن موجودًا، حاول الحصول عليه من أسرار هجين فيس
+ if not anthropic_key:
+ # استخدم نفس المفتاح لـ anthropic مؤقتًا
+ anthropic_key = st.session_state.ai_api_key
+ st.session_state.anthropic_api_key = anthropic_key
+
+ # تهيئة محادثة الذكاء الاصطناعي
+ if 'messages' not in st.session_state:
+ st.session_state.messages = []
+
+ if 'selected_model' not in st.session_state:
+ st.session_state.selected_model = "anthropic"
+
+ def run(self):
+ """تشغيل تطبيق مساعد الذكاء الاصطناعي"""
+ # عرض عنوان التطبيق
+ st.title("مساعد الذكاء الاصطناعي")
+
+ # إنشاء تبويبات التطبيق
+ tabs = st.tabs([
+ "المحادثة",
+ "تحليل المستندات",
+ "تحليل العقود",
+ "تقدير التكاليف",
+ "تحليل المخاطر",
+ "الإعدادات"
+ ])
+
+ # عرض محتوى كل تبويب
+ with tabs[0]:
+ self._render_chat_tab()
+
+ with tabs[1]:
+ self._render_document_analysis_tab()
+
+ with tabs[2]:
+ self._render_contract_analysis_tab()
+
+ with tabs[3]:
+ self._render_cost_estimation_tab()
+
+ with tabs[4]:
+ self._render_risk_analysis_tab()
+
+ with tabs[5]:
+ self._render_settings_tab()
+
+ def _render_chat_tab(self):
+ """عرض تبويب المحادثة مع الذكاء الاصطناعي"""
+ st.markdown("### المحادثة مع الذكاء الاصطناعي")
+
+ # اختيار نموذج الذكاء الاصطناعي
+ selected_model = st.selectbox(
+ "اختر نموذج الذكاء الاصطناعي:",
+ ["anthropic", "ai"],
+ index=0 if st.session_state.selected_model == "anthropic" else 1
+ )
+
+ # تحديث النموذج المختار إذا تغير
+ if selected_model != st.session_state.selected_model:
+ st.session_state.selected_model = selected_model
+ st.rerun() # تم تعديل هذا من st.experimental_rerun()
+
+ # التحقق من وجود مفتاح API للنموذج المختار
+ api_key_available = False
+ if selected_model == "anthropic" and st.session_state.anthropic_api_key:
+ api_key_available = True
+ elif selected_model == "ai" and st.session_state.ai_api_key:
+ api_key_available = True
+
+ # عرض رسائل المحادثة
+ for message in st.session_state.messages:
+ with st.chat_message(message["role"]):
+ st.markdown(message["content"])
+
+ # إذا لم يكن مفتاح API متاحًا، عرض رسالة خطأ
+ if not api_key_available:
+ st.error(f"لم يتم تكوين مفتاح API لنموذج {selected_model}. يرجى تكوين المفتاح في الإعدادات.")
+ else:
+ # مربع إدخال الرسالة
+ if prompt := st.chat_input("اكتب رسالتك هنا:"):
+ # إضافة رسالة المستخدم إلى المحادثة
+ st.session_state.messages.append({"role": "user", "content": prompt})
+
+ # عرض رسالة المستخدم
+ with st.chat_message("user"):
+ st.markdown(prompt)
+
+ # عرض مؤشر التحميل أثناء معالجة الرسالة
+ with st.chat_message("assistant"):
+ message_placeholder = st.empty()
+ message_placeholder.markdown("جاري التفكير...")
+
+ try:
+ # الحصول على رد من النموذج المختار
+ if selected_model == "anthropic":
+ response = self._get_anthropic_response(prompt)
+ else:
+ response = self._get_ai_response(prompt)
+
+ # عرض الرد
+ message_placeholder.markdown(response)
+
+ # إضافة رد المساعد إلى المحادثة
+ st.session_state.messages.append({"role": "assistant", "content": response})
+ except Exception as e:
+ message_placeholder.markdown(f"حدث خطأ: {str(e)}")
+
+ # زر لمسح المحادثة
+ if st.button("مسح المحادثة"):
+ st.session_state.messages = []
+ st.rerun() # تم تعديل هذا من st.experimental_rerun()
+
+ def _render_document_analysis_tab(self):
+ """عرض تبويب تحليل المستندات"""
+ st.markdown("### تحليل المستندات")
+
+ st.markdown("""
+ يمكنك استخدام هذه الأداة لتحليل المستندات والتقارير باستخدام الذكاء الاصطناعي.
+ الأداة تدعم تحليل الملفات التالية:
+ - ملفات PDF
+ - ملفات Word
+ - ملفات النصوص TXT
+ """)
+
+ # إنشاء تبويبات فرعية
+ doc_tabs = st.tabs([
+ "تحميل المستندات",
+ "استخراج النص",
+ "تحليل المحتوى",
+ "الملخص والتوصيات"
+ ])
+
+ # تبويب تحميل المستندات
+ with doc_tabs[0]:
+ st.markdown("#### تحميل المستندات")
+
+ uploaded_file = st.file_uploader(
+ "اختر ملفًا للتحليل (PDF, DOCX, TXT)",
+ type=["pdf", "docx", "txt"],
+ key="document_file_uploader"
+ )
+
+ if uploaded_file is not None:
+ # حفظ الملف المرفوع
+ file_details = {
+ "filename": uploaded_file.name,
+ "filetype": uploaded_file.type,
+ "filesize": uploaded_file.size
+ }
+
+ st.write("### تفاصيل الملف")
+ st.write(f"اسم الملف: {file_details['filename']}")
+ st.write(f"نوع الملف: {file_details['filetype']}")
+ st.write(f"حجم الملف: {file_details['filesize']} بايت")
+
+ # حفظ الملف في الذاكرة المؤقتة
+ self.uploaded_files["document"] = uploaded_file
+
+ st.success(f"تم تحميل الملف {uploaded_file.name} بنجاح!")
+
+ # تبويب استخراج النص
+ with doc_tabs[1]:
+ st.markdown("#### استخراج النص")
+
+ if "document" not in self.uploaded_files:
+ st.info("الرجاء تحميل مستند أولاً من تبويب 'تحميل المستندات'")
+ else:
+ if st.button("استخراج النص من المستند"):
+ with st.spinner("جاري استخراج النص..."):
+ # استخراج النص من الملف
+ extracted_text = self._extract_text_from_file(self.uploaded_files["document"])
+
+ # حفظ النص المستخرج
+ self.analysis_results["extracted_text"] = extracted_text
+
+ # عرض النص المستخرج
+ st.markdown("### النص المستخرج")
+ st.text_area("النص:", value=extracted_text, height=400, disabled=True)
+
+ st.success("تم استخراج النص بنجاح!")
+
+ # إذا كان النص قد تم استخراجه بالفعل، عرضه
+ if "extracted_text" in self.analysis_results:
+ st.markdown("### النص المستخرج")
+ st.text_area("النص:", value=self.analysis_results["extracted_text"], height=400, disabled=True)
+
+ # تبويب تحليل المحتوى
+ with doc_tabs[2]:
+ st.markdown("#### تحليل المحتوى")
+
+ if "extracted_text" not in self.analysis_results:
+ st.info("الرجاء استخراج النص أولاً من تبويب 'استخراج النص'")
+ else:
+ if st.button("تحليل المحتوى"):
+ with st.spinner("جاري تحليل المحتوى..."):
+ # تحليل المحتوى باستخدام الذكاء الاصطناعي
+ analysis_prompt = f"""
+ قم بتحليل النص التالي وتقديم:
+ 1. الموضوعات الرئيسية
+ 2. الكلمات المفتاحية
+ 3. الأفكار الرئيسية
+ 4. أي معلومات مهمة أخرى
+
+ النص:
+ {self.analysis_results["extracted_text"][:4000]}
+ """
+
+ # الحصول على التحليل من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ analysis_result = self._get_anthropic_response(analysis_prompt)
+ else:
+ analysis_result = self._get_ai_response(analysis_prompt)
+
+ # حفظ نتيجة التحليل
+ self.analysis_results["content_analysis"] = analysis_result
+
+ # عرض نتيجة التحليل
+ st.markdown("### نتيجة التحليل")
+ st.markdown(analysis_result)
+
+ st.success("تم تحليل المحتوى بنجاح!")
+
+ # إذا كان التحليل قد تم بالفعل، عرضه
+ if "content_analysis" in self.analysis_results:
+ st.markdown("### نتيجة التحليل")
+ st.markdown(self.analysis_results["content_analysis"])
+
+ # تبويب الملخص والتوصيات
+ with doc_tabs[3]:
+ st.markdown("#### الملخص والتوصيات")
+
+ if "content_analysis" not in self.analysis_results:
+ st.info("الرجاء تحليل المحتوى أولاً من تبويب 'تحليل المحتوى'")
+ else:
+ if st.button("إنشاء ملخص وتوصيات"):
+ with st.spinner("جاري إنشاء الملخص والتوصيات..."):
+ # إنشاء ملخص وتوصيات باستخدام الذكاء الاصطناعي
+ summary_prompt = f"""
+ بناءً على التحليل التالي، قم بإنشاء:
+ 1. ملخص موجز (لا يزيد عن 300 كلمة)
+ 2. توصيات عملية (3-5 توصيات)
+
+ التحليل:
+ {self.analysis_results["content_analysis"]}
+ """
+
+ # الحصول على الملخص والتوصيات من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ summary_result = self._get_anthropic_response(summary_prompt)
+ else:
+ summary_result = self._get_ai_response(summary_prompt)
+
+ # حفظ الملخص والتوصيات
+ self.analysis_results["summary_recommendations"] = summary_result
+
+ # عرض الملخص والتوصيات
+ st.markdown("### الملخص والتوصيات")
+ st.markdown(summary_result)
+
+ st.success("تم إنشاء الملخص والتوصيات بنجاح!")
+
+ # إذا كان الملخص والتوصيات قد تم إنشاؤهما بالفعل، عرضهما
+ if "summary_recommendations" in self.analysis_results:
+ st.markdown("### الملخص والتوصيات")
+ st.markdown(self.analysis_results["summary_recommendations"])
+
+ # زر لتصدير التقرير
+ report_content = f"""
+ # تقرير تحليل المستند
+
+ ## معلومات الملف
+ - اسم الملف: {self.uploaded_files["document"].name}
+ - نوع الملف: {self.uploaded_files["document"].type}
+ - حجم الملف: {self.uploaded_files["document"].size} بايت
+
+ ## تحليل المحتوى
+ {self.analysis_results["content_analysis"]}
+
+ ## الملخص والتوصيات
+ {self.analysis_results["summary_recommendations"]}
+
+ ## تم إنشاء هذا التقرير بواسطة مساعد الذكاء الاصطناعي
+ تاريخ الإنشاء: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
+ """
+
+ st.download_button(
+ label="تصدير التقرير (PDF)",
+ data=report_content.encode('utf-8'),
+ file_name=f"تقرير_تحليل_المستند_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf",
+ mime="application/pdf",
+ key="export_document_report_pdf"
+ )
+
+ def _render_contract_analysis_tab(self):
+ """عرض تبويب تحليل العقود"""
+ st.markdown("### تحليل العقود")
+
+ st.markdown("""
+ يمكنك استخدام هذه الأداة لتحليل العقود واستخراج البنود المهمة باستخدام الذكاء الاصطناعي.
+ الأداة تدعم تحليل الملفات التالية:
+ - ملفات PDF
+ - ملفات Word
+ - ملفات النصوص TXT
+ """)
+
+ # إنشاء تبويبات فرعية
+ contract_tabs = st.tabs([
+ "تحميل العقد",
+ "استخراج البنود",
+ "تحليل المخاطر",
+ "التقرير النهائي"
+ ])
+
+ # تبويب تحميل العقد
+ with contract_tabs[0]:
+ st.markdown("#### تحميل العقد")
+
+ uploaded_file = st.file_uploader(
+ "اختر ملف العقد للتحليل (PDF, DOCX, TXT)",
+ type=["pdf", "docx", "txt"],
+ key="contract_file_uploader"
+ )
+
+ if uploaded_file is not None:
+ # حفظ الملف المرفوع
+ file_details = {
+ "filename": uploaded_file.name,
+ "filetype": uploaded_file.type,
+ "filesize": uploaded_file.size
+ }
+
+ st.write("### تفاصيل الملف")
+ st.write(f"اسم الملف: {file_details['filename']}")
+ st.write(f"نوع الملف: {file_details['filetype']}")
+ st.write(f"حجم الملف: {file_details['filesize']} بايت")
+
+ # حفظ الملف في الذاكرة المؤقتة
+ self.uploaded_files["contract"] = uploaded_file
+
+ st.success(f"تم تحميل الملف {uploaded_file.name} بنجاح!")
+
+ # تبويب استخراج البنود
+ with contract_tabs[1]:
+ st.markdown("#### استخراج البنود")
+
+ if "contract" not in self.uploaded_files:
+ st.info("الرجاء تحميل ملف العقد أولاً من تبويب 'تحميل العقد'")
+ else:
+ if st.button("استخراج البنود من العقد"):
+ with st.spinner("جاري استخراج البنود..."):
+ # استخراج النص من الملف
+ extracted_text = self._extract_text_from_file(self.uploaded_files["contract"])
+
+ # حفظ النص المستخرج
+ self.analysis_results["contract_text"] = extracted_text
+
+ # استخراج البنود باستخدام الذكاء الاصطناعي
+ clauses_prompt = f"""
+ قم بتحليل نص العقد التالي واستخراج البنود المهمة التالية:
+ 1. الأطراف المتعاقدة
+ 2. موضوع العقد
+ 3. مدة العقد
+ 4. قيمة العقد
+ 5. شروط الدفع
+ 6. الالتزامات والمسؤوليات
+ 7. شروط الإنهاء
+ 8. تسوية النزاعات
+ 9. القانون الحاكم
+ 10. أي بنود أخرى مهمة
+
+ نص العقد:
+ {extracted_text[:4000]}
+ """
+
+ # الحصول على البنود من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ clauses_result = self._get_anthropic_response(clauses_prompt)
+ else:
+ clauses_result = self._get_ai_response(clauses_prompt)
+
+ # حفظ البنود المستخرجة
+ self.analysis_results["contract_clauses"] = clauses_result
+
+ # عرض البنود المستخرجة
+ st.markdown("### البنود المستخرجة")
+ st.markdown(clauses_result)
+
+ st.success("تم استخراج البنود بنجاح!")
+
+ # إذا كانت البنود قد تم استخراجها بالفعل، عرضها
+ if "contract_clauses" in self.analysis_results:
+ st.markdown("### البنود المستخرجة")
+ st.markdown(self.analysis_results["contract_clauses"])
+
+ # تبويب تحليل المخاطر
+ with contract_tabs[2]:
+ st.markdown("#### تحليل المخاطر")
+
+ if "contract_clauses" not in self.analysis_results:
+ st.info("الرجاء استخراج البنود أولاً من تبويب 'استخراج البنود'")
+ else:
+ if st.button("تحليل المخاطر في العقد"):
+ with st.spinner("جاري تحليل المخاطر..."):
+ # تحليل المخاطر باستخدام الذكاء الاصطناعي
+ risks_prompt = f"""
+ بناءً على البنود المستخرجة من العقد، قم بتحليل المخاطر المحتملة:
+ 1. المخاطر القانونية
+ 2. المخاطر المالية
+ 3. مخاطر التنفيذ
+ 4. مخاطر الجدول الزمني
+ 5. أي مخاطر أخرى
+
+ لكل مخاطرة، قدم:
+ - وصف المخاطرة
+ - احتمالية حدوثها (منخفضة، متوسطة، عالية)
+ - تأثيرها (منخفض، متوسط، عالي)
+ - توصيات للتخفيف من المخاطرة
+
+ البنود المستخرجة:
+ {self.analysis_results["contract_clauses"]}
+ """
+
+ # الحصول على تحليل المخاطر من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ risks_result = self._get_anthropic_response(risks_prompt)
+ else:
+ risks_result = self._get_ai_response(risks_prompt)
+
+ # حفظ تحليل المخاطر
+ self.analysis_results["contract_risks"] = risks_result
+
+ # عرض تحليل المخاطر
+ st.markdown("### تحليل المخاطر")
+ st.markdown(risks_result)
+
+ st.success("تم تحليل المخاطر بنجاح!")
+
+ # إذا كان تحليل المخاطر قد تم بالفعل، عرضه
+ if "contract_risks" in self.analysis_results:
+ st.markdown("### تحليل المخاطر")
+ st.markdown(self.analysis_results["contract_risks"])
+
+ # تبويب التقرير النهائي
+ with contract_tabs[3]:
+ st.markdown("#### التقرير النهائي")
+
+ if "contract_risks" not in self.analysis_results:
+ st.info("الرجاء تحليل المخاطر أولاً من تبويب 'تحليل المخاطر'")
+ else:
+ if st.button("إنشاء التقرير النهائي"):
+ with st.spinner("جاري إنشاء التقرير النهائي..."):
+ # إنشاء التقرير النهائي باستخدام الذكاء الاصطناعي
+ report_prompt = f"""
+ بناءً على البنود المستخرجة وتحليل المخاطر، قم بإنشاء تقرير نهائي يتضمن:
+ 1. ملخص تنفيذي
+ 2. تحليل البنود الرئيسية
+ 3. تحليل المخاطر
+ 4. التوصيات
+ 5. الخلاصة
+
+ البنود المستخرجة:
+ {self.analysis_results["contract_clauses"]}
+
+ تحليل المخاطر:
+ {self.analysis_results["contract_risks"]}
+ """
+
+ # الحصول على التقرير النهائي من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ report_result = self._get_anthropic_response(report_prompt)
+ else:
+ report_result = self._get_ai_response(report_prompt)
+
+ # حفظ التقرير النهائي
+ self.analysis_results["contract_report"] = report_result
+
+ # عرض التقرير النهائي
+ st.markdown("### التقرير النهائي")
+ st.markdown(report_result)
+
+ st.success("تم إنشاء التقرير النهائي بنجاح!")
+
+ # إذا كان التقرير النهائي قد تم إنشاؤه بالفعل، عرضه
+ if "contract_report" in self.analysis_results:
+ st.markdown("### التقرير النهائي")
+ st.markdown(self.analysis_results["contract_report"])
+
+ # زر لتصدير التقرير
+ report_content = f"""
+ # تقرير تحليل العقد
+
+ ## معلومات الملف
+ - اسم الملف: {self.uploaded_files["contract"].name}
+ - نوع الملف: {self.uploaded_files["contract"].type}
+ - حجم الملف: {self.uploaded_files["contract"].size} بايت
+
+ ## البنود المستخرجة
+ {self.analysis_results["contract_clauses"]}
+
+ ## تحليل المخاطر
+ {self.analysis_results["contract_risks"]}
+
+ ## التقرير النهائي
+ {self.analysis_results["contract_report"]}
+
+ ## تم إنشاء هذا التقرير بواسطة مساعد الذكاء الاصطناعي
+ تاريخ الإنشاء: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
+ """
+
+ st.download_button(
+ label="تصدير التقرير (PDF)",
+ data=report_content.encode('utf-8'),
+ file_name=f"تقرير_تحليل_العقد_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf",
+ mime="application/pdf",
+ key="export_contract_report_pdf"
+ )
+
+ def _render_cost_estimation_tab(self):
+ """عرض تبويب تقدير التكاليف باستخدام نماذج هجين فيس"""
+
+ st.markdown("### تقدير التكاليف")
+
+ st.markdown("""
+ يمكنك استخدام هذه الأداة لتقدير تكاليف المشاريع باستخدام نماذج الذكاء الاصطناعي المتقدمة من بيئة هجين فيس.
+ الأداة تدعم تحليل الملفات التالية:
+ - ملفات PDF (كراسات الشروط، المواصفات الفنية)
+ - ملفات DWG (المخططات الهندسية)
+ - ملفات Excel (جداول الكميات، التكاليف)
+ - ملفات Word (العقود، المستندات)
+ - ملفات النصوص TXT
+ - ملفات الصور (PNG, JPG) للمخططات والرسومات
+ """)
+
+ # إنشاء تبويبات فرعية
+ cost_tabs = st.tabs([
+ "تحميل الملفات",
+ "تقدير التكاليف",
+ "تحليل البنود",
+ "المقارنة مع السوق",
+ "التقارير"
+ ])
+
+ # تبويب تحميل الملفات
+ with cost_tabs[0]:
+ st.markdown("#### تحميل ملفات المشروع")
+
+ uploaded_files = st.file_uploader(
+ "اختر ملفات المشروع للتحليل",
+ type=["pdf", "dwg", "xlsx", "docx", "txt", "png", "jpg"],
+ accept_multiple_files=True,
+ key="cost_files_uploader"
+ )
+
+ if uploaded_files:
+ st.write("### الملفات المرفوعة")
+
+ for uploaded_file in uploaded_files:
+ file_details = {
+ "filename": uploaded_file.name,
+ "filetype": uploaded_file.type,
+ "filesize": uploaded_file.size
+ }
+
+ # حفظ الملف في الذاكرة المؤقتة
+ self.uploaded_files[uploaded_file.name] = uploaded_file
+
+ # عرض تفاصيل الملف
+ col1, col2, col3, col4 = st.columns([3, 2, 2, 1])
+ with col1:
+ st.write(f"**{file_details['filename']}**")
+ with col2:
+ st.write(f"النوع: {self._detect_file_type(file_details['filename'])}")
+ with col3:
+ st.write(f"الحجم: {file_details['filesize']} بايت")
+ with col4:
+ if st.button("حذف", key=f"delete_{file_details['filename']}"):
+ del self.uploaded_files[file_details['filename']]
+ st.rerun() # تم تعديل هذا من st.experimental_rerun()
+
+ st.success(f"تم تحميل {len(uploaded_files)} ملف بنجاح!")
+
+ # تبويب تقدير التكاليف
+ with cost_tabs[1]:
+ st.markdown("#### تقدير التكاليف")
+
+ if not self.uploaded_files:
+ st.info("الرجاء تحميل ملفات المشروع أولاً من تبويب 'تحميل الملفات'")
+ else:
+ # نموذج إدخال معلومات المشروع
+ st.markdown("### معلومات المشروع")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ project_name = st.text_input("اسم المشروع")
+ project_location = st.text_input("موقع المشروع")
+ project_type = st.selectbox(
+ "نوع المشروع",
+ ["سكني", "تجاري", "صناعي", "بنية تحتية", "آخر"]
+ )
+
+ with col2:
+ project_area = st.number_input("مساحة المشروع (م²)", min_value=0.0, step=100.0)
+ project_duration = st.number_input("مدة المشروع (بالأشهر)", min_value=1, step=1)
+ project_quality = st.select_slider(
+ "مستوى الجودة",
+ options=["اقتصادي", "متوسط", "عالي", "فاخر"]
+ )
+
+ # زر لبدء تقدير التكاليف
+ if st.button("تقدير التكاليف"):
+ with st.spinner("جاري تقدير التكاليف..."):
+ # إنشاء تقدير التكاليف باستخدام الذكاء الاصطناعي
+
+ # تجميع معلومات المشروع
+ project_info = f"""
+ اسم المشروع: {project_name}
+ موقع المشروع: {project_location}
+ نوع المشروع: {project_type}
+ مساحة المشروع: {project_area} م²
+ مدة المشروع: {project_duration} أشهر
+ مستوى الجودة: {project_quality}
+
+ الملفات المرفوعة:
+ {', '.join(self.uploaded_files.keys())}
+ """
+
+ # إنشاء تقدير التكاليف
+ estimation_prompt = f"""
+ أنت خبير في تقدير تكاليف مشاريع البناء في المملكة العربية السعودية.
+ قم بتقدير تكاليف المشروع التالي بناءً على المعلومات المقدمة:
+
+ {project_info}
+
+ قدم تقديرًا مفصلاً للتكاليف يتضمن:
+ 1. تكلفة المواد الرئيسية (الخرسانة، حديد التسليح، الطوب، إلخ)
+ 2. تكلفة العمالة
+ 3. تكلفة المعدات
+ 4. التكاليف غير المباشرة
+ 5. هامش الربح المقترح
+ 6. إجمالي التكلفة المقدرة
+ 7. تكلفة المتر المربع
+
+ استخدم أسعار السوق الحالية في المملكة العربية السعودية لعام 2025.
+ """
+
+ # الحصول على تقدير التكاليف من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ estimation_result = self._get_anthropic_response(estimation_prompt)
+ else:
+ estimation_result = self._get_ai_response(estimation_prompt)
+
+ # حفظ تقدير التكاليف
+ self.analysis_results["cost_estimation"] = estimation_result
+
+ # عرض تقدير التكاليف
+ st.markdown("### تقدير التكاليف")
+ st.markdown(estimation_result)
+
+ st.success("تم تقدير التكاليف بنجاح!")
+
+ # إذا كان تقدير التكاليف قد تم بالفعل، عرضه
+ if "cost_estimation" in self.analysis_results:
+ st.markdown("### تقدير التكاليف")
+ st.markdown(self.analysis_results["cost_estimation"])
+
+ # زر لتصدير التقرير
+ st.download_button(
+ label="تصدير التقرير (PDF)",
+ data="تقرير تقدير التكاليف".encode('utf-8'), # تم تعديل هذا من b"تقرير تقدير التكاليف"
+ file_name=f"تقرير_تقدير_التكاليف_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf",
+ mime="application/pdf",
+ key="export_cost_report_pdf"
+ )
+
+ st.download_button(
+ label="تصدير البيانات (Excel)",
+ data="بيانات تقدير التكاليف".encode('utf-8'), # تم تعديل هذا من b"بيانات تقدير التكاليف"
+ file_name=f"بيانات_تقدير_التكاليف_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ key="export_cost_data_excel"
+ )
+
+ # تبويب تحليل البنود
+ with cost_tabs[2]:
+ st.markdown("#### تحليل البنود")
+
+ if "cost_estimation" not in self.analysis_results:
+ st.info("الرجاء تقدير التكاليف أولاً من تبويب 'تقدير التكاليف'")
+ else:
+ # إنشاء تحليل البنود باستخدام الذكاء الاصطناعي
+ if st.button("تحليل البنود"):
+ with st.spinner("جاري تحليل البنود..."):
+ # تحليل البنود
+ items_prompt = f"""
+ بناءً على تقدير التكاليف التالي، قم بتحليل البنود الرئيسية:
+
+ {self.analysis_results["cost_estimation"]}
+
+ قدم تحليلاً مفصلاً للبنود يتضمن:
+ 1. البنود ذات التكلفة الأعلى
+ 2. البنود التي يمكن تقليل تكلفتها
+ 3. البنود التي قد تتغير أسعارها بشكل كبير
+ 4. توصيات لتحسين التكلفة الإجمالية
+
+ قدم البيانات في شكل جدول حيثما أمكن.
+ """
+
+ # الحصول على تحليل البنود من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ items_result = self._get_anthropic_response(items_prompt)
+ else:
+ items_result = self._get_ai_response(items_prompt)
+
+ # حفظ تحليل البنود
+ self.analysis_results["items_analysis"] = items_result
+
+ # عرض تحليل البنود
+ st.markdown("### تحليل البنود")
+ st.markdown(items_result)
+
+ st.success("تم تحليل البنود بنجاح!")
+
+ # إذا كان تحليل البنود قد تم بالفعل، عرضه
+ if "items_analysis" in self.analysis_results:
+ st.markdown("### تحليل البنود")
+ st.markdown(self.analysis_results["items_analysis"])
+
+ # تبويب المقارنة مع السوق
+ with cost_tabs[3]:
+ st.markdown("#### المقارنة مع السوق")
+
+ if "items_analysis" not in self.analysis_results:
+ st.info("الرجاء تحليل البنود أولاً من تبويب 'تحليل البنود'")
+ else:
+ # إنشاء مقارنة مع السوق باستخدام الذكاء الاصطناعي
+ if st.button("مقارنة مع السوق"):
+ with st.spinner("جاري إجراء المقارنة مع السوق..."):
+ # مقارنة مع السوق
+ market_prompt = f"""
+ بناءً على تقدير التكاليف وتحليل البنود التاليين، قم بإجراء مقارنة مع أسعار السوق الحالية في المملكة العربية السعودية:
+
+ تقدير التكاليف:
+ {self.analysis_results["cost_estimation"]}
+
+ تحليل البنود:
+ {self.analysis_results["items_analysis"]}
+
+ قدم مقارنة مفصلة تتضمن:
+ 1. مقارنة أسعار المواد الرئيسية مع متوسط أسعار السوق
+ 2. مقارنة تكلفة المتر المربع مع المشاريع المماثلة
+ 3. تحليل الفروقات وأسبابها
+ 4. توصيات للحصول على أفضل الأسعار
+
+ استخدم أسعار السوق الحالية في المملكة العربية السعودية لعام 2025.
+ """
+
+ # الحصول على مقارنة مع السوق من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ market_result = self._get_anthropic_response(market_prompt)
+ else:
+ market_result = self._get_ai_response(market_prompt)
+
+ # حفظ مقارنة مع السوق
+ self.analysis_results["market_comparison"] = market_result
+
+ # عرض مقارنة مع السوق
+ st.markdown("### المقارنة مع السوق")
+ st.markdown(market_result)
+
+ st.success("تمت المقارنة مع السوق بنجاح!")
+
+ # إذا كانت المقارنة مع السوق قد تمت بالفعل، عرضها
+ if "market_comparison" in self.analysis_results:
+ st.markdown("### المقارنة مع السوق")
+ st.markdown(self.analysis_results["market_comparison"])
+
+ # تبويب التقارير
+ with cost_tabs[4]:
+ st.markdown("#### التقارير")
+
+ if "market_comparison" not in self.analysis_results:
+ st.info("الرجاء إجراء المقارنة مع السوق أولاً من تبويب 'المقارنة مع السوق'")
+ else:
+ # إنشاء التقرير النهائي
+ if st.button("إنشاء التقرير النهائي"):
+ with st.spinner("جاري إنشاء التقرير النهائي..."):
+ # إنشاء التقرير النهائي
+ report_prompt = f"""
+ بناءً على المعلومات التالية، قم بإنشاء تقرير نهائي شامل لتقدير تكاليف المشروع:
+
+ تقدير التكاليف:
+ {self.analysis_results["cost_estimation"]}
+
+ تحليل البنود:
+ {self.analysis_results["items_analysis"]}
+
+ المقارنة مع السوق:
+ {self.analysis_results["market_comparison"]}
+
+ قدم تقريرًا شاملاً يتضمن:
+ 1. ملخص تنفيذي
+ 2. معلومات المشروع
+ 3. منهجية تقدير التكاليف
+ 4. تقدير التكاليف المفصل
+ 5. تحليل البنود
+ 6. المقارنة مع السوق
+ 7. التوصيات
+ 8. الخلاصة
+
+ نسق التقرير بشكل احترافي وقابل للطباعة.
+ """
+
+ # الحصول على التقرير النهائي من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ report_result = self._get_anthropic_response(report_prompt)
+ else:
+ report_result = self._get_ai_response(report_prompt)
+
+ # حفظ التقرير النهائي
+ self.analysis_results["final_report"] = report_result
+
+ # عرض التقرير النهائي
+ st.markdown("### التقرير النهائي")
+ st.markdown(report_result)
+
+ st.success("تم إنشاء التقرير النهائي بنجاح!")
+
+ # إذا كان التقرير النهائي قد تم إنشاؤه بالفعل، عرضه
+ if "final_report" in self.analysis_results:
+ st.markdown("### التقرير النهائي")
+ st.markdown(self.analysis_results["final_report"])
+
+ # اسم ملف التقرير
+ report_file_name = f"تقرير_تقدير_تكاليف_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
+
+ # أزرار لتنزيل التقرير بصيغ مختلفة
+ st.download_button(
+ label="تنزيل التقرير (PDF)",
+ data="تقرير تقدير التكاليف".encode('utf-8'), # تم تعديل هذا من b"تقرير تقدير التكاليف"
+ file_name=f"{report_file_name}.pdf",
+ mime="application/pdf",
+ key="download_report_pdf"
+ )
+
+ st.download_button(
+ label="تنزيل التقرير (Excel)",
+ data="بيانات تقدير التكاليف".encode('utf-8'), # تم تعديل هذا من b"بيانات تقدير التكاليف"
+ file_name=f"{report_file_name}.xlsx",
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ key="download_report_excel"
+ )
+
+ st.download_button(
+ label="تنزيل التقرير (Word)",
+ data="تقرير تقدير التكاليف".encode('utf-8'), # تم تعديل هذا من b"تقرير تقدير التكاليف"
+ file_name=f"{report_file_name}.docx",
+ mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ key="download_report_word"
+ )
+
+ def _render_risk_analysis_tab(self):
+ """عرض تبويب تحليل المخاطر"""
+ st.markdown("### تحليل المخاطر")
+
+ st.markdown("""
+ يمكنك استخدام هذه الأداة لتحليل المخاطر المحتملة في المشاريع باستخدام الذكاء الاصطناعي.
+ """)
+
+ # إنشاء تبويبات فرعية
+ risk_tabs = st.tabs([
+ "تحديد المخاطر",
+ "تقييم المخاطر",
+ "خطة الاستجابة",
+ "التقرير النهائي"
+ ])
+
+ # تبويب تحديد المخاطر
+ with risk_tabs[0]:
+ st.markdown("#### تحديد المخاطر")
+
+ # نموذج إدخال معلومات المشروع
+ st.markdown("### معلومات المشروع")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ project_name = st.text_input("اسم المشروع", key="risk_project_name")
+ project_location = st.text_input("موقع المشروع", key="risk_project_location")
+ project_type = st.selectbox(
+ "نوع المشروع",
+ ["سكني", "تجاري", "صناعي", "بنية تحتية", "آخر"],
+ key="risk_project_type"
+ )
+
+ with col2:
+ project_budget = st.number_input("ميزانية المشروع (ريال سعودي)", min_value=0.0, step=100000.0)
+ project_duration = st.number_input("مدة المشروع (بالأشهر)", min_value=1, step=1, key="risk_project_duration")
+ project_complexity = st.select_slider(
+ "مستوى تعقيد المشروع",
+ options=["بسيط", "متوسط", "معقد", "معقد جدًا"]
+ )
+
+ # وصف المشروع
+ project_description = st.text_area("وصف المشروع", height=150)
+
+ # زر لتحديد المخاطر
+ if st.button("تحديد المخاطر"):
+ with st.spinner("جاري تحديد المخاطر..."):
+ # تجميع معلومات المشروع
+ project_info = f"""
+ اسم المشروع: {project_name}
+ موقع المشروع: {project_location}
+ نوع المشروع: {project_type}
+ ميزانية المشروع: {project_budget} ريال سعودي
+ مدة المشروع: {project_duration} أشهر
+ مستوى تعقيد المشروع: {project_complexity}
+
+ وصف المشروع:
+ {project_description}
+ """
+
+ # تحديد المخاطر باستخدام الذكاء الاصطناعي
+ risks_prompt = f"""
+ أنت خبير في إدارة المخاطر في مشاريع البناء في المملكة العربية السعودية.
+ قم بتحديد المخاطر المحتملة للمشروع التالي:
+
+ {project_info}
+
+ حدد المخاطر في الفئات التالية:
+ 1. المخاطر الفنية
+ 2. المخاطر الإدارية
+ 3. المخاطر المالية
+ 4. المخاطر التعاقدية
+ 5. المخاطر البيئية
+ 6. مخاطر الجدول الزمني
+ 7. مخاطر الموارد البشرية
+ 8. مخاطر الصحة والسلامة
+ 9. المخاطر القانونية والتنظيمية
+ 10. مخاطر أخرى
+
+ لكل فئة، قدم قائمة بالمخاطر المحتملة مع وصف موجز لكل مخاطرة.
+ """
+
+ # الحصول على تحديد المخاطر من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ risks_result = self._get_anthropic_response(risks_prompt)
+ else:
+ risks_result = self._get_ai_response(risks_prompt)
+
+ # حفظ تحديد المخاطر
+ self.analysis_results["identified_risks"] = risks_result
+
+ # حفظ معلومات المشروع
+ self.analysis_results["risk_project_info"] = project_info
+
+ # عرض تحديد المخاطر
+ st.markdown("### المخاطر المحددة")
+ st.markdown(risks_result)
+
+ st.success("تم تحديد المخاطر بنجاح!")
+
+ # إذا كان تحديد المخاطر قد تم بالفعل، عرضه
+ if "identified_risks" in self.analysis_results:
+ st.markdown("### المخاطر المحددة")
+ st.markdown(self.analysis_results["identified_risks"])
+
+ # تبويب تقييم المخاطر
+ with risk_tabs[1]:
+ st.markdown("#### تقييم المخاطر")
+
+ if "identified_risks" not in self.analysis_results:
+ st.info("الرجاء تحديد المخاطر أولاً من تبويب 'تحديد المخاطر'")
+ else:
+ # زر لتقييم المخاطر
+ if st.button("تقييم المخاطر"):
+ with st.spinner("جاري تقييم المخاطر..."):
+ # تقييم المخاطر باستخدام الذكاء الاصطناعي
+ assessment_prompt = f"""
+ بناءً على المخاطر المحددة للمشروع، قم بتقييم كل مخاطرة من حيث:
+
+ 1. احتمالية الحدوث (منخفضة، متوسطة، عالية)
+ 2. التأثير (منخفض، متوسط، عالي)
+ 3. درجة المخاطرة (منخفضة، متوسطة، عالية، حرجة)
+
+ معلومات المشروع:
+ {self.analysis_results["risk_project_info"]}
+
+ المخاطر المحددة:
+ {self.analysis_results["identified_risks"]}
+
+ قدم تقييمًا مفصلاً لكل مخاطرة في شكل جدول يتضمن:
+ - وصف المخاطرة
+ - الفئة
+ - احتمالية الحدوث
+ - التأثير
+ - درجة المخاطرة
+
+ ثم قم بترتيب المخاطر حسب درجة المخاطرة من الأعلى إلى الأدنى.
+ """
+
+ # الحصول على تقييم المخاطر من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ assessment_result = self._get_anthropic_response(assessment_prompt)
+ else:
+ assessment_result = self._get_ai_response(assessment_prompt)
+
+ # حفظ تقييم المخاطر
+ self.analysis_results["risk_assessment"] = assessment_result
+
+ # عرض تقييم المخاطر
+ st.markdown("### تقييم المخاطر")
+ st.markdown(assessment_result)
+
+ st.success("تم تقييم المخاطر بنجاح!")
+
+ # إذا كان تقييم المخاطر قد تم بالفعل، عرضه
+ if "risk_assessment" in self.analysis_results:
+ st.markdown("### تقييم المخاطر")
+ st.markdown(self.analysis_results["risk_assessment"])
+
+ # تبويب خطة الاستجابة
+ with risk_tabs[2]:
+ st.markdown("#### خطة الاستجابة")
+
+ if "risk_assessment" not in self.analysis_results:
+ st.info("الرجاء تقييم المخاطر أولاً من تبويب 'تقييم المخاطر'")
+ else:
+ # زر لإنشاء خطة الاستجابة
+ if st.button("إنشاء خطة الاستجابة"):
+ with st.spinner("جاري إنشاء خطة الاستجابة..."):
+ # إنشاء خطة الاستجابة باستخدام الذكاء الاصطناعي
+ response_prompt = f"""
+ بناءً على تقييم المخاطر للمشروع، قم بإنشاء خطة استجابة لكل مخاطرة تتضمن:
+
+ 1. استراتيجية الاستجابة (تجنب، تخفيف، نقل، قبول)
+ 2. الإجراءات المحددة
+ 3. المسؤول عن التنفيذ
+ 4. الموارد المطلوبة
+ 5. الجدول الزمني
+
+ معلومات المشروع:
+ {self.analysis_results["risk_project_info"]}
+
+ تقييم المخاطر:
+ {self.analysis_results["risk_assessment"]}
+
+ قدم خطة استجابة مفصلة لكل مخاطرة، مع التركيز على المخاطر ذات الدرجة العالية والحرجة.
+ """
+
+ # الحصول على خطة الاستجابة من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ response_result = self._get_anthropic_response(response_prompt)
+ else:
+ response_result = self._get_ai_response(response_prompt)
+
+ # حفظ خطة الاستجابة
+ self.analysis_results["risk_response"] = response_result
+
+ # عرض خطة الاستجابة
+ st.markdown("### خطة الاستجابة")
+ st.markdown(response_result)
+
+ st.success("تم إنشاء خطة الاستجابة بنجاح!")
+
+ # إذا كانت خطة الاستجابة قد تم إنشاؤها بالفعل، عرضها
+ if "risk_response" in self.analysis_results:
+ st.markdown("### خطة الاستجابة")
+ st.markdown(self.analysis_results["risk_response"])
+
+ # تبويب التقرير النهائي
+ with risk_tabs[3]:
+ st.markdown("#### التقرير النهائي")
+
+ if "risk_response" not in self.analysis_results:
+ st.info("الرجاء إنشاء خطة الاستجابة أولاً من تبويب 'خطة الاستجابة'")
+ else:
+ # زر لإنشاء التقرير النهائي
+ if st.button("إنشاء التقرير النهائي"):
+ with st.spinner("جاري إنشاء التقرير النهائي..."):
+ # إنشاء التقرير النهائي باستخدام الذكاء الاصطناعي
+ report_prompt = f"""
+ بناءً على المعلومات التالية، قم بإنشاء تقرير نهائي شامل لتحليل المخاطر في المشروع:
+
+ معلومات المشروع:
+ {self.analysis_results["risk_project_info"]}
+
+ المخاطر المحددة:
+ {self.analysis_results["identified_risks"]}
+
+ تقييم المخاطر:
+ {self.analysis_results["risk_assessment"]}
+
+ خطة الاستجابة:
+ {self.analysis_results["risk_response"]}
+
+ قدم تقريرًا شاملاً يتضمن:
+ 1. ملخص تنفيذي
+ 2. معلومات المشروع
+ 3. منهجية تحليل المخاطر
+ 4. المخاطر المحددة
+ 5. تقييم المخاطر
+ 6. خطة الاستجابة
+ 7. خطة المراقبة والتحكم
+ 8. التوصيات
+ 9. الخلاصة
+
+ نسق التقرير بشكل احترافي وقابل للطباعة.
+ """
+
+ # الحصول على التقرير النهائي من النموذج المختار
+ if st.session_state.selected_model == "anthropic":
+ report_result = self._get_anthropic_response(report_prompt)
+ else:
+ report_result = self._get_ai_response(report_prompt)
+
+ # حفظ التقرير النهائي
+ self.analysis_results["risk_report"] = report_result
+
+ # عرض التقرير النهائي
+ st.markdown("### التقرير النهائي")
+ st.markdown(report_result)
+
+ st.success("تم إنشاء التقرير النهائي بنجاح!")
+
+ # إذا كان التقرير النهائي قد تم إنشاؤه بالفعل، عرضه
+ if "risk_report" in self.analysis_results:
+ st.markdown("### التقرير النهائي")
+ st.markdown(self.analysis_results["risk_report"])
+
+ # اسم ملف التقرير
+ report_file_name = f"تقرير_تحليل_المخاطر_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
+
+ # أزرار لتنزيل التقرير بصيغ مختلفة
+ st.download_button(
+ label="تنزيل التقرير (PDF)",
+ data="تقرير تحليل المخاطر".encode('utf-8'), # تم تعديل هذا من b"تقرير تحليل المخاطر"
+ file_name=f"{report_file_name}.pdf",
+ mime="application/pdf",
+ key="download_risk_report_pdf"
+ )
+
+ st.download_button(
+ label="تنزيل التقرير (Word)",
+ data="تقرير تحليل المخاطر".encode('utf-8'), # تم تعديل هذا من b"تقرير تحليل المخاطر"
+ file_name=f"{report_file_name}.docx",
+ mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ key="download_risk_report_word"
+ )
+
+ def _render_settings_tab(self):
+ """عرض تبويب الإعدادات"""
+ st.markdown("### الإعدادات")
+
+ st.markdown("#### إعدادات نماذج الذكاء الاصطناعي")
+
+ # إعدادات مفاتيح API
+ st.markdown("##### مفاتيح API")
+
+ # مفتاح API لنموذج ai
+ ai_api_key = st.text_input(
+ "مفتاح API لنموذج ai",
+ value=st.session_state.ai_api_key,
+ type="password"
+ )
+
+ # مفتاح API لنموذج anthropic
+ anthropic_api_key = st.text_input(
+ "مفتاح API لنموذج anthropic",
+ value=st.session_state.anthropic_api_key,
+ type="password"
+ )
+
+ # زر لحفظ الإعدادات
+ if st.button("حفظ الإعدادات"):
+ # تحديث مفاتيح API
+ st.session_state.ai_api_key = ai_api_key
+ st.session_state.anthropic_api_key = anthropic_api_key
+
+ st.success("تم حفظ الإعدادات بنجاح!")
+
+ def _get_anthropic_response(self, prompt):
+ """الحصول على رد من نموذج anthropic"""
+ try:
+ # التحقق من وجود مفتاح API
+ if not st.session_state.anthropic_api_key:
+ return "لم يتم تكوين مفتاح API لنموذج anthropic. يرجى تكوين المفتاح في الإعدادات."
+
+ # إنشاء عميل anthropic
+ client = anthropic.Anthropic(api_key=st.session_state.anthropic_api_key)
+
+ # إرسال الطلب إلى النموذج
+ response = client.messages.create(
+ model="claude-3-opus-20240229",
+ max_tokens=4000,
+ temperature=0.7,
+ system="أنت مساعد ذكي متخصص في تحليل مشاريع البناء والمقاولات في المملكة العربية السعودية. تقدم تحليلات دقيقة وتوصيات عملية بناءً على البيانات المقدمة.",
+ messages=[
+ {"role": "user", "content": prompt}
+ ]
+ )
+
+ # إرجاع الرد
+ return response.content[0].text
+ except Exception as e:
+ return f"حدث خطأ أثناء الاتصال بنموذج anthropic: {str(e)}"
+
+ def _get_ai_response(self, prompt):
+ """الحصول على رد من نموذج ai"""
+ try:
+ # التحقق من وجود مفتاح API
+ if not st.session_state.ai_api_key:
+ return "لم يتم تكوين مفتاح API لنموذج ai. يرجى تكوين المفتاح في الإعدادات."
+
+ # إعداد رأس الطلب
+ headers = {
+ "Authorization": f"Bearer {st.session_state.ai_api_key}",
+ "Content-Type": "application/json"
+ }
+
+ # إعداد بيانات الطلب
+ data = {
+ "model": "gpt-4",
+ "messages": [
+ {
+ "role": "system",
+ "content": "أنت مساعد ذكي متخصص في تحليل مشاريع البناء والمقاولات في المملكة العربية السعودية. تقدم تحليلات دقيقة وتوصيات عملية بناءً على البيانات المقدمة."
+ },
+ {
+ "role": "user",
+ "content": prompt
+ }
+ ],
+ "temperature": 0.7,
+ "max_tokens": 4000
+ }
+
+ # إرسال الطلب إلى النموذج
+ response = requests.post(
+ "https://api.openai.com/v1/chat/completions",
+ headers=headers,
+ json=data
+ )
+
+ # التحقق من نجاح الطلب
+ if response.status_code == 200:
+ # إرجاع الرد
+ return response.json()["choices"][0]["message"]["content"]
+ else:
+ return f"حدث خطأ أثناء الاتصال بنموذج ai: {response.text}"
+ except Exception as e:
+ return f"حدث خطأ أثناء الاتصال بنموذج ai: {str(e)}"
+
+ def _extract_text_from_file(self, file):
+ """استخراج النص من الملف"""
+ try:
+ # تحديد نوع الملف
+ file_name = file.name
+ file_extension = file_name.split('.')[-1].lower()
+
+ # استخراج النص حسب نوع الملف
+ if file_extension == 'pdf':
+ # استخراج النص من ملف PDF
+ pdf_reader = PyPDF2.PdfReader(file)
+ text = ""
+ for page in pdf_reader.pages:
+ text += page.extract_text() + "\n"
+ return text
+ elif file_extension in ['docx', 'doc']:
+ # استخراج النص من ملف Word
+ doc = docx.Document(file)
+ text = ""
+ for para in doc.paragraphs:
+ text += para.text + "\n"
+ return text
+ elif file_extension == 'txt':
+ # استخراج النص من ملف نصي
+ return file.getvalue().decode('utf-8')
+ else:
+ return f"نوع الملف {file_extension} غير مدعوم لاستخراج النص."
+ except Exception as e:
+ return f"حدث خطأ أثناء استخراج النص من الملف: {str(e)}"
+
+ def _detect_file_type(self, file_name):
+ """تحديد نوع الملف بناءً على الامتداد"""
+ extension = file_name.split('.')[-1].lower()
+
+ if extension in ['pdf']:
+ return 'application/pdf'
+ elif extension in ['dwg']:
+ return 'application/acad'
+ elif extension in ['xlsx', 'xls']:
+ return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ elif extension in ['docx', 'doc']:
+ return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+ elif extension in ['txt']:
+ return 'text/plain'
+ elif extension in ['png']:
+ return 'image/png'
+ elif extension in ['jpg', 'jpeg']:
+ return 'image/jpeg'
+ else:
+ return 'application/octet-stream'
diff --git a/modules/ai_assistant/assistant.py b/modules/ai_assistant/assistant.py
index 00df7a6113f2597ccfebd77eb38bb0b24e77eaf1..6fd24bae3539fb36b7caeb79916a0ac638141c48 100644
--- a/modules/ai_assistant/assistant.py
+++ b/modules/ai_assistant/assistant.py
@@ -1,444 +1,3175 @@
+# -*- coding: utf-8 -*-
"""
-وحدة المساعد الذكي لنظام إدارة المناقصات - Hybrid Face
+وحدة المساعد الذكي
+
+هذا الملف يحتوي على الفئة الرئيسية لتطبيق المساعد الذكي مع دعم نموذج Claude AI.
"""
-import os
-import logging
-import threading
-import datetime
+import streamlit as st
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.express as px
+import requests
import json
-import re
-from pathlib import Path
-
-# تهيئة السجل
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger('ai_assistant')
-
-class AIAssistant:
- """المساعد الذكي"""
-
- def __init__(self, config=None, db=None):
- """تهيئة المساعد الذكي"""
- self.config = config
- self.db = db
- self.processing_in_progress = False
- self.current_query = None
- self.processing_results = {}
- self.conversation_history = []
-
- # إعدادات المساعد الذكي
- self.ai_model = config.AI_MODEL if config and hasattr(config, 'AI_MODEL') else "gpt-4"
- self.ai_temperature = config.AI_TEMPERATURE if config and hasattr(config, 'AI_TEMPERATURE') else 0.7
- self.ai_max_tokens = config.AI_MAX_TOKENS if config and hasattr(config, 'AI_MAX_TOKENS') else 2000
-
- # إنشاء مجلد المساعد الذكي إذا لم يكن موجوداً
- if config and hasattr(config, 'EXPORTS_PATH'):
- self.exports_path = Path(config.EXPORTS_PATH)
- else:
- self.exports_path = Path('data/exports')
-
- if not self.exports_path.exists():
- self.exports_path.mkdir(parents=True, exist_ok=True)
-
- def process_query(self, query, context=None, callback=None):
- """معالجة استعلام المستخدم"""
- if self.processing_in_progress:
- logger.warning("هناك عملية معالجة جارية بالفعل")
- return False
-
- self.processing_in_progress = True
- self.current_query = query
- self.processing_results = {
- "query": query,
- "context": context,
- "processing_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- "status": "جاري المعالجة",
- "response": "",
- "suggestions": [],
- "references": []
+import time
+import base64
+import logging
+import os
+from datetime import datetime, timedelta
+import io
+import tempfile
+import random
+from io import BytesIO
+from tempfile import NamedTemporaryFile
+from PIL import Image
+
+# استيراد النماذج المطلوبة
+try:
+ from models.inference import (
+ load_cost_prediction_model,
+ load_document_classifier_model,
+ load_risk_assessment_model,
+ load_local_content_model,
+ load_entity_recognition_model
+ )
+except ImportError:
+ # إنشاء دوال وهمية في حال عدم توفر النماذج
+ def load_cost_prediction_model():
+ return None
+
+ def load_document_classifier_model():
+ return None
+
+ def load_risk_assessment_model():
+ return None
+
+ def load_local_content_model():
+ return None
+
+ def load_entity_recognition_model():
+ return None
+
+try:
+ # استيراد مكتبة pdf2image للتعامل مع ملفات PDF
+ from pdf2image import convert_from_path
+ pdf_conversion_available = True
+except ImportError:
+ pdf_conversion_available = False
+ logging.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.")
+
+
+class ClaudeAIService:
+ """
+ فئة خدمة Claude AI للتحليل الذكي
+ """
+ def __init__(self):
+ """تهيئة خدمة Claude AI"""
+ self.api_url = "https://api.anthropic.com/v1/messages"
+
+ def get_api_key(self):
+ """الحصول على مفتاح API من متغيرات البيئة"""
+ api_key = os.environ.get("anthropic")
+ if not api_key:
+ raise ValueError("مفتاح API لـ Claude غير موجود في متغيرات البيئة")
+ return api_key
+
+ def get_available_models(self):
+ """
+ الحصول على قائمة بالنماذج المتاحة
+
+ العوائد:
+ dict: قائمة بالنماذج مع وصفها
+ """
+ return {
+ "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة",
+ "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية"
}
-
- # إضافة الاستعلام إلى سجل المحادثة
- self.conversation_history.append({
- "role": "user",
- "content": query,
- "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- })
-
- # بدء المعالجة في خيط منفصل
- thread = threading.Thread(
- target=self._process_query_thread,
- args=(query, context, callback)
- )
- thread.daemon = True
- thread.start()
-
- return True
-
- def _process_query_thread(self, query, context, callback):
- """خيط معالجة الاستعلام"""
+
+ def get_model_full_name(self, short_name):
+ """
+ تحويل الاسم المختصر للنموذج إلى الاسم الكامل
+
+ المعلمات:
+ short_name: الاسم المختصر للنموذج
+
+ العوائد:
+ str: الاسم الكامل للنموذج
+ """
+ valid_models = {
+ "claude-3-7-sonnet": "claude-3-7-sonnet-20250219",
+ "claude-3-5-haiku": "claude-3-5-haiku-20240307"
+ }
+
+ return valid_models.get(short_name, short_name)
+
+ def analyze_image(self, image_path, prompt, model_name="claude-3-7-sonnet"):
+ """
+ تحليل صورة باستخدام نموذج Claude AI
+
+ المعلمات:
+ image_path: مسار الصورة المراد تحليلها
+ prompt: التوجيه للنموذج
+ model_name: اسم نموذج Claude المراد استخدامه
+
+ العوائد:
+ dict: نتائج التحليل
+ """
try:
- # تحليل الاستعلام
- query_type = self._analyze_query(query)
-
- # معالجة الاستعلام بناءً على نوعه
- if query_type == "document_analysis":
- response = self._handle_document_analysis_query(query, context)
- elif query_type == "pricing":
- response = self._handle_pricing_query(query, context)
- elif query_type == "risk_analysis":
- response = self._handle_risk_analysis_query(query, context)
- elif query_type == "project_management":
- response = self._handle_project_management_query(query, context)
- elif query_type == "reporting":
- response = self._handle_reporting_query(query, context)
+ # الحصول على مفتاح API
+ api_key = self.get_api_key()
+
+ # قراءة محتوى الصورة
+ with open(image_path, 'rb') as f:
+ file_content = f.read()
+
+ # تحويل المحتوى إلى Base64
+ file_base64 = base64.b64encode(file_content).decode('utf-8')
+
+ # تحديد نوع الملف من امتداده
+ _, ext = os.path.splitext(image_path)
+ ext = ext.lower()
+
+ if ext in ('.jpg', '.jpeg'):
+ file_type = "image/jpeg"
+ elif ext == '.png':
+ file_type = "image/png"
+ elif ext == '.gif':
+ file_type = "image/gif"
+ elif ext == '.webp':
+ file_type = "image/webp"
else:
- response = self._handle_general_query(query, context)
-
- # توليد اقتراحات
- suggestions = self._generate_suggestions(query_type, query, response)
-
- # تحديث نتائج المعالجة
- self.processing_results["response"] = response
- self.processing_results["query_type"] = query_type
- self.processing_results["suggestions"] = suggestions
- self.processing_results["status"] = "اكتملت المعالجة"
- self.processing_results["processing_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-
- # إضافة الاستجابة إلى سجل المحادثة
- self.conversation_history.append({
- "role": "assistant",
- "content": response,
- "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- })
-
- logger.info(f"اكتملت معالجة الاستعلام: {query[:50]}...")
-
+ file_type = "image/jpeg" # افتراضي
+
+ # التحقق من اسم النموذج وتصحيحه إذا لزم الأمر
+ model_name = self.get_model_full_name(model_name)
+
+ # إعداد البيانات للطلب
+ headers = {
+ "Content-Type": "application/json",
+ "x-api-key": api_key,
+ "anthropic-version": "2023-06-01"
+ }
+
+ payload = {
+ "model": model_name,
+ "max_tokens": 4096,
+ "messages": [
+ {
+ "role": "user",
+ "content": [
+ {"type": "text", "text": prompt},
+ {
+ "type": "image",
+ "source": {
+ "type": "base64",
+ "media_type": file_type,
+ "data": file_base64
+ }
+ }
+ ]
+ }
+ ]
+ }
+
+ # إرسال الطلب إلى API
+ response = requests.post(
+ self.api_url,
+ headers=headers,
+ json=payload,
+ timeout=60
+ )
+
+ # التحقق من نجاح الطلب
+ if response.status_code != 200:
+ error_message = f"فشل طلب API: {response.status_code}"
+ try:
+ error_details = response.json()
+ error_message += f"\nتفاصيل: {error_details}"
+ except:
+ error_message += f"\nتفاصيل: {response.text}"
+
+ return {"error": error_message}
+
+ # معالجة الاستجابة
+ result = response.json()
+
+ return {
+ "success": True,
+ "content": result["content"][0]["text"],
+ "model": result["model"],
+ "usage": result.get("usage", {})
+ }
+
except Exception as e:
- logger.error(f"خطأ في معالجة الاستعلام: {str(e)}")
- self.processing_results["status"] = "فشلت المعالجة"
- self.processing_results["error"] = str(e)
-
- # إضافة رسالة الخطأ إلى سجل المحادثة
- self.conversation_history.append({
- "role": "system",
- "content": f"حدث خطأ أثناء معالجة الاستعلام: {str(e)}",
- "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- })
-
- finally:
- self.processing_in_progress = False
-
- # استدعاء دالة الاستجابة إذا تم توفيرها
- if callback and callable(callback):
- callback(self.processing_results)
-
- def _analyze_query(self, query):
- """تحليل نوع الاستعلام"""
- query = query.lower()
-
- # تحديد نوع الاستعلام بناءً على الكلمات المفتاحية
- if any(keyword in query for keyword in ["تحليل المستند", "تحليل وثيقة", "استخراج بيانات", "قراءة مستند"]):
- return "document_analysis"
- elif any(keyword in query for keyword in ["تسعير", "سعر", "تكلفة", "ميزانية", "تقدير"]):
- return "pricing"
- elif any(keyword in query for keyword in ["مخاطر", "تحليل المخاطر", "تقييم المخاطر"]):
- return "risk_analysis"
- elif any(keyword in query for keyword in ["مشروع", "إدارة المشروع", "جدول زمني", "خطة"]):
- return "project_management"
- elif any(keyword in query for keyword in ["تقرير", "إحصائيات", "تحليل البيانات", "رسم بياني"]):
- return "reporting"
- else:
- return "general"
-
- def _handle_document_analysis_query(self, query, context):
- """معالجة استعلام تحليل المستندات"""
- # محاكاة استجابة المساعد الذكي لاستعلام تحليل المستندات
- response = """
-يمكنني مساعدتك في تحليل المستندات واستخراج المعلومات المهمة منها. لتحليل مستند، يرجى اتباع الخطوات التالية:
-
-1. انتقل إلى وحدة "تحليل المستندات" من القائمة الجانبية.
-2. انقر على زر "تحميل مستند" واختر المستند المراد تحليله.
-3. حدد نوع المستند (مناقصة، عقد، مواصفات فنية، إلخ).
-4. انقر على زر "تحليل" لبدء عملية التحليل.
-
-سيقوم النظام باستخراج المعلومات التالية من المستند:
-- البنود والكميات
-- الكيانات (العميل، الموقع، المقاول، إلخ)
-- التواريخ المهمة
-- المبالغ والتكاليف
-- المخاطر المحتملة
-
-بعد اكتمال التحليل، يمكنك مراجعة النتائج وتعديلها إذا لزم الأمر، ثم استخدامها في وحدات النظام الأخرى مثل التسعير وتحليل المخاطر.
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل استخدام وحدة تحليل المستندات", "type": "manual"},
- {"title": "أنواع المستندات المدعومة", "type": "documentation"},
- {"title": "تقنيات استخراج البيانات من المستندات", "type": "article"}
- ]
-
- return response
-
- def _handle_pricing_query(self, query, context):
- """معالجة استعلام التسعير"""
- # محاكاة استجابة المساعد الذكي لاستعلام التسعير
- response = """
-يمكنني مساعدتك في تسعير المشاريع وتقدير التكاليف. لإنشاء تسعير لمشروع، يرجى اتباع الخطوات التالية:
-
-1. انتقل إلى وحدة "التسعير المتكامل" من القائمة الجانبية.
-2. اختر المشروع المراد تسعيره أو أنشئ مشروعاً جديداً.
-3. أدخل بنود المشروع والكميات التقديرية (يمكن استيرادها من نتائج تحليل المستندات).
-4. حدد الموارد المطلوبة (مواد، معدات، عمالة).
-5. اختر استراتيجية التسعير المناسبة:
- - شاملة: تغطية كاملة للتكاليف والمخاطر مع هامش ربح مناسب.
- - تنافسية: تخفيض الهوامش لتقديم سعر تنافسي.
- - متوازنة: توازن بين الربحية والتنافسية.
-6. انقر على زر "حساب التسعير" لإنشاء التسعير.
-
-سيقوم النظام بحساب:
-- التكاليف المباشرة (بنود المشروع)
-- التكاليف غير المباشرة (نفقات عامة، إدارية، ربح)
-- تكاليف المخاطر
-- ضريبة القيمة المضافة
-- السعر النهائي
-
-يمكنك تعديل المعلمات وإعادة حساب التسعير، ثم تصدير النتائج إلى تقرير مفصل.
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل استخدام وحدة التسعير المتكامل", "type": "manual"},
- {"title": "استراتيجيات التسعير", "type": "documentation"},
- {"title": "حساب التكاليف غير المباشرة", "type": "article"}
- ]
-
- return response
-
- def _handle_risk_analysis_query(self, query, context):
- """معالجة استعلام تحليل المخاطر"""
- # محاكاة استجابة المساعد الذكي لاستعلام تحليل المخاطر
- response = """
-يمكنني مساعدتك في تحليل وإدارة مخاطر المشروع. لإجراء تحليل للمخاطر، يرجى اتباع الخطوات التالية:
-
-1. انتقل إلى وحدة "تحليل المخاطر" من القائمة الجانبية.
-2. اختر المشروع المراد تحليل مخاطره.
-3. اختر طريقة التحليل:
- - شاملة: تحليل مفصل يغطي جميع جوانب المشروع.
- - أساسية: تحليل سريع للمخاطر الرئيسية.
-4. انقر على زر "تحليل المخاطر" لبدء التحليل.
-
-سيقوم النظام بما يلي:
-- تحديد المخاطر المحتملة بناءً على بيانات المشروع
-- تصنيف المخاطر إلى فئات (فني، مالي، إداري، إلخ)
-- إنشاء مصفوفة المخاطر (الاحتمالية × التأثير)
-- تطوير استراتيجيات التخفيف لكل مخاطرة
-- إنشاء ملخص للمخاطر وتوصيات
-
-يمكنك مراجعة نتائج التحليل وتعديلها، ثم تصدير التقرير النهائي واستخدامه في خطة إدارة المشروع.
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل استخدام وحدة تحليل المخاطر", "type": "manual"},
- {"title": "منهجيات تحليل المخاطر", "type": "documentation"},
- {"title": "استراتيجيات التخفيف من المخاطر", "type": "article"}
- ]
-
- return response
-
- def _handle_project_management_query(self, query, context):
- """معالجة استعلام إدارة المشاريع"""
- # محاكاة استجابة المساعد الذكي لاستعلام إدارة المشاريع
- response = """
-يمكنني مساعدتك في إدارة المشاريع وتتبع تقدمها. لإدارة مشروع، يرجى اتباع الخطوات التالية:
-
-1. انتقل إلى وحدة "إدارة المشاريع" من القائمة الجانبية.
-2. أنشئ مشروعاً جديداً أو اختر مشروعاً موجوداً.
-3. أدخل معلومات المشروع الأساسية (الاسم، العميل، الوصف، التواريخ).
-4. أضف بنود المشروع (يمكن استيرادها من نتائج تحليل المستندات).
-5. أنشئ الجدول الزمني للمشروع وحدد المراحل والمهام.
-6. عين الموارد للمهام وحدد التبعيات بينها.
-
-يمكنك استخدام وحدة إدارة المشاريع لـ:
-- تتبع تقدم المشروع ومقارنته بالخطة
-- إدارة الموارد وتوزيعها
-- متابعة المشكلات والمخاطر
-- إدارة التغييرات في نطاق العمل
-- إنشاء تقارير حالة المشروع
-
-كما يمكنك دمج نتائج التسعير وتحليل المخاطر في خطة المشروع لإدارة شاملة.
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل استخدام وحدة إدارة المشاريع", "type": "manual"},
- {"title": "أفضل ممارسات إدارة المشاريع", "type": "documentation"},
- {"title": "إنشاء جداول زمنية فعالة", "type": "article"}
- ]
-
- return response
-
- def _handle_reporting_query(self, query, context):
- """معالجة استعلام التقارير"""
- # محاكاة استجابة المساعد الذكي لاستعلام التقارير
- response = """
-يمكنني مساعدتك في إنشاء تقارير وتحليلات للمشاريع والمناقصات. لإنشاء تقرير، يرجى اتباع الخطوات التالية:
-
-1. انتقل إلى وحدة "التقارير والتحليلات" من القائمة الجانبية.
-2. اختر نوع التقرير:
- - تقرير المشروع: معلومات شاملة عن مشروع محدد
- - تقرير التسعير: تفاصيل تسعير مشروع أو مناقصة
- - تقرير المخاطر: تحليل مخاطر المشروع واستراتيجيات التخفيف
- - تقرير الأداء: مقارنة الأداء الفعلي بالمخطط
- - تقرير مالي: تحليل مالي للمشاريع والمناقصات
-3. حدد معلمات التقرير (المشروع، الفترة الزمنية، إلخ).
-4. انقر على زر "إنشاء التقرير".
-
-يمكنك تخصيص التقارير بإضافة أو إزالة أقسام، وتغيير طريقة عرض البيانات (جداول، رسوم بيانية، إلخ).
-
-التقارير المنشأة يمكن:
-- تصديرها بتنسيقات مختلفة (PDF، Excel، Word)
-- مشاركتها مع أعضاء الفريق أو العملاء
-- جدولتها للإنشاء التلقائي بشكل دوري
-- حفظها كقوالب لاستخدامها في المستقبل
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل استخدام وحدة التقارير والتحليلات", "type": "manual"},
- {"title": "أنواع التقارير المتاحة", "type": "documentation"},
- {"title": "إنشاء رسوم بيانية فعالة", "type": "article"}
- ]
-
- return response
-
- def _handle_general_query(self, query, context):
- """معالجة استعلام عام"""
- # محاكاة استجابة المساعد الذكي لاستعلام عام
- response = """
-مرحباً بك في المساعد الذكي لنظام إدارة المناقصات Hybrid Face. يمكنني مساعدتك في مجموعة متنوعة من المهام المتعلقة بإدارة المناقصات والمشاريع.
-
-يمكنني مساعدتك في:
-- تحليل مستندات المناقصات واستخراج المعلومات المهمة منها
-- تسعير المشاريع وتقدير التكاليف
-- تحليل وإدارة مخاطر المشاريع
-- إدارة المشاريع وتتبع تقدمها
-- إنشاء تقارير وتحليلات
-
-للحصول على مساعدة محددة، يرجى طرح سؤال يتعلق بإحدى هذه المجالات. على سبيل المثال:
-- "كيف يمكنني تحليل مستند مناقصة؟"
-- "ساعدني في تسعير مشروع جديد"
-- "كيف أقوم بتحليل مخاطر المشروع؟"
-- "أريد إنشاء تقرير عن حالة المشروع"
-
-يمكنك أيضاً استخدام الوحدات المختلفة في النظام مباشرة من القائمة الجانبية.
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل المستخدم الشامل", "type": "manual"},
- {"title": "نظرة عامة على النظام", "type": "documentation"},
- {"title": "الأسئلة الشائعة", "type": "faq"}
+ logging.error(f"خطأ أثناء تحليل الصورة: {str(e)}")
+ import traceback
+ stack_trace = traceback.format_exc()
+ return {"error": f"فشل في تحليل الصورة: {str(e)}\n{stack_trace}"}
+
+ def chat_completion(self, messages, model_name="claude-3-7-sonnet"):
+ """
+ إكمال محادثة باستخدام نموذج Claude AI
+
+ المعلمات:
+ messages: سجل المحادثة
+ model_name: اسم نموذج Claude المراد استخدامه
+
+ العوائد:
+ dict: نتائج الإكمال
+ """
+ try:
+ # الحصول على مفتاح API
+ api_key = self.get_api_key()
+
+ # تحويل رسائل streamlit إلى تنسيق Claude API
+ claude_messages = []
+ for msg in messages:
+ claude_messages.append({
+ "role": msg["role"],
+ "content": msg["content"]
+ })
+
+ # التحقق من اسم النموذج وتصحيحه إذا لزم الأمر
+ model_name = self.get_model_full_name(model_name)
+
+ # إعداد البيانات للطلب
+ headers = {
+ "Content-Type": "application/json",
+ "x-api-key": api_key,
+ "anthropic-version": "2023-06-01"
+ }
+
+ payload = {
+ "model": model_name,
+ "max_tokens": 2048,
+ "messages": claude_messages,
+ "temperature": 0.7
+ }
+
+ # إرسال الطلب إلى API
+ response = requests.post(
+ self.api_url,
+ headers=headers,
+ json=payload,
+ timeout=30
+ )
+
+ # التحقق من نجاح الطلب
+ if response.status_code != 200:
+ error_message = f"فشل طلب API: {response.status_code}"
+ try:
+ error_details = response.json()
+ error_message += f"\nتفاصيل: {error_details}"
+ except:
+ error_message += f"\nتفاصيل: {response.text}"
+
+ return {"error": error_message}
+
+ # معالجة الاستجابة
+ result = response.json()
+
+ return {
+ "success": True,
+ "content": result["content"][0]["text"],
+ "model": result["model"],
+ "usage": result.get("usage", {})
+ }
+
+ except Exception as e:
+ logging.error(f"خطأ أثناء إكمال المحادثة: {str(e)}")
+ import traceback
+ stack_trace = traceback.format_exc()
+ return {"error": f"فشل في إكمال المحادثة: {str(e)}\n{stack_trace}"}
+
+
+class AIAssistantApp:
+ """وحدة المساعد الذكي"""
+
+ def __init__(self):
+ """تهيئة وحدة المساعد الذكي"""
+ # تحميل النماذج عند بدء التشغيل
+ self.cost_model = load_cost_prediction_model()
+ self.document_model = load_document_classifier_model()
+ self.risk_model = load_risk_assessment_model()
+ self.local_content_model = load_local_content_model()
+ self.entity_model = load_entity_recognition_model()
+
+ # إنشاء خدمة Claude AI
+ self.claude_service = ClaudeAIService()
+
+ # تهيئة قائمة الأسئلة والإجابات الشائعة
+ self.faqs = [
+ {
+ "question": "كيف يمكنني إضافة مشروع جديد؟",
+ "answer": "يمكنك إضافة مشروع جديد من خلال الانتقال إلى وحدة إدارة المشاريع، ثم النقر على زر 'إضافة مشروع جديد'، وملء النموذج بالبيانات المطلوبة."
+ },
+ {
+ "question": "ما هي خطوات تسعير المناقصة؟",
+ "answer": "تتضمن خطوات تسعير المناقصة: 1) تحليل مستندات المناقصة، 2) تحديد بنود العمل، 3) تقدير التكاليف المباشرة، 4) إضافة المصاريف العامة والأرباح، 5) احتساب المحتوى المحلي، 6) مراجعة النتائج النهائية."
+ },
+ {
+ "question": "كيف يتم حساب المحتوى المحلي؟",
+ "answer": "يتم حساب المحتوى المحلي بتحديد نسبة المنتجات والخدمات والقوى العاملة المحلية من إجمالي التكاليف. يتم استخدام قاعدة بيانات الموردين المعتمدين وتطبيق معادلات خاصة حسب متطلبات هيئة المحتوى المحلي."
+ },
+ {
+ "question": "كيف يمكنني تصدير التقارير؟",
+ "answer": "يمكنك تصدير التقارير من وحدة التقارير والتحليلات، حيث يوجد زر 'تصدير' في كل تقرير. يمكن تصدير التقارير بتنسيقات مختلفة مثل Excel و PDF و CSV."
+ },
+ {
+ "question": "كيف يمكنني تقييم المخاطر للمشروع؟",
+ "answer": "يمكنك تقييم المخاطر للمشروع من خلال وحدة المخاطر، حيث يمكنك إضافة المخاطر المحتملة وتقييم تأثيرها واحتماليتها، ثم وضع خطة الاستجابة المناسبة."
+ },
+ {
+ "question": "ما هي طرق التسعير المتاحة في النظام؟",
+ "answer": "يوفر النظام أربع طرق للتسعير: 1) التسعير القياسي، 2) التسعير غير المتزن، 3) التسعير التنافسي، 4) التسعير الموجه بالربحية. يمكنك اختيار الطريقة المناسبة حسب طبيعة المشروع واستراتيجية الشركة."
+ },
+ {
+ "question": "كيف يمكنني معالجة مستندات المناقصة ضخمة الحجم؟",
+ "answer": "يمكنك استخدام وحدة تحليل المستندات لمعالجة مستندات المناقصة ضخمة الحجم، حيث تقوم الوحدة بتحليل المستندات واستخراج المعلومات المهمة مثل مواصفات المشروع ومتطلباته وشروطه تلقائياً."
+ }
]
-
- return response
-
- def _generate_suggestions(self, query_type, query, response):
- """توليد اقتراحات للمستخدم بناءً على الاستعلام والاستجابة"""
- suggestions = []
-
- if query_type == "document_analysis":
- suggestions = [
- "كيف يمكنني استيراد نتائج تحليل المستندات إلى وحدة التسعير؟",
- "ما هي أنواع المستندات المدعومة للتحليل؟",
- "كيف يمكنني تحسين دقة تحليل المستندات؟"
- ]
- elif query_type == "pricing":
- suggestions = [
- "ما هي استراتيجية التسعير المناسبة لمشروعي؟",
- "كيف يمكنني حساب التكاليف غير المباشرة؟",
- "كيف أضيف تكاليف المخاطر إلى التسعير؟"
- ]
- elif query_type == "risk_analysis":
- suggestions = [
- "ما هي أفضل استراتيجيات التخفيف من المخاطر؟",
- "كيف يمكنني تحديد المخاطر الحرجة في المشروع؟",
- "كيف أدمج تحليل المخاطر في خطة المشروع؟"
- ]
- elif query_type == "project_management":
- suggestions = [
- "كيف أنشئ جدولاً زمنياً فعالاً للمشروع؟",
- "كيف أتتبع تقدم المشروع مقارنة بالخطة؟",
- "كيف أدير التغييرات في نطاق المشروع؟"
- ]
- elif query_type == "reporting":
- suggestions = [
- "ما هي أنواع التقارير المتاحة في النظام؟",
- "كيف يمكنني تخصيص تقرير المشروع؟",
- "كيف أقوم بجدولة إنشاء تقارير دورية؟"
+
+ def render(self):
+ """عرض واجهة وحدة المساعد الذكي"""
+
+ st.markdown("وحدة المساعد الذكي ", unsafe_allow_html=True)
+
+ tabs = st.tabs([
+ "المساعد الذكي",
+ "التنبؤ بالتكاليف",
+ "تحليل المخاطر",
+ "تحليل المستندات",
+ "المحتوى المحلي",
+ "الأسئلة الشائعة"
+ ])
+
+ with tabs[0]:
+ self._render_ai_assistant_tab()
+
+ with tabs[1]:
+ self._render_cost_prediction_tab()
+
+ with tabs[2]:
+ self._render_risk_analysis_tab()
+
+ with tabs[3]:
+ self._render_document_analysis_tab()
+
+ with tabs[4]:
+ self._render_local_content_tab()
+
+ with tabs[5]:
+ self._render_faq_tab()
+
+ def _render_ai_assistant_tab(self):
+ """عرض تبويب المساعد الذكي مع دعم Claude AI"""
+
+ st.markdown("### المساعد الذكي لتسعير المناقصات")
+
+ # اختيار نموذج Claude
+ claude_models = self.claude_service.get_available_models()
+
+ selected_model = st.radio(
+ "اختر نموذج الذكاء الاصطناعي",
+ options=list(claude_models.keys()),
+ format_func=lambda x: claude_models[x],
+ horizontal=True,
+ key="assistant_ai_model"
+ )
+
+ # عرض واجهة المحادثة
+ st.markdown("""
+
+
+
+ """, unsafe_allow_html=True)
+
+ # تهيئة محفوظات المحادثة في حالة الجلسة إذا لم تكن موجودة
+ if 'ai_assistant_messages' not in st.session_state:
+ st.session_state.ai_assistant_messages = [
+ {"role": "assistant", "content": "مرحباً! أنا المساعد الذكي لنظام تسعير المناقصات. كيف يمكنني مساعدتك اليوم؟"}
]
+
+ # عرض محفوظات المحادثة بتنسيق محسن
+ chat_container = st.container()
+ with chat_container:
+ for message in st.session_state.ai_assistant_messages:
+ if message["role"] == "user":
+ st.markdown(f"""
+
+
+ {message["content"]}
+
+
+ """, unsafe_allow_html=True)
+ else:
+ st.markdown(f"""
+
+
+ {message["content"]}
+
+
+ """, unsafe_allow_html=True)
+
+ # إضافة خيار رفع الملفات
+ uploaded_file = st.file_uploader(
+ "اختياري: ارفع ملفًا للمساعدة (صورة، PDF)",
+ type=["jpg", "jpeg", "png", "pdf"],
+ key="assistant_file_upload"
+ )
+
+ # مربع إدخال الرسالة
+ user_input = st.text_input("اكتب رسالتك هنا", key="ai_assistant_input")
+
+ # التحقق من وجود مفتاح API
+ api_available = True
+ try:
+ self.claude_service.get_api_key()
+ except ValueError:
+ api_available = False
+ st.warning("مفتاح API لـ Claude غير متوفر. يرجى التأكد من تعيين متغير البيئة 'anthropic'.")
+
+ if user_input and api_available:
+ # إضافة رسالة المستخدم إلى المحفوظات
+ st.session_state.ai_assistant_messages.append({"role": "user", "content": user_input})
+
+ # عرض محفوظات المحادثة المحدثة
+ with chat_container:
+ st.markdown(f"""
+
+ """, unsafe_allow_html=True)
+
+ # معالجة الرد
+ with st.spinner("جاري التفكير..."):
+ # التحقق مما إذا كان هناك ملف مرفق
+ if uploaded_file:
+ # حفظ الملف المرفوع مؤقتاً
+ with tempfile.NamedTemporaryFile(delete=False, suffix=f".{uploaded_file.name.split('.')[-1]}") as temp_file:
+ temp_file.write(uploaded_file.getbuffer())
+ temp_file_path = temp_file.name
+
+ # إذا كان الملف PDF، تحويله إلى صورة
+ if uploaded_file.name.lower().endswith('.pdf'):
+ if pdf_conversion_available:
+ try:
+ # تحويل الصفحة الأولى فقط
+ images = convert_from_path(temp_file_path, first_page=1, last_page=1)
+ if images:
+ # حفظ الصورة بشكل مؤقت
+ temp_image_path = f"{temp_file_path}_image.jpg"
+ images[0].save(temp_image_path, 'JPEG')
+ # استخدام مسار الصورة بدلاً من PDF
+ os.remove(temp_file_path)
+ temp_file_path = temp_image_path
+ except Exception as e:
+ st.error(f"فشل في تحويل ملف PDF إلى صورة: {str(e)}")
+ else:
+ st.error("تحليل ملفات PDF يتطلب تثبيت مكتبة pdf2image.")
+ response = "عذراً، لا يمكنني تحليل ملفات PDF في الوقت الحالي. يرجى تحويل الملف إلى صورة أو مشاركة المعلومات كنص."
+
+ # تحليل الصورة باستخدام Claude
+ prompt = f"المستخدم قام برفع هذه الصورة وسأل: {user_input}\nقم بتحليل الصورة والرد على سؤال المستخدم بشكل تفصيلي."
+ results = self.claude_service.analyze_image(temp_file_path, prompt, model_name=selected_model)
+
+ # حذف الملف المؤقت
+ try:
+ os.remove(temp_file_path)
+ except:
+ pass
+
+ if "error" in results:
+ response = f"عذراً، حدث خطأ أثناء تحليل الملف: {results['error']}"
+ else:
+ response = results["content"]
+ else:
+ # استخدام خدمة Claude للرد على الرسائل النصية
+ results = self.claude_service.chat_completion(st.session_state.ai_assistant_messages, model_name=selected_model)
+
+ if "error" in results:
+ response = f"عذراً، حدث خطأ أثناء معالجة طلبك: {results['error']}"
+ else:
+ response = results["content"]
+
+ # إضافة رد المساعد إلى المحفوظات
+ st.session_state.ai_assistant_messages.append({"role": "assistant", "content": response})
+
+ # عرض رد المساعد
+ with chat_container:
+ st.markdown(f"""
+
+ """, unsafe_allow_html=True)
+
+ # إعادة تعيين قيمة الإدخال
+ st.text_input("اكتب رسالتك هنا", value="", key="ai_assistant_input_reset")
+
+ def _generate_ai_response(self, user_input, model_name="claude-3-7-sonnet"):
+ """توليد رد المساعد الذكي باستخدام Claude AI"""
+
+ # التحقق من وجود مفتاح API
+ try:
+ self.claude_service.get_api_key()
+ except ValueError:
+ return "عذراً، لا يمكنني الاتصال بخدمة الذكاء الاصطناعي في الوقت الحالي. يرجى التحقق من إعدادات API."
+
+ # البحث في الأسئلة الشائعة أولاً
+ for faq in self.faqs:
+ if any(keyword in user_input.lower() for keyword in faq["question"].lower().split()):
+ return f"{faq['answer']}\n\nهل تحتاج إلى مساعدة أخرى؟"
+
+ # إنشاء محادثة لإرسالها إلى Claude
+ messages = [
+ {"role": "user", "content": user_input}
+ ]
+
+ # استدعاء خدمة Claude
+ results = self.claude_service.chat_completion(messages, model_name=model_name)
+
+ if "error" in results:
+ # إذا فشل الاتصال، استخدم التوليد الافتراضي
+ logging.warning(f"فشل الاتصال بـ Claude AI: {results['error']}. استخدام التوليد الافتراضي.")
+ return self._generate_default_response(user_input)
+ else:
+ return results["content"]
+
+ def _generate_default_response(self, user_input):
+ """توليد رد افتراضي في حالة عدم توفر Claude AI"""
+
+ if "تسعير" in user_input or "سعر" in user_input or "تكلفة" in user_input:
+ return "يمكنك استخدام وحدة التنبؤ بالتكاليف لتقدير تكاليف المشروع بناءً على خصائصه. انتقل إلى تبويب 'التنبؤ بالتكاليف' وأدخل بيانات المشروع لتحصل على تقدير دقيق للتكاليف."
+
+ elif "مخاطر" in user_input or "مخاطرة" in user_input:
+ return "يمكنك استخدام وحدة تحليل المخاطر لتقييم المخاطر المحتملة للمشروع. انتقل إلى تبويب 'تحليل المخاطر' وأدخل بيانات المشروع وعوامل المخاطرة لتحصل على تحليل شامل للمخاطر واستراتيجيات الاستجابة المقترحة."
+
+ elif "مستند" in user_input or "ملف" in user_input or "وثيقة" in user_input or "مناقصة" in user_input:
+ return "يمكنك استخدام وحدة تحليل المستندات لتحليل مستندات المناقصة واستخراج المعلومات المهمة منها. انتقل إلى تبويب 'تحليل المستندات' وقم بتحميل ملفات المناقصة لتحصل على تحليل تفصيلي للمستندات."
+
+ elif "محتوى محلي" in user_input or "محلي" in user_input:
+ return "يمكنك استخدام وحدة المحتوى المحلي لحساب وتحسين نسبة المحتوى المحلي في مشروعك. انتقل إلى تبويب 'المحتوى المحلي' وأدخل بيانات مكونات المشروع لتحصل على تحليل شامل للمحتوى المحلي واقتراحات لتحسينه."
+
+ elif "تقرير" in user_input or "إحصائيات" in user_input or "بيانات" in user_input:
+ return "يمكنك استخدام وحدة التقارير والتحليلات للحصول على تقارير تفصيلية وإحصائيات عن المشاريع. يمكنك الوصول إليها من القائمة الرئيسية للنظام."
+
else:
- suggestions = [
- "كيف يمكنني البدء باستخدام النظام؟",
- "ما هي الوحدات المتاحة في النظام؟",
- "كيف يمكنني إنشاء مشروع جديد؟"
+ return "شكراً لاستفسارك. يمكنني مساعدتك في تسعير المناقصات، وتحليل المخاطر، وتحليل المستندات، وحساب المحتوى المحلي. يرجى توضيح استفسارك أكثر أو اختيار أحد الخيارات في الأعلى للحصول على المساعدة المطلوبة."
+
+ def _render_cost_prediction_tab(self):
+ """عرض تبويب التنبؤ بالتكاليف"""
+
+ st.markdown("### التنبؤ بالتكاليف")
+
+ # عرض نموذج إدخال بيانات المشروع
+ st.markdown("#### بيانات المشروع")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_type = st.selectbox(
+ "نوع المشروع",
+ [
+ "مباني سكنية",
+ "مباني تجارية",
+ "مباني حكومية",
+ "مراكز صحية",
+ "مدارس",
+ "بنية تحتية",
+ "طرق",
+ "جسور",
+ "صرف صحي",
+ "مياه",
+ "كهرباء"
+ ],
+ key="cost_project_type"
+ )
+
+ location = st.selectbox(
+ "الموقع",
+ [
+ "الرياض",
+ "جدة",
+ "الدمام",
+ "مكة",
+ "المدينة",
+ "تبوك",
+ "حائل",
+ "عسير",
+ "جازان",
+ "نجران",
+ "الباحة",
+ "الجوف",
+ "القصيم"
+ ],
+ key="cost_location"
+ )
+
+ client_type = st.selectbox(
+ "نوع العميل",
+ [
+ "حكومي",
+ "شبه حكومي",
+ "شركة كبيرة",
+ "شركة متوسطة",
+ "شركة صغيرة",
+ "أفراد"
+ ],
+ key="cost_client_type"
+ )
+
+ with col2:
+ area = st.number_input("المساحة (م²)", min_value=100, max_value=1000000, value=5000, key="cost_area")
+
+ floors = st.number_input("عدد الطوابق", min_value=1, max_value=100, value=3, key="cost_floors")
+
+ duration = st.number_input("مدة التنفيذ (شهور)", min_value=1, max_value=60, value=12, key="cost_duration")
+
+ tender_type = st.selectbox(
+ "نوع المناقصة",
+ [
+ "عامة",
+ "خاصة",
+ "أمر مباشر"
+ ],
+ key="cost_tender_type"
+ )
+
+ st.markdown("#### متغيرات إضافية")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ has_basement = st.checkbox("يتضمن بدروم", key="cost_has_basement")
+ has_special_finishing = st.checkbox("تشطيبات خاصة", key="cost_has_special_finishing")
+
+ with col2:
+ has_landscape = st.checkbox("أعمال تنسيق المواقع", key="cost_has_landscape")
+ has_parking = st.checkbox("مواقف متعددة الطوابق", key="cost_has_parking")
+
+ with col3:
+ has_smart_systems = st.checkbox("أنظمة ذكية", key="cost_has_smart_systems")
+ has_sustainability = st.checkbox("متطلبات استدامة", key="cost_has_sustainability")
+
+ # زر التنبؤ بالتكلفة مع دعم Claude AI
+ col1, col2 = st.columns([1, 3])
+
+ with col1:
+ predict_button = st.button("التنبؤ بالتكلفة", use_container_width=True, key="cost_predict_button")
+
+ with col2:
+ use_claude = st.checkbox("استخدام Claude AI للتحليل المتقدم", value=True, key="cost_use_claude")
+
+ if predict_button:
+ with st.spinner("جاري تحليل البيانات والتنبؤ بالتكاليف..."):
+ # محاكاة وقت المعالجة
+ time.sleep(2)
+
+ # تجهيز البيانات للنموذج
+ features = {
+ 'project_type': project_type,
+ 'location': location,
+ 'area': area,
+ 'floors': floors,
+ 'duration_months': duration,
+ 'tender_type': tender_type,
+ 'client_type': client_type,
+ 'has_basement': has_basement,
+ 'has_special_finishing': has_special_finishing,
+ 'has_landscape': has_landscape,
+ 'has_parking': has_parking,
+ 'has_smart_systems': has_smart_systems,
+ 'has_sustainability': has_sustainability
+ }
+
+ # استدعاء النموذج للتنبؤ
+ cost_prediction_results = self._predict_cost(features)
+
+ # إضافة تحليل إضافي باستخدام Claude AI إذا تم تفعيل الخيار
+ if use_claude:
+ try:
+ # إنشاء نص الميزات للتحليل
+ features_text = f"""
+ بيانات المشروع:
+ - نوع المشروع: {project_type}
+ - الموقع: {location}
+ - المساحة: {area} م²
+ - عدد الطوابق: {floors}
+ - مدة التنفيذ: {duration} شهر
+ - نوع المناقصة: {tender_type}
+ - نوع العميل: {client_type}
+ - يتضمن بدروم: {'نعم' if has_basement else 'لا'}
+ - تشطيبات خاصة: {'نعم' if has_special_finishing else 'لا'}
+ - أعمال تنسيق المواقع: {'نعم' if has_landscape else 'لا'}
+ - مواقف متعددة الطوابق: {'نعم' if has_parking else 'لا'}
+ - أنظمة ذكية: {'نعم' if has_smart_systems else 'لا'}
+ - متطلبات استدامة: {'نعم' if has_sustainability else 'لا'}
+
+ نتائج التنبؤ الأولية:
+ - التكلفة الإجمالية المقدرة: {cost_prediction_results['total_cost']:,.0f} ريال
+ - تكلفة المتر المربع: {cost_prediction_results['cost_per_sqm']:,.0f} ريال/م²
+ - تكلفة المواد: {cost_prediction_results['material_cost']:,.0f} ريال
+ - تكلفة العمالة: {cost_prediction_results['labor_cost']:,.0f} ريال
+ - تكلفة المعدات: {cost_prediction_results['equipment_cost']:,.0f} ريال
+ """
+
+ prompt = f"""تحليل بيانات مشروع وتكاليفه:
+
+ {features_text}
+
+ المطلوب:
+ 1. تحليل التكاليف المتوقعة ومعقوليتها مقارنة بمشاريع مماثلة في السوق السعودي
+ 2. تقديم توصيات وملاحظات لتحسين التكلفة
+ 3. تحديد أي مخاطر محتملة قد تؤثر على التكلفة
+ 4. تقديم نصائح لزيادة فعالية التكلفة
+ 5. تقديم رأي حول مدى تنافسية هذه التكلفة في السوق الحالي
+
+ يرجى تقديم تحليل مهني ومختصر يركز على الجوانب الأكثر أهمية.
+ """
+
+ # استدعاء Claude للتحليل
+ claude_analysis = self.claude_service.chat_completion(
+ [{"role": "user", "content": prompt}]
+ )
+
+ if "error" not in claude_analysis:
+ # إضافة تحليل Claude إلى النتائج
+ cost_prediction_results["claude_analysis"] = claude_analysis["content"]
+ except Exception as e:
+ st.warning(f"تعذر إجراء التحليل المتقدم: {str(e)}")
+
+ # عرض نتائج التنبؤ
+ self._display_cost_prediction_results(cost_prediction_results)
+
+ def _predict_cost(self, features):
+ """التنبؤ بتكاليف المشروع"""
+
+ # في البيئة الحقيقية، سيتم استدعاء نموذج التنبؤ بالتكاليف
+ # محاكاة نتائج التنبؤ للعرض
+
+ # حساب القيمة الأساسية للمتر المربع حسب نوع المشروع
+ base_cost_per_sqm = {
+ "مباني سكنية": 2500,
+ "مباني تجارية": 3000,
+ "مباني حكومية": 3500,
+ "مراكز صحية": 4000,
+ "مدارس": 3200,
+ "بنية تحتية": 2000,
+ "طرق": 1500,
+ "جسور": 5000,
+ "صرف صحي": 2200,
+ "مياه": 2000,
+ "كهرباء": 2500
+ }.get(features['project_type'], 2500)
+
+ # تطبيق معاملات التعديل حسب المتغيرات
+ location_factor = {
+ "الرياض": 1.1,
+ "جدة": 1.15,
+ "الدمام": 1.05,
+ "مكة": 1.2,
+ "المدينة": 1.1,
+ "تبوك": 0.95,
+ "حائل": 0.9,
+ "عسير": 0.95,
+ "جازان": 0.9,
+ "نجران": 0.85,
+ "الباحة": 0.9,
+ "الجوف": 0.85,
+ "القصيم": 0.9
+ }.get(features['location'], 1.0)
+
+ client_factor = {
+ "حكومي": 1.05,
+ "شبه حكومي": 1.0,
+ "شركة كبيرة": 0.95,
+ "شركة متوسطة": 0.9,
+ "شركة صغيرة": 0.85,
+ "أفراد": 0.8
+ }.get(features['client_type'], 1.0)
+
+ tender_factor = {
+ "عامة": 1.0,
+ "خاصة": 0.95,
+ "أمر مباشر": 0.9
+ }.get(features['tender_type'], 1.0)
+
+ # معاملات للميزات الإضافية
+ basement_factor = 1.1 if features['has_basement'] else 1.0
+ special_finishing_factor = 1.2 if features['has_special_finishing'] else 1.0
+ landscape_factor = 1.05 if features['has_landscape'] else 1.0
+ parking_factor = 1.1 if features['has_parking'] else 1.0
+ smart_systems_factor = 1.15 if features['has_smart_systems'] else 1.0
+ sustainability_factor = 1.1 if features['has_sustainability'] else 1.0
+
+ # معامل لعدد الطوابق
+ floors_factor = 1.0 + (features['floors'] - 1) * 0.05
+
+ # حساب التكلفة الإجمالية
+ total_sqm_cost = base_cost_per_sqm * location_factor * client_factor * tender_factor * \
+ basement_factor * special_finishing_factor * landscape_factor * \
+ parking_factor * smart_systems_factor * sustainability_factor * \
+ floors_factor
+
+ total_cost = total_sqm_cost * features['area']
+
+ # حساب التكاليف المفصلة
+ material_cost = total_cost * 0.6
+ labor_cost = total_cost * 0.25
+ equipment_cost = total_cost * 0.15
+
+ # إضافة هامش خطأ عشوائي للمحاكاة
+ error_margin = 0.05 # 5%
+ total_cost = total_cost * (1 + np.random.uniform(-error_margin, error_margin))
+
+ # إعداد النتائج
+ results = {
+ "total_cost": total_cost,
+ "cost_per_sqm": total_cost / features['area'],
+ "material_cost": material_cost,
+ "labor_cost": labor_cost,
+ "equipment_cost": equipment_cost,
+ "breakdown": {
+ "structural_works": total_cost * 0.35,
+ "architectural_works": total_cost * 0.25,
+ "mep_works": total_cost * 0.25,
+ "site_works": total_cost * 0.1,
+ "general_requirements": total_cost * 0.05
+ },
+ "confidence_level": 0.85, # مستوى الثقة في التنبؤ
+ "comparison": {
+ "market_average": total_cost * 1.1,
+ "historical_projects": total_cost * 0.95
+ }
+ }
+
+ return results
+
+ def _display_cost_prediction_results(self, results):
+ """عرض نتائج التنبؤ بالتكاليف"""
+
+ st.markdown("### نتائج التنبؤ بالتكاليف")
+
+ # عرض التكلفة الإجمالية وتكلفة المتر المربع
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric(
+ "التكلفة الإجمالية المتوقعة",
+ f"{results['total_cost']:,.0f} ريال",
+ delta=f"{(results['total_cost'] - results['comparison']['historical_projects']):,.0f} ريال"
+ )
+
+ with col2:
+ st.metric(
+ "تكلفة المتر المربع",
+ f"{results['cost_per_sqm']:,.0f} ريال/م²"
+ )
+
+ with col3:
+ st.metric(
+ "مستوى الثقة في التنبؤ",
+ f"{results['confidence_level'] * 100:.0f}%"
+ )
+
+ # عرض تفصيل التكاليف
+ st.markdown("#### تفصيل التكاليف")
+
+ # رسم مخطط دائري للتكاليف المفصلة
+ fig = px.pie(
+ values=[
+ results['material_cost'],
+ results['labor_cost'],
+ results['equipment_cost']
+ ],
+ names=["تكلفة المواد", "تكلفة العمالة", "تكلفة المعدات"],
+ title="توزيع التكاليف الرئيسية"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # رسم مخطط شريطي لتفصيل الأعمال
+ breakdown_data = pd.DataFrame({
+ 'فئة الأعمال': [
+ "الأعمال الإنشائية",
+ "الأعمال المعمارية",
+ "الأعمال الكهروميكانيكية",
+ "أعمال الموقع",
+ "المتطلبات العامة"
+ ],
+ 'التكلفة': [
+ results['breakdown']['structural_works'],
+ results['breakdown']['architectural_works'],
+ results['breakdown']['mep_works'],
+ results['breakdown']['site_works'],
+ results['breakdown']['general_requirements']
]
-
- return suggestions
-
- def get_processing_status(self):
- """الحصول على حالة المعالجة الحالية"""
- if not self.processing_in_progress:
- if not self.processing_results:
- return {"status": "لا توجد معالجة جارية"}
- else:
- return {"status": self.processing_results.get("status", "غير معروف")}
-
- return {
- "status": "جاري المعالجة",
- "query": self.current_query,
- "start_time": self.processing_results.get("processing_start_time")
+ })
+
+ fig = px.bar(
+ breakdown_data,
+ x='فئة الأعمال',
+ y='التكلفة',
+ title="تفصيل التكاليف حسب فئة الأعمال",
+ text_auto='.3s'
+ )
+
+ fig.update_traces(texttemplate='%{text:,.0f} ريال', textposition='outside')
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض مقارنة مع متوسط السوق
+ st.markdown("#### مقارنة مع متوسط السوق")
+
+ comparison_data = pd.DataFrame({
+ 'المصدر': [
+ "التكلفة المتوقعة",
+ "متوسط السوق",
+ "مشاريع مماثلة سابقة"
+ ],
+ 'التكلفة': [
+ results['total_cost'],
+ results['comparison']['market_average'],
+ results['comparison']['historical_projects']
+ ]
+ })
+
+ fig = px.bar(
+ comparison_data,
+ x='المصدر',
+ y='التكلفة',
+ title="مقارنة التكلفة المتوقعة مع السوق",
+ text_auto='.3s',
+ color='المصدر',
+ color_discrete_map={
+ "التكلفة المتوقعة": "#1f77b4",
+ "متوسط السوق": "#ff7f0e",
+ "مشاريع مماثلة سابقة": "#2ca02c"
+ }
+ )
+
+ fig.update_traces(texttemplate='%{text:,.0f} ريال', textposition='outside')
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض تحليل Claude AI إذا كان متوفراً
+ if "claude_analysis" in results:
+ st.markdown("### تحليل Claude AI المتقدم")
+ st.info(results["claude_analysis"])
+
+ # عرض ملاحظات وتوصيات
+ st.markdown("#### ملاحظات وتوصيات")
+
+ st.info("""
+ - تم التنبؤ بالتكاليف بناءً على البيانات المدخلة ونماذج التعلم الآلي المدربة على مشاريع مماثلة.
+ - مستوى الثقة في التنبؤ جيد، ولكن يجب مراجعة التكاليف بشكل تفصيلي قبل اتخاذ القرار النهائي.
+ - تكلفة المتر المربع متوافقة مع متوسط السوق لهذا النوع من المشاريع.
+ - ينصح بمراجعة التصميم لتحسين التكلفة وزيادة الكفاءة.
+ """)
+
+ # زر تصدير التقرير
+ if st.button("تصدير تقرير التكاليف"):
+ st.success("تم تصدير تقرير التكاليف بنجاح!")
+
+ def _render_risk_analysis_tab(self):
+ """عرض تبويب تحليل المخاطر"""
+
+ st.markdown("### تحليل المخاطر")
+
+ # عرض نموذج إدخال بيانات المشروع للمخاطر
+ st.markdown("#### بيانات المشروع")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_type = st.selectbox(
+ "نوع المشروع",
+ [
+ "مباني سكنية",
+ "مباني تجارية",
+ "مباني حكومية",
+ "مراكز صحية",
+ "مدارس",
+ "بنية تحتية",
+ "طرق",
+ "جسور",
+ "صرف صحي",
+ "مياه",
+ "كهرباء"
+ ],
+ key="risk_project_type"
+ )
+
+ location = st.selectbox(
+ "الموقع",
+ [
+ "الرياض",
+ "جدة",
+ "الدمام",
+ "مكة",
+ "المدينة",
+ "تبوك",
+ "حائل",
+ "عسير",
+ "جازان",
+ "نجران",
+ "الباحة",
+ "الجوف",
+ "القصيم"
+ ],
+ key="risk_location"
+ )
+
+ with col2:
+ client_type = st.selectbox(
+ "نوع العميل",
+ [
+ "حكومي",
+ "شبه حكومي",
+ "شركة كبيرة",
+ "شركة متوسطة",
+ "شركة صغيرة",
+ "أفراد"
+ ],
+ key="risk_client_type"
+ )
+
+ tender_type = st.selectbox(
+ "نوع المناقصة",
+ [
+ "عامة",
+ "خاصة",
+ "أمر مباشر"
+ ],
+ key="risk_tender_type"
+ )
+
+ st.markdown("#### عوامل المخاطرة")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ payment_terms = st.slider("شروط الدفع (1-10)", 1, 10, 5,
+ help="1: شروط دفع سيئة جداً، 10: شروط دفع ممتازة",
+ key="risk_payment_terms")
+ completion_deadline = st.slider("مهلة الإنجاز (1-10)", 1, 10, 5,
+ help="1: مهلة قصيرة جداً، 10: مهلة مريحة",
+ key="risk_completion_deadline")
+
+ with col2:
+ penalty_clause = st.slider("شروط الغرامات (1-10)", 1, 10, 5,
+ help="1: غرامات مرتفعة جداً، 10: غرامات معقولة",
+ key="risk_penalty_clause")
+ technical_complexity = st.slider("التعقيد الفني (1-10)", 1, 10, 5,
+ help="1: بسيط جداً، 10: معقد للغاية",
+ key="risk_technical_complexity")
+
+ with col3:
+ company_experience = st.slider("خبرة الشركة (1-10)", 1, 10, 7,
+ help="1: لا توجد خبرة، 10: خبرة عالية",
+ key="risk_company_experience")
+ market_volatility = st.slider("تقلبات السوق (1-10)", 1, 10, 5,
+ help="1: مستقر جداً، 10: متقلب للغاية",
+ key="risk_market_volatility")
+
+ # زر تحليل المخاطر مع دعم Claude AI
+ col1, col2 = st.columns([1, 3])
+
+ with col1:
+ analyze_button = st.button("تحليل المخاطر", use_container_width=True, key="risk_analyze_button")
+
+ with col2:
+ # Añadimos un key único para este checkbox
+ use_claude = st.checkbox("استخدام Claude AI للتحليل المتقدم", value=True, key="risk_use_claude")
+
+ if analyze_button:
+ with st.spinner("جاري تحليل المخاطر..."):
+ # محاكاة وقت المعالجة
+ time.sleep(2)
+
+ # تجهيز البيانات للنموذج
+ features = {
+ 'project_type': project_type,
+ 'location': location,
+ 'client_type': client_type,
+ 'tender_type': tender_type,
+ 'payment_terms': payment_terms,
+ 'completion_deadline': completion_deadline,
+ 'penalty_clause': penalty_clause,
+ 'technical_complexity': technical_complexity,
+ 'company_experience': company_experience,
+ 'market_volatility': market_volatility
+ }
+
+ # استدعاء النموذج لتحليل المخاطر
+ risk_analysis_results = self._analyze_risks(features)
+
+ # إضافة تحليل إضافي باستخدام Claude AI إذا تم تفعيل الخيار
+ if use_claude:
+ try:
+ # إنشاء نص الميزات للتحليل
+ features_text = f"""
+ بيانات المشروع:
+ - نوع المشروع: {project_type}
+ - الموقع: {location}
+ - نوع العميل: {client_type}
+ - نوع المناقصة: {tender_type}
+
+ عوامل المخاطرة:
+ - شروط الدفع: {payment_terms}/10
+ - مهلة الإنجاز: {completion_deadline}/10
+ - شروط الغرامات: {penalty_clause}/10
+ - التعقيد الفني: {technical_complexity}/10
+ - خبرة الشركة: {company_experience}/10
+ - تقلبات السوق: {market_volatility}/10
+
+ ملخص التحليل الأولي:
+ - متوسط درجة المخاطرة: {risk_analysis_results['avg_risk_score']:.1f}/10
+ - عدد المخاطر العالية: {risk_analysis_results['high_risks']}
+ - عدد المخاطر المتوسطة: {risk_analysis_results['medium_risks']}
+ - عدد المخاطر المنخفضة: {risk_analysis_results['low_risks']}
+
+ أعلى المخاطر:
+ """
+
+ # إضافة تفاصيل أعلى المخاطر
+ for i, risk in enumerate(risk_analysis_results['top_risks'][:3]):
+ features_text += f"""
+ {i+1}. {risk['name']} ({risk['category']})
+ - الاحتمالية: {risk['probability'] * 100:.0f}%
+ - التأثير: {risk['impact'] * 100:.0f}%
+ - درجة المخاطرة: {risk['risk_score']}/10
+ """
+
+ prompt = f"""تحليل مخاطر مشروع:
+
+ {features_text}
+
+ المطلوب:
+ 1. تحليل عوامل المخاطرة وتأثيرها على المشروع
+ 2. تقديم توصيات إضافية لإدارة المخاطر
+ 3. اقتراح استراتيجيات استجابة للمخاطر الرئيسية
+ 4. تقديم نصائح لتحسين شروط العقد لتقليل المخاطر
+ 5. تقييم مدى ملاءمة المشروع لاستراتيجية الشركة
+
+ يرجى تقديم تحليل مهني ومختصر يركز على الجوانب الأكثر أهمية.
+ """
+
+ # استدعاء Claude للتحليل
+ claude_analysis = self.claude_service.chat_completion(
+ [{"role": "user", "content": prompt}]
+ )
+
+ if "error" not in claude_analysis:
+ # إضافة تحليل Claude إلى النتائج
+ risk_analysis_results["claude_analysis"] = claude_analysis["content"]
+ except Exception as e:
+ st.warning(f"تعذر إجراء التحليل المتقدم: {str(e)}")
+
+ # عرض نتائج تحليل المخاطر
+ self._display_risk_analysis_results(risk_analysis_results)
+
+ def _analyze_risks(self, features):
+ """تحليل مخاطر المشروع"""
+
+ # في البيئة الحقيقية، سيتم استدعاء نموذج تحليل المخاطر
+ # محاكاة نتائج تحليل المخاطر للعرض
+
+ # تعريف قائمة من المخاطر المحتملة
+ potential_risks = [
+ {
+ "id": "R-001",
+ "name": "غرامة تأخير مرتفعة",
+ "category": "مخاطر مالية",
+ "description": "غرامة تأخير مرتفعة تصل إلى 10% من قيمة العقد، مما قد يؤثر سلباً على ربحية المشروع في حال التأخير.",
+ "probability": 0.6,
+ "impact": 0.8,
+ "risk_score": 7.8,
+ "response_strategy": "تخطيط مفصل للمشروع مع وضع مخزون زمني مناسب وتحديد نقاط التسليم المبكر."
+ },
+ {
+ "id": "R-002",
+ "name": "تقلبات أسعار المواد",
+ "category": "مخاطر السوق",
+ "description": "ارتفاع محتمل في أسعار المواد الخام خلال فترة تنفيذ المشروع، مما يؤثر على التكلفة الإجمالية.",
+ "probability": 0.7,
+ "impact": 0.7,
+ "risk_score": 7.5,
+ "response_strategy": "التعاقد المبكر مع الموردين وتثبيت الأسعار، أو إضافة بند تعديل سعري في العقد."
+ },
+ {
+ "id": "R-003",
+ "name": "ضعف تدفق المدفوعات",
+ "category": "مخاطر مالية",
+ "description": "تأخر العميل في سداد المستخلصات مما يؤثر على التدفق النقدي للمشروع.",
+ "probability": 0.5,
+ "impact": 0.8,
+ "risk_score": 7.2,
+ "response_strategy": "التفاوض على شروط دفع واضحة ومواعيد محددة، وإمكانية طلب دفعة مقدمة."
+ },
+ {
+ "id": "R-004",
+ "name": "نقص العمالة الماهرة",
+ "category": "مخاطر الموارد",
+ "description": "صعوبة توفير عمالة ماهرة لتنفيذ أجزاء محددة من المشروع.",
+ "probability": 0.5,
+ "impact": 0.6,
+ "risk_score": 6.5,
+ "response_strategy": "التخطيط المبكر للموارد البشرية وتوقيع عقود مع مقاولي الباطن المتخصصين."
+ },
+ {
+ "id": "R-005",
+ "name": "تغييرات في نطاق العمل",
+ "category": "مخاطر تعاقدية",
+ "description": "طلبات تغيير من العميل تؤدي إلى زيادة نطاق العمل دون تعديل مناسب للتكلفة والجدول الزمني.",
+ "probability": 0.6,
+ "impact": 0.6,
+ "risk_score": 6.0,
+ "response_strategy": "تضمين آلية واضحة لإدارة التغيير في العقد وتقييم تأثير أي تغييرات على التكلفة والزمن."
+ },
+ {
+ "id": "R-006",
+ "name": "مشاكل في الموقع",
+ "category": "مخاطر فنية",
+ "description": "ظروف موقع غير متوقعة تؤثر على تنفيذ الأعمال، مثل مشاكل في التربة أو مرافق تحت الأرض.",
+ "probability": 0.4,
+ "impact": 0.7,
+ "risk_score": 5.8,
+ "response_strategy": "إجراء دراسات واختبارات مفصلة للموقع قبل بدء التنفيذ، وتخصيص احتياطي للطوارئ."
+ },
+ {
+ "id": "R-007",
+ "name": "تضارب في التصاميم",
+ "category": "مخاطر فنية",
+ "description": "تعارض بين مختلف تخصصات التصميم (معماري، إنشائي، كهروميكانيكي) يؤدي إلى تأخير وإعادة عمل.",
+ "probability": 0.4,
+ "impact": 0.6,
+ "risk_score": 5.4,
+ "response_strategy": "مراجعة شاملة للتصاميم قبل البدء في التنفيذ واستخدام نمذجة معلومات البناء (BIM) لكشف التعارضات."
+ },
+ {
+ "id": "R-008",
+ "name": "تأخر الموافقات",
+ "category": "مخاطر تنظيمية",
+ "description": "تأخر في الحصول على الموافقات والتصاريح اللازمة من الجهات المختصة.",
+ "probability": 0.5,
+ "impact": 0.5,
+ "risk_score": 5.0,
+ "response_strategy": "التخطيط المبكر للتصاريح المطلوبة وبناء علاقات جيدة مع الجهات التنظيمية."
+ },
+ {
+ "id": "R-009",
+ "name": "عدم توفر المعدات",
+ "category": "مخاطر الموارد",
+ "description": "صعوبة في توفير المعدات المتخصصة في الوقت المطلوب.",
+ "probability": 0.3,
+ "impact": 0.6,
+ "risk_score": 4.8,
+ "response_strategy": "حجز المعدات مبكراً وتوفير بدائل محتملة في حالة عدم توفر المعدات الأساسية."
+ },
+ {
+ "id": "R-010",
+ "name": "ظروف جوية قاسية",
+ "category": "مخاطر خارجية",
+ "description": "تأثير الظروف الجوية القاسية (حرارة شديدة، أمطار غزيرة، عواصف رملية) على سير العمل.",
+ "probability": 0.3,
+ "impact": 0.5,
+ "risk_score": 4.5,
+ "response_strategy": "تخطيط الجدول الزمني مع مراعاة المواسم وإضافة مخزون زمني للظروف الجوية غير المتوقعة."
+ }
+ ]
+
+ # حساب درجات المخاطرة بناءً على الميزات المدخلة
+ for risk in potential_risks:
+ # تعديل احتمالية حدوث المخاطر بناءً على العوامل المدخلة
+ if risk["id"] == "R-001": # غرامة تأخير
+ risk["probability"] = risk["probability"] * (10 - features["penalty_clause"]) / 10
+ risk["probability"] = risk["probability"] * (10 - features["completion_deadline"]) / 10
+
+ elif risk["id"] == "R-002": # تقلبات أسعار المواد
+ risk["probability"] = risk["probability"] * features["market_volatility"] / 10
+
+ elif risk["id"] == "R-003": # ضعف تدفق المدفوعات
+ risk["probability"] = risk["probability"] * (10 - features["payment_terms"]) / 10
+
+ if features["client_type"] == "حكومي":
+ risk["probability"] = risk["probability"] * 0.6 # احتمالية أقل مع العملاء الحكوميين
+ elif features["client_type"] == "أفراد":
+ risk["probability"] = risk["probability"] * 1.3 # احتمالية أعلى مع العملاء الأفراد
+
+ elif risk["id"] == "R-004": # نقص العمالة الماهرة
+ risk["probability"] = risk["probability"] * features["technical_complexity"] / 10
+
+ elif risk["id"] == "R-005": # تغييرات في نطاق العمل
+ risk["probability"] = risk["probability"] * features["technical_complexity"] / 10
+
+ if features["client_type"] == "حكومي":
+ risk["probability"] = risk["probability"] * 1.2 # احتمالية أعلى للتغييرات مع العملاء الحكوميين
+
+ # تعديل تأثير المخاطر بناءً على العوامل المدخلة
+ if risk["category"] == "مخاطر فنية":
+ risk["impact"] = risk["impact"] * (10 - features["company_experience"]) / 10
+
+ # إعادة حساب درجة المخاطرة
+ risk["risk_score"] = round(risk["probability"] * risk["impact"] * 10, 1)
+
+ # ترتيب المخاطر تنازلياً حسب درجة المخاطرة
+ sorted_risks = sorted(potential_risks, key=lambda x: x["risk_score"], reverse=True)
+
+ # حساب عدد المخاطر حسب شدتها
+ high_risks = sum(1 for risk in sorted_risks if risk["risk_score"] >= 6.0)
+ medium_risks = sum(1 for risk in sorted_risks if 3.0 <= risk["risk_score"] < 6.0)
+ low_risks = sum(1 for risk in sorted_risks if risk["risk_score"] < 3.0)
+
+ # حساب متوسط درجة المخاطرة
+ avg_risk_score = sum(risk["risk_score"] for risk in sorted_risks) / len(sorted_risks)
+
+ # تجهيز النتائج
+ results = {
+ "top_risks": sorted_risks,
+ "high_risks": high_risks,
+ "medium_risks": medium_risks,
+ "low_risks": low_risks,
+ "avg_risk_score": avg_risk_score,
+ "risk_profile": {
+ "financial_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر مالية") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر مالية") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر مالية") > 0 else 0,
+ "technical_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر فنية") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر فنية") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر فنية") > 0 else 0,
+ "market_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر السوق") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر السوق") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر السوق") > 0 else 0,
+ "resource_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر الموارد") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر الموارد") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر الموارد") > 0 else 0,
+ "contract_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر تعاقدية") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر تعاقدية") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر تعاقدية") > 0 else 0,
+ "regulatory_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر تنظيمية") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر تنظيمية") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر تنظيمية") > 0 else 0
+ },
+ "overall_assessment": "",
+ "recommendation": ""
}
-
- def get_processing_results(self):
- """الحصول على نتائج المعالجة"""
- return self.processing_results
-
- def get_conversation_history(self, limit=10):
- """الحصول على سجل المحادثة"""
- if limit and limit > 0:
- return self.conversation_history[-limit:]
- return self.conversation_history
-
- def clear_conversation_history(self):
- """مسح سجل المحادثة"""
- self.conversation_history = []
- return True
-
- def export_conversation_history(self, output_path=None):
- """تصدير سجل المحادثة إلى ملف JSON"""
- if not self.conversation_history:
- logger.warning("لا يوجد سجل محادثة للتصدير")
- return None
-
- if not output_path:
- # إنشاء اسم ملف افتراضي
- timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
- filename = f"conversation_history_{timestamp}.json"
- output_path = os.path.join(self.exports_path, filename)
-
- try:
- with open(output_path, 'w', encoding='utf-8') as f:
- json.dump(self.conversation_history, f, ensure_ascii=False, indent=4)
-
- logger.info(f"تم تصدير سجل المحادثة إلى: {output_path}")
- return output_path
-
- except Exception as e:
- logger.error(f"خطأ في تصدير سجل المحادثة: {str(e)}")
- return None
+
+ # تقييم شامل للمخاطر
+ if avg_risk_score >= 6.0:
+ results["overall_assessment"] = "مشروع عالي المخاطر"
+ results["recommendation"] = "ينصح بإعادة التفاوض على شروط العقد أو إضافة هامش ربح أعلى لتغطية المخاطر."
+ elif avg_risk_score >= 4.0:
+ results["overall_assessment"] = "مشروع متوسط المخاطر"
+ results["recommendation"] = "متابعة دقيقة للمخاطر العالية ووضع خطط استجابة مفصلة لها."
+ else:
+ results["overall_assessment"] = "مشروع منخفض المخاطر"
+ results["recommendation"] = "مراقبة المخاطر بشكل دوري والتركيز على تحسين الأداء."
+
+ return results
+
+ def _display_risk_analysis_results(self, results):
+ """عرض نتائج تحليل المخاطر"""
+
+ st.markdown("### نتائج تحليل المخاطر")
+
+ # عرض ملخص تقييم المخاطر
+ st.markdown(f"#### التقييم العام: {results['overall_assessment']}")
+
+ # عرض الإحصائيات الرئيسية للمخاطر
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ st.metric("متوسط درجة المخاطرة", f"{results['avg_risk_score']:.1f}/10")
+
+ with col2:
+ st.metric("المخاطر العالية", f"{results['high_risks']}")
+
+ with col3:
+ st.metric("المخاطر المتوسطة", f"{results['medium_risks']}")
+
+ with col4:
+ st.metric("المخاطر المنخفضة", f"{results['low_risks']}")
+
+ # عرض ملف المخاطر حسب الفئة
+ st.markdown("#### ملف المخاطر حسب الفئة")
+
+ # تجهيز البيانات للرسم البياني
+ risk_profile_data = pd.DataFrame({
+ 'الفئة': [
+ "مخاطر مالية",
+ "مخاطر فنية",
+ "مخاطر السوق",
+ "مخاطر الموارد",
+ "مخاطر تعاقدية",
+ "مخاطر تنظيمية"
+ ],
+ 'درجة المخاطرة': [
+ results['risk_profile']['financial_risk'],
+ results['risk_profile']['technical_risk'],
+ results['risk_profile']['market_risk'],
+ results['risk_profile']['resource_risk'],
+ results['risk_profile']['contract_risk'],
+ results['risk_profile']['regulatory_risk']
+ ]
+ })
+
+ # رسم مخطط شعاعي لملف المخاطر
+ fig = px.line_polar(
+ risk_profile_data,
+ r='درجة المخاطرة',
+ theta='الفئة',
+ line_close=True,
+ range_r=[0, 10],
+ title="ملف المخاطر حسب الفئة"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض المخاطر الرئيسية
+ st.markdown("#### المخاطر الرئيسية")
+
+ # إنشاء جدول المخاطر
+ risk_table_data = []
+
+ for risk in results['top_risks'][:5]: # عرض أعلى 5 مخاطر فقط
+ risk_level = "عالية" if risk["risk_score"] >= 6.0 else "متوسطة" if risk["risk_score"] >= 3.0 else "منخفضة"
+ risk_color = "red" if risk_level == "عالية" else "orange" if risk_level == "متوسطة" else "green"
+
+ risk_table_data.append({
+ "المعرف": risk["id"],
+ "الوصف": risk["name"],
+ "الفئة": risk["category"],
+ "الاحتمالية": f"{risk['probability'] * 100:.0f}%",
+ "التأثير": f"{risk['impact'] * 100:.0f}%",
+ "درجة المخاطرة": risk["risk_score"],
+ "المستوى": risk_level,
+ "استراتيجية الاستجابة": risk["response_strategy"],
+ "color": risk_color
+ })
+
+ # عرض جدول المخاطر
+ risk_df = pd.DataFrame(risk_table_data)
+
+ # استخدام تنسيق HTML مخصص لعرض المخاطر الرئيسية
+ for index, row in risk_df.iterrows():
+ with st.container():
+ st.markdown(f"""
+
+
{row['المعرف']} - {row['الوصف']} {row['المستوى']}
+
الفئة: {row['الفئة']} | الاحتمالية: {row['الاحتمالية']} | التأثير: {row['التأثير']} | درجة المخاطرة: {row['درجة المخاطرة']}/10
+
استراتيجية الاستجابة: {row['استراتيجية الاستجابة']}
+
+ """, unsafe_allow_html=True)
+
+ # عرض توصيات عامة
+ st.markdown("#### التوصيات العامة")
+ st.info(results["recommendation"])
+
+ # عرض تحليل Claude AI إذا كان متوفراً
+ if "claude_analysis" in results:
+ st.markdown("### تحليل Claude AI المتقدم")
+ st.success(results["claude_analysis"])
+
+ # زر تصدير تقرير المخاطر
+ if st.button("تصدير تقرير المخاطر"):
+ st.success("تم تصدير تقرير المخاطر بنجاح!")
+
+ def _render_document_analysis_tab(self):
+ """عرض تبويب تحليل المستندات"""
+
+ st.markdown("### تحليل المستندات")
+
+ # خيارات رفع الملفات
+ st.markdown("#### رفع ملفات المناقصة")
+
+ # رفع الملفات
+ uploaded_files = st.file_uploader(
+ "اختر ملفات المناقصة للتحليل",
+ type=["pdf", "docx", "doc", "xls", "xlsx", "jpg", "jpeg", "png"],
+ accept_multiple_files=True,
+ key="document_analysis_files"
+ )
+
+ # اختيار نموذج التحليل
+ analysis_model = st.radio(
+ "اختر نموذج التحليل",
+ [
+ "استخراج البنود والمواصفات",
+ "استخراج الشروط التعاقدية",
+ "تحليل الكميات",
+ "تحليل المتطلبات القانونية",
+ "تحليل شامل (يستخدم Claude AI)"
+ ],
+ horizontal=True
+ )
+
+ # زر بدء التحليل
+ if uploaded_files and st.button("بدء تحليل المستندات"):
+ with st.spinner("جاري تحليل المستندات..."):
+ # محاكاة وقت المعالجة
+ time.sleep(3)
+
+ # معالجة الملفات المرفوعة
+ analysis_results = self._analyze_documents(uploaded_files, analysis_model)
+
+ # عرض نتائج التحليل
+ self._display_document_analysis_results(analysis_results)
+
+ def _analyze_documents(self, files, analysis_model):
+ """تحليل المستندات المرفوعة"""
+
+ # في البيئة الحقيقية، سيتم استدعاء نموذج تحليل المستندات
+ # محاكاة نتائج تحليل المستندات للعرض
+
+ # نتائج التحليل المبدئية
+ basic_results = {
+ "file_count": len(files),
+ "file_names": [file.name for file in files],
+ "file_sizes": [f"{file.size / 1024:.1f} KB" for file in files],
+ "file_types": [file.type or "غير محدد" for file in files],
+ "extracted_text_samples": {},
+ "entities": [],
+ "tender_items": [],
+ "contract_terms": [],
+ "quantities": [],
+ "legal_requirements": [],
+ "summary": ""
+ }
+
+ # محاكاة استخراج نص من الملفات
+ for file in files:
+ # استخراج عينة نصية (في البيئة الحقيقية سيتم استخراج النص الكامل)
+ sample_text = f"عينة نصية مستخرجة من الملف {file.name}. هذا النص لأغراض العرض فقط."
+ basic_results["extracted_text_samples"][file.name] = sample_text
+
+ # محاكاة تحليل المحتوى حسب نموذج التحليل المختار
+ if analysis_model == "استخراج البنود والمواصفات" or analysis_model == "تحليل شامل (يستخدم Claude AI)":
+ basic_results["tender_items"] = [
+ {
+ "id": "T-001",
+ "description": "أعمال الحفر والردم",
+ "unit": "م³",
+ "quantity": 1500,
+ "estimated_price": 85,
+ "specifications": "حفر في أي نوع من التربة بما في ذلك الصخور والردم باستخدام مواد معتمدة."
+ },
+ {
+ "id": "T-002",
+ "description": "أعمال الخرسانة المسلحة للأساسات",
+ "unit": "م³",
+ "quantity": 750,
+ "estimated_price": 1200,
+ "specifications": "خرسانة مسلحة بقوة 30 نيوتن/مم² بعد 28 يوم، مع حديد تسليح من الفئة 60."
+ },
+ {
+ "id": "T-003",
+ "description": "أعمال الخرسانة المسلحة للهيكل",
+ "unit": "م³",
+ "quantity": 1200,
+ "estimated_price": 1350,
+ "specifications": "خرسانة مسلحة بقوة 30 نيوتن/مم² بعد 28 يوم، مع حديد تسليح من الفئة 60."
+ },
+ {
+ "id": "T-004",
+ "description": "أعمال الطابوق",
+ "unit": "م²",
+ "quantity": 3500,
+ "estimated_price": 120,
+ "specifications": "جدران طابوق مفرغ سمك 20 سم مع مونة إسمنتية."
+ },
+ {
+ "id": "T-005",
+ "description": "أعمال التشطيبات الداخلية",
+ "unit": "م²",
+ "quantity": 5000,
+ "estimated_price": 200,
+ "specifications": "تشطيبات داخلية تشمل اللياسة والدهان والأرضيات حسب المواصفات المرفقة."
+ }
+ ]
+
+ if analysis_model == "استخراج الشروط التعاقدية" or analysis_model == "تحليل شامل (يستخدم Claude AI)":
+ basic_results["contract_terms"] = [
+ {
+ "id": "C-001",
+ "title": "مدة تنفيذ المشروع",
+ "description": "يجب إنجاز جميع الأعمال خلال 18 شهراً من تاريخ تسليم الموقع.",
+ "risk_level": "متوسط",
+ "notes": "مدة تنفيذ معقولة نسبياً للحجم المتوقع من الأعمال."
+ },
+ {
+ "id": "C-002",
+ "title": "غرامة التأخير",
+ "description": "تفرض غرامة تأخير بنسبة 0.1% من قيمة العقد عن كل يوم تأخير، بحد أقصى 10% من القيمة الإجمالية للعقد.",
+ "risk_level": "عالي",
+ "notes": "غرامة مرتفعة نسبياً، تتطلب جدولة دقيقة وإدارة استباقية للمخاطر."
+ },
+ {
+ "id": "C-003",
+ "title": "شروط الدفع",
+ "description": "يتم صرف المستخلصات خلال 45 يوماً من تاريخ تقديمها، مع خصم نسبة 10% كضمان حسن التنفيذ تسترد بعد فترة الضمان.",
+ "risk_level": "متوسط",
+ "notes": "فترة 45 يوماً طويلة نسبياً وقد تؤثر على التدفق النقدي."
+ },
+ {
+ "id": "C-004",
+ "title": "التزامات المحتوى المحلي",
+ "description": "يجب أن لا تقل نسبة المحتوى المحلي عن 30% من إجمالي قيمة العقد.",
+ "risk_level": "منخفض",
+ "notes": "يمكن تحقيق النسبة المطلوبة من خلال توريد المواد والعمالة المحلية."
+ },
+ {
+ "id": "C-005",
+ "title": "التغييرات والأعمال الإضافية",
+ "description": "يحق للمالك طلب تغييرات بنسبة ±10% من قيمة العقد دون تعديل أسعار الوحدات.",
+ "risk_level": "متوسط",
+ "notes": "نسبة معقولة، لكن يجب مراعاة احتمالية الطلبات الإضافية عند تسعير البنود."
+ }
+ ]
+
+ if analysis_model == "تحليل الكميات" or analysis_model == "تحليل شامل (يستخدم Claude AI)":
+ basic_results["quantities"] = [
+ {
+ "category": "أعمال الحفر والردم",
+ "volume": 1500,
+ "unit": "م³",
+ "estimated_cost": 127500
+ },
+ {
+ "category": "أعمال الخرسانة",
+ "volume": 1950,
+ "unit": "م³",
+ "estimated_cost": 2437500
+ },
+ {
+ "category": "أعمال الطابوق",
+ "volume": 3500,
+ "unit": "م²",
+ "estimated_cost": 420000
+ },
+ {
+ "category": "أعمال التشطيبات الداخلية",
+ "volume": 5000,
+ "unit": "م²",
+ "estimated_cost": 1000000
+ },
+ {
+ "category": "أعمال التشطيبات الخارجية",
+ "volume": 2200,
+ "unit": "م²",
+ "estimated_cost": 660000
+ },
+ {
+ "category": "أعمال الكهروميكانيكية",
+ "volume": 1,
+ "unit": "مقطوعية",
+ "estimated_cost": 1750000
+ }
+ ]
+
+ if analysis_model == "تحليل المتطلبات القانونية" or analysis_model == "تحليل شامل (يستخدم Claude AI)":
+ basic_results["legal_requirements"] = [
+ {
+ "id": "L-001",
+ "title": "متطلبات التراخيص",
+ "description": "يجب أن يكون المقاول حاصلاً على تصنيف في الفئة الأولى في مجال المباني.",
+ "compliance_status": "مطلوب التحقق",
+ "required_documents": "شهادة التصنيف سارية المفعول"
+ },
+ {
+ "id": "L-002",
+ "title": "متطلبات التأمين",
+ "description": "يجب تقديم بوليصة تأمين شاملة تغطي جميع مخاطر المشروع بقيمة لا تقل عن 100% من قيمة العقد.",
+ "compliance_status": "مطلوب التحقق",
+ "required_documents": "وثائق التأمين الشاملة"
+ },
+ {
+ "id": "L-003",
+ "title": "متطلبات الضمان البنكي",
+ "description": "يجب تقديم ضمان بنكي ابتدائي بنسبة 2% من قيمة العطاء، وضمان نهائي بنسبة 5% من قيمة العقد.",
+ "compliance_status": "مطلوب التحقق",
+ "required_documents": "نماذج الضمانات البنكية"
+ },
+ {
+ "id": "L-004",
+ "title": "متطلبات السعودة",
+ "description": "يجب الالتزام بنسبة السعودة المطلوبة حسب برنامج نطاقات وأن يكون المقاول في النطاق الأخضر.",
+ "compliance_status": "مطلوب التحقق",
+ "required_documents": "شهادة نطاقات سارية المفعول"
+ },
+ {
+ "id": "L-005",
+ "title": "متطلبات الزكاة والدخل",
+ "description": "يجب تقديم شهادة سداد الزكاة والضريبة سارية المفعول.",
+ "compliance_status": "مطلوب التحقق",
+ "required_documents": "شهادة الزكاة والدخل"
+ }
+ ]
+
+ # إعداد ملخص التحليل
+ basic_results["summary"] = f"""
+ تم تحليل {len(files)} ملفات بإجمالي حجم {sum([file.size for file in files]) / 1024 / 1024:.2f} ميجابايت.
+
+ نتائج التحليل الرئيسية:
+ - تم استخراج {len(basic_results.get('tender_items', []))} بنود رئيسية للمناقصة.
+ - تم تحديد {len(basic_results.get('contract_terms', []))} شروط تعاقدية هامة.
+ - تم تحليل الكميات لـ {len(basic_results.get('quantities', []))} فئات من الأعمال.
+ - تم تحديد {len(basic_results.get('legal_requirements', []))} متطلبات قانونية.
+
+ التوصيات:
+ - مراجعة شروط التعاقد وخاصة البنود المتعلقة بالغرامات والدفعات.
+ - تدقيق جداول الكميات والتأكد من تغطية جميع البنود اللازمة للتنفيذ.
+ - التحقق من استيفاء جميع المتطلبات القانونية قبل تقديم العطاء.
+ """
+
+ # إضافة تحليل متقدم باستخدام Claude AI إذا تم اختياره
+ if analysis_model == "تحليل شامل (يستخدم Claude AI)":
+ try:
+ # إنشاء مدخلات للتحليل
+ analysis_input = f"""
+ المناقصة: تطوير مبنى إداري متعدد الطوابق
+
+ ملفات تم تحليلها:
+ {', '.join(basic_results['file_names'])}
+
+ بنود رئيسية:
+ - أعمال الحفر والردم: 1500 م³
+ - أعمال الخرسانة المسلحة للأساسات: 750 م³
+ - أعمال الخرسانة المسلحة للهيكل: 1200 م³
+ - أعمال الطابوق: 3500 م²
+ - أعمال التشطيبات الداخلية: 5000 م²
+
+ شروط تعاقدية رئيسية:
+ - مدة التنفيذ: 18 شهر
+ - غرامة التأخير: 0.1% يومياً بحد أقصى 10%
+ - شروط الدفع: 45 يوم للمستخلصات مع خصم 10% ضمان
+ - المحتوى المحلي: 30% كحد أدنى
+
+ متطلبات قانونية:
+ - تصنيف الفئة الأولى مباني
+ - تأمين شامل بنسبة 100%
+ - ضمان بنكي ابتدائي 2% ونهائي 5%
+ - الالتزام بمتطلبات السعودة (النطاق الأخضر)
+
+ من فضلك قم بتحليل هذه المناقصة وتقديم:
+ 1. تقييم عام للمناقصة وجاذبيتها
+ 2. نقاط القوة والضعف الرئيسية
+ 3. المخاطر المحتملة التي يجب مراعاتها
+ 4. توصيات للتسعير المناسب
+ 5. استراتيجية مقترحة للتنافس على المناقصة
+ """
+
+ # استدعاء خدمة Claude للتحليل
+ claude_response = self.claude_service.chat_completion(
+ [{"role": "user", "content": analysis_input}]
+ )
+
+ if "error" not in claude_response:
+ # إضافة تحليل Claude إلى النتائج
+ basic_results["claude_analysis"] = claude_response["content"]
+ except Exception as e:
+ logging.error(f"فشل في تحليل المستندات باستخدام Claude AI: {str(e)}")
+
+ return basic_results
+
+ def _display_document_analysis_results(self, results):
+ """عرض نتائج تحليل المستندات"""
+
+ st.markdown("### نتائج تحليل المستندات")
+
+ # عرض ملخص التحليل
+ st.markdown("#### ملخص التحليل")
+ st.info(results["summary"])
+
+ # عرض البنود المستخرجة من المناقصة إذا وجدت
+ if results["tender_items"]:
+ st.markdown("#### بنود المناقصة المستخرجة")
+
+ # إنشاء DataFrame للبنود
+ items_df = pd.DataFrame(results["tender_items"])
+
+ # عرض الجدول بشكل منسق
+ st.dataframe(
+ items_df[["id", "description", "unit", "quantity", "estimated_price"]],
+ use_container_width=True
+ )
+
+ # عرض مخطط للتكاليف المقدرة
+ costs = [item["quantity"] * item["estimated_price"] for item in results["tender_items"]]
+ labels = [item["description"] for item in results["tender_items"]]
+
+ fig = px.pie(
+ names=labels,
+ values=costs,
+ title="توزيع التكاليف المقدرة حسب البنود"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض الشروط التعاقدية إذا وجدت
+ if results["contract_terms"]:
+ st.markdown("#### الشروط التعاقدية الهامة")
+
+ # عرض كل شرط في قسم منفصل
+ for term in results["contract_terms"]:
+ risk_color = "red" if term["risk_level"] == "عالي" else "orange" if term["risk_level"] == "متوسط" else "green"
+
+ st.markdown(f"""
+
+
{term['id']} - {term['title']} مستوى الخطورة: {term['risk_level']}
+
{term['description']}
+
ملاحظات: {term['notes']}
+
+ """, unsafe_allow_html=True)
+
+ # عرض تحليل الكميات إذا وجد
+ if results["quantities"]:
+ st.markdown("#### تحليل الكميات")
+
+ # إنشاء DataFrame للكميات
+ quantities_df = pd.DataFrame(results["quantities"])
+
+ # عرض الجدول بشكل منسق
+ st.dataframe(quantities_df, use_container_width=True)
+
+ # عرض مخطط شريطي للتكاليف المقدرة
+ fig = px.bar(
+ quantities_df,
+ x="category",
+ y="estimated_cost",
+ title="التكاليف المقدرة حسب فئة الأعمال",
+ labels={"category": "فئة الأعمال", "estimated_cost": "التكلفة المقدرة (ريال)"}
+ )
+
+ fig.update_traces(text=quantities_df["estimated_cost"], textposition="outside")
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض المتطلبات القانونية إذا وجدت
+ if results["legal_requirements"]:
+ st.markdown("#### المتطلبات القانونية")
+
+ # عرض المتطلبات في جدول
+ legal_df = pd.DataFrame(results["legal_requirements"])
+
+ # عرض الجدول بشكل منسق
+ st.dataframe(
+ legal_df[["id", "title", "description", "compliance_status", "required_documents"]],
+ use_container_width=True
+ )
+
+ # عرض قائمة تحقق للمتطلبات القانونية
+ st.markdown("##### قائمة التحقق من المتطلبات القانونية")
+
+ for req in results["legal_requirements"]:
+ st.checkbox(f"{req['title']} - {req['description']}", key=f"req_{req['id']}")
+
+ # عرض تحليل Claude AI المتقدم إذا وجد
+ if "claude_analysis" in results:
+ st.markdown("### تحليل Claude AI المتقدم")
+ st.success(results["claude_analysis"])
+
+ # أزرار إضافية
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if st.button("تصدير تقرير تحليل المستندات"):
+ st.success("تم تصدير تقرير تحليل المستندات بنجاح!")
+
+ with col2:
+ if st.button("استخراج جدول الكميات"):
+ st.success("تم استخراج جدول الكميات بنجاح!")
+
+ def _render_local_content_tab(self):
+ """عرض تبويب المحتوى المحلي"""
+
+ st.markdown("### المحتوى المحلي")
+
+ st.markdown("""
+ وحدة حساب المحتوى المحلي تساعدك في تحليل وتحسين نسبة المحتوى المحلي في مشروعك طبقاً لمتطلبات هيئة المحتوى المحلي والمشتريات الحكومية.
+ """)
+
+ # عرض علامات تبويب فرعية
+ lc_tabs = st.tabs([
+ "حساب المحتوى المحلي",
+ "قاعدة بيانات الموردين",
+ "التقارير",
+ "التحسين"
+ ])
+
+ with lc_tabs[0]:
+ self._render_lc_calculator_tab()
+
+ with lc_tabs[1]:
+ self._render_lc_suppliers_tab()
+
+ with lc_tabs[2]:
+ self._render_lc_reports_tab()
+
+ with lc_tabs[3]:
+ self._render_lc_optimization_tab()
+
+ def _render_lc_calculator_tab(self):
+ """عرض تبويب حساب المحتوى المحلي"""
+
+ st.markdown("#### حساب المحتوى المحلي")
+
+ # نموذج إدخال بيانات المشروع
+ st.markdown("##### بيانات المشروع")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_name = st.text_input("اسم المشروع", "مبنى إداري الرياض")
+ project_value = st.number_input("القيمة الإجمالية للمشروع (ريال)", min_value=1000, value=10000000)
+
+ with col2:
+ target_lc = st.slider("نسبة المحتوى المحلي المستهدفة (%)", 0, 100, 40)
+ calculation_method = st.selectbox(
+ "طريقة الحساب",
+ [
+ "الطريقة القياسية (المدخلات)",
+ "طريقة القيمة المضافة",
+ "الطريقة المختلطة"
+ ]
+ )
+
+ # جدول مكونات المشروع
+ st.markdown("##### مكونات المشروع")
+
+ # إعداد بيانات المكونات الافتراضية
+ if 'lc_components' not in st.session_state:
+ st.session_state.lc_components = [
+ {
+ "id": 1,
+ "name": "الخرسانة المسلحة",
+ "category": "مواد",
+ "value": 3000000,
+ "local_content": 85,
+ "supplier": "شركة الإنشاءات السعودية"
+ },
+ {
+ "id": 2,
+ "name": "الأعمال الكهربائية",
+ "category": "أنظمة",
+ "value": 1500000,
+ "local_content": 65,
+ "supplier": "مؤسسة الطاقة المتقدمة"
+ },
+ {
+ "id": 3,
+ "name": "أعمال التكييف",
+ "category": "أنظمة",
+ "value": 1200000,
+ "local_content": 55,
+ "supplier": "شركة التبريد العالمية"
+ },
+ {
+ "id": 4,
+ "name": "الواجهات والنوافذ",
+ "category": "مواد",
+ "value": 800000,
+ "local_content": 45,
+ "supplier": "شركة الزجاج المتطورة"
+ },
+ {
+ "id": 5,
+ "name": "أعمال التشطيبات",
+ "category": "مواد وعمالة",
+ "value": 1200000,
+ "local_content": 80,
+ "supplier": "مؤسسة التشطيبات الحديثة"
+ },
+ {
+ "id": 6,
+ "name": "الأثاث والتجهيزات",
+ "category": "أثاث",
+ "value": 900000,
+ "local_content": 30,
+ "supplier": "شركة الأثاث المكتبي"
+ },
+ {
+ "id": 7,
+ "name": "أنظمة الأمن والمراقبة",
+ "category": "أنظمة",
+ "value": 600000,
+ "local_content": 40,
+ "supplier": "شركة الأنظمة الأمنية المتقدمة"
+ },
+ {
+ "id": 8,
+ "name": "العمالة المباشرة",
+ "category": "عمالة",
+ "value": 800000,
+ "local_content": 50,
+ "supplier": "داخلي"
+ }
+ ]
+
+ # عرض جدول المكونات للتعديل
+ for i, component in enumerate(st.session_state.lc_components):
+ col1, col2, col3, col4, col5, col6 = st.columns([2, 1, 1, 1, 2, 1])
+
+ with col1:
+ st.session_state.lc_components[i]["name"] = st.text_input(
+ "المكون",
+ component["name"],
+ key=f"comp_name_{i}"
+ )
+
+ with col2:
+ st.session_state.lc_components[i]["category"] = st.selectbox(
+ "الفئة",
+ ["مواد", "أنظمة", "عمالة", "مواد وعمالة", "أثاث", "خدمات"],
+ index=["مواد", "أنظمة", "عمالة", "مواد وعمالة", "أثاث", "خدمات"].index(component["category"]),
+ key=f"comp_category_{i}"
+ )
+
+ with col3:
+ st.session_state.lc_components[i]["value"] = st.number_input(
+ "القيمة (ريال)",
+ min_value=0,
+ value=int(component["value"]),
+ key=f"comp_value_{i}"
+ )
+
+ with col4:
+ st.session_state.lc_components[i]["local_content"] = st.slider(
+ "المحتوى المحلي (%)",
+ 0, 100, int(component["local_content"]),
+ key=f"comp_lc_{i}"
+ )
+
+ with col5:
+ st.session_state.lc_components[i]["supplier"] = st.text_input(
+ "المورد",
+ component["supplier"],
+ key=f"comp_supplier_{i}"
+ )
+
+ with col6:
+ if st.button("حذف", key=f"delete_comp_{i}"):
+ st.session_state.lc_components.pop(i)
+ st.rerun()
+
+ # زر إضافة مكون جديد
+ if st.button("إضافة مكون جديد"):
+ new_id = max([c["id"] for c in st.session_state.lc_components]) + 1 if st.session_state.lc_components else 1
+ st.session_state.lc_components.append({
+ "id": new_id,
+ "name": f"مكون جديد {new_id}",
+ "category": "مواد",
+ "value": 100000,
+ "local_content": 50,
+ "supplier": "غير محدد"
+ })
+ st.rerun()
+
+ # زر حساب المحتوى المحلي
+ col1, col2 = st.columns([1, 3])
+
+ with col1:
+ calculate_button = st.button("حساب المحتوى المحلي", use_container_width=True)
+
+ with col2:
+ use_claude = st.checkbox("استخدام Claude AI للتحليل المتقدم", value=True, key="lc_use_claude")
+
+ if calculate_button:
+ with st.spinner("جاري حساب وتحليل المحتوى المحلي..."):
+ # محاكاة وقت المعالجة
+ time.sleep(2)
+
+ # حساب المحتوى المحلي
+ lc_results = self._calculate_local_content(st.session_state.lc_components, target_lc, calculation_method)
+
+ # إضافة تحليل إضافي باستخدام Claude AI إذا تم تفعيل الخيار
+ if use_claude:
+ try:
+ # إنشاء نص المكونات للتحليل
+ components_text = ""
+ for comp in st.session_state.lc_components:
+ components_text += f"""
+ - {comp['name']} ({comp['category']}):
+ القيمة: {comp['value']:,} ريال | المحتوى المحلي: {comp['local_content']}% | المورد: {comp['supplier']}
+ """
+
+ prompt = f"""تحليل وتحسين المحتوى المحلي:
+
+ بيانات المشروع:
+ - اسم المشروع: {project_name}
+ - القيمة الإجمالية: {project_value:,} ريال
+ - نسبة المحتوى المحلي المستهدفة: {target_lc}%
+ - النسبة المحسوبة: {lc_results['total_local_content']:.1f}%
+
+ مكونات المشروع:
+ {components_text}
+
+ المطلوب:
+ 1. تحليل نسبة المحتوى المحلي المحسوبة ومقارنتها بالمستهدف
+ 2. تحديد المكونات ذات المحتوى المحلي المنخفض التي يمكن تحسينها
+ 3. اقتراح بدائل محلية أو استراتيجيات لزيادة المحتوى المحلي
+ 4. تقديم توصيات عملية لتحقيق النسبة المستهدفة
+ 5. تحديد أي فرص إضافية لتحسين المحتوى المحلي في المشروع
+
+ يرجى تقديم تحليل مهني ومختصر يركز على الجوانب الأكثر أهمية.
+ """
+
+ # استدعاء Claude للتحليل
+ claude_analysis = self.claude_service.chat_completion(
+ [{"role": "user", "content": prompt}]
+ )
+
+ if "error" not in claude_analysis:
+ # إضافة تحليل Claude إلى النتائج
+ lc_results["claude_analysis"] = claude_analysis["content"]
+ except Exception as e:
+ st.warning(f"تعذر إجراء التحليل المتقدم: {str(e)}")
+
+ # عرض نتائج حساب المحتوى المحلي
+ self._display_local_content_results(lc_results, target_lc)
+
+ def _calculate_local_content(self, components, target_lc, calculation_method):
+ """حساب المحتوى المحلي"""
+
+ # حساب إجمالي قيمة المشروع
+ total_value = sum([comp["value"] for comp in components])
+
+ # حساب المحتوى المحلي الإجمالي
+ total_local_content_value = sum([comp["value"] * comp["local_content"] / 100 for comp in components])
+
+ # حساب نسبة المحتوى المحلي الإجمالية
+ total_local_content_percent = (total_local_content_value / total_value) * 100 if total_value > 0 else 0
+
+ # تحليل المحتوى المحلي حسب الفئة
+ categories = {}
+ for comp in components:
+ category = comp["category"]
+ if category not in categories:
+ categories[category] = {
+ "total_value": 0,
+ "local_content_value": 0
+ }
+
+ categories[category]["total_value"] += comp["value"]
+ categories[category]["local_content_value"] += comp["value"] * comp["local_content"] / 100
+
+ # حساب نسبة المحتوى المحلي لكل فئة
+ for category in categories:
+ if categories[category]["total_value"] > 0:
+ categories[category]["local_content_percent"] = (categories[category]["local_content_value"] / categories[category]["total_value"]) * 100
+ else:
+ categories[category]["local_content_percent"] = 0
+
+ # تحديد المكونات ذات المحتوى المحلي المنخفض
+ low_lc_components = sorted(
+ [comp for comp in components if comp["local_content"] < 50],
+ key=lambda x: x["local_content"]
+ )
+
+ # تحديد المكونات ذات المحتوى المحلي المرتفع
+ high_lc_components = sorted(
+ [comp for comp in components if comp["local_content"] >= 80],
+ key=lambda x: x["local_content"],
+ reverse=True
+ )
+
+ # تقديم توصيات لتحسين المحتوى المحلي
+ improvement_recommendations = []
+
+ # توصيات للمكونات ذات المحتوى المحلي المنخفض
+ for comp in low_lc_components[:3]: # أخذ أقل 3 مكونات
+ improvement_recommendations.append({
+ "component": comp["name"],
+ "current_lc": comp["local_content"],
+ "recommendation": f"البحث عن بدائل محلية لـ {comp['name']} التي تمثل {comp['value'] / total_value * 100:.1f}% من قيمة المشروع."
+ })
+
+ # حساب الفجوة بين المحتوى المحلي الفعلي والمستهدف
+ lc_gap = target_lc - total_local_content_percent
+
+ # إعداد النتائج
+ results = {
+ "total_value": total_value,
+ "total_local_content_value": total_local_content_value,
+ "total_local_content": total_local_content_percent,
+ "target_lc": target_lc,
+ "lc_gap": lc_gap,
+ "categories": categories,
+ "low_lc_components": low_lc_components,
+ "high_lc_components": high_lc_components,
+ "improvement_recommendations": improvement_recommendations,
+ "calculation_method": calculation_method,
+ "components": components
+ }
+
+ # تحديد حالة المحتوى المحلي
+ if lc_gap <= 0:
+ results["status"] = "تم تحقيق المستهدف"
+ results["color"] = "green"
+ elif lc_gap <= 5:
+ results["status"] = "قريب من المستهدف"
+ results["color"] = "orange"
+ else:
+ results["status"] = "بعيد عن المستهدف"
+ results["color"] = "red"
+
+ return results
+
+ def _display_local_content_results(self, results, target_lc):
+ """عرض نتائج حساب المحتوى المحلي"""
+
+ st.markdown("### نتائج حساب المحتوى المحلي")
+
+ # عرض نسبة المحتوى المحلي الإجمالية
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric(
+ "نسبة المحتوى المحلي الحالية",
+ f"{results['total_local_content']:.1f}%",
+ delta=f"{results['lc_gap']:.1f}%" if results['lc_gap'] < 0 else f"-{results['lc_gap']:.1f}%",
+ delta_color="normal" if results['lc_gap'] < 0 else "inverse"
+ )
+
+ with col2:
+ st.metric(
+ "النسبة المستهدفة",
+ f"{target_lc}%"
+ )
+
+ with col3:
+ # Aquí está el problema - no podemos usar 'green' como valor para delta_color
+ # En lugar de eso, usamos un texto formateado para mostrar el estado
+ st.markdown(f"""
+
+
{results["status"]}
+
+ """, unsafe_allow_html=True)
+
+ # Alternativa sin usar delta_color
+ # st.metric(
+ # "حالة المحتوى المحلي",
+ # results["status"]
+ # )
+
+
+ # عرض مخطط مقارنة بين النسبة الحالية والمستهدفة
+ comparison_data = pd.DataFrame({
+ 'النوع': ['النسبة الحالية', 'النسبة المستهدفة'],
+ 'النسبة': [results['total_local_content'], target_lc]
+ })
+
+ fig = px.bar(
+ comparison_data,
+ x='النوع',
+ y='النسبة',
+ title="مقارنة نسبة المحتوى المحلي الحالية مع المستهدفة",
+ color='النوع',
+ color_discrete_map={
+ 'النسبة الحالية': results["color"],
+ 'النسبة المستهدفة': 'blue'
+ }
+ )
+
+ fig.update_layout(yaxis_range=[0, 100])
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض توزيع المحتوى المحلي حسب الفئة
+ st.markdown("#### توزيع المحتوى المحلي حسب الفئة")
+
+ categories_data = []
+ for category, data in results["categories"].items():
+ categories_data.append({
+ 'الفئة': category,
+ 'القيمة الإجمالية': data["total_value"],
+ 'قيمة المحتوى المحلي': data["local_content_value"],
+ 'نسبة المحتوى المحلي': data["local_content_percent"]
+ })
+
+ categories_df = pd.DataFrame(categories_data)
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ fig = px.pie(
+ categories_df,
+ values='القيمة الإجمالية',
+ names='الفئة',
+ title="توزيع قيمة المشروع حسب الفئة"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ with col2:
+ fig = px.bar(
+ categories_df,
+ x='الفئة',
+ y='نسبة المحتوى المحلي',
+ title="نسبة المحتوى المحلي لكل فئة",
+ text_auto='.1f'
+ )
+
+ fig.update_traces(texttemplate='%{text}%', textposition='outside')
+ fig.update_layout(yaxis_range=[0, 100])
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض المكونات ذات المحتوى المحلي المنخفض
+ st.markdown("#### المكونات ذات المحتوى المحلي المنخفض")
+
+ if results["low_lc_components"]:
+ low_lc_df = pd.DataFrame([
+ {
+ 'المكون': comp["name"],
+ 'الفئة': comp["category"],
+ 'القيمة': comp["value"],
+ 'نسبة المحتوى المحلي': comp["local_content"],
+ 'المورد': comp["supplier"]
+ }
+ for comp in results["low_lc_components"]
+ ])
+
+ st.dataframe(low_lc_df, use_container_width=True)
+
+ # مخطط المكونات ذات المحتوى المحلي المنخفض
+ fig = px.bar(
+ low_lc_df,
+ x='المكون',
+ y='نسبة المحتوى المحلي',
+ color='القيمة',
+ title="المكونات ذات المحتوى المحلي المنخفض",
+ text_auto='.1f'
+ )
+
+ fig.update_traces(texttemplate='%{text}%', textposition='outside')
+
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.info("لا توجد مكونات ذات محتوى محلي منخفض (أقل من 50%).")
+
+ # عرض توصيات لتحسين المحتوى المحلي
+ st.markdown("#### توصيات لتحسين المحتوى المحلي")
+
+ if results["improvement_recommendations"]:
+ for recommendation in results["improvement_recommendations"]:
+ st.markdown(f"""
+
+
{recommendation['component']} (المحتوى المحلي الحالي: {recommendation['current_lc']}%)
+
{recommendation['recommendation']}
+
+ """, unsafe_allow_html=True)
+ else:
+ st.success("المحتوى المحلي جيد ولا توجد توصيات للتحسين.")
+
+ # عرض تحليل Claude AI المتقدم إذا كان متوفراً
+ if "claude_analysis" in results:
+ st.markdown("### تحليل Claude AI المتقدم")
+ st.info(results["claude_analysis"])
+
+ def _render_lc_suppliers_tab(self):
+ """عرض تبويب قاعدة بيانات الموردين للمحتوى المحلي"""
+
+ st.markdown("#### قاعدة بيانات الموردين المحليين")
+
+ # قائمة الفئات
+ categories = [
+ "جميع الفئات",
+ "مواد بناء",
+ "أنظمة كهربائية",
+ "أنظمة ميكانيكية",
+ "تشطيبات",
+ "أثاث ومفروشات",
+ "خدمات هندسية",
+ "أنظمة أمنية",
+ "معدات وآليات"
+ ]
+
+ # اختيار الفئة
+ selected_category = st.selectbox("فئة الموردين", categories)
+
+ # البحث
+ search_query = st.text_input("البحث عن مورد")
+
+ # إعداد قائمة الموردين
+ suppliers = [
+ {
+ "id": 1,
+ "name": "شركة الإنشاءات السعودية",
+ "category": "مواد بناء",
+ "lc_rating": 95,
+ "quality_rating": 4.5,
+ "location": "الرياض",
+ "contact": "info@saudiconstruction.com",
+ "description": "شركة متخصصة في توريد جميع أنواع مواد البناء ذات المنشأ المحلي."
+ },
+ {
+ "id": 2,
+ "name": "مؤسسة الطاقة المتقدمة",
+ "category": "أنظمة كهربائية",
+ "lc_rating": 85,
+ "quality_rating": 4.2,
+ "location": "جدة",
+ "contact": "sales@advancedpower.com",
+ "description": "مؤسسة متخصصة في توريد وتركيب الأنظمة الكهربائية والطاقة المتجددة."
+ },
+ {
+ "id": 3,
+ "name": "شركة التبريد العالمية",
+ "category": "أنظمة ميكانيكية",
+ "lc_rating": 75,
+ "quality_rating": 4.0,
+ "location": "الدمام",
+ "contact": "info@globalcooling.com",
+ "description": "شركة متخصصة في أنظمة التكييف والتبريد المركزي للمشاريع الكبرى."
+ },
+ {
+ "id": 4,
+ "name": "شركة الزجاج المتطورة",
+ "category": "مواد بناء",
+ "lc_rating": 80,
+ "quality_rating": 4.3,
+ "location": "الرياض",
+ "contact": "sales@advancedglass.com",
+ "description": "شركة متخصصة في إنتاج وتوريد الزجاج والواجهات الزجاجية للمباني."
+ },
+ {
+ "id": 5,
+ "name": "مؤسسة التشطيبات الحديثة",
+ "category": "تشطيبات",
+ "lc_rating": 90,
+ "quality_rating": 4.7,
+ "location": "جدة",
+ "contact": "info@modernfinishing.com",
+ "description": "مؤسسة متخصصة في أعمال التشطيبات الداخلية والخارجية بجودة عالية."
+ },
+ {
+ "id": 6,
+ "name": "شركة الأثاث المكتبي",
+ "category": "أثاث ومفروشات",
+ "lc_rating": 70,
+ "quality_rating": 4.0,
+ "location": "الرياض",
+ "contact": "sales@officefurniture.com",
+ "description": "شركة متخصصة في تصنيع وتوريد الأثاث المكتبي والتجهيزات المكتبية."
+ },
+ {
+ "id": 7,
+ "name": "شركة الأنظمة الأمنية المتقدمة",
+ "category": "أنظمة أمنية",
+ "lc_rating": 65,
+ "quality_rating": 4.1,
+ "location": "الدمام",
+ "contact": "info@advancedsecurity.com",
+ "description": "شركة متخصصة في أنظمة الأمن والمراقبة والإنذار للمباني والمنشآت."
+ },
+ {
+ "id": 8,
+ "name": "شركة المعدات الهندسية",
+ "category": "معدات وآليات",
+ "lc_rating": 85,
+ "quality_rating": 4.5,
+ "location": "جدة",
+ "contact": "sales@engineeringequipment.com",
+ "description": "شركة متخصصة في توريد وصيانة المعدات الهندسية والآليات للمشاريع."
+ },
+ {
+ "id": 9,
+ "name": "مكتب الاستشارات الهندسية",
+ "category": "خدمات هندسية",
+ "lc_rating": 100,
+ "quality_rating": 4.8,
+ "location": "الرياض",
+ "contact": "info@engineeringconsultants.com",
+ "description": "مكتب استشاري متخصص في تقديم الخدمات الهندسية والاستشارية للمشاريع."
+ },
+ {
+ "id": 10,
+ "name": "مصنع الحديد السعودي",
+ "category": "مواد بناء",
+ "lc_rating": 100,
+ "quality_rating": 4.6,
+ "location": "جدة",
+ "contact": "sales@saudisteel.com",
+ "description": "مصنع متخصص في إنتاج وتوريد منتجات الحديد والصلب للمشاريع الإنشائية."
+ }
+ ]
+
+ # تطبيق الفلترة حسب الفئة
+ if selected_category != "جميع الفئات":
+ filtered_suppliers = [s for s in suppliers if s["category"] == selected_category]
+ else:
+ filtered_suppliers = suppliers
+
+ # تطبيق فلترة البحث
+ if search_query:
+ filtered_suppliers = [s for s in filtered_suppliers if search_query.lower() in s["name"].lower() or search_query.lower() in s["description"].lower()]
+
+ # عرض الموردين
+ for supplier in filtered_suppliers:
+ with st.container():
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.markdown(f"""
+
+
{supplier['name']} ({supplier['category']})
+
الموقع: {supplier['location']} | التواصل: {supplier['contact']}
+
تصنيف المحتوى المحلي: {supplier['lc_rating']}% | تقييم الجودة: {supplier['quality_rating']}/5
+
{supplier['description']}
+
+ """, unsafe_allow_html=True)
+
+ with col2:
+ st.button(f"عرض التفاصيل #{supplier['id']}", key=f"supplier_details_{supplier['id']}")
+ st.button(f"إضافة للمشروع #{supplier['id']}", key=f"add_supplier_{supplier['id']}")
+
+ # زر إضافة مورد جديد
+ st.button("إضافة مورد جديد")
+
+ def _render_lc_reports_tab(self):
+ """عرض تبويب تقارير المحتوى المحلي"""
+
+ st.markdown("#### تقارير المحتوى المحلي")
+
+ # اختيار نوع التقرير
+ report_type = st.selectbox(
+ "نوع التقرير",
+ [
+ "تقرير المحتوى المحلي للمشروع الحالي",
+ "تقرير مقارنة المحتوى المحلي بين المشاريع",
+ "تقرير التطور التاريخي للمحتوى المحلي",
+ "تقرير الموردين ذوي المحتوى المحلي المرتفع",
+ "تقرير الامتثال لمتطلبات هيئة المحتوى المحلي"
+ ]
+ )
+
+ # عرض محاكاة للتقرير المختار
+ st.markdown(f"##### {report_type}")
+
+ if report_type == "تقرير المحتوى المحلي للمشروع الحالي":
+ # محاكاة تقرير المشروع الحالي
+ project_data = pd.DataFrame({
+ 'المكون': ['الخرسانة المسلحة', 'الأعمال الكهربائية', 'أعمال التكييف', 'الواجهات والنوافذ',
+ 'أعمال التشطيبات', 'الأثاث والتجهيزات', 'أنظمة الأمن والمراقبة', 'العمالة المباشرة'],
+ 'القيمة': [3000000, 1500000, 1200000, 800000, 1200000, 900000, 600000, 800000],
+ 'نسبة المحتوى المحلي': [85, 65, 55, 45, 80, 30, 40, 50]
+ })
+
+ # حساب قيمة المحتوى المحلي
+ project_data['قيمة المحتوى المحلي'] = project_data['القيمة'] * project_data['نسبة المحتوى المحلي'] / 100
+
+ # إضافة نسبة من إجمالي المشروع
+ total_value = project_data['القيمة'].sum()
+ project_data['نسبة من المشروع'] = project_data['القيمة'] / total_value * 100
+
+ # حساب النسبة الإجمالية للمحتوى المحلي
+ total_lc = project_data['قيمة المحتوى المحلي'].sum() / total_value * 100
+
+ # عرض الإجمالي
+ st.metric("نسبة المحتوى المحلي الإجمالية", f"{total_lc:.1f}%")
+
+ # عرض تفاصيل المكونات
+ st.dataframe(project_data.style.format({
+ 'القيمة': '{:,.0f} ريال',
+ 'قيمة المحتوى المحلي': '{:,.0f} ريال',
+ 'نسبة المحتوى المحلي': '{:.1f}%',
+ 'نسبة من المشروع': '{:.1f}%'
+ }), use_container_width=True)
+
+ # مخطط توزيع المحتوى المحلي
+ col1, col2 = st.columns(2)
+
+ with col1:
+ fig = px.pie(
+ project_data,
+ values='القيمة',
+ names='المكون',
+ title="توزيع قيمة المشروع"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ with col2:
+ fig = px.pie(
+ project_data,
+ values='قيمة المحتوى المحلي',
+ names='المكون',
+ title="توزيع قيمة المحتوى المحلي"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # مخطط شريطي للمحتوى المحلي
+ fig = px.bar(
+ project_data,
+ x='المكون',
+ y='نسبة المحتوى المحلي',
+ title="نسبة المحتوى المحلي لكل مكون",
+ text_auto='.1f',
+ color='نسبة من المشروع'
+ )
+
+ fig.update_traces(texttemplate='%{text}%', textposition='outside')
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ elif report_type == "تقرير مقارنة المحتوى المحلي بين المشاريع":
+ # محاكاة بيانات مقارنة المشاريع
+ projects_data = pd.DataFrame({
+ 'المشروع': ['مبنى إداري الرياض', 'مجمع سكني جدة', 'مستشفى الدمام', 'مركز تجاري المدينة', 'فندق مكة'],
+ 'القيمة': [10000000, 15000000, 20000000, 12000000, 18000000],
+ 'نسبة المحتوى المحلي': [65, 55, 70, 60, 50],
+ 'سنة الإنجاز': [2022, 2022, 2023, 2023, 2024]
+ })
+
+ # عرض جدول المقارنة
+ st.dataframe(projects_data.style.format({
+ 'القيمة': '{:,.0f} ريال',
+ 'نسبة المحتوى المحلي': '{:.1f}%'
+ }), use_container_width=True)
+
+ # مخطط شريطي للمقارنة
+ fig = px.bar(
+ projects_data,
+ x='المشروع',
+ y='نسبة المحتوى المحلي',
+ title="مقارنة نسبة المحتوى المحلي بين المشاريع",
+ text_auto='.1f',
+ color='سنة الإنجاز'
+ )
+
+ fig.update_traces(texttemplate='%{text}%', textposition='outside')
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # مخطط فقاعي للمقارنة
+ fig = px.scatter(
+ projects_data,
+ x='القيمة',
+ y='نسبة المحتوى المحلي',
+ size='القيمة',
+ color='سنة الإنجاز',
+ text='المشروع',
+ title="العلاقة بين قيمة المشروع ونسبة المحتوى المحلي"
+ )
+
+ fig.update_traces(textposition='top center')
+ fig.update_layout(xaxis_title="قيمة المشروع (ريال)", yaxis_title="نسبة المحتوى المحلي (%)")
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ elif report_type == "تقرير التطور التاريخي للمحتوى المحلي":
+ # محاكاة بيانات التطور التاريخي
+ historical_data = pd.DataFrame({
+ 'السنة': [2019, 2020, 2021, 2022, 2023, 2024],
+ 'نسبة المحتوى المحلي': [45, 48, 52, 58, 62, 66],
+ 'المستهدف': [40, 45, 50, 55, 60, 65]
+ })
+
+ # عرض جدول التطور التاريخي
+ st.dataframe(historical_data.style.format({
+ 'نسبة المحتوى المحلي': '{:.1f}%',
+ 'المستهدف': '{:.1f}%'
+ }), use_container_width=True)
+
+ # مخطط خطي للتطور التاريخي
+ fig = px.line(
+ historical_data,
+ x='السنة',
+ y=['نسبة المحتوى المحلي', 'المستهدف'],
+ title="التطور التاريخي لنسبة المحتوى المحلي",
+ markers=True,
+ labels={'value': 'النسبة (%)', 'variable': ''}
+ )
+
+ fig.update_layout(legend_title_text='')
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # مخطط شريطي للمقارنة بين الفعلي والمستهدف
+ historical_data['الفرق'] = historical_data['نسبة المحتوى المحلي'] - historical_data['المستهدف']
+
+ fig = px.bar(
+ historical_data,
+ x='السنة',
+ y='الفرق',
+ title="الفرق بين نسبة المحتوى المحلي الفعلية والمستهدفة",
+ text_auto='.1f',
+ color='الفرق',
+ color_continuous_scale=['red', 'green']
+ )
+
+ fig.update_traces(texttemplate='%{text}%', textposition='outside')
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ elif report_type == "تقرير الموردين ذوي المحتوى المحلي المرتفع":
+ # محاكاة بيانات الموردين
+ suppliers_data = pd.DataFrame({
+ 'المورد': ['شركة الإنشاءات السعودية', 'مؤسسة الطاقة المتقدمة', 'شركة التبريد العالمية',
+ 'شركة الزجاج المتطورة', 'مؤسسة التشطيبات الحديثة', 'مصنع الحديد السعودي',
+ 'شركة المعدات الهندسية', 'مكتب الاستشارات الهندسية'],
+ 'الفئة': ['مواد بناء', 'أنظمة كهربائية', 'أنظمة ميكانيكية', 'مواد بناء',
+ 'تشطيبات', 'مواد بناء', 'معدات وآليات', 'خدمات هندسية'],
+ 'نسبة المحتوى المحلي': [95, 85, 75, 80, 90, 100, 85, 100],
+ 'حجم التعامل': [3000000, 1500000, 1200000, 800000, 1200000, 2500000, 900000, 500000]
+ })
+
+ # عرض جدول الموردين
+ st.dataframe(suppliers_data.style.format({
+ 'نسبة المحتوى المحلي': '{:.0f}%',
+ 'حجم التعامل': '{:,.0f} ريال'
+ }), use_container_width=True)
+
+ # مخطط شريطي للموردين
+ fig = px.bar(
+ suppliers_data,
+ x='المورد',
+ y='نسبة المحتوى المحلي',
+ title="نسبة المحتوى المحلي للموردين",
+ text_auto='.0f',
+ color='الفئة'
+ )
+
+ fig.update_traces(texttemplate='%{text}%', textposition='outside')
+ fig.update_layout(xaxis_tickangle=-45)
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # مخطط فقاعي للموردين
+ fig = px.scatter(
+ suppliers_data,
+ x='نسبة المحتوى المحلي',
+ y='حجم التعامل',
+ size='حجم التعامل',
+ color='الفئة',
+ text='المورد',
+ title="العلاقة بين نسبة المحتوى المحلي وحجم التعامل مع الموردين"
+ )
+
+ fig.update_traces(textposition='top center')
+ fig.update_layout(xaxis_title="نسبة المحتوى المحلي (%)", yaxis_title="حجم التعامل (ريال)")
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ elif report_type == "تقرير الامتثال لمتطلبات هيئة المحتوى المحلي":
+ # محاكاة بيانات الامتثال
+ compliance_data = pd.DataFrame({
+ 'المتطلب': [
+ 'نسبة المحتوى المحلي الإجمالية',
+ 'نسبة السعودة في القوى العاملة',
+ 'نسبة المنتجات المحلية',
+ 'نسبة الخدمات المحلية',
+ 'نسبة الموردين المحليين',
+ 'المساهمة في تطوير المحتوى المحلي'
+ ],
+ 'المستهدف': [40, 30, 50, 60, 70, 20],
+ 'المحقق': [38, 35, 45, 65, 75, 25],
+ 'حالة الامتثال': ['قريب', 'ممتثل', 'غير ممتثل', 'ممتثل', 'ممتثل', 'ممتثل']
+ })
+
+ # إضافة ألوان لحالة الامتثال
+ colors = []
+ for status in compliance_data['حالة الامتثال']:
+ if status == 'ممتثل':
+ colors.append('green')
+ elif status == 'قريب':
+ colors.append('orange')
+ else:
+ colors.append('red')
+
+ compliance_data['اللون'] = colors
+
+ # عرض جدول الامتثال
+ st.dataframe(compliance_data.style.format({
+ 'المستهدف': '{:.0f}%',
+ 'المحقق': '{:.0f}%'
+ }), use_container_width=True)
+
+ # مخطط شريطي للامتثال
+ fig = px.bar(
+ compliance_data,
+ x='المتطلب',
+ y=['المستهدف', 'المحقق'],
+ title="مقارنة المتطلبات المستهدفة والمحققة",
+ barmode='group',
+ labels={'value': 'النسبة (%)', 'variable': ''}
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # مخطط دائري لحالة الامتثال
+ status_counts = compliance_data['حالة الامتثال'].value_counts().reset_index()
+ status_counts.columns = ['حالة الامتثال', 'العدد']
+
+ fig = px.pie(
+ status_counts,
+ values='العدد',
+ names='حالة الامتثال',
+ title="توزيع حالة الامتثال للمتطلبات",
+ color='حالة الامتثال',
+ color_discrete_map={
+ 'ممتثل': 'green',
+ 'قريب': 'orange',
+ 'غير ممتثل': 'red'
+ }
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # أزرار التصدير
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.download_button(
+ "تصدير التقرير كملف Excel",
+ "بيانات التقرير",
+ file_name=f"{report_type}.xlsx",
+ mime="application/vnd.ms-excel"
+ )
+
+ with col2:
+ st.download_button(
+ "تصدير التقرير كملف PDF",
+ "بيانات التقرير",
+ file_name=f"{report_type}.pdf",
+ mime="application/pdf"
+ )
+
+ def _render_lc_optimization_tab(self):
+ """عرض تبويب تحسين المحتوى المحلي"""
+
+ st.markdown("#### تحسين المحتوى المحلي")
+
+ st.markdown("""
+ تساعدك هذه الأداة في تحسين نسبة المحتوى المحلي في المشروع من خلال تقديم توصيات وبدائل للمكونات ذات المحتوى المحلي المنخفض.
+ """)
+
+ # عرض المكونات ذات المحتوى المحلي المنخفض
+ st.markdown("##### المكونات ذات المحتوى المحلي المنخفض")
+
+ # محاكاة بيانات المكونات ذات المحتوى المحلي المنخفض
+ low_lc_components = [
+ {
+ "id": 1,
+ "name": "الأثاث والتجهيزات",
+ "category": "أثاث",
+ "value": 900000,
+ "local_content": 30,
+ "supplier": "شركة الأثاث المكتبي"
+ },
+ {
+ "id": 2,
+ "name": "أنظمة الأمن والمراقبة",
+ "category": "أنظمة",
+ "value": 600000,
+ "local_content": 40,
+ "supplier": "شركة الأنظمة الأمنية المتقدمة"
+ },
+ {
+ "id": 3,
+ "name": "الواجهات والنوافذ",
+ "category": "مواد",
+ "value": 800000,
+ "local_content": 45,
+ "supplier": "شركة الزجاج المتطورة"
+ }
+ ]
+
+ # عرض جدول المكونات
+ low_lc_df = pd.DataFrame(low_lc_components)
+
+ st.dataframe(
+ low_lc_df[["name", "category", "value", "local_content", "supplier"]].rename(columns={
+ "name": "المكون",
+ "category": "الفئة",
+ "value": "القيمة",
+ "local_content": "المحتوى المحلي",
+ "supplier": "المورد"
+ }).style.format({
+ "القيمة": "{:,.0f} ريال",
+ "المحتوى المحلي": "{:.0f}%"
+ }),
+ use_container_width=True
+ )
+
+ # اختيار مكون للتحسين
+ selected_component = st.selectbox(
+ "اختر المكون للتحسين",
+ options=[comp["name"] for comp in low_lc_components],
+ index=0
+ )
+
+ # الحصول على المكون المختار
+ selected_comp_data = next((comp for comp in low_lc_components if comp["name"] == selected_component), None)
+
+ # عرض بدائل المكون المختار
+ if selected_comp_data:
+ st.markdown(f"##### البدائل المقترحة لـ {selected_component}")
+
+ # محاكاة بيانات البدائل
+ alternatives = []
+
+ if selected_component == "الأثاث والتجهيزات":
+ alternatives = [
+ {
+ "id": 1,
+ "name": "شركة الأثاث الوطني",
+ "description": "شركة متخصصة في تصنيع الأثاث المكتبي محلياً",
+ "local_content": 80,
+ "cost_factor": 1.05,
+ "quality_rating": 4.2
+ },
+ {
+ "id": 2,
+ "name": "مصنع التجهيزات المكتبية",
+ "description": "مصنع متخصص في إنتاج الأثاث المكتبي بخامات محلية",
+ "local_content": 90,
+ "cost_factor": 1.10,
+ "quality_rating": 4.5
+ },
+ {
+ "id": 3,
+ "name": "توزيع المكونات على موردين محليين",
+ "description": "تقسيم توريد الأثاث على عدة موردين محليين",
+ "local_content": 75,
+ "cost_factor": 1.00,
+ "quality_rating": 4.0
+ }
+ ]
+ elif selected_component == "أنظمة الأمن والمراقبة":
+ alternatives = [
+ {
+ "id": 1,
+ "name": "شركة التقنية الأمنية السعودية",
+ "description": "شركة متخصصة في تركيب وتجميع أنظمة الأمن محلياً",
+ "local_content": 70,
+ "cost_factor": 1.08,
+ "quality_rating": 4.0
+ },
+ {
+ "id": 2,
+ "name": "مؤسسة تقنيات الحماية",
+ "description": "توريد وتركيب أنظمة أمنية معتمدة من هيئة المحتوى المحلي",
+ "local_content": 65,
+ "cost_factor": 0.95,
+ "quality_rating": 3.8
+ },
+ {
+ "id": 3,
+ "name": "تجميع الأنظمة محلياً",
+ "description": "استيراد المكونات وتجميعها وبرمجتها محلياً",
+ "local_content": 60,
+ "cost_factor": 0.90,
+ "quality_rating": 3.7
+ }
+ ]
+ elif selected_component == "الواجهات والنوافذ":
+ alternatives = [
+ {
+ "id": 1,
+ "name": "مصنع الزجاج السعودي",
+ "description": "مصنع متخصص في إنتاج الزجاج والواجهات الزجاجية محلياً",
+ "local_content": 85,
+ "cost_factor": 1.15,
+ "quality_rating": 4.3
+ },
+ {
+ "id": 2,
+ "name": "شركة الألمنيوم الوطنية",
+ "description": "شركة متخصصة في إنتاج الواجهات والنوافذ من الألمنيوم محلياً",
+ "local_content": 90,
+ "cost_factor": 1.20,
+ "quality_rating": 4.5
+ },
+ {
+ "id": 3,
+ "name": "تعديل التصميم لاستخدام مواد محلية",
+ "description": "تعديل تصميم الواجهات لاستخدام نسبة أكبر من المواد المتوفرة محلياً",
+ "local_content": 75,
+ "cost_factor": 1.00,
+ "quality_rating": 4.0
+ }
+ ]
+
+ # عرض البدائل
+ for alt in alternatives:
+ with st.container():
+ col1, col2, col3 = st.columns([3, 1, 1])
+
+ with col1:
+ st.markdown(f"""
+
+
{alt['name']}
+
{alt['description']}
+
المحتوى المحلي: {alt['local_content']}% | معامل التكلفة: {alt['cost_factor']:.2f} | تقييم الجودة: {alt['quality_rating']}/5
+
+ """, unsafe_allow_html=True)
+
+ with col2:
+ st.button(f"تفاصيل #{alt['id']}", key=f"alt_details_{alt['id']}")
+
+ with col3:
+ if st.button(f"اختيار #{alt['id']}", key=f"select_alt_{alt['id']}"):
+ st.success(f"تم اختيار {alt['name']} كبديل لـ {selected_component}.")
+
+ # حساب تأثير البدائل على المحتوى المحلي الإجمالي
+ st.markdown("##### تأثير البدائل على المحتوى المحلي الإجمالي")
+
+ # محاكاة البيانات الإجمالية
+ total_value = 10000000
+ current_lc_value = 6000000
+ current_lc_percent = current_lc_value / total_value * 100
+
+ # حساب التأثير لكل بديل
+ impact_data = []
+ for alt in alternatives:
+ # القيمة الحالية للمحتوى المحلي في المكون
+ current_component_lc_value = selected_comp_data["value"] * selected_comp_data["local_content"] / 100
+
+ # القيمة المتوقعة للمحتوى المحلي مع البديل
+ new_component_value = selected_comp_data["value"] * alt["cost_factor"]
+ new_component_lc_value = new_component_value * alt["local_content"] / 100
+
+ # الفرق في قيمة المحتوى المحلي
+ lc_value_diff = new_component_lc_value - current_component_lc_value
+
+ # القيمة الإجمالية الجديدة للمشروع
+ new_total_value = total_value - selected_comp_data["value"] + new_component_value
+
+ # قيمة المحتوى المحلي الإجمالية الجديدة
+ new_total_lc_value = current_lc_value + lc_value_diff
+
+ # نسبة المحتوى المحلي الإجمالية الجديدة
+ new_total_lc_percent = new_total_lc_value / new_total_value * 100
+
+ # إضافة البيانات
+ impact_data.append({
+ "البديل": alt["name"],
+ "نسبة المحتوى المحلي الحالية": current_lc_percent,
+ "نسبة المحتوى المحلي المتوقعة": new_total_lc_percent,
+ "التغير": new_total_lc_percent - current_lc_percent,
+ "القيمة الإجمالية الجديدة": new_total_value,
+ "تقييم الجودة": alt["quality_rating"]
+ })
+
+ # عرض جدول التأثير
+ impact_df = pd.DataFrame(impact_data)
+
+ st.dataframe(
+ impact_df.style.format({
+ "نسبة المحتوى المحلي الحالية": "{:.1f}%",
+ "نسبة المحتوى المحلي المتوقعة": "{:.1f}%",
+ "التغير": "{:+.1f}%",
+ "القيمة الإجمالية الجديدة": "{:,.0f} ريال",
+ "تقييم الجودة": "{:.1f}/5"
+ }),
+ use_container_width=True
+ )
+
+ # مخطط مقارنة للبدائل
+ fig = px.bar(
+ impact_df,
+ x="البديل",
+ y=["نسبة المحتوى المحلي الحالية", "نسبة المحتوى المحلي المتوقعة"],
+ barmode="group",
+ title="مقارنة تأثير البدائل على نسبة المحتوى المحلي الإجمالية",
+ labels={"value": "نسبة المحتوى المحلي (%)", "variable": ""}
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # استخدام Claude AI للتحليل المتقدم
+ if st.checkbox("استخدام Claude AI لتحليل البدائل", value=False, key="lc_optimization_use_claude"):
+ with st.spinner("جاري تحليل البدائل..."):
+ # محاكاة وقت المعالجة
+ time.sleep(2)
+
+ try:
+ # إنشاء نص المدخلات للتحليل
+ prompt = f"""تحليل بدائل المحتوى المحلي لمكون {selected_component}:
+
+ المكون الحالي:
+ - الاسم: {selected_component}
+ - الفئة: {selected_comp_data['category']}
+ - القيمة: {selected_comp_data['value']:,} ريال
+ - نسبة المحتوى المحلي: {selected_comp_data['local_content']}%
+ - المورد: {selected_comp_data['supplier']}
+
+ البدائل المقترحة:
+ 1. {alternatives[0]['name']}:
+ - المحتوى المحلي: {alternatives[0]['local_content']}%
+ - معامل التكلفة: {alternatives[0]['cost_factor']:.2f}
+ - تقييم الجودة: {alternatives[0]['quality_rating']}/5
+ - الوصف: {alternatives[0]['description']}
+
+ 2. {alternatives[1]['name']}:
+ - المحتوى المحلي: {alternatives[1]['local_content']}%
+ - معامل التكلفة: {alternatives[1]['cost_factor']:.2f}
+ - تقييم الجودة: {alternatives[1]['quality_rating']}/5
+ - الوصف: {alternatives[1]['description']}
+
+ 3. {alternatives[2]['name']}:
+ - المحتوى المحلي: {alternatives[2]['local_content']}%
+ - معامل التكلفة: {alternatives[2]['cost_factor']:.2f}
+ - تقييم الجودة: {alternatives[2]['quality_rating']}/5
+ - الوصف: {alternatives[2]['description']}
+
+ المطلوب:
+ 1. تحليل مقارن شامل للبدائل من حيث المحتوى المحلي والتكلفة والجودة
+ 2. تحديد البديل الأفضل مع شرح أسباب اختياره
+ 3. تقديم توصيات إضافية لتحسين المحتوى المحلي لهذا المكون
+ 4. تحديد أي مخاطر محتملة في الانتقال للبديل المقترح
+
+ يرجى تقديم تحليل مهني ومختصر يركز على الجوانب الأكثر أهمية.
+ """
+
+ # استدعاء Claude للتحليل
+ claude_analysis = self.claude_service.chat_completion(
+ [{"role": "user", "content": prompt}]
+ )
+
+ if "error" not in claude_analysis:
+ # عرض تحليل Claude
+ st.markdown("##### تحليل متقدم للبدائل")
+ st.info(claude_analysis["content"])
+ else:
+ st.warning(f"تعذر إجراء التحليل المتقدم: {claude_analysis['error']}")
+ except Exception as e:
+ st.warning(f"تعذر إجراء التحليل المتقدم: {str(e)}")
+
+ # زر تطبيق البديل المختار
+ if st.button("تطبيق البديل المختار على المشروع"):
+ st.success("تم تطبيق البديل المختار على المشروع وتحديث نسبة المحتوى المحلي.")
+
+ def _render_faq_tab(self):
+ """عرض تبويب الأسئلة الشائعة"""
+
+ st.markdown("### الأسئلة الشائعة")
+
+ # البحث في الأسئلة الشائعة
+ search_query = st.text_input("البحث في الأسئلة الشائعة", key="faq_search")
+
+ # فلترة الأسئلة حسب البحث
+ if search_query:
+ filtered_faqs = [
+ faq for faq in self.faqs
+ if search_query.lower() in faq["question"].lower() or search_query.lower() in faq["answer"].lower()
+ ]
+ else:
+ filtered_faqs = self.faqs
+
+ # عرض الأسئلة والأجوبة
+ for i, faq in enumerate(filtered_faqs):
+ with st.expander(faq["question"]):
+ st.markdown(faq["answer"])
+
+ # زر التواصل مع الدعم
+ st.markdown("##### لم تجد إجابة لسؤالك؟")
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if st.button("التواصل مع الدعم الفني", use_container_width=True):
+ st.info("سيتم التواصل معك قريباً من قبل فريق الدعم الفني.")
+
+ with col2:
+ if st.button("طرح سؤال جديد", use_container_width=True):
+ st.text_area("اكتب سؤالك هنا")
+ st.button("إرسال")
\ No newline at end of file
diff --git a/modules/ai_assistant/contract_analyzer.py b/modules/ai_assistant/contract_analyzer.py
index 3f9fde11b2c85d12ae92d8aad6557e8ac10383f1..296712751db5bfe6761ff6166b627523f5689966 100644
--- a/modules/ai_assistant/contract_analyzer.py
+++ b/modules/ai_assistant/contract_analyzer.py
@@ -1,1333 +1,1333 @@
-"""
-محلل العقود المتقدم - وحدة تحليل العقود والمناقصات باستخدام الذكاء الاصطناعي
-
-هذا الملف يحتوي على الفئات والدوال اللازمة لتحليل العقود والمناقصات باستخدام نماذج OpenAI وClaude.
-"""
-
-import os
-import json
-import re
-import time
-import requests
-from datetime import datetime
-import pandas as pd
-import numpy as np
-from pathlib import Path
-import tempfile
-import base64
-import io
-import streamlit as st
-
-# محاكاة استيراد مكتبات الذكاء الاصطناعي
-try:
- import openai
- import anthropic
- from transformers import pipeline
- import torch
- import nltk
- import gensim
- MODELS_AVAILABLE = True
-except ImportError:
- MODELS_AVAILABLE = False
-
-class ContractAnalyzer:
- """فئة تحليل العقود والمناقصات باستخدام الذكاء الاصطناعي"""
-
- def __init__(self, api_key_source="security_section"):
- """
- تهيئة محلل العقود
-
- المعلمات:
- api_key_source (str): مصدر مفتاح API، إما "security_section" أو "manual"
- """
- self.api_key_source = api_key_source
- self.openai_api_key = None
- self.claude_api_key = None
- self.hybrid_environment = True
-
- # تهيئة مفاتيح API
- self._initialize_api_keys()
-
- # تهيئة نماذج الذكاء الاصطناعي
- self._initialize_ai_models()
-
- def _initialize_api_keys(self):
- """تهيئة مفاتيح API"""
- if self.api_key_source == "security_section":
- # محاكاة الحصول على مفاتيح API من قسم الأمان
- self.openai_api_key = "sk-security-section-openai-key"
- self.claude_api_key = "sk-security-section-claude-key"
- else:
- # استخدام مفاتيح API المدخلة يدوياً
- self.openai_api_key = st.session_state.get("openai_api_key", "")
- self.claude_api_key = st.session_state.get("claude_api_key", "")
-
- def _initialize_ai_models(self):
- """تهيئة نماذج الذكاء الاصطناعي"""
- if MODELS_AVAILABLE:
- # تهيئة نماذج OpenAI
- if self.openai_api_key:
- openai.api_key = self.openai_api_key
-
- # تهيئة نماذج Claude
- if self.claude_api_key:
- self.claude_client = anthropic.Anthropic(api_key=self.claude_api_key)
-
- # تهيئة نماذج Hugging Face
- self.summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
- self.ner_model = pipeline("ner", model="dbmdz/bert-large-cased-finetuned-conll03-english")
-
- # تهيئة NLTK
- nltk.download('punkt', quiet=True)
- nltk.download('stopwords', quiet=True)
-
- def analyze_contract(self, file_path, analysis_type="comprehensive"):
- """
- تحليل العقد باستخدام الذكاء الاصطناعي
-
- المعلمات:
- file_path (str): مسار ملف العقد
- analysis_type (str): نوع التحليل، إما "comprehensive" أو "quick" أو "legal" أو "financial"
-
- العوائد:
- dict: نتائج التحليل
- """
- # استخراج النص من الملف
- contract_text = self._extract_text_from_file(file_path)
-
- # تحليل العقد باستخدام الذكاء الاصطناعي
- if analysis_type == "comprehensive":
- return self._comprehensive_analysis(contract_text, os.path.basename(file_path))
- elif analysis_type == "quick":
- return self._quick_analysis(contract_text, os.path.basename(file_path))
- elif analysis_type == "legal":
- return self._legal_analysis(contract_text, os.path.basename(file_path))
- elif analysis_type == "financial":
- return self._financial_analysis(contract_text, os.path.basename(file_path))
- else:
- return self._comprehensive_analysis(contract_text, os.path.basename(file_path))
-
- def analyze_tender(self, file_path, analysis_type="comprehensive"):
- """
- تحليل المناقصة باستخدام الذكاء الاصطناعي
-
- المعلمات:
- file_path (str): مسار ملف المناقصة
- analysis_type (str): نوع التحليل، إما "comprehensive" أو "quick" أو "technical" أو "financial"
-
- العوائد:
- dict: نتائج التحليل
- """
- # استخراج النص من الملف
- tender_text = self._extract_text_from_file(file_path)
-
- # تحليل المناقصة باستخدام الذكاء الاصطناعي
- if analysis_type == "comprehensive":
- return self._comprehensive_tender_analysis(tender_text, os.path.basename(file_path))
- elif analysis_type == "quick":
- return self._quick_tender_analysis(tender_text, os.path.basename(file_path))
- elif analysis_type == "technical":
- return self._technical_tender_analysis(tender_text, os.path.basename(file_path))
- elif analysis_type == "financial":
- return self._financial_tender_analysis(tender_text, os.path.basename(file_path))
- else:
- return self._comprehensive_tender_analysis(tender_text, os.path.basename(file_path))
-
- def analyze_dwg_file(self, file_path):
- """
- تحليل ملف DWG باستخدام الذكاء الاصطناعي
-
- المعلمات:
- file_path (str): مسار ملف DWG
-
- العوائد:
- dict: نتائج التحليل
- """
- # محاكاة تحليل ملف DWG
- return self._simulate_dwg_analysis(file_path)
-
- def compare_contracts(self, file_path1, file_path2):
- """
- مقارنة عقدين باستخدام الذكاء الاصطناعي
-
- المعلمات:
- file_path1 (str): مسار الملف الأول
- file_path2 (str): مسار الملف الثاني
-
- العوائد:
- dict: نتائج المقارنة
- """
- # استخراج النص من الملفين
- text1 = self._extract_text_from_file(file_path1)
- text2 = self._extract_text_from_file(file_path2)
-
- # مقارنة العقدين باستخدام الذكاء الاصطناعي
- return self._compare_documents(text1, text2, os.path.basename(file_path1), os.path.basename(file_path2))
-
- def extract_key_terms(self, file_path):
- """
- استخراج الشروط الرئيسية من العقد
-
- المعلمات:
- file_path (str): مسار ملف العقد
-
- العوائد:
- dict: الشروط الرئيسية المستخرجة
- """
- # استخراج النص من الملف
- contract_text = self._extract_text_from_file(file_path)
-
- # استخراج الشروط الرئيسية باستخدام الذكاء الاصطناعي
- return self._extract_key_terms_from_text(contract_text)
-
- def identify_risks(self, file_path):
- """
- تحديد المخاطر في العقد
-
- المعلمات:
- file_path (str): مسار ملف العقد
-
- العوائد:
- dict: المخاطر المحددة
- """
- # استخراج النص من الملف
- contract_text = self._extract_text_from_file(file_path)
-
- # تحديد المخاطر باستخدام الذكاء الاصطناعي
- return self._identify_risks_in_text(contract_text)
-
- def suggest_improvements(self, file_path):
- """
- اقتراح تحسينات للعقد
-
- المعلمات:
- file_path (str): مسار ملف العقد
-
- العوائد:
- dict: التحسينات المقترحة
- """
- # استخراج النص من الملف
- contract_text = self._extract_text_from_file(file_path)
-
- # اقتراح تحسينات باستخدام الذكاء الاصطناعي
- return self._suggest_improvements_for_text(contract_text)
-
- def _extract_text_from_file(self, file_path):
- """
- استخراج النص من الملف
-
- المعلمات:
- file_path (str): مسار الملف
-
- العوائد:
- str: النص المستخرج
- """
- # محاكاة استخراج النص من الملف
- file_extension = os.path.splitext(file_path)[1].lower()
-
- # محاكاة نص العقد
- if "contract" in file_path.lower() or "عقد" in file_path:
- return """
- عقد إنشاء مبنى إداري
-
- المادة الأولى: أطراف العقد
- الطرف الأول: وزارة المالية، ويمثلها السيد/ أحمد محمد علي، بصفته وكيل الوزارة للمشاريع.
- الطرف الثاني: شركة الإنشاءات المتطورة، ويمثلها السيد/ خالد عبدالله محمد، بصفته المدير العام.
-
- المادة الثانية: موضوع العقد
- يتعهد الطرف الثاني بتنفيذ مشروع إنشاء مبنى إداري لصالح الطرف الأول وفقاً للمواصفات والشروط المرفقة بهذا العقد.
-
- المادة الثالثة: قيمة العقد
- قيمة العقد الإجمالية هي 25,000,000 ريال (خمسة وعشرون مليون ريال) شاملة جميع الضرائب والرسوم.
-
- المادة الرابعة: مدة التنفيذ
- مدة تنفيذ المشروع 18 شهراً تبدأ من تاريخ استلام الموقع.
-
- المادة الخامسة: الدفعات
- يتم سداد قيمة العقد على دفعات شهرية حسب نسبة الإنجاز، مع احتجاز 10% من قيمة كل دفعة كضمان حسن التنفيذ.
-
- المادة السادسة: الضمانات
- يقدم الطرف الثاني ضماناً نهائياً بنسبة 5% من قيمة العقد ساري المفعول حتى انتهاء فترة الضمان.
-
- المادة السابعة: غرامات التأخير
- في حالة تأخر الطرف الثاني عن التنفيذ في الموعد المحدد، يتم تطبيق غرامة تأخير بنسبة 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10% من قيمة العقد.
-
- المادة الثامنة: فترة الضمان
- فترة ضمان المشروع سنة واحدة من تاريخ الاستلام الابتدائي.
-
- المادة التاسعة: فسخ العقد
- يحق للطرف الأول فسخ العقد في حالة إخلال الطرف الثاني بالتزاماته التعاقدية بعد إنذاره كتابياً.
-
- المادة العاشرة: تسوية النزاعات
- في حالة نشوء أي نزاع بين الطرفين، يتم حله ودياً، وفي حالة تعذر ذلك يتم اللجوء إلى التحكيم وفقاً لأنظمة المملكة العربية السعودية.
-
- المادة الحادية عشر: القانون الواجب التطبيق
- تخضع جميع بنود هذا العقد لأنظمة المملكة العربية السعودية.
-
- حرر هذا العقد من نسختين أصليتين بتاريخ 15/03/2024م.
-
- الطرف الأول الطرف الثاني
- وزارة المالية شركة الإنشاءات المتطورة
- """
- elif "tender" in file_path.lower() or "مناقصة" in file_path:
- return """
- كراسة الشروط والمواصفات
- مناقصة إنشاء مبنى إداري
-
- أولاً: معلومات المناقصة
- رقم المناقصة: T-2024-001
- الجهة المالكة: وزارة المالية
- موقع المشروع: الرياض - حي العليا
- تاريخ الطرح: 01/03/2024م
- تاريخ الإقفال: 15/04/2024م
-
- ثانياً: وصف المشروع
- يتكون المشروع من إنشاء مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع. يشمل المشروع الأعمال الإنشائية والمعمارية والكهربائية والميكانيكية وأعمال التشطيبات.
-
- ثالثاً: شروط التأهيل
- 1. أن يكون المقاول مصنفاً في مجال المباني من الدرجة الأولى.
- 2. أن يكون لديه خبرة سابقة في تنفيذ مشاريع مماثلة لا تقل عن 3 مشاريع خلال الخمس سنوات الماضية.
- 3. أن يكون لديه سيولة مالية كافية لتنفيذ المشروع.
-
- رابعاً: الضمانات المطلوبة
- 1. ضمان ابتدائي: 2% من قيمة العطاء ساري المفعول لمدة 90 يوماً من تاريخ تقديم العطاء.
- 2. ضمان نهائي: 5% من قيمة العقد ساري المفعول حتى انتهاء فترة الضمان.
-
- خامساً: مدة التنفيذ
- مدة تنفيذ المشروع 18 شهراً من تاريخ استلام الموقع.
-
- سادساً: غرامات التأخير
- في حالة تأخر المقاول عن التنفيذ في الموعد المحدد، يتم تطبيق غرامة تأخير بنسبة 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10% من قيمة العقد.
-
- سابعاً: شروط الدفع
- يتم سداد قيمة العقد على دفعات شهرية حسب نسبة الإنجاز، مع احتجاز 10% من قيمة كل دفعة كضمان حسن التنفيذ.
-
- ثامناً: المواصفات الفنية
- 1. الأعمال الإنشائية:
- - الخرسانة المسلحة: مقاومة لا تقل عن 300 كجم/سم²
- - حديد التسليح: درجة 60
-
- 2. الأعمال المعمارية:
- - الواجهات: زجاج عاكس وحجر طبيعي
- - الأرضيات: رخام للمداخل وبورسلين للمكاتب
- - الأسقف: أسقف مستعارة من الجبس المزخرف
-
- 3. الأعمال الكهربائية:
- - نظام إنارة موفر للطاقة
- - نظام إنذار وإطفاء حريق آلي
- - نظام مراقبة بالكاميرات
-
- 4. الأعمال الميكانيكية:
- - نظام تكييف مركزي
- - نظام تهوية متطور
- - مصاعد عدد 3
-
- تاسعاً: معايير التقييم
- 1. السعر: 50%
- 2. الجودة الفنية: 30%
- 3. الخبرة السابقة: 15%
- 4. مدة التنفيذ: 5%
-
- عاشراً: تقديم العطاءات
- يتم تقديم العطاءات في مظروفين منفصلين (فني ومالي) إلى إدارة المشتريات بوزارة المالية في موعد أقصاه الساعة 12 ظهراً من يوم 15/04/2024م.
- """
- else:
- return """
- محتوى الملف غير معروف. يرجى التأكد من نوع الملف وصيغته.
- """
-
- def _comprehensive_analysis(self, contract_text, file_name):
- """
- تحليل شامل للعقد
-
- المعلمات:
- contract_text (str): نص العقد
- file_name (str): اسم الملف
-
- العوائد:
- dict: نتائج التحليل
- """
- # محاكاة تحليل شامل للعقد
-
- # استخراج الأطراف
- parties = self._extract_parties(contract_text)
-
- # استخراج قيمة العقد
- contract_value = self._extract_contract_value(contract_text)
-
- # استخراج مدة التنفيذ
- duration = self._extract_duration(contract_text)
-
- # استخراج الضمانات
- guarantees = self._extract_guarantees(contract_text)
-
- # استخراج غرامات التأخير
- penalties = self._extract_penalties(contract_text)
-
- # استخراج شروط الدفع
- payment_terms = self._extract_payment_terms(contract_text)
-
- # استخراج فترة الضمان
- warranty_period = self._extract_warranty_period(contract_text)
-
- # استخراج شروط فسخ العقد
- termination_terms = self._extract_termination_terms(contract_text)
-
- # استخراج آلية تسوية النزاعات
- dispute_resolution = self._extract_dispute_resolution(contract_text)
-
- # تحديد المخاطر
- risks = self._identify_risks_in_text(contract_text)
-
- # اقتراح تحسينات
- improvements = self._suggest_improvements_for_text(contract_text)
-
- # إعداد النتائج
- results = {
- "title": f"تحليل شامل - {file_name}",
- "date": datetime.now().strftime("%Y-%m-%d"),
- "summary": "هذا العقد يتعلق بإنشاء مبنى إداري لصالح وزارة المالية. يتضمن العقد شروطاً متعلقة بقيمة العقد، مدة التنفيذ، الدفعات، الضمانات، غرامات التأخير، فترة الضمان، فسخ العقد، وتسوية النزاعات.",
- "key_points": [
- f"قيمة العقد: {contract_value}",
- f"مدة التنفيذ: {duration}",
- f"الضمانات: {guarantees}",
- f"غرامات التأخير: {penalties}",
- f"شروط الدفع: {payment_terms}",
- f"فترة الضمان: {warranty_period}"
- ],
- "entities": {
- "الأطراف": parties,
- "قيمة العقد": contract_value,
- "مدة التنفيذ": duration,
- "الضمانات": guarantees,
- "غرامات التأخير": penalties,
- "شروط الدفع": payment_terms,
- "فترة الضمان": warranty_period,
- "شروط فسخ العقد": termination_terms,
- "آلية تسوية النزاعات": dispute_resolution
- },
- "risks": risks,
- "improvements": improvements,
- "recommendations": [
- "مراجعة قيمة الضمان النهائي للتأكد من كفايتها",
- "توضيح آلية احتساب نسبة الإنجاز للدفعات الشهرية",
- "إضافة بند يتعلق بالتغييرات في نطاق العمل",
- "توضيح حقوق الملكية الفكرية للتصاميم والمخططات",
- "إضافة بند يتعلق بالقوة القاهرة"
- ]
- }
-
- return results
-
- def _quick_analysis(self, contract_text, file_name):
- """
- تحليل سريع للعقد
-
- المعلمات:
- contract_text (str): نص العقد
- file_name (str): اسم الملف
-
- العوائد:
- dict: نتائج التحليل
- """
- # محاكاة تحليل سريع للعقد
-
- # استخراج الأطراف
- parties = self._extract_parties(contract_text)
-
- # استخراج قيمة العقد
- contract_value = self._extract_contract_value(contract_text)
-
- # استخراج مدة التنفيذ
- duration = self._extract_duration(contract_text)
-
- # استخراج الضمانات
- guarantees = self._extract_guarantees(contract_text)
-
- # إعداد النتائج
- results = {
- "title": f"تحليل سريع - {file_name}",
- "date": datetime.now().strftime("%Y-%m-%d"),
- "summary": "هذا العقد يتعلق بإنشاء مبنى إداري لصالح وزارة المالية.",
- "key_points": [
- f"قيمة العقد: {contract_value}",
- f"مدة التنفيذ: {duration}",
- f"الضمانات: {guarantees}"
- ],
- "entities": {
- "الأطراف": parties,
- "قيمة العقد": contract_value,
- "مدة التنفيذ": duration,
- "الضمانات": guarantees
- },
- "recommendations": [
- "مراجعة قيمة الضمان النهائي للتأكد من كفايتها",
- "توضيح آلية احتساب نسبة الإنجاز للدفعات الشهرية"
- ]
- }
-
- return results
-
- def _legal_analysis(self, contract_text, file_name):
- """
- تحليل قانوني للعقد
-
- المعلمات:
- contract_text (str): نص العقد
- file_name (str): اسم الملف
-
- العوائد:
- dict: نتائج التحليل
- """
- # محاكاة تحليل قانوني للعقد
-
- # استخراج الأطراف
- parties = self._extract_parties(contract_text)
-
- # استخراج الضمانات
- guarantees = self._extract_guarantees(contract_text)
-
- # استخراج غرامات التأخير
- penalties = self._extract_penalties(contract_text)
-
- # استخراج شروط فسخ العقد
- termination_terms = self._extract_termination_terms(contract_text)
-
- # استخراج آلية تسوية النزاعات
- dispute_resolution = self._extract_dispute_resolution(contract_text)
-
- # تحديد المخاطر القانونية
- legal_risks = [
- "عدم وضوح آلية تسوية النزاعات",
- "عدم تحديد المحكمة المختصة",
- "عدم وضوح شروط فسخ العقد",
- "عدم وجود بند يتعلق بالقوة القاهرة",
- "عدم وضوح حقوق الملكية الفكرية"
- ]
-
- # إعداد النتائج
- results = {
- "title": f"تحليل قانوني - {file_name}",
- "date": datetime.now().strftime("%Y-%m-%d"),
- "summary": "هذا العقد يتضمن بعض الثغرات القانونية التي قد تؤدي إلى نزاعات مستقبلية.",
- "key_points": [
- f"الضمانات: {guarantees}",
- f"غرامات التأخير: {penalties}",
- f"شروط فسخ العقد: {termination_terms}",
- f"آلية تسوية النزاعات: {dispute_resolution}"
- ],
- "entities": {
- "الأطراف": parties,
- "الضمانات": guarantees,
- "غرامات التأخير": penalties,
- "شروط فسخ العقد": termination_terms,
- "آلية تسوية النزاعات": dispute_resolution
- },
- "legal_risks": legal_risks,
- "recommendations": [
- "توضيح آلية تسوية النزاعات وتحديد المحكمة المختصة",
- "إضافة بند يتعلق بالقوة القاهرة",
- "توضيح شروط فسخ العقد بشكل أكثر تفصيلاً",
- "إضافة بند يتعلق بحقوق الملكية الفكرية",
- "توضيح آلية تعديل العقد"
- ]
- }
-
- return results
-
- def _financial_analysis(self, contract_text, file_name):
- """
- تحليل مالي للعقد
-
- المعلمات:
- contract_text (str): نص العقد
- file_name (str): اسم الملف
-
- العوائد:
- dict: نتائج التحليل
- """
- # محاكاة تحليل مالي للعقد
-
- # استخراج قيمة العقد
- contract_value = self._extract_contract_value(contract_text)
-
- # استخراج شروط الدفع
- payment_terms = self._extract_payment_terms(contract_text)
-
- # استخراج الضمانات
- guarantees = self._extract_guarantees(contract_text)
-
- # استخراج غرامات التأخير
- penalties = self._extract_penalties(contract_text)
-
- # تحليل التدفقات النقدية
- cash_flow = [
- {"month": 1, "income": 1250000, "expense": 1000000, "net": 250000},
- {"month": 2, "income": 1250000, "expense": 1100000, "net": 150000},
- {"month": 3, "income": 1250000, "expense": 1200000, "net": 50000},
- {"month": 4, "income": 1250000, "expense": 1000000, "net": 250000},
- {"month": 5, "income": 1250000, "expense": 900000, "net": 350000},
- {"month": 6, "income": 1250000, "expense": 950000, "net": 300000}
- ]
-
- # تحليل المخاطر المالية
- financial_risks = [
- "تأخر الدفعات",
- "زيادة أسعار المواد",
- "نقص السيولة",
- "تغير أسعار العملات",
- "زيادة تكاليف العمالة"
- ]
-
- # إعداد النتائج
- results = {
- "title": f"تحليل مالي - {file_name}",
- "date": datetime.now().strftime("%Y-%m-%d"),
- "summary": f"هذا العقد بقيمة {contract_value} يتضمن شروط دفع شهرية حسب نسبة الإنجاز مع احتجاز 10% من قيمة كل دفعة كضمان حسن التنفيذ.",
- "key_points": [
- f"قيمة العقد: {contract_value}",
- f"شروط الدفع: {payment_terms}",
- f"الضمانات: {guarantees}",
- f"غرامات التأخير: {penalties}"
- ],
- "entities": {
- "قيمة العقد": contract_value,
- "شروط الدفع": payment_terms,
- "الضمانات": guarantees,
- "غرامات التأخير": penalties
- },
- "cash_flow": cash_flow,
- "financial_risks": financial_risks,
- "recommendations": [
- "توضيح آلية احتساب نسبة الإنجاز للدفعات الشهرية",
- "إضافة بند يتعلق بتعديل قيمة العقد في حالة تغير أسعار المواد",
- "تقليل نسبة احتجاز ضمان حسن التنفيذ",
- "إضافة بند يتعلق بالدفعة المقدمة",
- "توضيح آلية تسوية المستخلصات النهائية"
- ]
- }
-
- return results
-
- def _comprehensive_tender_analysis(self, tender_text, file_name):
- """
- تحليل شامل للمناقصة
-
- المعلمات:
- tender_text (str): نص المناقصة
- file_name (str): اسم الملف
-
- العوائد:
- dict: نتائج التحليل
- """
- # محاكاة تحليل شامل للمناقصة
-
- # استخراج معلومات المناقصة
- tender_info = self._extract_tender_info(tender_text)
-
- # استخراج وصف المشروع
- project_description = self._extract_project_description(tender_text)
-
- # استخراج شروط التأهيل
- qualification_conditions = self._extract_qualification_conditions(tender_text)
-
- # استخراج الضمانات المطلوبة
- required_guarantees = self._extract_required_guarantees(tender_text)
-
- # استخراج مدة التنفيذ
- duration = self._extract_duration(tender_text)
-
- # استخراج غرامات التأخير
- penalties = self._extract_penalties(tender_text)
-
- # استخراج شروط الدفع
- payment_terms = self._extract_payment_terms(tender_text)
-
- # استخراج المواصفات الفنية
- technical_specifications = self._extract_technical_specifications(tender_text)
-
- # استخراج معايير التقييم
- evaluation_criteria = self._extract_evaluation_criteria(tender_text)
-
- # تحليل المنافسة
- competition_analysis = self._analyze_competition(tender_info)
-
- # تحليل المخاطر
- risk_analysis = self._analyze_risks(tender_text)
-
- # تحليل الفرص
- opportunity_analysis = self._analyze_opportunities(tender_text)
-
- # إعداد النتائج
- results = {
- "title": f"تحليل شامل - {file_name}",
- "date": datetime.now().strftime("%Y-%m-%d"),
- "summary": f"هذه المناقصة تتعلق بإنشاء مبنى إداري لصالح وزارة المالية. تتضمن المناقصة شروطاً متعلقة بالتأهيل، الضمانات، مدة التنفيذ، غرامات التأخير، شروط الدفع، المواصفات الفنية، ومعايير التقييم.",
- "key_points": [
- f"رقم المناقصة: {tender_info.get('رقم المناقصة', 'غير محدد')}",
- f"الجهة المالكة: {tender_info.get('الجهة المالكة', 'غير محدد')}",
- f"موقع المشروع: {tender_info.get('موقع المشروع', 'غير محدد')}",
- f"تاريخ الطرح: {tender_info.get('تاريخ الطرح', 'غير محدد')}",
- f"تاريخ الإقفال: {tender_info.get('تاريخ الإقفال', 'غير محدد')}",
- f"مدة التنفيذ: {duration}",
- f"الضمانات المطلوبة: {required_guarantees}"
- ],
- "entities": {
- "معلومات المناقصة": tender_info,
- "وصف المشروع": project_description,
- "شروط التأهيل": qualification_conditions,
- "الضمانات المطلوبة": required_guarantees,
- "مدة التنفيذ": duration,
- "غرامات التأخير": penalties,
- "شروط الدفع": payment_terms,
- "المواصفات الفنية": technical_specifications,
- "معايير التقييم": evaluation_criteria
- },
- "competition_analysis": competition_analysis,
- "risk_analysis": risk_analysis,
- "opportunity_analysis": opportunity_analysis,
- "recommendations": [
- "تقديم عرض سعر تنافسي يقل بنسبة 5-10% عن الميزانية التقديرية",
- "التركيز على الجوانب الفنية في العرض",
- "إبراز الخبرات السابقة في مشاريع مماثلة",
- "تقديم جدول زمني مفصل للتنفيذ",
- "تقديم حلول مبتكرة لتقليل التكاليف وزيادة الجودة"
- ]
- }
-
- return results
-
- def _quick_tender_analysis(self, tender_text, file_name):
- """
- تحليل سريع للمناقصة
-
- المعلمات:
- tender_text (str): نص المناقصة
- file_name (str): اسم الملف
-
- العوائد:
- dict: نتائج التحليل
- """
- # محاكاة تحليل سريع للمناقصة
-
- # استخراج معلومات المناقصة
- tender_info = self._extract_tender_info(tender_text)
-
- # استخراج وصف المشروع
- project_description = self._extract_project_description(tender_text)
-
- # استخراج مدة التنفيذ
- duration = self._extract_duration(tender_text)
-
- # استخراج الضمانات المطلوبة
- required_guarantees = self._extract_required_guarantees(tender_text)
-
- # إعداد النتائج
- results = {
- "title": f"تحليل سريع - {file_name}",
- "date": datetime.now().strftime("%Y-%m-%d"),
- "summary": f"هذه المناقصة تتعلق بإنشاء مبنى إداري لصالح وزارة المالية.",
- "key_points": [
- f"رقم المناقصة: {tender_info.get('رقم المناقصة', 'غير محدد')}",
- f"الجهة المالكة: {tender_info.get('الجهة المالكة', 'غير محدد')}",
- f"موقع المشروع: {tender_info.get('موقع المشروع', 'غير محدد')}",
- f"تاريخ الإقفال: {tender_info.get('تاريخ الإقفال', 'غير محدد')}",
- f"مدة التنفيذ: {duration}"
- ],
- "entities": {
- "معلومات المناقصة": tender_info,
- "وصف المشروع": project_description,
- "مدة التنفيذ": duration,
- "الضمانات المطلوبة": required_guarantees
- },
- "recommendations": [
- "تقديم عرض سعر تنافسي",
- "إبراز الخبرات السابقة في مشاريع مماثلة"
- ]
- }
-
- return results
-
- def _technical_tender_analysis(self, tender_text, file_name):
- """
- تحليل فني للمناقصة
-
- المعلمات:
- tender_text (str): نص المناقصة
- file_name (str): اسم الملف
-
- العوائد:
- dict: نتائج التحليل
- """
- # محاكاة تحليل فني للمناقصة
-
- # استخراج وصف المشروع
- project_description = self._extract_project_description(tender_text)
-
- # استخراج المواصفات الفنية
- technical_specifications = self._extract_technical_specifications(tender_text)
-
- # استخراج مدة التنفيذ
- duration = self._extract_duration(tender_text)
-
- # تحليل المتطلبات الفنية
- technical_requirements_analysis = self._analyze_technical_requirements(technical_specifications)
-
- # تحليل المخاطر الفنية
- technical_risks = self._analyze_technical_risks(technical_specifications)
-
- # إعداد النتائج
- results = {
- "title": f"تحليل فني - {file_name}",
- "date": datetime.now().strftime("%Y-%m-%d"),
- "summary": f"هذه المناقصة تتضمن متطلبات فنية متوسطة المستوى. المشروع يتكون من إنشاء مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع.",
- "key_points": [
- f"وصف المشروع: {project_description}",
- f"مدة التنفيذ: {duration}",
- "المواصفات الفنية تشمل الأعمال الإنشائية والمعمارية والكهربائية والميكانيكية"
- ],
- "entities": {
- "وصف المشروع": project_description,
- "المواصفات الفنية": technical_specifications,
- "مدة التنفيذ": duration
- },
- "technical_requirements_analysis": technical_requirements_analysis,
- "technical_risks": technical_risks,
- "recommendations": [
- "التركيز على الجوانب الفنية في العرض",
- "تقديم حلول مبتكرة لتحسين الجودة",
- "تقديم جدول زمني مفصل للتنفيذ",
- "اقتراح بدائل فنية لتقليل التكاليف",
- "تقديم خطة ضمان الجودة"
- ]
- }
-
- return results
-
- def _financial_tender_analysis(self, tender_text, file_name):
- """
- تحليل مالي للمناقصة
-
- المعلمات:
- tender_text (str): نص المناقصة
- file_name (str): اسم الملف
-
- العوائد:
- dict: نتائج التحليل
- """
- # محاكاة تحليل مالي للمناقصة
-
- # استخراج معلومات المناقصة
- tender_info = self._extract_tender_info(tender_text)
-
- # استخراج وصف المشروع
- project_description = self._extract_project_description(tender_text)
-
- # استخراج الضمانات المطلوبة
- required_guarantees = self._extract_required_guarantees(tender_text)
-
- # استخراج شروط الدفع
- payment_terms = self._extract_payment_terms(tender_text)
-
- # استخراج غرامات التأخير
- penalties = self._extract_penalties(tender_text)
-
- # تقدير التكاليف
- cost_estimation = self._estimate_costs(project_description)
-
- # تحليل التدفقات النقدية
- cash_flow = self._analyze_cash_flow(payment_terms, cost_estimation)
-
- # تحليل المخاطر المالية
- financial_risks = self._analyze_financial_risks(tender_text)
-
- # إعداد النتائج
- results = {
- "title": f"تحليل مالي - {file_name}",
- "date": datetime.now().strftime("%Y-%m-%d"),
- "summary": f"هذه المناقصة تتطلب ضماناً ابتدائياً بنسبة 2% من قيمة العطاء وضماناً نهائياً بنسبة 5% من قيمة العقد. شروط الدفع هي دفعات شهرية حسب نسبة الإنجاز مع احتجاز 10% من قيمة كل دفعة.",
- "key_points": [
- f"الضمانات المطلوبة: {required_guarantees}",
- f"شروط الدفع: {payment_terms}",
- f"غرامات التأخير: {penalties}",
- f"تقدير التكلفة الإجمالية: {cost_estimation.get('total_cost', 0):,} ريال"
- ],
- "entities": {
- "معلومات المناقصة": tender_info,
- "الضمانات المطلوبة": required_guarantees,
- "شروط الدفع": payment_terms,
- "غرامات التأخير": penalties
- },
- "cost_estimation": cost_estimation,
- "cash_flow": cash_flow,
- "financial_risks": financial_risks,
- "recommendations": [
- "تقديم عرض سعر يقل بنسبة 5-10% عن الميزانية التقديرية",
- "طلب دفعة مقدمة لتحسين التدفق النقدي",
- "تقليل نسبة احتجاز ضمان حسن التنفيذ",
- "تقديم بدائل لتقليل التكاليف",
- "وضع خطة للتعامل مع المخاطر المالية"
- ]
- }
-
- return results
-
- def _simulate_dwg_analysis(self, file_path):
- """
- محاكاة تحليل ملف DWG
-
- المعلمات:
- file_path (str): مسار ملف DWG
-
- العوائد:
- dict: نتائج التحليل
- """
- # محاكاة تحليل ملف DWG
- results = {
- "file_name": os.path.basename(file_path),
- "file_size": f"{np.random.randint(1, 10)} MB",
- "elements_count": np.random.randint(100, 1000),
- "layers_count": np.random.randint(5, 20),
- "dimensions": {
- "width": f"{np.random.randint(10, 100)} م",
- "height": f"{np.random.randint(10, 100)} م",
- "area": f"{np.random.randint(100, 10000)} م²"
- },
- "elements": {
- "walls": np.random.randint(10, 100),
- "doors": np.random.randint(5, 50),
- "windows": np.random.randint(5, 50),
- "columns": np.random.randint(5, 50),
- "stairs": np.random.randint(1, 10)
- },
- "materials": [
- {"name": "خرسانة", "volume": f"{np.random.randint(10, 1000)} م³"},
- {"name": "حديد", "weight": f"{np.random.randint(1, 100)} طن"},
- {"name": "طابوق", "count": f"{np.random.randint(1000, 10000)} قطعة"},
- {"name": "زجاج", "area": f"{np.random.randint(10, 1000)} م²"},
- {"name": "خشب", "volume": f"{np.random.randint(1, 50)} م³"}
- ],
- "cost_estimate": {
- "materials": np.random.randint(100000, 1000000),
- "labor": np.random.randint(50000, 500000),
- "equipment": np.random.randint(10000, 100000),
- "total": np.random.randint(200000, 2000000)
- },
- "recommendations": [
- "يمكن تقليل تكلفة المواد باستخدام بدائل أقل تكلفة",
- "يمكن تحسين كفاءة استخدام المساحة",
- "يمكن تقليل عدد الأعمدة لتوفير التكلفة",
- "يمكن تحسين تصميم السلالم لزيادة السلامة",
- "يمكن تحسين توزيع النوافذ لزيادة الإضاءة الطبيعية"
- ]
- }
-
- return results
-
- def _compare_documents(self, text1, text2, file_name1, file_name2):
- """
- مقارنة مستندين
-
- المعلمات:
- text1 (str): نص المستند الأول
- text2 (str): نص المستند الثاني
- file_name1 (str): اسم الملف الأول
- file_name2 (str): اسم الملف الثاني
-
- العوائد:
- dict: نتائج المقارنة
- """
- # محاكاة مقارنة مستندين
-
- # تحليل المستند الأول
- doc1_analysis = self._quick_analysis(text1, file_name1)
-
- # تحليل المستند الثاني
- doc2_analysis = self._quick_analysis(text2, file_name2)
-
- # تحديد أوجه التشابه
- similarities = [
- "كلا المستندين يتعلقان بمشاريع إنشائية",
- "كلا المستندين يتضمنان شروطاً متعلقة بالضمانات",
- "كلا المستندين يتضمنان شروطاً متعلقة بغرامات التأخير",
- "كلا المستندين يتضمنان شروطاً متعلقة بشروط الدفع"
- ]
-
- # تحديد أوجه الاختلاف
- differences = [
- "المستند الأول يتعلق بعقد إنشاء، بينما المستند الثاني يتعلق بمناقصة",
- "قيمة الضمان النهائي في المستند الأول 5%، بينما في المستند الثاني 10%",
- "مدة التنفيذ في المستند الأول 18 شهراً، بينما في المستند الثاني 24 شهراً",
- "شروط الدفع في المستند الأول تتضمن دفعة مقدمة، بينما في المستند الثاني لا توجد دفعة مقدمة"
- ]
-
- # إعداد النتائج
- results = {
- "title": f"مقارنة بين {file_name1} و {file_name2}",
- "date": datetime.now().strftime("%Y-%m-%d"),
- "summary": "هذه المقارنة تظهر أوجه التشابه والاختلاف بين المستندين. يتشابه المستندان في كونهما يتعلقان بمشاريع إنشائية ويتضمنان شروطاً متعلقة بالضمانات وغرامات التأخير وشروط الدفع. ومع ذلك، هناك اختلافات في نوع المستند وقيمة الضمان النهائي ومدة التنفيذ وشروط الدفع.",
- "document1": {
- "title": doc1_analysis.get("title", ""),
- "summary": doc1_analysis.get("summary", ""),
- "key_points": doc1_analysis.get("key_points", [])
- },
- "document2": {
- "title": doc2_analysis.get("title", ""),
- "summary": doc2_analysis.get("summary", ""),
- "key_points": doc2_analysis.get("key_points", [])
- },
- "similarities": similarities,
- "differences": differences,
- "recommendations": [
- "مراجعة الاختلافات في قيمة الضمان النهائي",
- "مراجعة الاختلافات في مدة التنفيذ",
- "مراجعة الاختلافات في شروط الدفع",
- "توحيد الشروط في المستندين إذا كانا يتعلقان بنفس المشروع"
- ]
- }
-
- return results
-
- def _extract_key_terms_from_text(self, text):
- """
- استخراج الشروط الرئيسية من النص
-
- المعلمات:
- text (str): نص العقد
-
- العوائد:
- dict: الشروط الرئيسية المستخرجة
- """
- # محاكاة استخراج الشروط الرئيسية
- key_terms = {
- "الأطراف": self._extract_parties(text),
- "قيمة العقد": self._extract_contract_value(text),
- "مدة التنفيذ": self._extract_duration(text),
- "الضمانات": self._extract_guarantees(text),
- "غرامات التأخير": self._extract_penalties(text),
- "شروط الدفع": self._extract_payment_terms(text),
- "فترة الضمان": self._extract_warranty_period(text),
- "شروط فسخ العقد": self._extract_termination_terms(text),
- "آلية تسوية النزاعات": self._extract_dispute_resolution(text)
- }
-
- return key_terms
-
- def _identify_risks_in_text(self, text):
- """
- تحديد المخاطر في النص
-
- المعلمات:
- text (str): نص العقد
-
- العوائد:
- list: المخاطر المحددة
- """
- # محاكاة تحديد المخاطر
- risks = [
- {"risk": "ارتفاع أسعار المواد", "probability": "متوسطة", "impact": "عالي", "mitigation": "تثبيت أسعار المواد الرئيسية مع الموردين"},
- {"risk": "تأخر التنفيذ", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع خطة تنفيذ مفصلة مع هوامش زمنية"},
- {"risk": "نقص العمالة الماهرة", "probability": "منخفضة", "impact": "متوسط", "mitigation": "التعاقد المسبق مع مقاولي الباطن"},
- {"risk": "تغيير نطاق العمل", "probability": "متوسطة", "impact": "عالي", "mitigation": "توثيق نطاق العمل بدقة وتحديد إجراءات التغيير"},
- {"risk": "مشاكل في التربة", "probability": "منخفضة", "impact": "عالي", "mitigation": "إجراء فحوصات شاملة للتربة قبل البدء"}
- ]
-
- return risks
-
- def _suggest_improvements_for_text(self, text):
- """
- اقتراح تحسينات للنص
-
- المعلمات:
- text (str): نص العقد
-
- العوائد:
- list: التحسينات المقترحة
- """
- # محاكاة اقتراح تحسينات
- improvements = [
- "إضافة بند يتعلق بالقوة القاهرة",
- "توضيح آلية تسوية النزاعات وتحديد المحكمة المختصة",
- "إضافة بند يتعلق بحقوق الملكية الفكرية",
- "توضيح آلية تعديل العقد",
- "إضافة بند يتعلق بالتأمين على المشروع",
- "توضيح آلية احتساب نسبة الإنجاز للدفعات الشهرية",
- "إضافة بند يتعلق بالتغييرات في نطاق العمل",
- "توضيح مسؤوليات كل طرف بشكل أكثر تفصيلاً"
- ]
-
- return improvements
-
- def _extract_parties(self, text):
- """استخراج الأطراف من النص"""
- if "الطرف الأول: وزارة المالية" in text:
- return {
- "الطرف الأول": "وزارة المالية",
- "الطرف الثاني": "شركة الإنشاءات المتطورة"
- }
- elif "الجهة المالكة: وزارة المالية" in text:
- return {
- "الجهة المالكة": "وزارة المالية"
- }
- else:
- return {
- "الطرف الأول": "غير محدد",
- "الطرف الثاني": "غير محدد"
- }
-
- def _extract_contract_value(self, text):
- """استخراج قيمة العقد من النص"""
- if "قيمة العقد الإجمالية هي 25,000,000 ريال" in text:
- return "25,000,000 ريال"
- else:
- return "غير محدد"
-
- def _extract_duration(self, text):
- """استخراج مدة التنفيذ من النص"""
- if "مدة تنفيذ المشروع 18 شهراً" in text:
- return "18 شهراً"
- else:
- return "غير محدد"
-
- def _extract_guarantees(self, text):
- """استخراج الضمانات من النص"""
- if "ضماناً نهائياً بنسبة 5% من قيمة العقد" in text:
- return "ضمان نهائي بنسبة 5% من قيمة العقد"
- elif "ضمان ابتدائي: 2% من قيمة العطاء" in text:
- return "ضمان ابتدائي بنسبة 2% من قيمة العطاء، وضمان نهائي بنسبة 5% من قيمة العقد"
- else:
- return "غير محدد"
-
- def _extract_penalties(self, text):
- """استخراج غرامات التأخير من النص"""
- if "غرامة تأخير بنسبة 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10% من قيمة العقد" in text:
- return "1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10% من قيمة العقد"
- else:
- return "غير محدد"
-
- def _extract_payment_terms(self, text):
- """استخراج شروط الدفع من النص"""
- if "دفعات شهرية حسب نسبة الإنجاز، مع احتجاز 10% من قيمة كل دفعة كضمان حسن التنفيذ" in text:
- return "دفعات شهرية حسب نسبة الإنجاز، مع احتجاز 10% من قيمة كل دفعة كضمان حسن التنفيذ"
- else:
- return "غير محدد"
-
- def _extract_warranty_period(self, text):
- """استخراج فترة الضمان من النص"""
- if "فترة ضمان المشروع سنة واحدة من تاريخ الاستلام الابتدائي" in text:
- return "سنة واحدة من تاريخ الاستلام الابتدائي"
- else:
- return "غير محدد"
-
- def _extract_termination_terms(self, text):
- """استخراج شروط فسخ العقد من النص"""
- if "يحق للطرف الأول فسخ العقد في حالة إخلال الطرف الثاني بالتزاماته التعاقدية بعد إنذاره كتابياً" in text:
- return "يحق للطرف الأول فسخ العقد في حالة إخلال الطرف الثاني بالتزاماته التعاقدية بعد إنذاره كتابياً"
- else:
- return "غير محدد"
-
- def _extract_dispute_resolution(self, text):
- """استخراج آلية تسوية النزاعات من النص"""
- if "في حالة نشوء أي نزاع بين الطرفين، يتم حله ودياً، وفي حالة تعذر ذلك يتم اللجوء إلى التحكيم" in text:
- return "يتم حل النزاعات ودياً، وفي حالة تعذر ذلك يتم اللجوء إلى التحكيم وفقاً لأنظمة المملكة العربية السعودية"
- else:
- return "غير محدد"
-
- def _extract_tender_info(self, text):
- """استخراج معلومات المناقصة من النص"""
- tender_info = {}
-
- if "رقم المناقصة: T-2024-001" in text:
- tender_info["رقم المناقصة"] = "T-2024-001"
-
- if "الجهة المالكة: وزارة المالية" in text:
- tender_info["الجهة المالكة"] = "وزارة المالية"
-
- if "موقع المشروع: الرياض - حي العليا" in text:
- tender_info["موقع المشروع"] = "الرياض - حي العليا"
-
- if "تاريخ الطرح: 01/03/2024م" in text:
- tender_info["تاريخ الطرح"] = "01/03/2024م"
-
- if "تاريخ الإقفال: 15/04/2024م" in text:
- tender_info["تاريخ الإقفال"] = "15/04/2024م"
-
- return tender_info
-
- def _extract_project_description(self, text):
- """استخراج وصف المشروع من النص"""
- if "يتكون المشروع من إنشاء مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع" in text:
- return "إنشاء مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع. يشمل المشروع الأعمال الإنشائية والمعمارية والكهربائية والميكانيكية وأعمال التشطيبات."
- else:
- return "غير محدد"
-
- def _extract_qualification_conditions(self, text):
- """استخراج شروط التأهيل من النص"""
- if "أن يكون المقاول مصنفاً في مجال المباني من الدرجة الأولى" in text:
- return [
- "أن يكون المقاول مصنفاً في مجال المباني من الدرجة الأولى",
- "أن يكون لديه خبرة سابقة في تنفيذ مشاريع مماثلة لا تقل عن 3 مشاريع خلال الخمس سنوات الماضية",
- "أن يكون لديه سيولة مالية كافية لتنفيذ المشروع"
- ]
- else:
- return ["غير محدد"]
-
- def _extract_required_guarantees(self, text):
- """استخراج الضمانات المطلوبة من النص"""
- if "ضمان ابتدائي: 2% من قيمة العطاء" in text:
- return [
- "ضمان ابتدائي: 2% من قيمة العطاء ساري المفعول لمدة 90 يوماً من تاريخ تقديم العطاء",
- "ضمان نهائي: 5% من قيمة العقد ساري المفعول حتى انتهاء فترة الضمان"
- ]
- else:
- return ["غير محدد"]
-
- def _extract_technical_specifications(self, text):
- """استخراج المواصفات الفنية من النص"""
- if "الأعمال الإنشائية:" in text:
- return {
- "الأعمال الإنشائية": [
- "الخرسانة المسلحة: مقاومة لا تقل عن 300 كجم/سم²",
- "حديد التسليح: درجة 60"
- ],
- "الأعمال المعمارية": [
- "الواجهات: زجاج عاكس وحجر طبيعي",
- "الأرضيات: رخام للمداخل وبورسلين للمكاتب",
- "الأسقف: أسقف مستعارة من الجبس المزخرف"
- ],
- "الأعمال الكهربائية": [
- "نظام إنارة موفر للطاقة",
- "نظام إنذار وإطفاء حريق آلي",
- "نظام مراقبة بالكاميرات"
- ],
- "الأعمال الميكانيكية": [
- "نظام تكييف مركزي",
- "نظام تهوية متطور",
- "مصاعد عدد 3"
- ]
- }
- else:
- return {"غير محدد": ["غير محدد"]}
-
- def _extract_evaluation_criteria(self, text):
- """استخراج معايير التقييم من النص"""
- if "السعر: 50%" in text:
- return {
- "السعر": "50%",
- "الجودة الفنية": "30%",
- "الخبرة السابقة": "15%",
- "مدة التنفيذ": "5%"
- }
- else:
- return {"غير محدد": "غير محدد"}
-
- def _analyze_competition(self, tender_info):
- """تحليل المنافسة"""
- return {
- "expected_competitors": [
- {"name": "شركة الإنشاءات المتطورة", "strength": "خبرة طويلة في مشاريع مماثلة", "weakness": "أسعار مرتفعة", "win_probability": 30},
- {"name": "شركة البناء الحديث", "strength": "أسعار تنافسية", "weakness": "خبرة محدودة", "win_probability": 25},
- {"name": "شركة التطوير العمراني", "strength": "جودة عالية", "weakness": "بطء في التنفيذ", "win_probability": 20}
- ],
- "competitive_advantages": [
- "خبرة في مشاريع مماثلة",
- "فريق فني متميز",
- "علاقات جيدة مع الموردين",
- "تقنيات حديثة في التنفيذ"
- ],
- "competitive_disadvantages": [
- "محدودية الموارد المالية",
- "قلة الخبرة في بعض الجوانب الفنية"
- ]
- }
-
- def _analyze_risks(self, text):
- """تحليل المخاطر"""
- return [
- {"risk": "ارتفاع أسعار المواد", "probability": "متوسطة", "impact": "عالي", "mitigation": "تثبيت أسعار المواد الرئيسية مع الموردين"},
- {"risk": "تأخر التنفيذ", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع خطة تنفيذ مفصلة مع هوامش زمنية"},
- {"risk": "نقص العمالة الماهرة", "probability": "منخفضة", "impact": "متوسط", "mitigation": "التعاقد المسبق مع مقاولي الباطن"},
- {"risk": "تغيير نطاق العمل", "probability": "متوسطة", "impact": "عالي", "mitigation": "توثيق نطاق العمل بدقة وتحديد إجراءات التغيير"},
- {"risk": "مشاكل في التربة", "probability": "منخفضة", "impact": "عالي", "mitigation": "إجراء فحوصات شاملة للتربة قبل البدء"}
- ]
-
- def _analyze_opportunities(self, text):
- """تحليل الفرص"""
- return [
- {"opportunity": "تقديم حلول مبتكرة لتقليل التكاليف", "benefit": "زيادة هامش الربح", "implementation": "استخدام تقنيات حديثة في التنفيذ"},
- {"opportunity": "تقديم جدول زمني أقصر من المطلوب", "benefit": "زيادة فرص الفوز", "implementation": "استخدام فرق عمل متعددة"},
- {"opportunity": "تقديم خدمات إضافية", "benefit": "تعزيز العلاقة مع العميل", "implementation": "تقديم خدمات الصيانة بعد انتهاء فترة الضمان"},
- {"opportunity": "استخدام مواد صديقة للبيئة", "benefit": "تحسين السمعة", "implementation": "استخدام مواد معتمدة من هيئات البيئة"},
- {"opportunity": "تطوير شراكات مع موردين", "benefit": "تقليل التكاليف", "implementation": "توقيع اتفاقيات طويلة الأمد مع الموردين"}
- ]
-
- def _analyze_technical_requirements(self, technical_specifications):
- """تحليل المتطلبات الفنية"""
- return {
- "complexity_level": "متوسط",
- "special_requirements": [
- "نظام إنارة موفر للطاقة",
- "نظام إنذار وإطفاء حريق آلي",
- "نظام تكييف مركزي"
- ],
- "technical_challenges": [
- "تنفيذ الواجهات الزجاجية بالمواصفات المطلوبة",
- "تركيب نظام التكييف المركزي",
- "تنفيذ أعمال التشطيبات بالجودة المطلوبة"
- ],
- "required_expertise": [
- "خبرة في تنفيذ المباني الإدارية",
- "خبرة في تركيب أنظمة التكييف المركزي",
- "خبرة في تنفيذ الواجهات الزجاجية"
- ]
- }
-
- def _analyze_technical_risks(self, technical_specifications):
- """تحليل المخاطر الفنية"""
- return [
- {"risk": "عدم توفر المواد بالمواصفات المطلوبة", "probability": "منخفضة", "impact": "عالي", "mitigation": "التعاقد المسبق مع الموردين"},
- {"risk": "صعوبة تنفيذ الواجهات الزجاجية", "probability": "متوسطة", "impact": "متوسط", "mitigation": "الاستعانة بمقاول باطن متخصص"},
- {"risk": "مشاكل في تركيب نظام التكييف المركزي", "probability": "منخفضة", "impact": "عالي", "mitigation": "الاستعانة بخبراء في تركيب أنظمة التكييف"},
- {"risk": "عدم مطابقة التشطيبات للمواصفات", "probability": "متوسطة", "impact": "متوسط", "mitigation": "تطبيق نظام ضبط الجودة"},
- {"risk": "تأخر توريد المواد", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع خطة توريد مفصلة مع هوامش زمنية"}
- ]
-
- def _estimate_costs(self, project_description):
- """تقدير التكاليف"""
- return {
- "total_cost": 25000000,
- "cost_breakdown": [
- {"category": "الأعمال الإنشائية", "amount": 10000000, "percentage": 40},
- {"category": "الأعمال المعمارية", "amount": 6250000, "percentage": 25},
- {"category": "الأعمال الكهربائية", "amount": 3750000, "percentage": 15},
- {"category": "الأعمال الميكانيكية", "amount": 3750000, "percentage": 15},
- {"category": "أعمال الموقع", "amount": 1250000, "percentage": 5}
- ],
- "cost_per_sqm": 5000,
- "cost_saving_opportunities": [
- {"item": "استخدام مواد بديلة", "potential_saving": 1250000},
- {"item": "تحسين إنتاجية العمالة", "potential_saving": 750000},
- {"item": "تأجير المعدات بدلاً من شرائها", "potential_saving": 500000}
- ]
- }
-
- def _analyze_cash_flow(self, payment_terms, cost_estimation):
- """تحليل التدفقات النقدية"""
- return [
- {"month": 1, "income": 0, "expense": 2500000, "net": -2500000, "cumulative": -2500000},
- {"month": 2, "income": 1125000, "expense": 2000000, "net": -875000, "cumulative": -3375000},
- {"month": 3, "income": 1125000, "expense": 1500000, "net": -375000, "cumulative": -3750000},
- {"month": 4, "income": 1125000, "expense": 1500000, "net": -375000, "cumulative": -4125000},
- {"month": 5, "income": 1125000, "expense": 1500000, "net": -375000, "cumulative": -4500000},
- {"month": 6, "income": 1125000, "expense": 1500000, "net": -375000, "cumulative": -4875000}
- ]
-
- def _analyze_financial_risks(self, text):
- """تحليل المخاطر المالية"""
- return [
- {"risk": "تأخر الدفعات", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع شروط واضحة للدفعات في العقد"},
- {"risk": "زيادة أسعار المواد", "probability": "عالية", "impact": "عالي", "mitigation": "تثبيت أسعار المواد الرئيسية مع الموردين"},
- {"risk": "نقص السيولة", "probability": "متوسطة", "impact": "عالي", "mitigation": "الحصول على تسهيلات بنكية"},
- {"risk": "تغير أسعار العملات", "probability": "منخفضة", "impact": "متوسط", "mitigation": "استخدام عقود التحوط"},
- {"risk": "زيادة تكاليف العمالة", "probability": "متوسطة", "impact": "متوسط", "mitigation": "التعاقد مع مقاولي الباطن بأسعار ثابتة"}
- ]
+"""
+محلل العقود المتقدم - وحدة تحليل العقود والمناقصات باستخدام الذكاء الاصطناعي
+
+هذا الملف يحتوي على الفئات والدوال اللازمة لتحليل العقود والمناقصات باستخدام نماذج OpenAI وClaude.
+"""
+
+import os
+import json
+import re
+import time
+import requests
+from datetime import datetime
+import pandas as pd
+import numpy as np
+from pathlib import Path
+import tempfile
+import base64
+import io
+import streamlit as st
+
+# محاكاة استيراد مكتبات الذكاء الاصطناعي
+try:
+ import openai
+ import anthropic
+ from transformers import pipeline
+ import torch
+ import nltk
+ import gensim
+ MODELS_AVAILABLE = True
+except ImportError:
+ MODELS_AVAILABLE = False
+
+class ContractAnalyzer:
+ """فئة تحليل العقود والمناقصات باستخدام الذكاء الاصطناعي"""
+
+ def __init__(self, api_key_source="security_section"):
+ """
+ تهيئة محلل العقود
+
+ المعلمات:
+ api_key_source (str): مصدر مفتاح API، إما "security_section" أو "manual"
+ """
+ self.api_key_source = api_key_source
+ self.openai_api_key = None
+ self.claude_api_key = None
+ self.hybrid_environment = True
+
+ # تهيئة مفاتيح API
+ self._initialize_api_keys()
+
+ # تهيئة نماذج الذكاء الاصطناعي
+ self._initialize_ai_models()
+
+ def _initialize_api_keys(self):
+ """تهيئة مفاتيح API"""
+ if self.api_key_source == "security_section":
+ # محاكاة الحصول على مفاتيح API من قسم الأمان
+ self.openai_api_key = "sk-security-section-openai-key"
+ self.claude_api_key = "sk-security-section-claude-key"
+ else:
+ # استخدام مفاتيح API المدخلة يدوياً
+ self.openai_api_key = st.session_state.get("openai_api_key", "")
+ self.claude_api_key = st.session_state.get("claude_api_key", "")
+
+ def _initialize_ai_models(self):
+ """تهيئة نماذج الذكاء الاصطناعي"""
+ if MODELS_AVAILABLE:
+ # تهيئة نماذج OpenAI
+ if self.openai_api_key:
+ openai.api_key = self.openai_api_key
+
+ # تهيئة نماذج Claude
+ if self.claude_api_key:
+ self.claude_client = anthropic.Anthropic(api_key=self.claude_api_key)
+
+ # تهيئة نماذج Hugging Face
+ self.summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
+ self.ner_model = pipeline("ner", model="dbmdz/bert-large-cased-finetuned-conll03-english")
+
+ # تهيئة NLTK
+ nltk.download('punkt', quiet=True)
+ nltk.download('stopwords', quiet=True)
+
+ def analyze_contract(self, file_path, analysis_type="comprehensive"):
+ """
+ تحليل العقد باستخدام الذكاء الاصطناعي
+
+ المعلمات:
+ file_path (str): مسار ملف العقد
+ analysis_type (str): نوع التحليل، إما "comprehensive" أو "quick" أو "legal" أو "financial"
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # استخراج النص من الملف
+ contract_text = self._extract_text_from_file(file_path)
+
+ # تحليل العقد باستخدام الذكاء الاصطناعي
+ if analysis_type == "comprehensive":
+ return self._comprehensive_analysis(contract_text, os.path.basename(file_path))
+ elif analysis_type == "quick":
+ return self._quick_analysis(contract_text, os.path.basename(file_path))
+ elif analysis_type == "legal":
+ return self._legal_analysis(contract_text, os.path.basename(file_path))
+ elif analysis_type == "financial":
+ return self._financial_analysis(contract_text, os.path.basename(file_path))
+ else:
+ return self._comprehensive_analysis(contract_text, os.path.basename(file_path))
+
+ def analyze_tender(self, file_path, analysis_type="comprehensive"):
+ """
+ تحليل المناقصة باستخدام الذكاء الاصطناعي
+
+ المعلمات:
+ file_path (str): مسار ملف المناقصة
+ analysis_type (str): نوع التحليل، إما "comprehensive" أو "quick" أو "technical" أو "financial"
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # استخراج النص من الملف
+ tender_text = self._extract_text_from_file(file_path)
+
+ # تحليل المناقصة باستخدام الذكاء الاصطناعي
+ if analysis_type == "comprehensive":
+ return self._comprehensive_tender_analysis(tender_text, os.path.basename(file_path))
+ elif analysis_type == "quick":
+ return self._quick_tender_analysis(tender_text, os.path.basename(file_path))
+ elif analysis_type == "technical":
+ return self._technical_tender_analysis(tender_text, os.path.basename(file_path))
+ elif analysis_type == "financial":
+ return self._financial_tender_analysis(tender_text, os.path.basename(file_path))
+ else:
+ return self._comprehensive_tender_analysis(tender_text, os.path.basename(file_path))
+
+ def analyze_dwg_file(self, file_path):
+ """
+ تحليل ملف DWG باستخدام الذكاء الاصطناعي
+
+ المعلمات:
+ file_path (str): مسار ملف DWG
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # محاكاة تحليل ملف DWG
+ return self._simulate_dwg_analysis(file_path)
+
+ def compare_contracts(self, file_path1, file_path2):
+ """
+ مقارنة عقدين باستخدام الذكاء الاصطناعي
+
+ المعلمات:
+ file_path1 (str): مسار الملف الأول
+ file_path2 (str): مسار الملف الثاني
+
+ العوائد:
+ dict: نتائج المقارنة
+ """
+ # استخراج النص من الملفين
+ text1 = self._extract_text_from_file(file_path1)
+ text2 = self._extract_text_from_file(file_path2)
+
+ # مقارنة العقدين باستخدام الذكاء الاصطناعي
+ return self._compare_documents(text1, text2, os.path.basename(file_path1), os.path.basename(file_path2))
+
+ def extract_key_terms(self, file_path):
+ """
+ استخراج الشروط الرئيسية من العقد
+
+ المعلمات:
+ file_path (str): مسار ملف العقد
+
+ العوائد:
+ dict: الشروط الرئيسية المستخرجة
+ """
+ # استخراج النص من الملف
+ contract_text = self._extract_text_from_file(file_path)
+
+ # استخراج الشروط الرئيسية باستخدام الذكاء الاصطناعي
+ return self._extract_key_terms_from_text(contract_text)
+
+ def identify_risks(self, file_path):
+ """
+ تحديد المخاطر في العقد
+
+ المعلمات:
+ file_path (str): مسار ملف العقد
+
+ العوائد:
+ dict: المخاطر المحددة
+ """
+ # استخراج النص من الملف
+ contract_text = self._extract_text_from_file(file_path)
+
+ # تحديد المخاطر باستخدام الذكاء الاصطناعي
+ return self._identify_risks_in_text(contract_text)
+
+ def suggest_improvements(self, file_path):
+ """
+ اقتراح تحسينات للعقد
+
+ المعلمات:
+ file_path (str): مسار ملف العقد
+
+ العوائد:
+ dict: التحسينات المقترحة
+ """
+ # استخراج النص من الملف
+ contract_text = self._extract_text_from_file(file_path)
+
+ # اقتراح تحسينات باستخدام الذكاء الاصطناعي
+ return self._suggest_improvements_for_text(contract_text)
+
+ def _extract_text_from_file(self, file_path):
+ """
+ استخراج النص من الملف
+
+ المعلمات:
+ file_path (str): مسار الملف
+
+ العوائد:
+ str: النص المستخرج
+ """
+ # محاكاة استخراج النص من الملف
+ file_extension = os.path.splitext(file_path)[1].lower()
+
+ # محاكاة نص العقد
+ if "contract" in file_path.lower() or "عقد" in file_path:
+ return """
+ عقد إنشاء مبنى إداري
+
+ المادة الأولى: أطراف العقد
+ الطرف الأول: وزارة المالية، ويمثلها السيد/ أحمد محمد علي، بصفته وكيل الوزارة للمشاريع.
+ الطرف الثاني: شركة الإنشاءات المتطورة، ويمثلها السيد/ خالد عبدالله محمد، بصفته المدير العام.
+
+ المادة الثانية: موضوع العقد
+ يتعهد الطرف الثاني بتنفيذ مشروع إنشاء مبنى إداري لصالح الطرف الأول وفقاً للمواصفات والشروط المرفقة بهذا العقد.
+
+ المادة الثالثة: قيمة العقد
+ قيمة العقد الإجمالية هي 25,000,000 ريال (خمسة وعشرون مليون ريال) شاملة جميع الضرائب والرسوم.
+
+ المادة الرابعة: مدة التنفيذ
+ مدة تنفيذ المشروع 18 شهراً تبدأ من تاريخ استلام الموقع.
+
+ المادة الخامسة: الدفعات
+ يتم سداد قيمة العقد على دفعات شهرية حسب نسبة الإنجاز، مع احتجاز 10% من قيمة كل دفعة كضمان حسن التنفيذ.
+
+ المادة السادسة: الضمانات
+ يقدم الطرف الثاني ضماناً نهائياً بنسبة 5% من قيمة العقد ساري المفعول حتى انتهاء فترة الضمان.
+
+ المادة السابعة: غرامات التأخير
+ في حالة تأخر الطرف الثاني عن التنفيذ في الموعد المحدد، يتم تطبيق غرامة تأخير بنسبة 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10% من قيمة العقد.
+
+ المادة الثامنة: فترة الضمان
+ فترة ضمان المشروع سنة واحدة من تاريخ الاستلام الابتدائي.
+
+ المادة التاسعة: فسخ العقد
+ يحق للطرف الأول فسخ العقد في حالة إخلال الطرف الثاني بالتزاماته التعاقدية بعد إنذاره كتابياً.
+
+ المادة العاشرة: تسوية النزاعات
+ في حالة نشوء أي نزاع بين الطرفين، يتم حله ودياً، وفي حالة تعذر ذلك يتم اللجوء إلى التحكيم وفقاً لأنظمة المملكة العربية السعودية.
+
+ المادة الحادية عشر: القانون الواجب التطبيق
+ تخضع جميع بنود هذا العقد لأنظمة المملكة العربية السعودية.
+
+ حرر هذا العقد من نسختين أصليتين بتاريخ 15/03/2024م.
+
+ الطرف الأول الطرف الثاني
+ وزارة المالية شركة الإنشاءات المتطورة
+ """
+ elif "tender" in file_path.lower() or "مناقصة" in file_path:
+ return """
+ كراسة الشروط والمواصفات
+ مناقصة إنشاء مبنى إداري
+
+ أولاً: معلومات المناقصة
+ رقم المناقصة: T-2024-001
+ الجهة المالكة: وزارة المالية
+ موقع المشروع: الرياض - حي العليا
+ تاريخ الطرح: 01/03/2024م
+ تاريخ الإقفال: 15/04/2024م
+
+ ثانياً: وصف المشروع
+ يتكون المشروع من إنشاء مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع. يشمل المشروع الأعمال الإنشائية والمعمارية والكهربائية والميكانيكية وأعمال التشطيبات.
+
+ ثالثاً: شروط التأهيل
+ 1. أن يكون المقاول مصنفاً في مجال المباني من الدرجة الأولى.
+ 2. أن يكون لديه خبرة سابقة في تنفيذ مشاريع مماثلة لا تقل عن 3 مشاريع خلال الخمس سنوات الماضية.
+ 3. أن يكون لديه سيولة مالية كافية لتنفيذ المشروع.
+
+ رابعاً: الضمانات المطلوبة
+ 1. ضمان ابتدائي: 2% من قيمة العطاء ساري المفعول لمدة 90 يوماً من تاريخ تقديم العطاء.
+ 2. ضمان نهائي: 5% من قيمة العقد ساري المفعول حتى انتهاء فترة الضمان.
+
+ خامساً: مدة التنفيذ
+ مدة تنفيذ المشروع 18 شهراً من تاريخ استلام الموقع.
+
+ سادساً: غرامات التأخير
+ في حالة تأخر المقاول عن التنفيذ في الموعد المحدد، يتم تطبيق غرامة تأخير بنسبة 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10% من قيمة العقد.
+
+ سابعاً: شروط الدفع
+ يتم سداد قيمة العقد على دفعات شهرية حسب نسبة الإنجاز، مع احتجاز 10% من قيمة كل دفعة كضمان حسن التنفيذ.
+
+ ثامناً: المواصفات الفنية
+ 1. الأعمال الإنشائية:
+ - الخرسانة المسلحة: مقاومة لا تقل عن 300 كجم/سم²
+ - حديد التسليح: درجة 60
+
+ 2. الأعمال المعمارية:
+ - الواجهات: زجاج عاكس وحجر طبيعي
+ - الأرضيات: رخام للمداخل وبورسلين للمكاتب
+ - الأسقف: أسقف مستعارة من الجبس المزخرف
+
+ 3. الأعمال الكهربائية:
+ - نظام إنارة موفر للطاقة
+ - نظام إنذار وإطفاء حريق آلي
+ - نظام مراقبة بالكاميرات
+
+ 4. الأعمال الميكانيكية:
+ - نظام تكييف مركزي
+ - نظام تهوية متطور
+ - مصاعد عدد 3
+
+ تاسعاً: معايير التقييم
+ 1. السعر: 50%
+ 2. الجودة الفنية: 30%
+ 3. الخبرة السابقة: 15%
+ 4. مدة التنفيذ: 5%
+
+ عاشراً: تقديم العطاءات
+ يتم تقديم العطاءات في مظروفين منفصلين (فني ومالي) إلى إدارة المشتريات بوزارة المالية في موعد أقصاه الساعة 12 ظهراً من يوم 15/04/2024م.
+ """
+ else:
+ return """
+ محتوى الملف غير معروف. يرجى التأكد من نوع الملف وصيغته.
+ """
+
+ def _comprehensive_analysis(self, contract_text, file_name):
+ """
+ تحليل شامل للعقد
+
+ المعلمات:
+ contract_text (str): نص العقد
+ file_name (str): اسم الملف
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # محاكاة تحليل شامل للعقد
+
+ # استخراج الأطراف
+ parties = self._extract_parties(contract_text)
+
+ # استخراج قيمة العقد
+ contract_value = self._extract_contract_value(contract_text)
+
+ # استخراج مدة التنفيذ
+ duration = self._extract_duration(contract_text)
+
+ # استخراج الضمانات
+ guarantees = self._extract_guarantees(contract_text)
+
+ # استخراج غرامات التأخير
+ penalties = self._extract_penalties(contract_text)
+
+ # استخراج شروط الدفع
+ payment_terms = self._extract_payment_terms(contract_text)
+
+ # استخراج فترة الضمان
+ warranty_period = self._extract_warranty_period(contract_text)
+
+ # استخراج شروط فسخ العقد
+ termination_terms = self._extract_termination_terms(contract_text)
+
+ # استخراج آلية تسوية النزاعات
+ dispute_resolution = self._extract_dispute_resolution(contract_text)
+
+ # تحديد المخاطر
+ risks = self._identify_risks_in_text(contract_text)
+
+ # اقتراح تحسينات
+ improvements = self._suggest_improvements_for_text(contract_text)
+
+ # إعداد النتائج
+ results = {
+ "title": f"تحليل شامل - {file_name}",
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "summary": "هذا العقد يتعلق بإنشاء مبنى إداري لصالح وزارة المالية. يتضمن العقد شروطاً متعلقة بقيمة العقد، مدة التنفيذ، الدفعات، الضمانات، غرامات التأخير، فترة الضمان، فسخ العقد، وتسوية النزاعات.",
+ "key_points": [
+ f"قيمة العقد: {contract_value}",
+ f"مدة التنفيذ: {duration}",
+ f"الضمانات: {guarantees}",
+ f"غرامات التأخير: {penalties}",
+ f"شروط الدفع: {payment_terms}",
+ f"فترة الضمان: {warranty_period}"
+ ],
+ "entities": {
+ "الأطراف": parties,
+ "قيمة العقد": contract_value,
+ "مدة التنفيذ": duration,
+ "الضمانات": guarantees,
+ "غرامات التأخير": penalties,
+ "شروط الدفع": payment_terms,
+ "فترة الضمان": warranty_period,
+ "شروط فسخ العقد": termination_terms,
+ "آلية تسوية النزاعات": dispute_resolution
+ },
+ "risks": risks,
+ "improvements": improvements,
+ "recommendations": [
+ "مراجعة قيمة الضمان النهائي للتأكد من كفايتها",
+ "توضيح آلية احتساب نسبة الإنجاز للدفعات الشهرية",
+ "إضافة بند يتعلق بالتغييرات في نطاق العمل",
+ "توضيح حقوق الملكية الفكرية للتصاميم والمخططات",
+ "إضافة بند يتعلق بالقوة القاهرة"
+ ]
+ }
+
+ return results
+
+ def _quick_analysis(self, contract_text, file_name):
+ """
+ تحليل سريع للعقد
+
+ المعلمات:
+ contract_text (str): نص العقد
+ file_name (str): اسم الملف
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # محاكاة تحليل سريع للعقد
+
+ # استخراج الأطراف
+ parties = self._extract_parties(contract_text)
+
+ # استخراج قيمة العقد
+ contract_value = self._extract_contract_value(contract_text)
+
+ # استخراج مدة التنفيذ
+ duration = self._extract_duration(contract_text)
+
+ # استخراج الضمانات
+ guarantees = self._extract_guarantees(contract_text)
+
+ # إعداد النتائج
+ results = {
+ "title": f"تحليل سريع - {file_name}",
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "summary": "هذا العقد يتعلق بإنشاء مبنى إداري لصالح وزارة المالية.",
+ "key_points": [
+ f"قيمة العقد: {contract_value}",
+ f"مدة التنفيذ: {duration}",
+ f"الضمانات: {guarantees}"
+ ],
+ "entities": {
+ "الأطراف": parties,
+ "قيمة العقد": contract_value,
+ "مدة التنفيذ": duration,
+ "الضمانات": guarantees
+ },
+ "recommendations": [
+ "مراجعة قيمة الضمان النهائي للتأكد من كفايتها",
+ "توضيح آلية احتساب نسبة الإنجاز للدفعات الشهرية"
+ ]
+ }
+
+ return results
+
+ def _legal_analysis(self, contract_text, file_name):
+ """
+ تحليل قانوني للعقد
+
+ المعلمات:
+ contract_text (str): نص العقد
+ file_name (str): اسم الملف
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # محاكاة تحليل قانوني للعقد
+
+ # استخراج الأطراف
+ parties = self._extract_parties(contract_text)
+
+ # استخراج الضمانات
+ guarantees = self._extract_guarantees(contract_text)
+
+ # استخراج غرامات التأخير
+ penalties = self._extract_penalties(contract_text)
+
+ # استخراج شروط فسخ العقد
+ termination_terms = self._extract_termination_terms(contract_text)
+
+ # استخراج آلية تسوية النزاعات
+ dispute_resolution = self._extract_dispute_resolution(contract_text)
+
+ # تحديد المخاطر القانونية
+ legal_risks = [
+ "عدم وضوح آلية تسوية النزاعات",
+ "عدم تحديد المحكمة المختصة",
+ "عدم وضوح شروط فسخ العقد",
+ "عدم وجود بند يتعلق بالقوة القاهرة",
+ "عدم وضوح حقوق الملكية الفكرية"
+ ]
+
+ # إعداد النتائج
+ results = {
+ "title": f"تحليل قانوني - {file_name}",
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "summary": "هذا العقد يتضمن بعض الثغرات القانونية التي قد تؤدي إلى نزاعات مستقبلية.",
+ "key_points": [
+ f"الضمانات: {guarantees}",
+ f"غرامات التأخير: {penalties}",
+ f"شروط فسخ العقد: {termination_terms}",
+ f"آلية تسوية النزاعات: {dispute_resolution}"
+ ],
+ "entities": {
+ "الأطراف": parties,
+ "الضمانات": guarantees,
+ "غرامات التأخير": penalties,
+ "شروط فسخ العقد": termination_terms,
+ "آلية تسوية النزاعات": dispute_resolution
+ },
+ "legal_risks": legal_risks,
+ "recommendations": [
+ "توضيح آلية تسوية النزاعات وتحديد المحكمة المختصة",
+ "إضافة بند يتعلق بالقوة القاهرة",
+ "توضيح شروط فسخ العقد بشكل أكثر تفصيلاً",
+ "إضافة بند يتعلق بحقوق الملكية الفكرية",
+ "توضيح آلية تعديل العقد"
+ ]
+ }
+
+ return results
+
+ def _financial_analysis(self, contract_text, file_name):
+ """
+ تحليل مالي للعقد
+
+ المعلمات:
+ contract_text (str): نص العقد
+ file_name (str): اسم الملف
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # محاكاة تحليل مالي للعقد
+
+ # استخراج قيمة العقد
+ contract_value = self._extract_contract_value(contract_text)
+
+ # استخراج شروط الدفع
+ payment_terms = self._extract_payment_terms(contract_text)
+
+ # استخراج الضمانات
+ guarantees = self._extract_guarantees(contract_text)
+
+ # استخراج غرامات التأخير
+ penalties = self._extract_penalties(contract_text)
+
+ # تحليل التدفقات النقدية
+ cash_flow = [
+ {"month": 1, "income": 1250000, "expense": 1000000, "net": 250000},
+ {"month": 2, "income": 1250000, "expense": 1100000, "net": 150000},
+ {"month": 3, "income": 1250000, "expense": 1200000, "net": 50000},
+ {"month": 4, "income": 1250000, "expense": 1000000, "net": 250000},
+ {"month": 5, "income": 1250000, "expense": 900000, "net": 350000},
+ {"month": 6, "income": 1250000, "expense": 950000, "net": 300000}
+ ]
+
+ # تحليل المخاطر المالية
+ financial_risks = [
+ "تأخر الدفعات",
+ "زيادة أسعار المواد",
+ "نقص السيولة",
+ "تغير أسعار العملات",
+ "زيادة تكاليف العمالة"
+ ]
+
+ # إعداد النتائج
+ results = {
+ "title": f"تحليل مالي - {file_name}",
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "summary": f"هذا العقد بقيمة {contract_value} يتضمن شروط دفع شهرية حسب نسبة الإنجاز مع احتجاز 10% من قيمة كل دفعة كضمان حسن التنفيذ.",
+ "key_points": [
+ f"قيمة العقد: {contract_value}",
+ f"شروط الدفع: {payment_terms}",
+ f"الضمانات: {guarantees}",
+ f"غرامات التأخير: {penalties}"
+ ],
+ "entities": {
+ "قيمة العقد": contract_value,
+ "شروط الدفع": payment_terms,
+ "الضمانات": guarantees,
+ "غرامات التأخير": penalties
+ },
+ "cash_flow": cash_flow,
+ "financial_risks": financial_risks,
+ "recommendations": [
+ "توضيح آلية احتساب نسبة الإنجاز للدفعات الشهرية",
+ "إضافة بند يتعلق بتعديل قيمة العقد في حالة تغير أسعار المواد",
+ "تقليل نسبة احتجاز ضمان حسن التنفيذ",
+ "إضافة بند يتعلق بالدفعة المقدمة",
+ "توضيح آلية تسوية المستخلصات النهائية"
+ ]
+ }
+
+ return results
+
+ def _comprehensive_tender_analysis(self, tender_text, file_name):
+ """
+ تحليل شامل للمناقصة
+
+ المعلمات:
+ tender_text (str): نص المناقصة
+ file_name (str): اسم الملف
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # محاكاة تحليل شامل للمناقصة
+
+ # استخراج معلومات المناقصة
+ tender_info = self._extract_tender_info(tender_text)
+
+ # استخراج وصف المشروع
+ project_description = self._extract_project_description(tender_text)
+
+ # استخراج شروط التأهيل
+ qualification_conditions = self._extract_qualification_conditions(tender_text)
+
+ # استخراج الضمانات المطلوبة
+ required_guarantees = self._extract_required_guarantees(tender_text)
+
+ # استخراج مدة التنفيذ
+ duration = self._extract_duration(tender_text)
+
+ # استخراج غرامات التأخير
+ penalties = self._extract_penalties(tender_text)
+
+ # استخراج شروط الدفع
+ payment_terms = self._extract_payment_terms(tender_text)
+
+ # استخراج المواصفات الفنية
+ technical_specifications = self._extract_technical_specifications(tender_text)
+
+ # استخراج معايير التقييم
+ evaluation_criteria = self._extract_evaluation_criteria(tender_text)
+
+ # تحليل المنافسة
+ competition_analysis = self._analyze_competition(tender_info)
+
+ # تحليل المخاطر
+ risk_analysis = self._analyze_risks(tender_text)
+
+ # تحليل الفرص
+ opportunity_analysis = self._analyze_opportunities(tender_text)
+
+ # إعداد النتائج
+ results = {
+ "title": f"تحليل شامل - {file_name}",
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "summary": f"هذه المناقصة تتعلق بإنشاء مبنى إداري لصالح وزارة المالية. تتضمن المناقصة شروطاً متعلقة بالتأهيل، الضمانات، مدة التنفيذ، غرامات التأخير، شروط الدفع، المواصفات الفنية، ومعايير التقييم.",
+ "key_points": [
+ f"رقم المناقصة: {tender_info.get('رقم المناقصة', 'غير محدد')}",
+ f"الجهة المالكة: {tender_info.get('الجهة المالكة', 'غير محدد')}",
+ f"موقع المشروع: {tender_info.get('موقع المشروع', 'غير محدد')}",
+ f"تاريخ الطرح: {tender_info.get('تاريخ الطرح', 'غير محدد')}",
+ f"تاريخ الإقفال: {tender_info.get('تاريخ الإقفال', 'غير محدد')}",
+ f"مدة التنفيذ: {duration}",
+ f"الضمانات المطلوبة: {required_guarantees}"
+ ],
+ "entities": {
+ "معلومات المناقصة": tender_info,
+ "وصف المشروع": project_description,
+ "شروط التأهيل": qualification_conditions,
+ "الضمانات المطلوبة": required_guarantees,
+ "مدة التنفيذ": duration,
+ "غرامات التأخير": penalties,
+ "شروط الدفع": payment_terms,
+ "المواصفات الفنية": technical_specifications,
+ "معايير التقييم": evaluation_criteria
+ },
+ "competition_analysis": competition_analysis,
+ "risk_analysis": risk_analysis,
+ "opportunity_analysis": opportunity_analysis,
+ "recommendations": [
+ "تقديم عرض سعر تنافسي يقل بنسبة 5-10% عن الميزانية التقديرية",
+ "التركيز على الجوانب الفنية في العرض",
+ "إبراز الخبرات السابقة في مشاريع مماثلة",
+ "تقديم جدول زمني مفصل للتنفيذ",
+ "تقديم حلول مبتكرة لتقليل التكاليف وزيادة الجودة"
+ ]
+ }
+
+ return results
+
+ def _quick_tender_analysis(self, tender_text, file_name):
+ """
+ تحليل سريع للمناقصة
+
+ المعلمات:
+ tender_text (str): نص المناقصة
+ file_name (str): اسم الملف
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # محاكاة تحليل سريع للمناقصة
+
+ # استخراج معلومات المناقصة
+ tender_info = self._extract_tender_info(tender_text)
+
+ # استخراج وصف المشروع
+ project_description = self._extract_project_description(tender_text)
+
+ # استخراج مدة التنفيذ
+ duration = self._extract_duration(tender_text)
+
+ # استخراج الضمانات المطلوبة
+ required_guarantees = self._extract_required_guarantees(tender_text)
+
+ # إعداد النتائج
+ results = {
+ "title": f"تحليل سريع - {file_name}",
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "summary": f"هذه المناقصة تتعلق بإنشاء مبنى إداري لصالح وزارة المالية.",
+ "key_points": [
+ f"رقم المناقصة: {tender_info.get('رقم المناقصة', 'غير محدد')}",
+ f"الجهة المالكة: {tender_info.get('الجهة المالكة', 'غير محدد')}",
+ f"موقع المشروع: {tender_info.get('موقع المشروع', 'غير محدد')}",
+ f"تاريخ الإقفال: {tender_info.get('تاريخ الإقفال', 'غير محدد')}",
+ f"مدة التنفيذ: {duration}"
+ ],
+ "entities": {
+ "معلومات المناقصة": tender_info,
+ "وصف المشروع": project_description,
+ "مدة التنفيذ": duration,
+ "الضمانات المطلوبة": required_guarantees
+ },
+ "recommendations": [
+ "تقديم عرض سعر تنافسي",
+ "إبراز الخبرات السابقة في مشاريع مماثلة"
+ ]
+ }
+
+ return results
+
+ def _technical_tender_analysis(self, tender_text, file_name):
+ """
+ تحليل فني للمناقصة
+
+ المعلمات:
+ tender_text (str): نص المناقصة
+ file_name (str): اسم الملف
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # محاكاة تحليل فني للمناقصة
+
+ # استخراج وصف المشروع
+ project_description = self._extract_project_description(tender_text)
+
+ # استخراج المواصفات الفنية
+ technical_specifications = self._extract_technical_specifications(tender_text)
+
+ # استخراج مدة التنفيذ
+ duration = self._extract_duration(tender_text)
+
+ # تحليل المتطلبات الفنية
+ technical_requirements_analysis = self._analyze_technical_requirements(technical_specifications)
+
+ # تحليل المخاطر الفنية
+ technical_risks = self._analyze_technical_risks(technical_specifications)
+
+ # إعداد النتائج
+ results = {
+ "title": f"تحليل فني - {file_name}",
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "summary": f"هذه المناقصة تتضمن متطلبات فنية متوسطة المستوى. المشروع يتكون من إنشاء مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع.",
+ "key_points": [
+ f"وصف المشروع: {project_description}",
+ f"مدة التنفيذ: {duration}",
+ "المواصفات الفنية تشمل الأعمال الإنشائية والمعمارية والكهربائية والميكانيكية"
+ ],
+ "entities": {
+ "وصف المشروع": project_description,
+ "المواصفات الفنية": technical_specifications,
+ "مدة التنفيذ": duration
+ },
+ "technical_requirements_analysis": technical_requirements_analysis,
+ "technical_risks": technical_risks,
+ "recommendations": [
+ "التركيز على الجوانب الفنية في العرض",
+ "تقديم حلول مبتكرة لتحسين الجودة",
+ "تقديم جدول زمني مفصل للتنفيذ",
+ "اقتراح بدائل فنية لتقليل التكاليف",
+ "تقديم خطة ضمان الجودة"
+ ]
+ }
+
+ return results
+
+ def _financial_tender_analysis(self, tender_text, file_name):
+ """
+ تحليل مالي للمناقصة
+
+ المعلمات:
+ tender_text (str): نص المناقصة
+ file_name (str): اسم الملف
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # محاكاة تحليل مالي للمناقصة
+
+ # استخراج معلومات المناقصة
+ tender_info = self._extract_tender_info(tender_text)
+
+ # استخراج وصف المشروع
+ project_description = self._extract_project_description(tender_text)
+
+ # استخراج الضمانات المطلوبة
+ required_guarantees = self._extract_required_guarantees(tender_text)
+
+ # استخراج شروط الدفع
+ payment_terms = self._extract_payment_terms(tender_text)
+
+ # استخراج غرامات التأخير
+ penalties = self._extract_penalties(tender_text)
+
+ # تقدير التكاليف
+ cost_estimation = self._estimate_costs(project_description)
+
+ # تحليل التدفقات النقدية
+ cash_flow = self._analyze_cash_flow(payment_terms, cost_estimation)
+
+ # تحليل المخاطر المالية
+ financial_risks = self._analyze_financial_risks(tender_text)
+
+ # إعداد النتائج
+ results = {
+ "title": f"تحليل مالي - {file_name}",
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "summary": f"هذه المناقصة تتطلب ضماناً ابتدائياً بنسبة 2% من قيمة العطاء وضماناً نهائياً بنسبة 5% من قيمة العقد. شروط الدفع هي دفعات شهرية حسب نسبة الإنجاز مع احتجاز 10% من قيمة كل دفعة.",
+ "key_points": [
+ f"الضمانات المطلوبة: {required_guarantees}",
+ f"شروط الدفع: {payment_terms}",
+ f"غرامات التأخير: {penalties}",
+ f"تقدير التكلفة الإجمالية: {cost_estimation.get('total_cost', 0):,} ريال"
+ ],
+ "entities": {
+ "معلومات المناقصة": tender_info,
+ "الضمانات المطلوبة": required_guarantees,
+ "شروط الدفع": payment_terms,
+ "غرامات التأخير": penalties
+ },
+ "cost_estimation": cost_estimation,
+ "cash_flow": cash_flow,
+ "financial_risks": financial_risks,
+ "recommendations": [
+ "تقديم عرض سعر يقل بنسبة 5-10% عن الميزانية التقديرية",
+ "طلب دفعة مقدمة لتحسين التدفق النقدي",
+ "تقليل نسبة احتجاز ضمان حسن التنفيذ",
+ "تقديم بدائل لتقليل التكاليف",
+ "وضع خطة للتعامل مع المخاطر المالية"
+ ]
+ }
+
+ return results
+
+ def _simulate_dwg_analysis(self, file_path):
+ """
+ محاكاة تحليل ملف DWG
+
+ المعلمات:
+ file_path (str): مسار ملف DWG
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # محاكاة تحليل ملف DWG
+ results = {
+ "file_name": os.path.basename(file_path),
+ "file_size": f"{np.random.randint(1, 10)} MB",
+ "elements_count": np.random.randint(100, 1000),
+ "layers_count": np.random.randint(5, 20),
+ "dimensions": {
+ "width": f"{np.random.randint(10, 100)} م",
+ "height": f"{np.random.randint(10, 100)} م",
+ "area": f"{np.random.randint(100, 10000)} م²"
+ },
+ "elements": {
+ "walls": np.random.randint(10, 100),
+ "doors": np.random.randint(5, 50),
+ "windows": np.random.randint(5, 50),
+ "columns": np.random.randint(5, 50),
+ "stairs": np.random.randint(1, 10)
+ },
+ "materials": [
+ {"name": "خرسانة", "volume": f"{np.random.randint(10, 1000)} م³"},
+ {"name": "حديد", "weight": f"{np.random.randint(1, 100)} طن"},
+ {"name": "طابوق", "count": f"{np.random.randint(1000, 10000)} قطعة"},
+ {"name": "زجاج", "area": f"{np.random.randint(10, 1000)} م²"},
+ {"name": "خشب", "volume": f"{np.random.randint(1, 50)} م³"}
+ ],
+ "cost_estimate": {
+ "materials": np.random.randint(100000, 1000000),
+ "labor": np.random.randint(50000, 500000),
+ "equipment": np.random.randint(10000, 100000),
+ "total": np.random.randint(200000, 2000000)
+ },
+ "recommendations": [
+ "يمكن تقليل تكلفة المواد باستخدام بدائل أقل تكلفة",
+ "يمكن تحسين كفاءة استخدام المساحة",
+ "يمكن تقليل عدد الأعمدة لتوفير التكلفة",
+ "يمكن تحسين تصميم السلالم لزيادة السلامة",
+ "يمكن تحسين توزيع النوافذ لزيادة الإضاءة الطبيعية"
+ ]
+ }
+
+ return results
+
+ def _compare_documents(self, text1, text2, file_name1, file_name2):
+ """
+ مقارنة مستندين
+
+ المعلمات:
+ text1 (str): نص المستند الأول
+ text2 (str): نص المستند الثاني
+ file_name1 (str): اسم الملف الأول
+ file_name2 (str): اسم الملف الثاني
+
+ العوائد:
+ dict: نتائج المقارنة
+ """
+ # محاكاة مقارنة مستندين
+
+ # تحليل المستند الأول
+ doc1_analysis = self._quick_analysis(text1, file_name1)
+
+ # تحليل المستند الثاني
+ doc2_analysis = self._quick_analysis(text2, file_name2)
+
+ # تحديد أوجه التشابه
+ similarities = [
+ "كلا المستندين يتعلقان بمشاريع إنشائية",
+ "كلا المستندين يتضمنان شروطاً متعلقة بالضمانات",
+ "كلا المستندين يتضمنان شروطاً متعلقة بغرامات التأخير",
+ "كلا المستندين يتضمنان شروطاً متعلقة بشروط الدفع"
+ ]
+
+ # تحديد أوجه الاختلاف
+ differences = [
+ "المستند الأول يتعلق بعقد إنشاء، بينما المستند الثاني يتعلق بمناقصة",
+ "قيمة الضمان النهائي في المستند الأول 5%، بينما في المستند الثاني 10%",
+ "مدة التنفيذ في المستند الأول 18 شهراً، بينما في المستند الثاني 24 شهراً",
+ "شروط الدفع في المستند الأول تتضمن دفعة مقدمة، بينما في المستند الثاني لا توجد دفعة مقدمة"
+ ]
+
+ # إعداد النتائج
+ results = {
+ "title": f"مقارنة بين {file_name1} و {file_name2}",
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "summary": "هذه المقارنة تظهر أوجه التشابه والاختلاف بين المستندين. يتشابه المستندان في كونهما يتعلقان بمشاريع إنشائية ويتضمنان شروطاً متعلقة بالضمانات وغرامات التأخير وشروط الدفع. ومع ذلك، هناك اختلافات في نوع المستند وقيمة الضمان النهائي ومدة التنفيذ وشروط الدفع.",
+ "document1": {
+ "title": doc1_analysis.get("title", ""),
+ "summary": doc1_analysis.get("summary", ""),
+ "key_points": doc1_analysis.get("key_points", [])
+ },
+ "document2": {
+ "title": doc2_analysis.get("title", ""),
+ "summary": doc2_analysis.get("summary", ""),
+ "key_points": doc2_analysis.get("key_points", [])
+ },
+ "similarities": similarities,
+ "differences": differences,
+ "recommendations": [
+ "مراجعة الاختلافات في قيمة الضمان النهائي",
+ "مراجعة الاختلافات في مدة التنفيذ",
+ "مراجعة الاختلافات في شروط الدفع",
+ "توحيد الشروط في المستندين إذا كانا يتعلقان بنفس المشروع"
+ ]
+ }
+
+ return results
+
+ def _extract_key_terms_from_text(self, text):
+ """
+ استخراج الشروط الرئيسية من النص
+
+ المعلمات:
+ text (str): نص العقد
+
+ العوائد:
+ dict: الشروط الرئيسية المستخرجة
+ """
+ # محاكاة استخراج الشروط الرئيسية
+ key_terms = {
+ "الأطراف": self._extract_parties(text),
+ "قيمة العقد": self._extract_contract_value(text),
+ "مدة التنفيذ": self._extract_duration(text),
+ "الضمانات": self._extract_guarantees(text),
+ "غرامات التأخير": self._extract_penalties(text),
+ "شروط الدفع": self._extract_payment_terms(text),
+ "فترة الضمان": self._extract_warranty_period(text),
+ "شروط فسخ العقد": self._extract_termination_terms(text),
+ "آلية تسوية النزاعات": self._extract_dispute_resolution(text)
+ }
+
+ return key_terms
+
+ def _identify_risks_in_text(self, text):
+ """
+ تحديد المخاطر في النص
+
+ المعلمات:
+ text (str): نص العقد
+
+ العوائد:
+ list: المخاطر المحددة
+ """
+ # محاكاة تحديد المخاطر
+ risks = [
+ {"risk": "ارتفاع أسعار المواد", "probability": "متوسطة", "impact": "عالي", "mitigation": "تثبيت أسعار المواد الرئيسية مع الموردين"},
+ {"risk": "تأخر التنفيذ", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع خطة تنفيذ مفصلة مع هوامش زمنية"},
+ {"risk": "نقص العمالة الماهرة", "probability": "منخفضة", "impact": "متوسط", "mitigation": "التعاقد المسبق مع مقاولي الباطن"},
+ {"risk": "تغيير نطاق العمل", "probability": "متوسطة", "impact": "عالي", "mitigation": "توثيق نطاق العمل بدقة وتحديد إجراءات التغيير"},
+ {"risk": "مشاكل في التربة", "probability": "منخفضة", "impact": "عالي", "mitigation": "إجراء فحوصات شاملة للتربة قبل البدء"}
+ ]
+
+ return risks
+
+ def _suggest_improvements_for_text(self, text):
+ """
+ اقتراح تحسينات للنص
+
+ المعلمات:
+ text (str): نص العقد
+
+ العوائد:
+ list: التحسينات المقترحة
+ """
+ # محاكاة اقتراح تحسينات
+ improvements = [
+ "إضافة بند يتعلق بالقوة القاهرة",
+ "توضيح آلية تسوية النزاعات وتحديد المحكمة المختصة",
+ "إضافة بند يتعلق بحقوق الملكية الفكرية",
+ "توضيح آلية تعديل العقد",
+ "إضافة بند يتعلق بالتأمين على المشروع",
+ "توضيح آلية احتساب نسبة الإنجاز للدفعات الشهرية",
+ "إضافة بند يتعلق بالتغييرات في نطاق العمل",
+ "توضيح مسؤوليات كل طرف بشكل أكثر تفصيلاً"
+ ]
+
+ return improvements
+
+ def _extract_parties(self, text):
+ """استخراج الأطراف من النص"""
+ if "الطرف الأول: وزارة المالية" in text:
+ return {
+ "الطرف الأول": "وزارة المالية",
+ "الطرف الثاني": "شركة الإنشاءات المتطورة"
+ }
+ elif "الجهة المالكة: وزارة المالية" in text:
+ return {
+ "الجهة المالكة": "وزارة المالية"
+ }
+ else:
+ return {
+ "الطرف الأول": "غير محدد",
+ "الطرف الثاني": "غير محدد"
+ }
+
+ def _extract_contract_value(self, text):
+ """استخراج قيمة العقد من النص"""
+ if "قيمة العقد الإجمالية هي 25,000,000 ريال" in text:
+ return "25,000,000 ريال"
+ else:
+ return "غير محدد"
+
+ def _extract_duration(self, text):
+ """استخراج مدة التنفيذ من النص"""
+ if "مدة تنفيذ المشروع 18 شهراً" in text:
+ return "18 شهراً"
+ else:
+ return "غير محدد"
+
+ def _extract_guarantees(self, text):
+ """استخراج الضمانات من النص"""
+ if "ضماناً نهائياً بنسبة 5% من قيمة العقد" in text:
+ return "ضمان نهائي بنسبة 5% من قيمة العقد"
+ elif "ضمان ابتدائي: 2% من قيمة العطاء" in text:
+ return "ضمان ابتدائي بنسبة 2% من قيمة العطاء، وضمان نهائي بنسبة 5% من قيمة العقد"
+ else:
+ return "غير محدد"
+
+ def _extract_penalties(self, text):
+ """استخراج غرامات التأخير من النص"""
+ if "غرامة تأخير بنسبة 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10% من قيمة العقد" in text:
+ return "1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10% من قيمة العقد"
+ else:
+ return "غير محدد"
+
+ def _extract_payment_terms(self, text):
+ """استخراج شروط الدفع من النص"""
+ if "دفعات شهرية حسب نسبة الإنجاز، مع احتجاز 10% من قيمة كل دفعة كضمان حسن التنفيذ" in text:
+ return "دفعات شهرية حسب نسبة الإنجاز، مع احتجاز 10% من قيمة كل دفعة كضمان حسن التنفيذ"
+ else:
+ return "غير محدد"
+
+ def _extract_warranty_period(self, text):
+ """استخراج فترة الضمان من النص"""
+ if "فترة ضمان المشروع سنة واحدة من تاريخ الاستلام الابتدائي" in text:
+ return "سنة واحدة من تاريخ الاستلام الابتدائي"
+ else:
+ return "غير محدد"
+
+ def _extract_termination_terms(self, text):
+ """استخراج شروط فسخ العقد من النص"""
+ if "يحق للطرف الأول فسخ العقد في حالة إخلال الطرف الثاني بالتزاماته التعاقدية بعد إنذاره كتابياً" in text:
+ return "يحق للطرف الأول فسخ العقد في حالة إخلال الطرف الثاني بالتزاماته التعاقدية بعد إنذاره كتابياً"
+ else:
+ return "غير محدد"
+
+ def _extract_dispute_resolution(self, text):
+ """استخراج آلية تسوية النزاعات من النص"""
+ if "في حالة نشوء أي نزاع بين الطرفين، يتم حله ودياً، وفي حالة تعذر ذلك يتم اللجوء إلى التحكيم" in text:
+ return "يتم حل النزاعات ودياً، وفي حالة تعذر ذلك يتم اللجوء إلى التحكيم وفقاً لأنظمة المملكة العربية السعودية"
+ else:
+ return "غير محدد"
+
+ def _extract_tender_info(self, text):
+ """استخراج معلومات المناقصة من النص"""
+ tender_info = {}
+
+ if "رقم المناقصة: T-2024-001" in text:
+ tender_info["رقم المناقصة"] = "T-2024-001"
+
+ if "الجهة المالكة: وزارة المالية" in text:
+ tender_info["الجهة المالكة"] = "وزارة المالية"
+
+ if "موقع المشروع: الرياض - حي العليا" in text:
+ tender_info["موقع المشروع"] = "الرياض - حي العليا"
+
+ if "تاريخ الطرح: 01/03/2024م" in text:
+ tender_info["تاريخ الطرح"] = "01/03/2024م"
+
+ if "تاريخ الإقفال: 15/04/2024م" in text:
+ tender_info["تاريخ الإقفال"] = "15/04/2024م"
+
+ return tender_info
+
+ def _extract_project_description(self, text):
+ """استخراج وصف المشروع من النص"""
+ if "يتكون المشروع من إنشاء مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع" in text:
+ return "إنشاء مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع. يشمل المشروع الأعمال الإنشائية والمعمارية والكهربائية والميكانيكية وأعمال التشطيبات."
+ else:
+ return "غير محدد"
+
+ def _extract_qualification_conditions(self, text):
+ """استخراج شروط التأهيل من النص"""
+ if "أن يكون المقاول مصنفاً في مجال المباني من الدرجة الأولى" in text:
+ return [
+ "أن يكون المقاول مصنفاً في مجال المباني من الدرجة الأولى",
+ "أن يكون لديه خبرة سابقة في تنفيذ مشاريع مماثلة لا تقل عن 3 مشاريع خلال الخمس سنوات الماضية",
+ "أن يكون لديه سيولة مالية كافية لتنفيذ المشروع"
+ ]
+ else:
+ return ["غير محدد"]
+
+ def _extract_required_guarantees(self, text):
+ """استخراج الضمانات المطلوبة من النص"""
+ if "ضمان ابتدائي: 2% من قيمة العطاء" in text:
+ return [
+ "ضمان ابتدائي: 2% من قيمة العطاء ساري المفعول لمدة 90 يوماً من تاريخ تقديم العطاء",
+ "ضمان نهائي: 5% من قيمة العقد ساري المفعول حتى انتهاء فترة الضمان"
+ ]
+ else:
+ return ["غير محدد"]
+
+ def _extract_technical_specifications(self, text):
+ """استخراج المواصفات الفنية من النص"""
+ if "الأعمال الإنشائية:" in text:
+ return {
+ "الأعمال الإنشائية": [
+ "الخرسانة المسلحة: مقاومة لا تقل عن 300 كجم/سم²",
+ "حديد التسليح: درجة 60"
+ ],
+ "الأعمال المعمارية": [
+ "الواجهات: زجاج عاكس وحجر طبيعي",
+ "الأرضيات: رخام للمداخل وبورسلين للمكاتب",
+ "الأسقف: أسقف مستعارة من الجبس المزخرف"
+ ],
+ "الأعمال الكهربائية": [
+ "نظام إنارة موفر للطاقة",
+ "نظام إنذار وإطفاء حريق آلي",
+ "نظام مراقبة بالكاميرات"
+ ],
+ "الأعمال الميكانيكية": [
+ "نظام تكييف مركزي",
+ "نظام تهوية متطور",
+ "مصاعد عدد 3"
+ ]
+ }
+ else:
+ return {"غير محدد": ["غير محدد"]}
+
+ def _extract_evaluation_criteria(self, text):
+ """استخراج معايير التقييم من النص"""
+ if "السعر: 50%" in text:
+ return {
+ "السعر": "50%",
+ "الجودة الفنية": "30%",
+ "الخبرة السابقة": "15%",
+ "مدة التنفيذ": "5%"
+ }
+ else:
+ return {"غير محدد": "غير محدد"}
+
+ def _analyze_competition(self, tender_info):
+ """تحليل المنافسة"""
+ return {
+ "expected_competitors": [
+ {"name": "شركة الإنشاءات المتطورة", "strength": "خبرة طويلة في مشاريع مماثلة", "weakness": "أسعار مرتفعة", "win_probability": 30},
+ {"name": "شركة البناء الحديث", "strength": "أسعار تنافسية", "weakness": "خبرة محدودة", "win_probability": 25},
+ {"name": "شركة التطوير العمراني", "strength": "جودة عالية", "weakness": "بطء في التنفيذ", "win_probability": 20}
+ ],
+ "competitive_advantages": [
+ "خبرة في مشاريع مماثلة",
+ "فريق فني متميز",
+ "علاقات جيدة مع الموردين",
+ "تقنيات حديثة في التنفيذ"
+ ],
+ "competitive_disadvantages": [
+ "محدودية الموارد المالية",
+ "قلة الخبرة في بعض الجوانب الفنية"
+ ]
+ }
+
+ def _analyze_risks(self, text):
+ """تحليل المخاطر"""
+ return [
+ {"risk": "ارتفاع أسعار المواد", "probability": "متوسطة", "impact": "عالي", "mitigation": "تثبيت أسعار المواد الرئيسية مع الموردين"},
+ {"risk": "تأخر التنفيذ", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع خطة تنفيذ مفصلة مع هوامش زمنية"},
+ {"risk": "نقص العمالة الماهرة", "probability": "منخفضة", "impact": "متوسط", "mitigation": "التعاقد المسبق مع مقاولي الباطن"},
+ {"risk": "تغيير نطاق العمل", "probability": "متوسطة", "impact": "عالي", "mitigation": "توثيق نطاق العمل بدقة وتحديد إجراءات التغيير"},
+ {"risk": "مشاكل في التربة", "probability": "منخفضة", "impact": "عالي", "mitigation": "إجراء فحوصات شاملة للتربة قبل البدء"}
+ ]
+
+ def _analyze_opportunities(self, text):
+ """تحليل الفرص"""
+ return [
+ {"opportunity": "تقديم حلول مبتكرة لتقليل التكاليف", "benefit": "زيادة هامش الربح", "implementation": "استخدام تقنيات حديثة في التنفيذ"},
+ {"opportunity": "تقديم جدول زمني أقصر من المطلوب", "benefit": "زيادة فرص الفوز", "implementation": "استخدام فرق عمل متعددة"},
+ {"opportunity": "تقديم خدمات إضافية", "benefit": "تعزيز العلاقة مع العميل", "implementation": "تقديم خدمات الصيانة بعد انتهاء فترة الضمان"},
+ {"opportunity": "استخدام مواد صديقة للبيئة", "benefit": "تحسين السمعة", "implementation": "استخدام مواد معتمدة من هيئات البيئة"},
+ {"opportunity": "تطوير شراكات مع موردين", "benefit": "تقليل التكاليف", "implementation": "توقيع اتفاقيات طويلة الأمد مع الموردين"}
+ ]
+
+ def _analyze_technical_requirements(self, technical_specifications):
+ """تحليل المتطلبات الفنية"""
+ return {
+ "complexity_level": "متوسط",
+ "special_requirements": [
+ "نظام إنارة موفر للطاقة",
+ "نظام إنذار وإطفاء حريق آلي",
+ "نظام تكييف مركزي"
+ ],
+ "technical_challenges": [
+ "تنفيذ الواجهات الزجاجية بالمواصفات المطلوبة",
+ "تركيب نظام التكييف المركزي",
+ "تنفيذ أعمال التشطيبات بالجودة المطلوبة"
+ ],
+ "required_expertise": [
+ "خبرة في تنفيذ المباني الإدارية",
+ "خبرة في تركيب أنظمة التكييف المركزي",
+ "خبرة في تنفيذ الواجهات الزجاجية"
+ ]
+ }
+
+ def _analyze_technical_risks(self, technical_specifications):
+ """تحليل المخاطر الفنية"""
+ return [
+ {"risk": "عدم توفر المواد بالمواصفات المطلوبة", "probability": "منخفضة", "impact": "عالي", "mitigation": "التعاقد المسبق مع الموردين"},
+ {"risk": "صعوبة تنفيذ الواجهات الزجاجية", "probability": "متوسطة", "impact": "متوسط", "mitigation": "الاستعانة بمقاول باطن متخصص"},
+ {"risk": "مشاكل في تركيب نظام التكييف المركزي", "probability": "منخفضة", "impact": "عالي", "mitigation": "الاستعانة بخبراء في تركيب أنظمة التكييف"},
+ {"risk": "عدم مطابقة التشطيبات للمواصفات", "probability": "متوسطة", "impact": "متوسط", "mitigation": "تطبيق نظام ضبط الجودة"},
+ {"risk": "تأخر توريد المواد", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع خطة توريد مفصلة مع هوامش زمنية"}
+ ]
+
+ def _estimate_costs(self, project_description):
+ """تقدير التكاليف"""
+ return {
+ "total_cost": 25000000,
+ "cost_breakdown": [
+ {"category": "الأعمال الإنشائية", "amount": 10000000, "percentage": 40},
+ {"category": "الأعمال المعمارية", "amount": 6250000, "percentage": 25},
+ {"category": "الأعمال الكهربائية", "amount": 3750000, "percentage": 15},
+ {"category": "الأعمال الميكانيكية", "amount": 3750000, "percentage": 15},
+ {"category": "أعمال الموقع", "amount": 1250000, "percentage": 5}
+ ],
+ "cost_per_sqm": 5000,
+ "cost_saving_opportunities": [
+ {"item": "استخدام مواد بديلة", "potential_saving": 1250000},
+ {"item": "تحسين إنتاجية العمالة", "potential_saving": 750000},
+ {"item": "تأجير المعدات بدلاً من شرائها", "potential_saving": 500000}
+ ]
+ }
+
+ def _analyze_cash_flow(self, payment_terms, cost_estimation):
+ """تحليل التدفقات النقدية"""
+ return [
+ {"month": 1, "income": 0, "expense": 2500000, "net": -2500000, "cumulative": -2500000},
+ {"month": 2, "income": 1125000, "expense": 2000000, "net": -875000, "cumulative": -3375000},
+ {"month": 3, "income": 1125000, "expense": 1500000, "net": -375000, "cumulative": -3750000},
+ {"month": 4, "income": 1125000, "expense": 1500000, "net": -375000, "cumulative": -4125000},
+ {"month": 5, "income": 1125000, "expense": 1500000, "net": -375000, "cumulative": -4500000},
+ {"month": 6, "income": 1125000, "expense": 1500000, "net": -375000, "cumulative": -4875000}
+ ]
+
+ def _analyze_financial_risks(self, text):
+ """تحليل المخاطر المالية"""
+ return [
+ {"risk": "تأخر الدفعات", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع شروط واضحة للدفعات في العقد"},
+ {"risk": "زيادة أسعار المواد", "probability": "عالية", "impact": "عالي", "mitigation": "تثبيت أسعار المواد الرئيسية مع الموردين"},
+ {"risk": "نقص السيولة", "probability": "متوسطة", "impact": "عالي", "mitigation": "الحصول على تسهيلات بنكية"},
+ {"risk": "تغير أسعار العملات", "probability": "منخفضة", "impact": "متوسط", "mitigation": "استخدام عقود التحوط"},
+ {"risk": "زيادة تكاليف العمالة", "probability": "متوسطة", "impact": "متوسط", "mitigation": "التعاقد مع مقاولي الباطن بأسعار ثابتة"}
+ ]
diff --git a/modules/ai_assistant/data_integration.py b/modules/ai_assistant/data_integration.py
index 8fea19448438a5ab3fb922a8934f6e9df1a77227..9d70fb6d670da4b99e574a046f5f406853189206 100644
--- a/modules/ai_assistant/data_integration.py
+++ b/modules/ai_assistant/data_integration.py
@@ -1,577 +1,577 @@
-"""
-وحدة تكامل البيانات مع الذكاء الاصطناعي
-
-هذا الملف يحتوي على الفئات والدوال اللازمة لتكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي.
-"""
-
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import plotly.express as px
-import plotly.graph_objects as go
-from datetime import datetime
-import json
-import os
-import sys
-from pathlib import Path
-
-# إضافة المسار للوصول إلى وحدة تحليل البيانات
-current_dir = os.path.dirname(os.path.abspath(__file__))
-parent_dir = os.path.dirname(os.path.dirname(current_dir))
-if parent_dir not in sys.path:
- sys.path.append(parent_dir)
-
-# محاولة استيراد وحدة تحليل البيانات
-try:
- from modules.data_analysis.data_analysis_app import DataAnalysisApp
-except ImportError:
- # تعريف فئة بديلة في حالة فشل الاستيراد
- class DataAnalysisApp:
- def __init__(self):
- pass
-
- def run(self):
- pass
-
-class DataAIIntegration:
- """فئة تكامل البيانات مع الذكاء الاصطناعي"""
-
- def __init__(self):
- """تهيئة فئة تكامل البيانات مع الذكاء الاصطناعي"""
- self.data_analysis_app = DataAnalysisApp()
-
- def analyze_tender_data(self, tender_data):
- """
- تحليل بيانات المناقصة باستخدام الذكاء الاصطناعي
-
- المعلمات:
- tender_data (dict): بيانات المناقصة
-
- العوائد:
- dict: نتائج التحليل
- """
- # تحويل البيانات إلى DataFrame
- if isinstance(tender_data, dict):
- df = pd.DataFrame([tender_data])
- elif isinstance(tender_data, list):
- df = pd.DataFrame(tender_data)
- else:
- df = tender_data
-
- # تحليل البيانات
- results = {
- 'summary': self._generate_summary(df),
- 'recommendations': self._generate_recommendations(df),
- 'risk_analysis': self._analyze_risks(df),
- 'cost_analysis': self._analyze_costs(df),
- 'competitive_analysis': self._analyze_competition(df)
- }
-
- return results
-
- def analyze_historical_data(self, project_type=None, location=None, time_period=None):
- """
- تحليل البيانات التاريخية للمناقصات
-
- المعلمات:
- project_type (str): نوع المشروع (اختياري)
- location (str): الموقع (اختياري)
- time_period (str): الفترة الزمنية (اختياري)
-
- العوائد:
- dict: نتائج التحليل
- """
- # الحصول على البيانات التاريخية (محاكاة)
- historical_data = self._get_historical_data()
-
- # تطبيق التصفية إذا تم تحديدها
- filtered_data = historical_data.copy()
-
- if project_type:
- filtered_data = filtered_data[filtered_data['نوع المشروع'] == project_type]
-
- if location:
- filtered_data = filtered_data[filtered_data['الموقع'] == location]
-
- if time_period:
- # تنفيذ تصفية الفترة الزمنية (محاكاة)
- pass
-
- # تحليل البيانات
- results = {
- 'win_rate': self._calculate_win_rate(filtered_data),
- 'avg_profit_margin': self._calculate_avg_profit_margin(filtered_data),
- 'price_trends': self._analyze_price_trends(filtered_data),
- 'success_factors': self._identify_success_factors(filtered_data),
- 'visualizations': self._generate_visualizations(filtered_data)
- }
-
- return results
-
- def predict_tender_success(self, tender_data):
- """
- التنبؤ بفرص نجاح المناقصة
-
- المعلمات:
- tender_data (dict): بيانات المناقصة
-
- العوائد:
- dict: نتائج التنبؤ
- """
- # تحويل البيانات إلى DataFrame
- if isinstance(tender_data, dict):
- df = pd.DataFrame([tender_data])
- elif isinstance(tender_data, list):
- df = pd.DataFrame(tender_data)
- else:
- df = tender_data
-
- # تنفيذ التنبؤ (محاكاة)
- success_probability = np.random.uniform(0, 100)
-
- # تحديد العوامل المؤثرة (محاكاة)
- factors = [
- {'name': 'السعر التنافسي', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
- {'name': 'الخبرة السابقة', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
- {'name': 'الجودة الفنية', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
- {'name': 'المدة الزمنية', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
- {'name': 'المنافسة', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'}
- ]
-
- # ترتيب العوامل حسب التأثير
- factors = sorted(factors, key=lambda x: x['impact'], reverse=True)
-
- # إعداد النتائج
- results = {
- 'success_probability': success_probability,
- 'confidence': np.random.uniform(70, 95),
- 'factors': factors,
- 'recommendations': self._generate_success_recommendations(factors)
- }
-
- return results
-
- def optimize_pricing(self, tender_data, competitors_data=None):
- """
- تحسين التسعير للمناقصة
-
- المعلمات:
- tender_data (dict): بيانات المناقصة
- competitors_data (list): بيانات المنافسين (اختياري)
-
- العوائد:
- dict: نتائج التحسين
- """
- # تحويل البيانات إلى DataFrame
- if isinstance(tender_data, dict):
- df = pd.DataFrame([tender_data])
- elif isinstance(tender_data, list):
- df = pd.DataFrame(tender_data)
- else:
- df = tender_data
-
- # تحليل بيانات المنافسين إذا كانت متوفرة
- if competitors_data:
- competitors_df = pd.DataFrame(competitors_data)
- else:
- # استخدام بيانات افتراضية للمنافسين
- competitors_df = self._get_competitors_data()
-
- # تنفيذ تحسين التسعير (محاكاة)
- base_price = float(df['الميزانية التقديرية'].iloc[0]) if 'الميزانية التقديرية' in df.columns else 10000000
-
- # حساب نطاق السعر المقترح
- min_price = base_price * 0.85
- optimal_price = base_price * 0.92
- max_price = base_price * 0.98
-
- # تحليل حساسية السعر
- price_sensitivity = []
- for price_factor in np.linspace(0.8, 1.1, 7):
- price = base_price * price_factor
- win_probability = max(0, min(100, 100 - (price_factor - 0.9) * 200))
- profit = price - (base_price * 0.75)
- expected_value = win_probability / 100 * profit
-
- price_sensitivity.append({
- 'price_factor': price_factor,
- 'price': price,
- 'win_probability': win_probability,
- 'profit': profit,
- 'expected_value': expected_value
- })
-
- # إعداد النتائج
- results = {
- 'min_price': min_price,
- 'optimal_price': optimal_price,
- 'max_price': max_price,
- 'price_sensitivity': price_sensitivity,
- 'market_position': self._analyze_market_position(optimal_price, competitors_df),
- 'recommendations': self._generate_pricing_recommendations(optimal_price, price_sensitivity)
- }
-
- return results
-
- def analyze_dwg_files(self, file_path):
- """
- تحليل ملفات DWG باستخدام الذكاء الاصطناعي
-
- المعلمات:
- file_path (str): مسار ملف DWG
-
- العوائد:
- dict: نتائج التحليل
- """
- # محاكاة تحليل ملف DWG
- results = {
- 'file_name': os.path.basename(file_path),
- 'file_size': f"{np.random.randint(1, 10)} MB",
- 'elements_count': np.random.randint(100, 1000),
- 'layers_count': np.random.randint(5, 20),
- 'dimensions': {
- 'width': f"{np.random.randint(10, 100)} م",
- 'height': f"{np.random.randint(10, 100)} م",
- 'area': f"{np.random.randint(100, 10000)} م²"
- },
- 'elements': {
- 'walls': np.random.randint(10, 100),
- 'doors': np.random.randint(5, 50),
- 'windows': np.random.randint(5, 50),
- 'columns': np.random.randint(5, 50),
- 'stairs': np.random.randint(1, 10)
- },
- 'materials': [
- {'name': 'خرسانة', 'volume': f"{np.random.randint(10, 1000)} م³"},
- {'name': 'حديد', 'weight': f"{np.random.randint(1, 100)} طن"},
- {'name': 'طابوق', 'count': f"{np.random.randint(1000, 10000)} قطعة"},
- {'name': 'زجاج', 'area': f"{np.random.randint(10, 1000)} م²"},
- {'name': 'خشب', 'volume': f"{np.random.randint(1, 50)} م³"}
- ],
- 'cost_estimate': {
- 'materials': np.random.randint(100000, 1000000),
- 'labor': np.random.randint(50000, 500000),
- 'equipment': np.random.randint(10000, 100000),
- 'total': np.random.randint(200000, 2000000)
- },
- 'recommendations': [
- 'يمكن تقليل تكلفة المواد باستخدام بدائل أقل تكلفة',
- 'يمكن تحسين كفاءة استخدام المساحة',
- 'يمكن تقليل عدد الأعمدة لتوفير التكلفة',
- 'يمكن تحسين تصميم السلالم لزيادة السلامة',
- 'يمكن تحسين توزيع النوافذ لزيادة الإضاءة الطبيعية'
- ]
- }
-
- return results
-
- def integrate_with_ai_assistant(self, ai_assistant):
- """
- تكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي
-
- المعلمات:
- ai_assistant: كائن وحدة الذكاء الاصطناعي
-
- العوائد:
- bool: نجاح التكامل
- """
- try:
- # إضافة وظائف تحليل البيانات إلى وحدة الذكاء الاصطناعي
- ai_assistant.data_integration = self
-
- # إضافة دوال التحليل إلى وحدة الذكاء الاصطناعي
- ai_assistant.analyze_tender_data = self.analyze_tender_data
- ai_assistant.analyze_historical_data = self.analyze_historical_data
- ai_assistant.predict_tender_success = self.predict_tender_success
- ai_assistant.optimize_pricing = self.optimize_pricing
- ai_assistant.analyze_dwg_files = self.analyze_dwg_files
-
- return True
- except Exception as e:
- print(f"خطأ في تكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي: {str(e)}")
- return False
-
- # دوال مساعدة داخلية
-
- def _get_historical_data(self):
- """الحصول على البيانات التاريخية"""
- # محاكاة البيانات التاريخية
- np.random.seed(42)
-
- n_tenders = 50
- tender_ids = [f"T-{2021 + i//20}-{i%20 + 1:03d}" for i in range(n_tenders)]
- tender_types = np.random.choice(["مبنى إداري", "مبنى سكني", "مدرسة", "مستشفى", "طرق", "جسور", "بنية تحتية"], n_tenders)
- tender_locations = np.random.choice(["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك"], n_tenders)
- tender_areas = np.random.randint(1000, 10000, n_tenders)
- tender_durations = np.random.randint(6, 36, n_tenders)
- tender_budgets = np.random.randint(1000000, 50000000, n_tenders)
- tender_costs = np.array([budget * np.random.uniform(0.8, 1.1) for budget in tender_budgets])
- tender_profits = tender_budgets - tender_costs
- tender_profit_margins = tender_profits / tender_budgets * 100
- tender_statuses = np.random.choice(["فائز", "خاسر", "قيد التنفيذ", "منجز"], n_tenders)
- 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)]
-
- # إنشاء DataFrame للمناقصات السابقة
- tenders_data = {
- "رقم المناقصة": tender_ids,
- "نوع المشروع": tender_types,
- "الموقع": tender_locations,
- "المساحة (م2)": tender_areas,
- "المدة (شهر)": tender_durations,
- "الميزانية (ريال)": tender_budgets,
- "التكلفة (ريال)": tender_costs,
- "الربح (ريال)": tender_profits,
- "هامش الربح (%)": tender_profit_margins,
- "الحالة": tender_statuses,
- "تاريخ التقديم": tender_dates
- }
-
- return pd.DataFrame(tenders_data)
-
- def _get_competitors_data(self):
- """الحصول على بيانات المنافسين"""
- # محاكاة بيانات المنافسين
- n_competitors = 10
- competitor_ids = [f"C-{i+1:02d}" for i in range(n_competitors)]
- competitor_names = [
- "شركة الإنشاءات المتطورة", "شركة البناء الحديث", "شركة التطوير العمراني", "شركة الإعمار الدولية",
- "شركة البنية التحتية المتكاملة", "شركة المقاولات العامة", "شركة التشييد والبناء", "شركة الهندسة والإنشاءات",
- "شركة المشاريع الكبرى", "شركة التطوير العقاري"
- ]
- competitor_specialties = np.random.choice(["مباني", "طرق", "جسور", "بنية تحتية", "متعددة"], n_competitors)
- competitor_sizes = np.random.choice(["صغيرة", "متوسطة", "كبيرة"], n_competitors)
- competitor_market_shares = np.random.uniform(1, 15, n_competitors)
- competitor_win_rates = np.random.uniform(10, 60, n_competitors)
- competitor_avg_margins = np.random.uniform(5, 20, n_competitors)
-
- # إنشاء DataFrame للمنافسين
- competitors_data = {
- "رمز المنافس": competitor_ids,
- "اسم المنافس": competitor_names,
- "التخصص": competitor_specialties,
- "الحجم": competitor_sizes,
- "حصة السوق (%)": competitor_market_shares,
- "معدل الفوز (%)": competitor_win_rates,
- "متوسط هامش الربح (%)": competitor_avg_margins
- }
-
- return pd.DataFrame(competitors_data)
-
- def _generate_summary(self, df):
- """توليد ملخص للبيانات"""
- # محاكاة توليد ملخص
- return "تحليل البيانات يشير إلى أن هذه المناقصة تتعلق بمشروع إنشائي متوسط الحجم. تتضمن المناقصة متطلبات فنية متوسطة المستوى وشروط تعاقدية معيارية. بناءً على البيانات التاريخية، هناك فرصة جيدة للفوز بهذه المناقصة إذا تم تقديم عرض تنافسي مع التركيز على الجوانب الفنية والجودة."
-
- def _generate_recommendations(self, df):
- """توليد توصيات بناءً على البيانات"""
- # محاكاة توليد توصيات
- return [
- "تقديم عرض سعر تنافسي يقل بنسبة 5-10% عن الميزانية التقديرية",
- "التركيز على الخبرات السابقة في مشاريع مماثلة",
- "تقديم حلول مبتكرة لتقليل مدة التنفيذ",
- "تعزيز الجوانب الفنية في العرض",
- "تقديم خطة تنفيذ مفصلة مع جدول زمني واضح"
- ]
-
- def _analyze_risks(self, df):
- """تحليل المخاطر"""
- # محاكاة تحليل المخاطر
- return [
- {"risk": "ارتفاع أسعار المواد", "probability": "متوسطة", "impact": "عالي", "mitigation": "تثبيت أسعار المواد الرئيسية مع الموردين"},
- {"risk": "تأخر التنفيذ", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع خطة تنفيذ مفصلة مع هوامش زمنية"},
- {"risk": "نقص العمالة الماهرة", "probability": "منخفضة", "impact": "متوسط", "mitigation": "التعاقد المسبق مع مقاولي الباطن"},
- {"risk": "تغيير نطاق العمل", "probability": "متوسطة", "impact": "عالي", "mitigation": "توثيق نطاق العمل بدقة وتحديد إجراءات التغيير"},
- {"risk": "مشاكل في التربة", "probability": "منخفضة", "impact": "عالي", "mitigation": "إجراء فحوصات شاملة للتربة قبل البدء"}
- ]
-
- def _analyze_costs(self, df):
- """تحليل التكاليف"""
- # محاكاة تحليل التكاليف
- total_budget = float(df['الميزانية التقديرية'].iloc[0]) if 'الميزانية التقديرية' in df.columns else 10000000
-
- # توزيع التكاليف
- materials_cost = total_budget * 0.6
- labor_cost = total_budget * 0.25
- equipment_cost = total_budget * 0.1
- overhead_cost = total_budget * 0.05
-
- return {
- "total_budget": total_budget,
- "cost_breakdown": [
- {"category": "المواد", "amount": materials_cost, "percentage": 60},
- {"category": "العمالة", "amount": labor_cost, "percentage": 25},
- {"category": "المعدات", "amount": equipment_cost, "percentage": 10},
- {"category": "المصاريف العامة", "amount": overhead_cost, "percentage": 5}
- ],
- "cost_saving_opportunities": [
- {"item": "استخدام مواد بديلة", "potential_saving": total_budget * 0.05},
- {"item": "تحسين إنتاجية العمالة", "potential_saving": total_budget * 0.03},
- {"item": "تأجير المعدات بدلاً من شرائها", "potential_saving": total_budget * 0.02}
- ]
- }
-
- def _analyze_competition(self, df):
- """تحليل المنافسة"""
- # محاكاة تحليل المنافسة
- return {
- "expected_competitors": [
- {"name": "شركة الإنشاءات المتطورة", "strength": "خبرة طويلة في مشاريع مماثلة", "weakness": "أسعار مرتفعة", "win_probability": 30},
- {"name": "شركة البناء الحديث", "strength": "أسعار تنافسية", "weakness": "خبرة محدودة", "win_probability": 25},
- {"name": "شركة التطوير العمراني", "strength": "جودة عالية", "weakness": "بطء في التنفيذ", "win_probability": 20}
- ],
- "competitive_advantages": [
- "خبرة في مشاريع مماثلة",
- "فريق فني متميز",
- "علاقات جيدة مع الموردين",
- "تقنيات حديثة في التنفيذ"
- ],
- "competitive_disadvantages": [
- "محدودية الموارد المالية",
- "قلة الخبرة في بعض الجوانب الفنية"
- ]
- }
-
- def _calculate_win_rate(self, df):
- """حساب معدل الفوز"""
- # محاكاة حساب معدل الفوز
- if 'الحالة' in df.columns:
- total_tenders = len(df)
- won_tenders = len(df[df['الحالة'] == 'فائز'])
- win_rate = won_tenders / total_tenders * 100 if total_tenders > 0 else 0
- else:
- win_rate = 35 # قيمة افتراضية
-
- return {
- "overall_win_rate": win_rate,
- "win_rate_by_type": [
- {"type": "مبنى إداري", "win_rate": 40},
- {"type": "مبنى سكني", "win_rate": 35},
- {"type": "مدرسة", "win_rate": 45},
- {"type": "مستشفى", "win_rate": 30},
- {"type": "طرق", "win_rate": 25},
- {"type": "جسور", "win_rate": 20},
- {"type": "بنية تحتية", "win_rate": 30}
- ],
- "win_rate_by_location": [
- {"location": "الرياض", "win_rate": 40},
- {"location": "جدة", "win_rate": 35},
- {"location": "الدمام", "win_rate": 30},
- {"location": "مكة", "win_rate": 25},
- {"location": "المدينة", "win_rate": 30},
- {"location": "أبها", "win_rate": 35},
- {"location": "تبوك", "win_rate": 40}
- ]
- }
-
- def _calculate_avg_profit_margin(self, df):
- """حساب متوسط هامش الربح"""
- # محاكاة حساب متوسط هامش الربح
- if 'هامش الربح (%)' in df.columns:
- avg_profit_margin = df['هامش الربح (%)'].mean()
- else:
- avg_profit_margin = 15 # قيمة افتراضية
-
- return {
- "overall_avg_profit_margin": avg_profit_margin,
- "profit_margin_by_type": [
- {"type": "مبنى إداري", "profit_margin": 18},
- {"type": "مبنى سكني", "profit_margin": 15},
- {"type": "مدرسة", "profit_margin": 20},
- {"type": "مستشفى", "profit_margin": 12},
- {"type": "طرق", "profit_margin": 10},
- {"type": "جسور", "profit_margin": 8},
- {"type": "بنية تحتية", "profit_margin": 14}
- ],
- "profit_margin_by_location": [
- {"location": "الرياض", "profit_margin": 16},
- {"location": "جدة", "profit_margin": 14},
- {"location": "الدمام", "profit_margin": 15},
- {"location": "مكة", "profit_margin": 12},
- {"location": "المدينة", "profit_margin": 13},
- {"location": "أبها", "profit_margin": 18},
- {"location": "تبوك", "profit_margin": 17}
- ]
- }
-
- def _analyze_price_trends(self, df):
- """تحليل اتجاهات الأسعار"""
- # محاكاة تحليل اتجاهات الأسعار
- return {
- "price_trends_by_year": [
- {"year": 2021, "avg_price_per_sqm": 3500},
- {"year": 2022, "avg_price_per_sqm": 3800},
- {"year": 2023, "avg_price_per_sqm": 4200},
- {"year": 2024, "avg_price_per_sqm": 4500}
- ],
- "price_trends_by_material": [
- {"material": "خرسانة", "price_change": 15},
- {"material": "حديد", "price_change": 20},
- {"material": "أسمنت", "price_change": 10},
- {"material": "طابوق", "price_change": 5},
- {"material": "ألمنيوم", "price_change": 25}
- ],
- "price_forecast": [
- {"year": 2025, "forecasted_price_change": 8},
- {"year": 2026, "forecasted_price_change": 5},
- {"year": 2027, "forecasted_price_change": 3}
- ]
- }
-
- def _identify_success_factors(self, df):
- """تحديد عوامل النجاح"""
- # محاكاة تحديد عوامل النجاح
- return [
- {"factor": "السعر التنافسي", "importance": 0.8, "description": "تقديم أسعار أقل من المنافسين بنسبة 5-10%"},
- {"factor": "الجودة الفنية", "importance": 0.7, "description": "تقديم حلول فنية متميزة ومبتكرة"},
- {"factor": "الخبرة السابقة", "importance": 0.6, "description": "إظهار خبرة سابقة في مشاريع مماثلة"},
- {"factor": "مدة التنفيذ", "importance": 0.5, "description": "تقديم جدول زمني أقصر من المطلوب"},
- {"factor": "السمعة", "importance": 0.4, "description": "سمعة جيدة في السوق وعلاقات قوية مع العملاء"}
- ]
-
- def _generate_visualizations(self, df):
- """توليد الرسوم البيانية"""
- # محاكاة توليد الرسوم البيانية
- return {
- "visualization_types": [
- "توزيع المناقصات حسب النوع",
- "توزيع المناقصات حسب الموقع",
- "معدل الفوز حسب النوع",
- "معدل الفوز حسب الموقع",
- "متوسط هامش الربح حسب النوع",
- "متوسط هامش الربح حسب الموقع",
- "اتجاهات الأسعار عبر الزمن"
- ]
- }
-
- def _generate_success_recommendations(self, factors):
- """توليد توصيات لزيادة فرص النجاح"""
- # محاكاة توليد توصيات
- return [
- "تخفيض السعر بنسبة 5-10% لزيادة التنافسية",
- "تعزيز الجوانب الفنية في العرض",
- "إبراز الخبرات السابقة في مشاريع مماثلة",
- "تقديم جدول زمني أقصر من المطلوب",
- "تقديم ضمانات إضافية للجودة"
- ]
-
- def _analyze_market_position(self, price, competitors_df):
- """تحليل الموقف التنافسي في السوق"""
- # محاكاة تحليل الموقف التنافسي
- return {
- "market_position": "متوسط",
- "price_percentile": 45,
- "competitors_below": 3,
- "competitors_above": 7,
- "price_competitiveness": "عالية"
- }
-
- def _generate_pricing_recommendations(self, optimal_price, price_sensitivity):
- """توليد توصيات التسعير"""
- # محاكاة توليد توصيات التسعير
- return [
- f"السعر الأمثل: {optimal_price:,.0f} ريال",
- "تقديم خصم إضافي للعميل المتكرر",
- "تقديم خيارات دفع مرنة",
- "تضمين خدمات إضافية لتعزيز القيمة",
- "تقديم ضمانات إضافية لتبرير السعر"
- ]
+"""
+وحدة تكامل البيانات مع الذكاء الاصطناعي
+
+هذا الملف يحتوي على الفئات والدوال اللازمة لتكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي.
+"""
+
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.express as px
+import plotly.graph_objects as go
+from datetime import datetime
+import json
+import os
+import sys
+from pathlib import Path
+
+# إضافة المسار للوصول إلى وحدة تحليل البيانات
+current_dir = os.path.dirname(os.path.abspath(__file__))
+parent_dir = os.path.dirname(os.path.dirname(current_dir))
+if parent_dir not in sys.path:
+ sys.path.append(parent_dir)
+
+# محاولة استيراد وحدة تحليل البيانات
+try:
+ from modules.data_analysis.data_analysis_app import DataAnalysisApp
+except ImportError:
+ # تعريف فئة بديلة في حالة فشل الاستيراد
+ class DataAnalysisApp:
+ def __init__(self):
+ pass
+
+ def run(self):
+ pass
+
+class DataAIIntegration:
+ """فئة تكامل البيانات مع الذكاء الاصطناعي"""
+
+ def __init__(self):
+ """تهيئة فئة تكامل البيانات مع الذكاء الاصطناعي"""
+ self.data_analysis_app = DataAnalysisApp()
+
+ def analyze_tender_data(self, tender_data):
+ """
+ تحليل بيانات المناقصة باستخدام الذكاء الاصطناعي
+
+ المعلمات:
+ tender_data (dict): بيانات المناقصة
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # تحويل البيانات إلى DataFrame
+ if isinstance(tender_data, dict):
+ df = pd.DataFrame([tender_data])
+ elif isinstance(tender_data, list):
+ df = pd.DataFrame(tender_data)
+ else:
+ df = tender_data
+
+ # تحليل البيانات
+ results = {
+ 'summary': self._generate_summary(df),
+ 'recommendations': self._generate_recommendations(df),
+ 'risk_analysis': self._analyze_risks(df),
+ 'cost_analysis': self._analyze_costs(df),
+ 'competitive_analysis': self._analyze_competition(df)
+ }
+
+ return results
+
+ def analyze_historical_data(self, project_type=None, location=None, time_period=None):
+ """
+ تحليل البيانات التاريخية للمناقصات
+
+ المعلمات:
+ project_type (str): نوع المشروع (اختياري)
+ location (str): الموقع (اختياري)
+ time_period (str): الفترة الزمنية (اختياري)
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # الحصول على البيانات التاريخية (محاكاة)
+ historical_data = self._get_historical_data()
+
+ # تطبيق التصفية إذا تم تحديدها
+ filtered_data = historical_data.copy()
+
+ if project_type:
+ filtered_data = filtered_data[filtered_data['نوع المشروع'] == project_type]
+
+ if location:
+ filtered_data = filtered_data[filtered_data['الموقع'] == location]
+
+ if time_period:
+ # تنفيذ تصفية الفترة الزمنية (محاكاة)
+ pass
+
+ # تحليل البيانات
+ results = {
+ 'win_rate': self._calculate_win_rate(filtered_data),
+ 'avg_profit_margin': self._calculate_avg_profit_margin(filtered_data),
+ 'price_trends': self._analyze_price_trends(filtered_data),
+ 'success_factors': self._identify_success_factors(filtered_data),
+ 'visualizations': self._generate_visualizations(filtered_data)
+ }
+
+ return results
+
+ def predict_tender_success(self, tender_data):
+ """
+ التنبؤ بفرص نجاح المناقصة
+
+ المعلمات:
+ tender_data (dict): بيانات المناقصة
+
+ العوائد:
+ dict: نتائج التنبؤ
+ """
+ # تحويل البيانات إلى DataFrame
+ if isinstance(tender_data, dict):
+ df = pd.DataFrame([tender_data])
+ elif isinstance(tender_data, list):
+ df = pd.DataFrame(tender_data)
+ else:
+ df = tender_data
+
+ # تنفيذ التنبؤ (محاكاة)
+ success_probability = np.random.uniform(0, 100)
+
+ # تحديد العوامل المؤثرة (محاكاة)
+ factors = [
+ {'name': 'السعر التنافسي', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
+ {'name': 'الخبرة السابقة', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
+ {'name': 'الجودة الفنية', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
+ {'name': 'المدة الزمنية', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
+ {'name': 'المنافسة', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'}
+ ]
+
+ # ترتيب العوامل حسب التأثير
+ factors = sorted(factors, key=lambda x: x['impact'], reverse=True)
+
+ # إعداد النتائج
+ results = {
+ 'success_probability': success_probability,
+ 'confidence': np.random.uniform(70, 95),
+ 'factors': factors,
+ 'recommendations': self._generate_success_recommendations(factors)
+ }
+
+ return results
+
+ def optimize_pricing(self, tender_data, competitors_data=None):
+ """
+ تحسين التسعير للمناقصة
+
+ المعلمات:
+ tender_data (dict): بيانات المناقصة
+ competitors_data (list): بيانات المنافسين (اختياري)
+
+ العوائد:
+ dict: نتائج التحسين
+ """
+ # تحويل البيانات إلى DataFrame
+ if isinstance(tender_data, dict):
+ df = pd.DataFrame([tender_data])
+ elif isinstance(tender_data, list):
+ df = pd.DataFrame(tender_data)
+ else:
+ df = tender_data
+
+ # تحليل بيانات المنافسين إذا كانت متوفرة
+ if competitors_data:
+ competitors_df = pd.DataFrame(competitors_data)
+ else:
+ # استخدام بيانات افتراضية للمنافسين
+ competitors_df = self._get_competitors_data()
+
+ # تنفيذ تحسين التسعير (محاكاة)
+ base_price = float(df['الميزانية التقديرية'].iloc[0]) if 'الميزانية التقديرية' in df.columns else 10000000
+
+ # حساب نطاق السعر المقترح
+ min_price = base_price * 0.85
+ optimal_price = base_price * 0.92
+ max_price = base_price * 0.98
+
+ # تحليل حساسية السعر
+ price_sensitivity = []
+ for price_factor in np.linspace(0.8, 1.1, 7):
+ price = base_price * price_factor
+ win_probability = max(0, min(100, 100 - (price_factor - 0.9) * 200))
+ profit = price - (base_price * 0.75)
+ expected_value = win_probability / 100 * profit
+
+ price_sensitivity.append({
+ 'price_factor': price_factor,
+ 'price': price,
+ 'win_probability': win_probability,
+ 'profit': profit,
+ 'expected_value': expected_value
+ })
+
+ # إعداد النتائج
+ results = {
+ 'min_price': min_price,
+ 'optimal_price': optimal_price,
+ 'max_price': max_price,
+ 'price_sensitivity': price_sensitivity,
+ 'market_position': self._analyze_market_position(optimal_price, competitors_df),
+ 'recommendations': self._generate_pricing_recommendations(optimal_price, price_sensitivity)
+ }
+
+ return results
+
+ def analyze_dwg_files(self, file_path):
+ """
+ تحليل ملفات DWG باستخدام الذكاء الاصطناعي
+
+ المعلمات:
+ file_path (str): مسار ملف DWG
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ # محاكاة تحليل ملف DWG
+ results = {
+ 'file_name': os.path.basename(file_path),
+ 'file_size': f"{np.random.randint(1, 10)} MB",
+ 'elements_count': np.random.randint(100, 1000),
+ 'layers_count': np.random.randint(5, 20),
+ 'dimensions': {
+ 'width': f"{np.random.randint(10, 100)} م",
+ 'height': f"{np.random.randint(10, 100)} م",
+ 'area': f"{np.random.randint(100, 10000)} م²"
+ },
+ 'elements': {
+ 'walls': np.random.randint(10, 100),
+ 'doors': np.random.randint(5, 50),
+ 'windows': np.random.randint(5, 50),
+ 'columns': np.random.randint(5, 50),
+ 'stairs': np.random.randint(1, 10)
+ },
+ 'materials': [
+ {'name': 'خرسانة', 'volume': f"{np.random.randint(10, 1000)} م³"},
+ {'name': 'حديد', 'weight': f"{np.random.randint(1, 100)} طن"},
+ {'name': 'طابوق', 'count': f"{np.random.randint(1000, 10000)} قطعة"},
+ {'name': 'زجاج', 'area': f"{np.random.randint(10, 1000)} م²"},
+ {'name': 'خشب', 'volume': f"{np.random.randint(1, 50)} م³"}
+ ],
+ 'cost_estimate': {
+ 'materials': np.random.randint(100000, 1000000),
+ 'labor': np.random.randint(50000, 500000),
+ 'equipment': np.random.randint(10000, 100000),
+ 'total': np.random.randint(200000, 2000000)
+ },
+ 'recommendations': [
+ 'يمكن تقليل تكلفة المواد باستخدام بدائل أقل تكلفة',
+ 'يمكن تحسين كفاءة استخدام المساحة',
+ 'يمكن تقليل عدد الأعمدة لتوفير التكلفة',
+ 'يمكن تحسين تصميم السلالم لزيادة السلامة',
+ 'يمكن تحسين توزيع النوافذ لزيادة الإضاءة الطبيعية'
+ ]
+ }
+
+ return results
+
+ def integrate_with_ai_assistant(self, ai_assistant):
+ """
+ تكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي
+
+ المعلمات:
+ ai_assistant: كائن وحدة الذكاء الاصطناعي
+
+ العوائد:
+ bool: نجاح التكامل
+ """
+ try:
+ # إضافة وظائف تحليل البيانات إلى وحدة الذكاء الاصطناعي
+ ai_assistant.data_integration = self
+
+ # إضافة دوال التحليل إلى وحدة الذكاء الاصطناعي
+ ai_assistant.analyze_tender_data = self.analyze_tender_data
+ ai_assistant.analyze_historical_data = self.analyze_historical_data
+ ai_assistant.predict_tender_success = self.predict_tender_success
+ ai_assistant.optimize_pricing = self.optimize_pricing
+ ai_assistant.analyze_dwg_files = self.analyze_dwg_files
+
+ return True
+ except Exception as e:
+ print(f"خطأ في تكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي: {str(e)}")
+ return False
+
+ # دوال مساعدة داخلية
+
+ def _get_historical_data(self):
+ """الحصول على البيانات التاريخية"""
+ # محاكاة البيانات التاريخية
+ np.random.seed(42)
+
+ n_tenders = 50
+ tender_ids = [f"T-{2021 + i//20}-{i%20 + 1:03d}" for i in range(n_tenders)]
+ tender_types = np.random.choice(["مبنى إداري", "مبنى سكني", "مدرسة", "مستشفى", "طرق", "جسور", "بنية تحتية"], n_tenders)
+ tender_locations = np.random.choice(["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك"], n_tenders)
+ tender_areas = np.random.randint(1000, 10000, n_tenders)
+ tender_durations = np.random.randint(6, 36, n_tenders)
+ tender_budgets = np.random.randint(1000000, 50000000, n_tenders)
+ tender_costs = np.array([budget * np.random.uniform(0.8, 1.1) for budget in tender_budgets])
+ tender_profits = tender_budgets - tender_costs
+ tender_profit_margins = tender_profits / tender_budgets * 100
+ tender_statuses = np.random.choice(["فائز", "خاسر", "قيد التنفيذ", "منجز"], n_tenders)
+ 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)]
+
+ # إنشاء DataFrame للمناقصات السابقة
+ tenders_data = {
+ "رقم المناقصة": tender_ids,
+ "نوع المشروع": tender_types,
+ "الموقع": tender_locations,
+ "المساحة (م2)": tender_areas,
+ "المدة (شهر)": tender_durations,
+ "الميزانية (ريال)": tender_budgets,
+ "التكلفة (ريال)": tender_costs,
+ "الربح (ريال)": tender_profits,
+ "هامش الربح (%)": tender_profit_margins,
+ "الحالة": tender_statuses,
+ "تاريخ التقديم": tender_dates
+ }
+
+ return pd.DataFrame(tenders_data)
+
+ def _get_competitors_data(self):
+ """الحصول على بيانات المنافسين"""
+ # محاكاة بيانات المنافسين
+ n_competitors = 10
+ competitor_ids = [f"C-{i+1:02d}" for i in range(n_competitors)]
+ competitor_names = [
+ "شركة الإنشاءات المتطورة", "شركة البناء الحديث", "شركة التطوير العمراني", "شركة الإعمار الدولية",
+ "شركة البنية التحتية المتكاملة", "شركة المقاولات العامة", "شركة التشييد والبناء", "شركة الهندسة والإنشاءات",
+ "شركة المشاريع الكبرى", "شركة التطوير العقاري"
+ ]
+ competitor_specialties = np.random.choice(["مباني", "طرق", "جسور", "بنية تحتية", "متعددة"], n_competitors)
+ competitor_sizes = np.random.choice(["صغيرة", "متوسطة", "كبيرة"], n_competitors)
+ competitor_market_shares = np.random.uniform(1, 15, n_competitors)
+ competitor_win_rates = np.random.uniform(10, 60, n_competitors)
+ competitor_avg_margins = np.random.uniform(5, 20, n_competitors)
+
+ # إنشاء DataFrame للمنافسين
+ competitors_data = {
+ "رمز المنافس": competitor_ids,
+ "اسم المنافس": competitor_names,
+ "التخصص": competitor_specialties,
+ "الحجم": competitor_sizes,
+ "حصة السوق (%)": competitor_market_shares,
+ "معدل الفوز (%)": competitor_win_rates,
+ "متوسط هامش الربح (%)": competitor_avg_margins
+ }
+
+ return pd.DataFrame(competitors_data)
+
+ def _generate_summary(self, df):
+ """توليد ملخص للبيانات"""
+ # محاكاة توليد ملخص
+ return "تحليل البيانات يشير إلى أن هذه المناقصة تتعلق بمشروع إنشائي متوسط الحجم. تتضمن المناقصة متطلبات فنية متوسطة المستوى وشروط تعاقدية معيارية. بناءً على البيانات التاريخية، هناك فرصة جيدة للفوز بهذه المناقصة إذا تم تقديم عرض تنافسي مع التركيز على الجوانب الفنية والجودة."
+
+ def _generate_recommendations(self, df):
+ """توليد توصيات بناءً على البيانات"""
+ # محاكاة توليد توصيات
+ return [
+ "تقديم عرض سعر تنافسي يقل بنسبة 5-10% عن الميزانية التقديرية",
+ "التركيز على الخبرات السابقة في مشاريع مماثلة",
+ "تقديم حلول مبتكرة لتقليل مدة التنفيذ",
+ "تعزيز الجوانب الفنية في العرض",
+ "تقديم خطة تنفيذ مفصلة مع جدول زمني واضح"
+ ]
+
+ def _analyze_risks(self, df):
+ """تحليل المخاطر"""
+ # محاكاة تحليل المخاطر
+ return [
+ {"risk": "ارتفاع أسعار المواد", "probability": "متوسطة", "impact": "عالي", "mitigation": "تثبيت أسعار المواد الرئيسية مع الموردين"},
+ {"risk": "تأخر التنفيذ", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع خطة تنفيذ مفصلة مع هوامش زمنية"},
+ {"risk": "نقص العمالة الماهرة", "probability": "منخفضة", "impact": "متوسط", "mitigation": "التعاقد المسبق مع مقاولي الباطن"},
+ {"risk": "تغيير نطاق العمل", "probability": "متوسطة", "impact": "عالي", "mitigation": "توثيق نطاق العمل بدقة وتحديد إجراءات التغيير"},
+ {"risk": "مشاكل في التربة", "probability": "منخفضة", "impact": "عالي", "mitigation": "إجراء فحوصات شاملة للتربة قبل البدء"}
+ ]
+
+ def _analyze_costs(self, df):
+ """تحليل التكاليف"""
+ # محاكاة تحليل التكاليف
+ total_budget = float(df['الميزانية التقديرية'].iloc[0]) if 'الميزانية التقديرية' in df.columns else 10000000
+
+ # توزيع التكاليف
+ materials_cost = total_budget * 0.6
+ labor_cost = total_budget * 0.25
+ equipment_cost = total_budget * 0.1
+ overhead_cost = total_budget * 0.05
+
+ return {
+ "total_budget": total_budget,
+ "cost_breakdown": [
+ {"category": "المواد", "amount": materials_cost, "percentage": 60},
+ {"category": "العمالة", "amount": labor_cost, "percentage": 25},
+ {"category": "المعدات", "amount": equipment_cost, "percentage": 10},
+ {"category": "المصاريف العامة", "amount": overhead_cost, "percentage": 5}
+ ],
+ "cost_saving_opportunities": [
+ {"item": "استخدام مواد بديلة", "potential_saving": total_budget * 0.05},
+ {"item": "تحسين إنتاجية العمالة", "potential_saving": total_budget * 0.03},
+ {"item": "تأجير المعدات بدلاً من شرائها", "potential_saving": total_budget * 0.02}
+ ]
+ }
+
+ def _analyze_competition(self, df):
+ """تحليل المنافسة"""
+ # محاكاة تحليل المنافسة
+ return {
+ "expected_competitors": [
+ {"name": "شركة الإنشاءات المتطورة", "strength": "خبرة طويلة في مشاريع مماثلة", "weakness": "أسعار مرتفعة", "win_probability": 30},
+ {"name": "شركة البناء الحديث", "strength": "أسعار تنافسية", "weakness": "خبرة محدودة", "win_probability": 25},
+ {"name": "شركة التطوير العمراني", "strength": "جودة عالية", "weakness": "بطء في التنفيذ", "win_probability": 20}
+ ],
+ "competitive_advantages": [
+ "خبرة في مشاريع مماثلة",
+ "فريق فني متميز",
+ "علاقات جيدة مع الموردين",
+ "تقنيات حديثة في التنفيذ"
+ ],
+ "competitive_disadvantages": [
+ "محدودية الموارد المالية",
+ "قلة الخبرة في بعض الجوانب الفنية"
+ ]
+ }
+
+ def _calculate_win_rate(self, df):
+ """حساب معدل الفوز"""
+ # محاكاة حساب معدل الفوز
+ if 'الحالة' in df.columns:
+ total_tenders = len(df)
+ won_tenders = len(df[df['الحالة'] == 'فائز'])
+ win_rate = won_tenders / total_tenders * 100 if total_tenders > 0 else 0
+ else:
+ win_rate = 35 # قيمة افتراضية
+
+ return {
+ "overall_win_rate": win_rate,
+ "win_rate_by_type": [
+ {"type": "مبنى إداري", "win_rate": 40},
+ {"type": "مبنى سكني", "win_rate": 35},
+ {"type": "مدرسة", "win_rate": 45},
+ {"type": "مستشفى", "win_rate": 30},
+ {"type": "طرق", "win_rate": 25},
+ {"type": "جسور", "win_rate": 20},
+ {"type": "بنية تحتية", "win_rate": 30}
+ ],
+ "win_rate_by_location": [
+ {"location": "الرياض", "win_rate": 40},
+ {"location": "جدة", "win_rate": 35},
+ {"location": "الدمام", "win_rate": 30},
+ {"location": "مكة", "win_rate": 25},
+ {"location": "المدينة", "win_rate": 30},
+ {"location": "أبها", "win_rate": 35},
+ {"location": "تبوك", "win_rate": 40}
+ ]
+ }
+
+ def _calculate_avg_profit_margin(self, df):
+ """حساب متوسط هامش الربح"""
+ # محاكاة حساب متوسط هامش الربح
+ if 'هامش الربح (%)' in df.columns:
+ avg_profit_margin = df['هامش الربح (%)'].mean()
+ else:
+ avg_profit_margin = 15 # قيمة افتراضية
+
+ return {
+ "overall_avg_profit_margin": avg_profit_margin,
+ "profit_margin_by_type": [
+ {"type": "مبنى إداري", "profit_margin": 18},
+ {"type": "مبنى سكني", "profit_margin": 15},
+ {"type": "مدرسة", "profit_margin": 20},
+ {"type": "مستشفى", "profit_margin": 12},
+ {"type": "طرق", "profit_margin": 10},
+ {"type": "جسور", "profit_margin": 8},
+ {"type": "بنية تحتية", "profit_margin": 14}
+ ],
+ "profit_margin_by_location": [
+ {"location": "الرياض", "profit_margin": 16},
+ {"location": "جدة", "profit_margin": 14},
+ {"location": "الدمام", "profit_margin": 15},
+ {"location": "مكة", "profit_margin": 12},
+ {"location": "المدينة", "profit_margin": 13},
+ {"location": "أبها", "profit_margin": 18},
+ {"location": "تبوك", "profit_margin": 17}
+ ]
+ }
+
+ def _analyze_price_trends(self, df):
+ """تحليل اتجاهات الأسعار"""
+ # محاكاة تحليل اتجاهات الأسعار
+ return {
+ "price_trends_by_year": [
+ {"year": 2021, "avg_price_per_sqm": 3500},
+ {"year": 2022, "avg_price_per_sqm": 3800},
+ {"year": 2023, "avg_price_per_sqm": 4200},
+ {"year": 2024, "avg_price_per_sqm": 4500}
+ ],
+ "price_trends_by_material": [
+ {"material": "خرسانة", "price_change": 15},
+ {"material": "حديد", "price_change": 20},
+ {"material": "أسمنت", "price_change": 10},
+ {"material": "طابوق", "price_change": 5},
+ {"material": "ألمنيوم", "price_change": 25}
+ ],
+ "price_forecast": [
+ {"year": 2025, "forecasted_price_change": 8},
+ {"year": 2026, "forecasted_price_change": 5},
+ {"year": 2027, "forecasted_price_change": 3}
+ ]
+ }
+
+ def _identify_success_factors(self, df):
+ """تحديد عوامل النجاح"""
+ # محاكاة تحديد عوامل النجاح
+ return [
+ {"factor": "السعر التنافسي", "importance": 0.8, "description": "تقديم أسعار أقل من المنافسين بنسبة 5-10%"},
+ {"factor": "الجودة الفنية", "importance": 0.7, "description": "تقديم حلول فنية متميزة ومبتكرة"},
+ {"factor": "الخبرة السابقة", "importance": 0.6, "description": "إظهار خبرة سابقة في مشاريع مماثلة"},
+ {"factor": "مدة التنفيذ", "importance": 0.5, "description": "تقديم جدول زمني أقصر من المطلوب"},
+ {"factor": "السمعة", "importance": 0.4, "description": "سمعة جيدة في السوق وعلاقات قوية مع العملاء"}
+ ]
+
+ def _generate_visualizations(self, df):
+ """توليد الرسوم البيانية"""
+ # محاكاة توليد الرسوم البيانية
+ return {
+ "visualization_types": [
+ "توزيع المناقصات حسب النوع",
+ "توزيع المناقصات حسب الموقع",
+ "معدل الفوز حسب النوع",
+ "معدل الفوز حسب الموقع",
+ "متوسط هامش الربح حسب النوع",
+ "متوسط هامش الربح حسب الموقع",
+ "اتجاهات الأسعار عبر الزمن"
+ ]
+ }
+
+ def _generate_success_recommendations(self, factors):
+ """توليد توصيات لزيادة فرص النجاح"""
+ # محاكاة توليد توصيات
+ return [
+ "تخفيض السعر بنسبة 5-10% لزيادة التنافسية",
+ "تعزيز الجوانب الفنية في العرض",
+ "إبراز الخبرات السابقة في مشاريع مماثلة",
+ "تقديم جدول زمني أقصر من المطلوب",
+ "تقديم ضمانات إضافية للجودة"
+ ]
+
+ def _analyze_market_position(self, price, competitors_df):
+ """تحليل الموقف التنافسي في السوق"""
+ # محاكاة تحليل الموقف التنافسي
+ return {
+ "market_position": "متوسط",
+ "price_percentile": 45,
+ "competitors_below": 3,
+ "competitors_above": 7,
+ "price_competitiveness": "عالية"
+ }
+
+ def _generate_pricing_recommendations(self, optimal_price, price_sensitivity):
+ """توليد توصيات التسعير"""
+ # محاكاة توليد توصيات التسعير
+ return [
+ f"السعر الأمثل: {optimal_price:,.0f} ريال",
+ "تقديم خصم إضافي للعميل المتكرر",
+ "تقديم خيارات دفع مرنة",
+ "تضمين خدمات إضافية لتعزيز القيمة",
+ "تقديم ضمانات إضافية لتبرير السعر"
+ ]
diff --git a/modules/ai_assistant/document_analyzer.py b/modules/ai_assistant/document_analyzer.py
index d54eef10b7d97902e04942fef6437bf28d2b28bd..9d755438e0ff6f4203fe3c85fa8970c94398aa93 100644
--- a/modules/ai_assistant/document_analyzer.py
+++ b/modules/ai_assistant/document_analyzer.py
@@ -1,507 +1,507 @@
-# -*- coding: utf-8 -*-
-"""
-وحدة تحليل المستندات المتقدمة
-
-هذا الملف يحتوي على الفئات المسؤولة عن تحليل المستندات بشكل احترافي
-باستخدام تقنيات الذكاء الاصطناعي المتقدمة.
-"""
-
-# استيراد المكتبات القياسية
-import os
-import sys
-import logging
-import base64
-import json
-import time
-from io import BytesIO
-from pathlib import Path
-from urllib.parse import urlparse
-from tempfile import NamedTemporaryFile
-
-# استيراد مكتبة Streamlit
-import streamlit as st
-
-# استيراد المكتبات الإضافية
-import requests
-from PIL import Image
-import pandas as pd
-import numpy as np
-
-# تكوين التسجيل
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-logger = logging.getLogger(__name__)
-
-try:
- # استيراد مكتبة pdf2image للتعامل مع ملفات PDF
- from pdf2image import convert_from_path
- pdf_conversion_available = True
-except ImportError:
- pdf_conversion_available = False
- logger.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.")
-
-
-class TextExtractor:
- """فئة استخراج النصوص من المستندات"""
-
- def __init__(self, config=None):
- """تهيئة مستخرج النصوص"""
- self.config = config or {}
-
- def extract_from_pdf(self, file_path):
- """استخراج النص من ملف PDF"""
- try:
- # محاكاة استخراج النص من PDF
- # في التطبيق الحقيقي، يمكن استخدام مكتبات مثل PyPDF2 أو pdfplumber
- return f"تم استخراج النص من ملف PDF: {file_path}"
- except Exception as e:
- logger.error(f"خطأ في استخراج النص من PDF: {str(e)}")
- return f"حدث خطأ أثناء استخراج النص: {str(e)}"
-
- def extract_from_docx(self, file_path):
- """استخراج النص من ملف DOCX"""
- try:
- # محاكاة استخراج النص من DOCX
- # في التطبيق الحقيقي، يمكن استخدام مكتبة python-docx
- return f"تم استخراج النص من ملف DOCX: {file_path}"
- except Exception as e:
- logger.error(f"خطأ في استخراج النص من DOCX: {str(e)}")
- return f"حدث خطأ أثناء استخراج النص: {str(e)}"
-
- def extract_from_image(self, file_path):
- """استخراج النص من صورة باستخدام OCR"""
- try:
- # محاكاة استخراج النص من صورة
- # في التطبيق الحقيقي، يمكن استخدام مكتبة pytesseract
- return f"تم استخراج النص من صورة: {file_path}"
- except Exception as e:
- logger.error(f"خطأ في استخراج النص من صورة: {str(e)}")
- return f"حدث خطأ أثناء استخراج النص: {str(e)}"
-
- def extract(self, file_path):
- """استخراج النص من ملف بناءً على نوعه"""
- _, ext = os.path.splitext(file_path)
- ext = ext.lower()
-
- if ext == '.pdf':
- return self.extract_from_pdf(file_path)
- elif ext in ('.doc', '.docx'):
- return self.extract_from_docx(file_path)
- elif ext in ('.jpg', '.jpeg', '.png'):
- return self.extract_from_image(file_path)
- else:
- return "نوع ملف غير مدعوم"
-
-
-class ItemExtractor:
- """فئة استخراج العناصر من المستندات"""
-
- def __init__(self, config=None):
- """تهيئة مستخرج العناصر"""
- self.config = config or {}
-
- def extract_tables(self, document):
- """استخراج الجداول من المستند"""
- try:
- # محاكاة استخراج الجداول
- # في التطبيق الحقيقي، يمكن استخدام مكتبات مثل camelot-py أو tabula-py
- return [
- {
- "عنوان": "جدول البنود والكميات",
- "بيانات": [
- {"البند": "أعمال الحفر", "الكمية": 1000, "الوحدة": "م³", "السعر": 50, "الإجمالي": 50000},
- {"البند": "أعمال الخرسانة", "الكمية": 500, "الوحدة": "م³", "السعر": 300, "الإجمالي": 150000},
- {"البند": "أعمال التشطيبات", "الكمية": 2000, "الوحدة": "م²", "السعر": 100, "الإجمالي": 200000}
- ]
- },
- {
- "عنوان": "جدول الجدول الزمني",
- "بيانات": [
- {"المرحلة": "التصميم", "المدة": "30 يوم", "تاريخ البدء": "2025-04-01", "تاريخ الانتهاء": "2025-04-30"},
- {"المرحلة": "الإنشاء", "المدة": "180 يوم", "تاريخ البدء": "2025-05-01", "تاريخ الانتهاء": "2025-10-31"},
- {"المرحلة": "التسليم", "المدة": "30 يوم", "تاريخ البدء": "2025-11-01", "تاريخ الانتهاء": "2025-11-30"}
- ]
- }
- ]
- except Exception as e:
- logger.error(f"خطأ في استخراج الجداول: {str(e)}")
- return []
-
- def extract_items(self, file_path):
- """استخراج البنود من المستند"""
- try:
- # محاكاة استخراج البنود
- return [
- {"بند": "أعمال الحفر والردم", "قيمة": 250000, "نسبة": "10%"},
- {"بند": "أعمال الخرسانة المسلحة", "قيمة": 750000, "نسبة": "30%"},
- {"بند": "أعمال التشطيبات", "قيمة": 500000, "نسبة": "20%"},
- {"بند": "أعمال الكهرباء", "قيمة": 350000, "نسبة": "14%"},
- {"بند": "أعمال السباكة", "قيمة": 300000, "نسبة": "12%"},
- {"بند": "أعمال التكييف", "قيمة": 350000, "نسبة": "14%"}
- ]
- except Exception as e:
- logger.error(f"خطأ في استخراج البنود: {str(e)}")
- return []
-
- def extract(self, file_path):
- """استخراج جميع العناصر من المستند"""
- return {
- "بنود": self.extract_items(file_path),
- "جداول": self.extract_tables(file_path)
- }
-
-
-class DocumentParser:
- """فئة تحليل المستندات"""
-
- def __init__(self, config=None):
- """تهيئة محلل المستندات"""
- self.config = config or {}
- self.text_extractor = TextExtractor(config)
- self.item_extractor = ItemExtractor(config)
-
- def parse_contract(self, file_path):
- """تحليل مستند عقد"""
- try:
- # محاكاة تحليل عقد
- return {
- "نوع المستند": "عقد",
- "معلومات العقد": {
- "رقم العقد": "CT-2025-001",
- "تاريخ العقد": "2025-03-15",
- "قيمة العقد": "2,500,000 ريال",
- "مدة العقد": "12 شهر",
- "تاريخ البدء": "2025-04-01",
- "تاريخ الانتهاء": "2026-03-31"
- },
- "أطراف العقد": {
- "الطرف الأول": "وزارة الإسكان",
- "الطرف الثاني": "شركة الإنشاءات المتطورة"
- },
- "بنود العقد": [
- "يلتزم الطرف الثاني بتنفيذ المشروع وفقاً للمواصفات والشروط المرفقة",
- "مدة تنفيذ المشروع 12 شهراً من تاريخ استلام الموقع",
- "قيمة العقد 2,500,000 ريال شاملة جميع الضرائب والرسوم",
- "يتم الدفع على دفعات شهرية حسب نسبة الإنجاز",
- "غرامة التأخير 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%"
- ],
- "المرفقات": [
- "جدول الكميات",
- "المواصفات الفنية",
- "الجدول الزمني",
- "الضمانات والتأمينات"
- ],
- "درجة الثقة": "95%"
- }
- except Exception as e:
- logger.error(f"خطأ في تحليل العقد: {str(e)}")
- return {"error": f"حدث خطأ أثناء تحليل العقد: {str(e)}"}
-
- def parse_tender(self, file_path):
- """تحليل مستند مناقصة"""
- try:
- # محاكاة تحليل مناقصة
- return {
- "نوع المستند": "مناقصة",
- "معلومات المناقصة": {
- "رقم المناقصة": "T-2025-002",
- "اسم المشروع": "إنشاء مبنى إداري",
- "الجهة المالكة": "وزارة المالية",
- "تاريخ الطرح": "2025-03-01",
- "تاريخ الإقفال": "2025-04-15",
- "القيمة التقديرية": "3,000,000 ريال"
- },
- "شروط المناقصة": [
- "تصنيف المقاول: الدرجة الأولى في مجال المباني",
- "خبرة سابقة: 5 مشاريع مماثلة خلال الـ 10 سنوات الماضية",
- "الضمان الابتدائي: 2% من قيمة العطاء",
- "الضمان النهائي: 5% من قيمة العقد",
- "مدة تنفيذ المشروع: 18 شهراً"
- ],
- "المستندات المطلوبة": [
- "شهادة التصنيف",
- "السجل التجاري",
- "شهادة الزكاة والدخل",
- "شهادة التأمينات الاجتماعية",
- "قائمة المشاريع المماثلة"
- ],
- "معايير التقييم": [
- {"المعيار": "السعر", "الوزن": "50%"},
- {"المعيار": "الخبرة الفنية", "الوزن": "25%"},
- {"المعيار": "الجدول الزمني", "الوزن": "15%"},
- {"المعيار": "فريق العمل", "الوزن": "10%"}
- ],
- "درجة الثقة": "92%"
- }
- except Exception as e:
- logger.error(f"خطأ في تحليل المناقصة: {str(e)}")
- return {"error": f"حدث خطأ أثناء تحليل المناقصة: {str(e)}"}
-
- def parse_specifications(self, file_path):
- """تحليل كراسة الشروط والمواصفات"""
- try:
- # محاكاة تحليل كراسة الشروط والمواصفات
- return {
- "نوع المستند": "كراسة شروط ومواصفات",
- "معلومات المشروع": {
- "اسم المشروع": "إنشاء مبنى إداري",
- "الموقع": "الرياض - حي العليا",
- "المساحة": "5000 متر مربع",
- "عدد الطوابق": "5 طوابق"
- },
- "المواصفات الفنية": {
- "الهيكل الإنشائي": "خرسانة مسلحة",
- "الواجهات": "زجاج عاكس وحجر طبيعي",
- "التشطيبات الداخلية": "رخام للأرضيات، جبس للأسقف، دهانات عالية الجودة للجدران",
- "أنظمة الكهرباء": "نظام إنارة LED موفر للطاقة، نظام تحكم ذكي",
- "أنظمة التكييف": "نظام تكييف مركزي مع تحكم منفصل لكل منطقة",
- "أنظمة السلامة": "نظام إنذار وإطفاء حريق آلي، كاميرات مراقبة، نظام تحكم في الدخول"
- },
- "الشروط العامة": [
- "الالتزام بكود البناء السعودي",
- "الالتزام بمتطلبات الدفاع المدني",
- "الالتزام بمتطلبات الاستدامة وكفاءة الطاقة",
- "تقديم مخططات تنفيذية معتمدة قبل البدء في التنفيذ",
- "تقديم عينات للمواد للاعتماد قبل التوريد"
- ],
- "المرفقات": [
- "المخططات المعمارية",
- "المخططات الإنشائية",
- "مخططات الكهرباء",
- "مخططات التكييف",
- "مخططات السباكة",
- "جدول الكميات"
- ],
- "درجة الثقة": "90%"
- }
- except Exception as e:
- logger.error(f"خطأ في تحليل كراسة الشروط والمواصفات: {str(e)}")
- return {"error": f"حدث خطأ أثناء تحليل كراسة الشروط والمواصفات: {str(e)}"}
-
- def parse_dwg(self, file_path):
- """تحليل ملف DWG"""
- try:
- # محاكاة تحليل ملف DWG
- return {
- "نوع المستند": "ملف DWG",
- "معلومات الملف": {
- "اسم الملف": os.path.basename(file_path),
- "حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
- "تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
- },
- "محتويات الملف": {
- "عدد الطبقات": 15,
- "عدد الكائنات": 1250,
- "أبعاد الرسم": "50م × 30م"
- },
- "تحليل المساحات": {
- "المساحة الإجمالية": "4,500 م²",
- "مساحة البناء": "3,200 م²",
- "مساحة الخدمات": "800 م²",
- "مساحة الممرات": "500 م²"
- },
- "تحليل العناصر": {
- "عدد الغرف": 25,
- "عدد الأبواب": 40,
- "عدد النوافذ": 30,
- "عدد الأعمدة": 20
- },
- "ملاحظات": [
- "تصميم يتوافق مع متطلبات كود البناء السعودي",
- "توزيع جيد للمساحات",
- "تصميم يراعي متطلبات ذوي الاحتياجات الخاصة",
- "تصميم يراعي متطلبات السلامة والإخلاء"
- ],
- "درجة الثقة": "85%"
- }
- except Exception as e:
- logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
- return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
-
- def parse(self, file_path):
- """تحليل المستند بناءً على نوعه"""
- try:
- _, ext = os.path.splitext(file_path)
- ext = ext.lower()
-
- # تحديد نوع المستند بناءً على محتواه (محاكاة)
- file_name = os.path.basename(file_path).lower()
-
- if ext == '.dwg':
- return self.parse_dwg(file_path)
- elif 'contract' in file_name or 'عقد' in file_name:
- return self.parse_contract(file_path)
- elif 'tender' in file_name or 'مناقصة' in file_name:
- return self.parse_tender(file_path)
- elif 'spec' in file_name or 'شروط' in file_name or 'مواصفات' in file_name:
- return self.parse_specifications(file_path)
- else:
- # تحليل عام للمستند
- return {
- "نوع المستند": "مستند عام",
- "معلومات الملف": {
- "اسم الملف": os.path.basename(file_path),
- "حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
- "تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
- },
- "محتوى المستند": {
- "نص": self.text_extractor.extract(file_path),
- "عناصر": self.item_extractor.extract(file_path)
- },
- "درجة الثقة": "75%"
- }
- except Exception as e:
- logger.error(f"خطأ في تحليل المستند: {str(e)}")
- return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
-
-
-class AIDocumentAnalyzer:
- """فئة تحليل المستندات باستخدام الذكاء الاصطناعي"""
-
- def __init__(self):
- """تهيئة محلل المستندات الذكي"""
- self.document_parser = DocumentParser()
- self.api_keys = {}
-
- def set_api_key(self, provider, key):
- """تعيين مفتاح API لمزود خدمة الذكاء الاصطناعي"""
- self.api_keys[provider] = key
-
- def get_api_key(self, provider):
- """الحصول على مفتاح API لمزود خدمة الذكاء الاصطناعي"""
- return self.api_keys.get(provider)
-
- def analyze_document(self, file_path, provider="local"):
- """تحليل المستند باستخدام الذكاء الاصطناعي"""
- try:
- # تحليل محلي للمستند
- local_analysis = self.document_parser.parse(file_path)
-
- if provider == "local":
- return local_analysis
-
- # تحليل باستخدام خدمات الذكاء الاصطناعي السحابية
- if provider == "openai":
- # محاكاة تحليل باستخدام OpenAI
- enhanced_analysis = self._enhance_with_openai(local_analysis)
- return enhanced_analysis
- elif provider == "claude":
- # محاكاة تحليل باستخدام Claude
- enhanced_analysis = self._enhance_with_claude(local_analysis)
- return enhanced_analysis
- else:
- return local_analysis
- except Exception as e:
- logger.error(f"خطأ في تحليل المستند: {str(e)}")
- return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
-
- def _enhance_with_openai(self, analysis):
- """تحسين التحليل باستخدام OpenAI"""
- # محاكاة تحسين التحليل باستخدام OpenAI
- analysis["مصدر التحليل"] = "OpenAI"
- analysis["درجة الثقة"] = "98%"
-
- # إضافة تحليل المخاطر
- if "تحليل المخاطر" not in analysis:
- analysis["تحليل المخاطر"] = [
- {"المخاطرة": "تأخر التوريدات", "الاحتمالية": "متوسطة", "التأثير": "عالي", "استراتيجية التخفيف": "وضع خطة توريدات بديلة"},
- {"المخاطرة": "زيادة أسعار المواد", "الاحتمالية": "عالية", "التأثير": "عالي", "استراتيجية التخفيف": "تثبيت أسعار المواد الرئيسية مع الموردين"},
- {"المخاطرة": "نقص العمالة الماهرة", "الاحتمالية": "متوسطة", "التأثير": "متوسط", "استراتيجية التخفيف": "التعاقد المسبق مع مقاولي الباطن"},
- {"المخاطرة": "تغيير نطاق العمل", "الاحتمالية": "منخفضة", "التأثير": "عالي", "استراتيجية التخفيف": "توثيق نطاق العمل بدقة وإدارة التغيير"}
- ]
-
- # إضافة توصيات
- if "التوصيات" not in analysis:
- analysis["التوصيات"] = [
- "مراجعة بنود العقد بدقة قبل التوقيع",
- "التأكد من وضوح نطاق العمل وعدم وجود غموض",
- "التحقق من توافق المواصفات الفنية مع المعايير المحلية",
- "وضع خطة إدارة مخاطر شاملة للمشروع",
- "تخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع"
- ]
-
- return analysis
-
- def _enhance_with_claude(self, analysis):
- """تحسين التحليل باستخدام Claude"""
- # محاكاة تحسين التحليل باستخدام Claude
- analysis["مصدر التحليل"] = "Claude"
- analysis["درجة الثقة"] = "97%"
-
- # إضافة تحليل الفرص
- if "تحليل الفرص" not in analysis:
- analysis["تحليل الفرص"] = [
- {"الفرصة": "تحسين التصميم", "الفائدة": "تقليل التكلفة بنسبة 5-10%", "المتطلبات": "مراجعة هندسية شاملة"},
- {"الفرصة": "استخدام مواد بديلة", "الفائدة": "تقليل وقت التنفيذ", "المتطلبات": "اعتماد المواصفات الجديدة"},
- {"الفرصة": "زيادة المحتوى المحلي", "الفائدة": "تحسين التصنيف في برنامج القيمة المضافة", "المتطلبات": "تحديد الموردين المحليين"},
- {"الفرصة": "تطبيق تقنيات البناء الحديثة", "الفائدة": "تحسين الجودة وتقليل الهدر", "المتطلبات": "تدريب فريق العمل"}
- ]
-
- # إضافة ملخص تنفيذي
- if "الملخص التنفيذي" not in analysis:
- analysis["الملخص التنفيذي"] = """
- يتضمن هذا المستند تفاصيل مشروع إنشاء مبنى إداري بمساحة إجمالية 5000 متر مربع.
- المشروع يتكون من 5 طوابق ويتضمن مواصفات فنية عالية الجودة.
- تقدر تكلفة المشروع بحوالي 3 مليون ريال ومدة التنفيذ 18 شهراً.
- يتميز المشروع بتصميم يراعي متطلبات الاستدامة وكفاءة الطاقة.
- تم تحديد عدة مخاطر محتملة للمشروع مع استراتيجيات التخفيف المناسبة.
- كما تم تحديد عدة فرص لتحسين المشروع من حيث التكلفة والجودة ووقت التنفيذ.
- """
-
- return analysis
-
- def analyze_dwg(self, file_path, provider="local"):
- """تحليل ملف DWG باستخدام الذكاء الاصطناعي"""
- try:
- # تحليل محلي لملف DWG
- local_analysis = self.document_parser.parse_dwg(file_path)
-
- if provider == "local":
- return local_analysis
-
- # تحسين التحليل باستخدام خدمات الذكاء الاصطناعي
- if provider == "openai" or provider == "claude":
- # إضافة تحليل متقدم
- local_analysis["تحليل متقدم"] = {
- "تقييم التصميم": "جيد جداً",
- "كفاءة استخدام المساحة": "90%",
- "توافق مع المعايير": "متوافق مع كود البناء السعودي",
- "اقتراحات التحسين": [
- "تحسين توزيع المساحات لزيادة كفاءة استخدام المساحة",
- "تحسين تصميم الممرات لتسهيل الحركة",
- "إضافة عناصر تصميم مستدامة لتقليل استهلاك الطاقة",
- "تحسين تصميم الواجهات لزيادة الإضاءة الطبيعية"
- ]
- }
-
- # إضافة تقدير التكلفة
- local_analysis["تقدير التكلفة"] = {
- "التكلفة الإجمالية التقديرية": "3,200,000 ريال",
- "تكلفة المتر المربع": "800 ريال",
- "توزيع التكلفة": [
- {"البند": "الهيكل الإنشائي", "النسبة": "35%", "القيمة": "1,120,000 ريال"},
- {"البند": "التشطيبات", "النسبة": "25%", "القيمة": "800,000 ريال"},
- {"البند": "الأنظمة الكهربائية", "النسبة": "15%", "القيمة": "480,000 ريال"},
- {"البند": "الأنظمة الميكانيكية", "النسبة": "15%", "القيمة": "480,000 ريال"},
- {"البند": "الأعمال الخارجية", "النسبة": "10%", "القيمة": "320,000 ريال"}
- ]
- }
-
- # إضافة الجدول الزمني
- local_analysis["الجدول الزمني التقديري"] = {
- "المدة الإجمالية": "18 شهر",
- "المراحل": [
- {"المرحلة": "أعمال الحفر والأساسات", "المدة": "3 أشهر", "النسبة": "15%"},
- {"المرحلة": "الهيكل الإنشائي", "المدة": "6 أشهر", "النسبة": "35%"},
- {"المرحلة": "التشطيبات الداخلية", "المدة": "5 أشهر", "النسبة": "25%"},
- {"المرحلة": "الأنظمة الكهربائية والميكانيكية", "المدة": "3 أشهر", "النسبة": "15%"},
- {"المرحلة": "الأعمال الخارجية والتسليم", "المدة": "1 شهر", "النسبة": "10%"}
- ]
- }
-
- local_analysis["مصدر التحليل"] = provider.capitalize()
- local_analysis["درجة الثقة"] = "95%"
-
- return local_analysis
- except Exception as e:
- logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
- return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
+# -*- coding: utf-8 -*-
+"""
+وحدة تحليل المستندات المتقدمة
+
+هذا الملف يحتوي على الفئات المسؤولة عن تحليل المستندات بشكل احترافي
+باستخدام تقنيات الذكاء الاصطناعي المتقدمة.
+"""
+
+# استيراد المكتبات القياسية
+import os
+import sys
+import logging
+import base64
+import json
+import time
+from io import BytesIO
+from pathlib import Path
+from urllib.parse import urlparse
+from tempfile import NamedTemporaryFile
+
+# استيراد مكتبة Streamlit
+import streamlit as st
+
+# استيراد المكتبات الإضافية
+import requests
+from PIL import Image
+import pandas as pd
+import numpy as np
+
+# تكوين التسجيل
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
+
+try:
+ # استيراد مكتبة pdf2image للتعامل مع ملفات PDF
+ from pdf2image import convert_from_path
+ pdf_conversion_available = True
+except ImportError:
+ pdf_conversion_available = False
+ logger.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.")
+
+
+class TextExtractor:
+ """فئة استخراج النصوص من المستندات"""
+
+ def __init__(self, config=None):
+ """تهيئة مستخرج النصوص"""
+ self.config = config or {}
+
+ def extract_from_pdf(self, file_path):
+ """استخراج النص من ملف PDF"""
+ try:
+ # محاكاة استخراج النص من PDF
+ # في التطبيق الحقيقي، يمكن استخدام مكتبات مثل PyPDF2 أو pdfplumber
+ return f"تم استخراج النص من ملف PDF: {file_path}"
+ except Exception as e:
+ logger.error(f"خطأ في استخراج النص من PDF: {str(e)}")
+ return f"حدث خطأ أثناء استخراج النص: {str(e)}"
+
+ def extract_from_docx(self, file_path):
+ """استخراج النص من ملف DOCX"""
+ try:
+ # محاكاة استخراج النص من DOCX
+ # في التطبيق الحقيقي، يمكن استخدام مكتبة python-docx
+ return f"تم استخراج النص من ملف DOCX: {file_path}"
+ except Exception as e:
+ logger.error(f"خطأ في استخراج النص من DOCX: {str(e)}")
+ return f"حدث خطأ أثناء استخراج النص: {str(e)}"
+
+ def extract_from_image(self, file_path):
+ """استخراج النص من صورة باستخدام OCR"""
+ try:
+ # محاكاة استخراج النص من صورة
+ # في التطبيق الحقيقي، يمكن استخدام مكتبة pytesseract
+ return f"تم استخراج النص من صورة: {file_path}"
+ except Exception as e:
+ logger.error(f"خطأ في استخراج النص من صورة: {str(e)}")
+ return f"حدث خطأ أثناء استخراج النص: {str(e)}"
+
+ def extract(self, file_path):
+ """استخراج النص من ملف بناءً على نوعه"""
+ _, ext = os.path.splitext(file_path)
+ ext = ext.lower()
+
+ if ext == '.pdf':
+ return self.extract_from_pdf(file_path)
+ elif ext in ('.doc', '.docx'):
+ return self.extract_from_docx(file_path)
+ elif ext in ('.jpg', '.jpeg', '.png'):
+ return self.extract_from_image(file_path)
+ else:
+ return "نوع ملف غير مدعوم"
+
+
+class ItemExtractor:
+ """فئة استخراج العناصر من المستندات"""
+
+ def __init__(self, config=None):
+ """تهيئة مستخرج العناصر"""
+ self.config = config or {}
+
+ def extract_tables(self, document):
+ """استخراج الجداول من المستند"""
+ try:
+ # محاكاة استخراج الجداول
+ # في التطبيق الحقيقي، يمكن استخدام مكتبات مثل camelot-py أو tabula-py
+ return [
+ {
+ "عنوان": "جدول البنود والكميات",
+ "بيانات": [
+ {"البند": "أعمال الحفر", "الكمية": 1000, "الوحدة": "م³", "السعر": 50, "الإجمالي": 50000},
+ {"البند": "أعمال الخرسانة", "الكمية": 500, "الوحدة": "م³", "السعر": 300, "الإجمالي": 150000},
+ {"البند": "أعمال التشطيبات", "الكمية": 2000, "الوحدة": "م²", "السعر": 100, "الإجمالي": 200000}
+ ]
+ },
+ {
+ "عنوان": "جدول الجدول الزمني",
+ "بيانات": [
+ {"المرحلة": "التصميم", "المدة": "30 يوم", "تاريخ البدء": "2025-04-01", "تاريخ الانتهاء": "2025-04-30"},
+ {"المرحلة": "الإنشاء", "المدة": "180 يوم", "تاريخ البدء": "2025-05-01", "تاريخ الانتهاء": "2025-10-31"},
+ {"المرحلة": "التسليم", "المدة": "30 يوم", "تاريخ البدء": "2025-11-01", "تاريخ الانتهاء": "2025-11-30"}
+ ]
+ }
+ ]
+ except Exception as e:
+ logger.error(f"خطأ في استخراج الجداول: {str(e)}")
+ return []
+
+ def extract_items(self, file_path):
+ """استخراج البنود من المستند"""
+ try:
+ # محاكاة استخراج البنود
+ return [
+ {"بند": "أعمال الحفر والردم", "قيمة": 250000, "نسبة": "10%"},
+ {"بند": "أعمال الخرسانة المسلحة", "قيمة": 750000, "نسبة": "30%"},
+ {"بند": "أعمال التشطيبات", "قيمة": 500000, "نسبة": "20%"},
+ {"بند": "أعمال الكهرباء", "قيمة": 350000, "نسبة": "14%"},
+ {"بند": "أعمال السباكة", "قيمة": 300000, "نسبة": "12%"},
+ {"بند": "أعمال التكييف", "قيمة": 350000, "نسبة": "14%"}
+ ]
+ except Exception as e:
+ logger.error(f"خطأ في استخراج البنود: {str(e)}")
+ return []
+
+ def extract(self, file_path):
+ """استخراج جميع العناصر من المستند"""
+ return {
+ "بنود": self.extract_items(file_path),
+ "جداول": self.extract_tables(file_path)
+ }
+
+
+class DocumentParser:
+ """فئة تحليل المستندات"""
+
+ def __init__(self, config=None):
+ """تهيئة محلل المستندات"""
+ self.config = config or {}
+ self.text_extractor = TextExtractor(config)
+ self.item_extractor = ItemExtractor(config)
+
+ def parse_contract(self, file_path):
+ """تحليل مستند عقد"""
+ try:
+ # محاكاة تحليل عقد
+ return {
+ "نوع المستند": "عقد",
+ "معلومات العقد": {
+ "رقم العقد": "CT-2025-001",
+ "تاريخ العقد": "2025-03-15",
+ "قيمة العقد": "2,500,000 ريال",
+ "مدة العقد": "12 شهر",
+ "تاريخ البدء": "2025-04-01",
+ "تاريخ الانتهاء": "2026-03-31"
+ },
+ "أطراف العقد": {
+ "الطرف الأول": "وزارة الإسكان",
+ "الطرف الثاني": "شركة الإنشاءات المتطورة"
+ },
+ "بنود العقد": [
+ "يلتزم الطرف الثاني بتنفيذ المشروع وفقاً للمواصفات والشروط المرفقة",
+ "مدة تنفيذ المشروع 12 شهراً من تاريخ استلام الموقع",
+ "قيمة العقد 2,500,000 ريال شاملة جميع الضرائب والرسوم",
+ "يتم الدفع على دفعات شهرية حسب نسبة الإنجاز",
+ "غرامة التأخير 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%"
+ ],
+ "المرفقات": [
+ "جدول الكميات",
+ "المواصفات الفنية",
+ "الجدول الزمني",
+ "الضمانات والتأمينات"
+ ],
+ "درجة الثقة": "95%"
+ }
+ except Exception as e:
+ logger.error(f"خطأ في تحليل العقد: {str(e)}")
+ return {"error": f"حدث خطأ أثناء تحليل العقد: {str(e)}"}
+
+ def parse_tender(self, file_path):
+ """تحليل مستند مناقصة"""
+ try:
+ # محاكاة تحليل مناقصة
+ return {
+ "نوع المستند": "مناقصة",
+ "معلومات المناقصة": {
+ "رقم المناقصة": "T-2025-002",
+ "اسم المشروع": "إنشاء مبنى إداري",
+ "الجهة المالكة": "وزارة المالية",
+ "تاريخ الطرح": "2025-03-01",
+ "تاريخ الإقفال": "2025-04-15",
+ "القيمة التقديرية": "3,000,000 ريال"
+ },
+ "شروط المناقصة": [
+ "تصنيف المقاول: الدرجة الأولى في مجال المباني",
+ "خبرة سابقة: 5 مشاريع مماثلة خلال الـ 10 سنوات الماضية",
+ "الضمان الابتدائي: 2% من قيمة العطاء",
+ "الضمان النهائي: 5% من قيمة العقد",
+ "مدة تنفيذ المشروع: 18 شهراً"
+ ],
+ "المستندات المطلوبة": [
+ "شهادة التصنيف",
+ "السجل التجاري",
+ "شهادة الزكاة والدخل",
+ "شهادة التأمينات الاجتماعية",
+ "قائمة المشاريع المماثلة"
+ ],
+ "معايير التقييم": [
+ {"المعيار": "السعر", "الوزن": "50%"},
+ {"المعيار": "الخبرة الفنية", "الوزن": "25%"},
+ {"المعيار": "الجدول الزمني", "الوزن": "15%"},
+ {"المعيار": "فريق العمل", "الوزن": "10%"}
+ ],
+ "درجة الثقة": "92%"
+ }
+ except Exception as e:
+ logger.error(f"خطأ في تحليل المناقصة: {str(e)}")
+ return {"error": f"حدث خطأ أثناء تحليل المناقصة: {str(e)}"}
+
+ def parse_specifications(self, file_path):
+ """تحليل كراسة الشروط والمواصفات"""
+ try:
+ # محاكاة تحليل كراسة الشروط والمواصفات
+ return {
+ "نوع المستند": "كراسة شروط ومواصفات",
+ "معلومات المشروع": {
+ "اسم المشروع": "إنشاء مبنى إداري",
+ "الموقع": "الرياض - حي العليا",
+ "المساحة": "5000 متر مربع",
+ "عدد الطوابق": "5 طوابق"
+ },
+ "المواصفات الفنية": {
+ "الهيكل الإنشائي": "خرسانة مسلحة",
+ "الواجهات": "زجاج عاكس وحجر طبيعي",
+ "التشطيبات الداخلية": "رخام للأرضيات، جبس للأسقف، دهانات عالية الجودة للجدران",
+ "أنظمة الكهرباء": "نظام إنارة LED موفر للطاقة، نظام تحكم ذكي",
+ "أنظمة التكييف": "نظام تكييف مركزي مع تحكم منفصل لكل منطقة",
+ "أنظمة السلامة": "نظام إنذار وإطفاء حريق آلي، كاميرات مراقبة، نظام تحكم في الدخول"
+ },
+ "الشروط العامة": [
+ "الالتزام بكود البناء السعودي",
+ "الالتزام بمتطلبات الدفاع المدني",
+ "الالتزام بمتطلبات الاستدامة وكفاءة الطاقة",
+ "تقديم مخططات تنفيذية معتمدة قبل البدء في التنفيذ",
+ "تقديم عينات للمواد للاعتماد قبل التوريد"
+ ],
+ "المرفقات": [
+ "المخططات المعمارية",
+ "المخططات الإنشائية",
+ "مخططات الكهرباء",
+ "مخططات التكييف",
+ "مخططات السباكة",
+ "جدول الكميات"
+ ],
+ "درجة الثقة": "90%"
+ }
+ except Exception as e:
+ logger.error(f"خطأ في تحليل كراسة الشروط والمواصفات: {str(e)}")
+ return {"error": f"حدث خطأ أثناء تحليل كراسة الشروط والمواصفات: {str(e)}"}
+
+ def parse_dwg(self, file_path):
+ """تحليل ملف DWG"""
+ try:
+ # محاكاة تحليل ملف DWG
+ return {
+ "نوع المستند": "ملف DWG",
+ "معلومات الملف": {
+ "اسم الملف": os.path.basename(file_path),
+ "حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
+ "تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
+ },
+ "محتويات الملف": {
+ "عدد الطبقات": 15,
+ "عدد الكائنات": 1250,
+ "أبعاد الرسم": "50م × 30م"
+ },
+ "تحليل المساحات": {
+ "المساحة الإجمالية": "4,500 م²",
+ "مساحة البناء": "3,200 م²",
+ "مساحة الخدمات": "800 م²",
+ "مساحة الممرات": "500 م²"
+ },
+ "تحليل العناصر": {
+ "عدد الغرف": 25,
+ "عدد الأبواب": 40,
+ "عدد النوافذ": 30,
+ "عدد الأعمدة": 20
+ },
+ "ملاحظات": [
+ "تصميم يتوافق مع متطلبات كود البناء السعودي",
+ "توزيع جيد للمساحات",
+ "تصميم يراعي متطلبات ذوي الاحتياجات الخاصة",
+ "تصميم يراعي متطلبات السلامة والإخلاء"
+ ],
+ "درجة الثقة": "85%"
+ }
+ except Exception as e:
+ logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
+ return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
+
+ def parse(self, file_path):
+ """تحليل المستند بناءً على نوعه"""
+ try:
+ _, ext = os.path.splitext(file_path)
+ ext = ext.lower()
+
+ # تحديد نوع المستند بناءً على محتواه (محاكاة)
+ file_name = os.path.basename(file_path).lower()
+
+ if ext == '.dwg':
+ return self.parse_dwg(file_path)
+ elif 'contract' in file_name or 'عقد' in file_name:
+ return self.parse_contract(file_path)
+ elif 'tender' in file_name or 'مناقصة' in file_name:
+ return self.parse_tender(file_path)
+ elif 'spec' in file_name or 'شروط' in file_name or 'مواصفات' in file_name:
+ return self.parse_specifications(file_path)
+ else:
+ # تحليل عام للمستند
+ return {
+ "نوع المستند": "مستند عام",
+ "معلومات الملف": {
+ "اسم الملف": os.path.basename(file_path),
+ "حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
+ "تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
+ },
+ "محتوى المستند": {
+ "نص": self.text_extractor.extract(file_path),
+ "عناصر": self.item_extractor.extract(file_path)
+ },
+ "درجة الثقة": "75%"
+ }
+ except Exception as e:
+ logger.error(f"خطأ في تحليل المستند: {str(e)}")
+ return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
+
+
+class AIDocumentAnalyzer:
+ """فئة تحليل المستندات باستخدام الذكاء الاصطناعي"""
+
+ def __init__(self):
+ """تهيئة محلل المستندات الذكي"""
+ self.document_parser = DocumentParser()
+ self.api_keys = {}
+
+ def set_api_key(self, provider, key):
+ """تعيين مفتاح API لمزود خدمة الذكاء الاصطناعي"""
+ self.api_keys[provider] = key
+
+ def get_api_key(self, provider):
+ """الحصول على مفتاح API لمزود خدمة الذكاء الاصطناعي"""
+ return self.api_keys.get(provider)
+
+ def analyze_document(self, file_path, provider="local"):
+ """تحليل المستند باستخدام الذكاء الاصطناعي"""
+ try:
+ # تحليل محلي للمستند
+ local_analysis = self.document_parser.parse(file_path)
+
+ if provider == "local":
+ return local_analysis
+
+ # تحليل باستخدام خدمات الذكاء الاصطناعي السحابية
+ if provider == "openai":
+ # محاكاة تحليل باستخدام OpenAI
+ enhanced_analysis = self._enhance_with_openai(local_analysis)
+ return enhanced_analysis
+ elif provider == "claude":
+ # محاكاة تحليل باستخدام Claude
+ enhanced_analysis = self._enhance_with_claude(local_analysis)
+ return enhanced_analysis
+ else:
+ return local_analysis
+ except Exception as e:
+ logger.error(f"خطأ في تحليل المستند: {str(e)}")
+ return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
+
+ def _enhance_with_openai(self, analysis):
+ """تحسين التحليل باستخدام OpenAI"""
+ # محاكاة تحسين التحليل باستخدام OpenAI
+ analysis["مصدر التحليل"] = "OpenAI"
+ analysis["درجة الثقة"] = "98%"
+
+ # إضافة تحليل المخاطر
+ if "تحليل المخاطر" not in analysis:
+ analysis["تحليل المخاطر"] = [
+ {"المخاطرة": "تأخر التوريدات", "الاحتمالية": "متوسطة", "التأثير": "عالي", "استراتيجية التخفيف": "وضع خطة توريدات بديلة"},
+ {"المخاطرة": "زيادة أسعار المواد", "الاحتمالية": "عالية", "التأثير": "عالي", "استراتيجية التخفيف": "تثبيت أسعار المواد الرئيسية مع الموردين"},
+ {"المخاطرة": "نقص العمالة الماهرة", "الاحتمالية": "متوسطة", "التأثير": "متوسط", "استراتيجية التخفيف": "التعاقد المسبق مع مقاولي الباطن"},
+ {"المخاطرة": "تغيير نطاق العمل", "الاحتمالية": "منخفضة", "التأثير": "عالي", "استراتيجية التخفيف": "توثيق نطاق العمل بدقة وإدارة التغيير"}
+ ]
+
+ # إضافة توصيات
+ if "التوصيات" not in analysis:
+ analysis["التوصيات"] = [
+ "مراجعة بنود العقد بدقة قبل التوقيع",
+ "التأكد من وضوح نطاق العمل وعدم وجود غموض",
+ "التحقق من توافق المواصفات الفنية مع المعايير المحلية",
+ "وضع خطة إدارة مخاطر شاملة للمشروع",
+ "تخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع"
+ ]
+
+ return analysis
+
+ def _enhance_with_claude(self, analysis):
+ """تحسين التحليل باستخدام Claude"""
+ # محاكاة تحسين التحليل باستخدام Claude
+ analysis["مصدر التحليل"] = "Claude"
+ analysis["درجة الثقة"] = "97%"
+
+ # إضافة تحليل الفرص
+ if "تحليل الفرص" not in analysis:
+ analysis["تحليل الفرص"] = [
+ {"الفرصة": "تحسين التصميم", "الفائدة": "تقليل التكلفة بنسبة 5-10%", "المتطلبات": "مراجعة هندسية شاملة"},
+ {"الفرصة": "استخدام مواد بديلة", "الفائدة": "تقليل وقت التنفيذ", "المتطلبات": "اعتماد المواصفات الجديدة"},
+ {"الفرصة": "زيادة المحتوى المحلي", "الفائدة": "تحسين التصنيف في برنامج القيمة المضافة", "المتطلبات": "تحديد الموردين المحليين"},
+ {"الفرصة": "تطبيق تقنيات البناء الحديثة", "الفائدة": "تحسين الجودة وتقليل الهدر", "المتطلبات": "تدريب فريق العمل"}
+ ]
+
+ # إضافة ملخص تنفيذي
+ if "الملخص التنفيذي" not in analysis:
+ analysis["الملخص التنفيذي"] = """
+ يتضمن هذا المستند تفاصيل مشروع إنشاء مبنى إداري بمساحة إجمالية 5000 متر مربع.
+ المشروع يتكون من 5 طوابق ويتضمن مواصفات فنية عالية الجودة.
+ تقدر تكلفة المشروع بحوالي 3 مليون ريال ومدة التنفيذ 18 شهراً.
+ يتميز المشروع بتصميم يراعي متطلبات الاستدامة وكفاءة الطاقة.
+ تم تحديد عدة مخاطر محتملة للمشروع مع استراتيجيات التخفيف المناسبة.
+ كما تم تحديد عدة فرص لتحسين المشروع من حيث التكلفة والجودة ووقت التنفيذ.
+ """
+
+ return analysis
+
+ def analyze_dwg(self, file_path, provider="local"):
+ """تحليل ملف DWG باستخدام الذكاء الاصطناعي"""
+ try:
+ # تحليل محلي لملف DWG
+ local_analysis = self.document_parser.parse_dwg(file_path)
+
+ if provider == "local":
+ return local_analysis
+
+ # تحسين التحليل باستخدام خدمات الذكاء الاصطناعي
+ if provider == "openai" or provider == "claude":
+ # إضافة تحليل متقدم
+ local_analysis["تحليل متقدم"] = {
+ "تقييم التصميم": "جيد جداً",
+ "كفاءة استخدام المساحة": "90%",
+ "توافق مع المعايير": "متوافق مع كود البناء السعودي",
+ "اقتراحات التحسين": [
+ "تحسين توزيع المساحات لزيادة كفاءة استخدام المساحة",
+ "تحسين تصميم الممرات لتسهيل الحركة",
+ "إضافة عناصر تصميم مستدامة لتقليل استهلاك الطاقة",
+ "تحسين تصميم الواجهات لزيادة الإضاءة الطبيعية"
+ ]
+ }
+
+ # إضافة تقدير التكلفة
+ local_analysis["تقدير التكلفة"] = {
+ "التكلفة الإجمالية التقديرية": "3,200,000 ريال",
+ "تكلفة المتر المربع": "800 ريال",
+ "توزيع التكلفة": [
+ {"البند": "الهيكل الإنشائي", "النسبة": "35%", "القيمة": "1,120,000 ريال"},
+ {"البند": "التشطيبات", "النسبة": "25%", "القيمة": "800,000 ريال"},
+ {"البند": "الأنظمة الكهربائية", "النسبة": "15%", "القيمة": "480,000 ريال"},
+ {"البند": "الأنظمة الميكانيكية", "النسبة": "15%", "القيمة": "480,000 ريال"},
+ {"البند": "الأعمال الخارجية", "النسبة": "10%", "القيمة": "320,000 ريال"}
+ ]
+ }
+
+ # إضافة الجدول الزمني
+ local_analysis["الجدول الزمني التقديري"] = {
+ "المدة الإجمالية": "18 شهر",
+ "المراحل": [
+ {"المرحلة": "أعمال الحفر والأساسات", "المدة": "3 أشهر", "النسبة": "15%"},
+ {"المرحلة": "الهيكل الإنشائي", "المدة": "6 أشهر", "النسبة": "35%"},
+ {"المرحلة": "التشطيبات الداخلية", "المدة": "5 أشهر", "النسبة": "25%"},
+ {"المرحلة": "الأنظمة الكهربائية والميكانيكية", "المدة": "3 أشهر", "النسبة": "15%"},
+ {"المرحلة": "الأعمال الخارجية والتسليم", "المدة": "1 شهر", "النسبة": "10%"}
+ ]
+ }
+
+ local_analysis["مصدر التحليل"] = provider.capitalize()
+ local_analysis["درجة الثقة"] = "95%"
+
+ return local_analysis
+ except Exception as e:
+ logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
+ return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
diff --git a/modules/data_analysis/data_analysis_app.py b/modules/data_analysis/data_analysis_app.py
index e20309ded7cd3264c105704939ef7bf5e1f11c4a..22b19007a03505dc24c09ad6a57889d86db7d721 100644
--- a/modules/data_analysis/data_analysis_app.py
+++ b/modules/data_analysis/data_analysis_app.py
@@ -1,502 +1,502 @@
-import streamlit as st
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import plotly.express as px
-import plotly.graph_objects as go
-import seaborn as sns
-from datetime import datetime
-import os
-import sys
-from pathlib import Path
-
-# إضافة المسار للوصول إلى الوحدات الأخرى
-current_dir = os.path.dirname(os.path.abspath(__file__))
-parent_dir = os.path.dirname(os.path.dirname(current_dir))
-if parent_dir not in sys.path:
- sys.path.append(parent_dir)
-
-class DataAnalysisApp:
- """تطبيق تحليل البيانات"""
-
- def __init__(self):
- """تهيئة تطبيق تحليل البيانات"""
- self.data = None
- self.file_path = None
-
- def run(self):
- """تشغيل تطبيق تحليل البيانات"""
- # استيراد مدير التكوين
- from config_manager import ConfigManager
-
- # محاولة تعيين تكوين الصفحة (سيتم تجاهلها إذا كان التكوين معينًا بالفعل)
- config_manager = ConfigManager()
- config_manager.set_page_config_if_needed(
- page_title="تحليل البيانات",
- page_icon="📊",
- layout="wide"
- )
-
- # عرض عنوان التطبيق
- st.title("تحليل البيانات")
- st.write("استخدم هذه الأداة لتحليل بيانات المناقصات والمشاريع")
-
- # إنشاء علامات تبويب للتطبيق
- tabs = st.tabs(["تحميل البيانات", "استكشاف البيانات", "تحليل متقدم", "التصور المرئي", "التقارير"])
-
- with tabs[0]:
- self._load_data_tab()
-
- with tabs[1]:
- self._explore_data_tab()
-
- with tabs[2]:
- self._advanced_analysis_tab()
-
- with tabs[3]:
- self._visualization_tab()
-
- with tabs[4]:
- self._reports_tab()
-
- def _load_data_tab(self):
- """علامة تبويب تحميل البيانات"""
- st.header("تحميل البيانات")
-
- # خيارات تحميل البيانات
- data_source = st.radio(
- "اختر مصدر البيانات:",
- ["تحميل ملف", "استيراد من قاعدة البيانات", "استخدام بيانات نموذجية"]
- )
-
- if data_source == "تحميل ملف":
- uploaded_file = st.file_uploader("اختر ملف CSV أو Excel", type=["csv", "xlsx", "xls"])
-
- if uploaded_file is not None:
- try:
- if uploaded_file.name.endswith('.csv'):
- self.data = pd.read_csv(uploaded_file)
- else:
- self.data = pd.read_excel(uploaded_file)
-
- st.success(f"تم تحميل الملف بنجاح! عدد الصفوف: {self.data.shape[0]}, عدد الأعمدة: {self.data.shape[1]}")
- st.write("معاينة البيانات:")
- st.dataframe(self.data.head())
- except Exception as e:
- st.error(f"حدث خطأ أثناء تحميل الملف: {str(e)}")
-
- elif data_source == "استيراد من قاعدة البيانات":
- st.info("هذه الميزة قيد التطوير")
-
- # محاكاة الاتصال بقاعدة البيانات
- if st.button("اتصال بقاعدة البيانات"):
- with st.spinner("جاري الاتصال بقاعدة البيانات..."):
- # محاكاة تأخير الاتصال
- import time
- time.sleep(2)
-
- # إنشاء بيانات نموذجية
- self.data = self._create_sample_data()
-
- st.success("تم الاتصال بقاعدة البيانات بنجاح!")
- st.write("معاينة البيانات:")
- st.dataframe(self.data.head())
-
- elif data_source == "استخدام بيانات نموذجية":
- if st.button("تحميل بيانات نموذجية"):
- self.data = self._create_sample_data()
- st.success("تم تحميل البيانات النموذجية بنجاح!")
- st.write("معاينة البيانات:")
- st.dataframe(self.data.head())
-
- def _explore_data_tab(self):
- """علامة تبويب استكشاف البيانات"""
- st.header("استكشاف البيانات")
-
- if self.data is None:
- st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
- return
-
- # عرض معلومات عامة عن البيانات
- st.subheader("معلومات عامة")
- col1, col2 = st.columns(2)
-
- with col1:
- st.write(f"عدد الصفوف: {self.data.shape[0]}")
- st.write(f"عدد الأعمدة: {self.data.shape[1]}")
- st.write(f"القيم المفقودة: {self.data.isna().sum().sum()}")
-
- with col2:
- st.write(f"أنواع البيانات:")
- st.write(self.data.dtypes)
-
- # عرض إحصاءات وصفية
- st.subheader("إحصاءات وصفية")
- st.dataframe(self.data.describe())
-
- # عرض معلومات عن الأعمدة
- st.subheader("معلومات الأعمدة")
-
- selected_column = st.selectbox("اختر عمودًا لتحليله:", self.data.columns)
-
- if selected_column:
- col1, col2 = st.columns(2)
-
- with col1:
- st.write(f"نوع البيانات: {self.data[selected_column].dtype}")
- st.write(f"القيم الفريدة: {self.data[selected_column].nunique()}")
- st.write(f"القيم المفقودة: {self.data[selected_column].isna().sum()}")
-
- with col2:
- if pd.api.types.is_numeric_dtype(self.data[selected_column]):
- st.write(f"الحد الأدنى: {self.data[selected_column].min()}")
- st.write(f"الحد الأقصى: {self.data[selected_column].max()}")
- st.write(f"المتوسط: {self.data[selected_column].mean()}")
- st.write(f"الوسيط: {self.data[selected_column].median()}")
- else:
- st.write("القيم الأكثر تكرارًا:")
- st.write(self.data[selected_column].value_counts().head())
-
- # عرض رسم بياني للعمود المحدد
- st.subheader(f"رسم بياني لـ {selected_column}")
-
- if pd.api.types.is_numeric_dtype(self.data[selected_column]):
- fig = px.histogram(self.data, x=selected_column, title=f"توزيع {selected_column}")
- st.plotly_chart(fig, use_container_width=True)
- else:
- # الكود المعدل لحل مشكلة الرسم البياني
- value_counts_df = self.data[selected_column].value_counts().reset_index()
- value_counts_df.columns = ['القيمة', 'العدد'] # تسمية الأعمدة بأسماء واضحة
- fig = px.bar(value_counts_df, x='القيمة', y='العدد', title=f"توزيع {selected_column}")
- fig.update_layout(xaxis_title="القيمة", yaxis_title="العدد")
- st.plotly_chart(fig, use_container_width=True)
-
- def _advanced_analysis_tab(self):
- """علامة تبويب التحليل المتقدم"""
- st.header("تحليل متقدم")
-
- if self.data is None:
- st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
- return
-
- # أنواع التحليل المتقدم
- analysis_type = st.selectbox(
- "اختر نوع التحليل:",
- ["تحليل الارتباط", "تحليل الاتجاهات", "تحليل المجموعات", "تحليل التباين"]
- )
-
- if analysis_type == "تحليل الارتباط":
- st.subheader("تحليل الارتباط")
-
- # اختيار الأعمدة الرقمية فقط
- numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
-
- if len(numeric_columns) < 2:
- st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإجراء تحليل الارتباط")
- return
-
- # حساب مصفوفة الارتباط
- correlation_matrix = self.data[numeric_columns].corr()
-
- # عرض مصفوفة الارتباط
- st.write("مصفوفة الارتباط:")
- st.dataframe(correlation_matrix)
-
- # رسم خريطة حرارية للارتباط
- st.write("خريطة حرارية للارتباط:")
- fig = px.imshow(correlation_matrix, text_auto=True, aspect="auto",
- title="خريطة حرارية لمصفوفة الارتباط")
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل الارتباط بين عمودين محددين
- st.subheader("تحليل الارتباط بين عمودين محددين")
-
- col1 = st.selectbox("اختر العمود الأول:", numeric_columns, key="corr_col1")
- col2 = st.selectbox("اختر العمود الثاني:", numeric_columns, key="corr_col2")
-
- if col1 != col2:
- # حساب معامل الارتباط
- correlation = self.data[col1].corr(self.data[col2])
-
- st.write(f"معامل الارتباط بين {col1} و {col2}: {correlation:.4f}")
-
- # رسم مخطط التشتت
- fig = px.scatter(self.data, x=col1, y=col2, title=f"مخطط التشتت: {col1} مقابل {col2}")
- fig.update_layout(xaxis_title=col1, yaxis_title=col2)
- st.plotly_chart(fig, use_container_width=True)
- else:
- st.warning("الرجاء اختيار عمودين مختلفين")
-
- elif analysis_type == "تحليل الاتجاهات":
- st.subheader("تحليل الاتجاهات")
- st.info("هذه الميزة قيد التطوير")
-
- elif analysis_type == "تحليل المجموعات":
- st.subheader("تحليل المجموعات")
- st.info("هذه الميزة قيد التطوير")
-
- elif analysis_type == "تحليل التباين":
- st.subheader("تحليل التباين")
- st.info("هذه الميزة قيد التطوير")
-
- def _visualization_tab(self):
- """علامة تبويب التصور المرئي"""
- st.header("التصور المرئي")
-
- if self.data is None:
- st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
- return
-
- # أنواع الرسوم البيانية
- chart_type = st.selectbox(
- "اختر نوع الرسم البياني:",
- ["مخطط شريطي", "مخطط خطي", "مخطط دائري", "مخطط تشتت", "مخطط صندوقي", "مخطط حراري"]
- )
-
- # اختيار الأعمدة حسب نوع الرسم البياني
- if chart_type == "مخطط شريطي":
- st.subheader("مخطط شريطي")
-
- x_column = st.selectbox("اختر عمود المحور الأفقي (x):", self.data.columns, key="bar_x")
- y_column = st.selectbox("اختر عمود المحور الرأسي (y):",
- self.data.select_dtypes(include=['number']).columns.tolist(),
- key="bar_y")
-
- # خيارات إضافية
- color_column = st.selectbox("اختر عمود اللون (اختياري):",
- ["لا يوجد"] + self.data.columns.tolist(),
- key="bar_color")
-
- # إنشاء الرسم البياني
- if color_column == "لا يوجد":
- fig = px.bar(self.data, x=x_column, y=y_column, title=f"{y_column} حسب {x_column}")
- else:
- fig = px.bar(self.data, x=x_column, y=y_column, color=color_column,
- title=f"{y_column} حسب {x_column} (مصنف حسب {color_column})")
-
- fig.update_layout(xaxis_title=x_column, yaxis_title=y_column)
- st.plotly_chart(fig, use_container_width=True)
-
- elif chart_type == "مخطط خطي":
- st.subheader("مخطط خطي")
-
- x_column = st.selectbox("اختر عمود المحور الأفقي (x):", self.data.columns, key="line_x")
- y_columns = st.multiselect("اختر أعمدة المحور الرأسي (y):",
- self.data.select_dtypes(include=['number']).columns.tolist(),
- key="line_y")
-
- if y_columns:
- # إنشاء الرسم البياني
- fig = go.Figure()
-
- for y_column in y_columns:
- fig.add_trace(go.Scatter(x=self.data[x_column], y=self.data[y_column],
- mode='lines+markers', name=y_column))
-
- fig.update_layout(title=f"مخطط خطي", xaxis_title=x_column, yaxis_title="القيمة")
- st.plotly_chart(fig, use_container_width=True)
- else:
- st.warning("الرجاء اختيار عمود واحد على الأقل للمحور الرأسي")
-
- elif chart_type == "مخطط دائري":
- st.subheader("مخطط دائري")
-
- column = st.selectbox("اختر العمود:", self.data.columns, key="pie_column")
-
- # إنشاء الرسم البياني
- # تعديل لحل مشكلة مماثلة في مخطط دائري
- value_counts_df = self.data[column].value_counts().reset_index()
- value_counts_df.columns = ['القيمة', 'العدد']
- fig = px.pie(value_counts_df, names='القيمة', values='العدد', title=f"توزيع {column}")
- st.plotly_chart(fig, use_container_width=True)
-
- elif chart_type == "مخطط تشتت":
- st.subheader("مخطط تشتت")
-
- numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
-
- if len(numeric_columns) < 2:
- st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإنشاء مخطط تشتت")
- return
-
- x_column = st.selectbox("اختر عمود المحور الأفقي (x):", numeric_columns, key="scatter_x")
- y_column = st.selectbox("اختر عمود المحور الرأسي (y):", numeric_columns, key="scatter_y")
-
- # خيارات إضافية
- color_column = st.selectbox("اختر عمود اللون (اختياري):",
- ["لا يوجد"] + self.data.columns.tolist(),
- key="scatter_color")
-
- size_column = st.selectbox("اختر عمود الحجم (اختياري):",
- ["لا يوجد"] + numeric_columns,
- key="scatter_size")
-
- # إنشاء الرسم البياني
- if color_column == "لا يوجد" and size_column == "لا يوجد":
- fig = px.scatter(self.data, x=x_column, y=y_column,
- title=f"{y_column} مقابل {x_column}")
- elif color_column != "لا يوجد" and size_column == "لا يوجد":
- fig = px.scatter(self.data, x=x_column, y=y_column, color=color_column,
- title=f"{y_column} مقابل {x_column} (مصنف حسب {color_column})")
- elif color_column == "لا يوجد" and size_column != "لا يوجد":
- fig = px.scatter(self.data, x=x_column, y=y_column, size=size_column,
- title=f"{y_column} مقابل {x_column} (الحجم حسب {size_column})")
- else:
- fig = px.scatter(self.data, x=x_column, y=y_column, color=color_column, size=size_column,
- title=f"{y_column} مقابل {x_column} (مصنف حسب {color_column}, الحجم حسب {size_column})")
-
- fig.update_layout(xaxis_title=x_column, yaxis_title=y_column)
- st.plotly_chart(fig, use_container_width=True)
-
- elif chart_type == "مخطط صندوقي":
- st.subheader("مخطط صندوقي")
-
- numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
-
- if not numeric_columns:
- st.warning("يجب أن يكون هناك عمود رقمي واحد على الأقل لإنشاء مخطط صندوقي")
- return
-
- y_column = st.selectbox("اختر عمود القيمة:", numeric_columns, key="box_y")
-
- # خيارات إضافية
- x_column = st.selectbox("اختر عمود التصنيف (اختياري):",
- ["لا يوجد"] + self.data.columns.tolist(),
- key="box_x")
-
- # إنشاء الرسم البياني
- if x_column == "لا يوجد":
- fig = px.box(self.data, y=y_column, title=f"مخطط صندوقي لـ {y_column}")
- else:
- fig = px.box(self.data, x=x_column, y=y_column,
- title=f"مخطط صندوقي لـ {y_column} حسب {x_column}")
-
- st.plotly_chart(fig, use_container_width=True)
-
- elif chart_type == "مخطط حراري":
- st.subheader("مخطط حراري")
-
- numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
-
- if len(numeric_columns) < 2:
- st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإنشاء مخطط حراري")
- return
-
- # اختيار الأعمدة للمخطط الحراري
- selected_columns = st.multiselect("اختر الأعمدة للمخطط الحراري:",
- numeric_columns,
- default=numeric_columns[:5] if len(numeric_columns) > 5 else numeric_columns)
-
- if selected_columns:
- # حساب مصفوفة الارتباط
- correlation_matrix = self.data[selected_columns].corr()
-
- # إنشاء الرسم البياني
- fig = px.imshow(correlation_matrix, text_auto=True, aspect="auto",
- title="مخطط حراري لمصفوفة الارتباط")
- st.plotly_chart(fig, use_container_width=True)
- else:
- st.warning("الرجاء اختيار عمود واحد على الأقل")
-
- def _reports_tab(self):
- """علامة تبويب التقارير"""
- st.header("التقارير")
-
- if self.data is None:
- st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
- return
-
- st.subheader("إنشاء تقرير")
-
- # خيارات التقرير
- report_type = st.selectbox(
- "اختر نوع التقرير:",
- ["تقرير ملخص", "تقرير تحليلي", "تقرير مقارنة"]
- )
-
- if report_type == "تقرير ملخص":
- st.write("محتوى التقرير:")
-
- # إنشاء ملخص للبيانات
- st.write("### ملخص البيانات")
- st.write(f"عدد الصفوف: {self.data.shape[0]}")
- st.write(f"عدد الأعمدة: {self.data.shape[1]}")
-
- # إحصاءات وصفية
- st.write("### إحصاءات وصفية")
- st.dataframe(self.data.describe())
-
- # معلومات عن القيم المفقودة
- st.write("### القيم المفقودة")
- missing_data = pd.DataFrame({
- 'العمود': self.data.columns,
- 'عدد القيم المفقودة': self.data.isna().sum().values,
- 'نسبة القيم المفقودة (%)': (self.data.isna().sum().values / len(self.data) * 100).round(2)
- })
- st.dataframe(missing_data)
-
- # توزيع البيانات الرقمية
- st.write("### توزيع البيانات الرقمية")
- numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
-
- if numeric_columns:
- for i in range(0, len(numeric_columns), 2):
- cols = st.columns(2)
- for j in range(2):
- if i + j < len(numeric_columns):
- col = numeric_columns[i + j]
- with cols[j]:
- fig = px.histogram(self.data, x=col, title=f"توزيع {col}")
- st.plotly_chart(fig, use_container_width=True)
-
- # خيارات تصدير التقرير
- st.subheader("تصدير التقرير")
- export_format = st.radio("اختر صيغة التصدير:", ["PDF", "Excel", "HTML"])
-
- if st.button("تصدير التقرير"):
- st.success(f"تم تصدير التقرير بصيغة {export_format} بنجاح!")
-
- elif report_type == "تقرير تحليلي":
- st.info("هذه الميزة قيد التطوير")
-
- elif report_type == "تقرير مقارنة":
- st.info("هذه الميزة قيد التطوير")
-
- def _create_sample_data(self):
- """إنشاء بيانات نموذجية للمناقصات"""
- # إنشاء تواريخ عشوائية
- start_date = datetime(2023, 1, 1)
- end_date = datetime(2025, 3, 31)
- days = (end_date - start_date).days
-
- # إنشاء بيانات نموذجية
- data = {
- 'رقم المناقصة': [f'T-{i:04d}' for i in range(1, 101)],
- 'اسم المشروع': [f'مشروع {i}' for i in range(1, 101)],
- 'نوع المشروع': np.random.choice(['بناء', 'صيانة', 'تطوير', 'توريد', 'خدمات'], 100),
- 'الموقع': np.random.choice(['الرياض', 'جدة', 'الدمام', 'مكة', 'المدينة', 'تبوك', 'أبها'], 100),
- 'تاريخ الإعلان': [start_date + pd.Timedelta(days=np.random.randint(0, days)) for _ in range(100)],
- 'تاريخ الإغلاق': [start_date + pd.Timedelta(days=np.random.randint(30, days)) for _ in range(100)],
- 'الميزانية التقديرية': np.random.uniform(1000000, 50000000, 100),
- 'عدد المتقدمين': np.random.randint(1, 20, 100),
- 'سعر العرض': np.random.uniform(900000, 55000000, 100),
- 'نسبة الفوز (%)': np.random.uniform(0, 100, 100),
- 'مدة التنفيذ (أشهر)': np.random.randint(3, 36, 100),
- 'عدد العمال': np.random.randint(10, 500, 100),
- 'تكلفة المواد': np.random.uniform(500000, 30000000, 100),
- 'تكلفة العمالة': np.random.uniform(200000, 15000000, 100),
- 'تكلفة المعدات': np.random.uniform(100000, 10000000, 100),
- 'هامش الربح (%)': np.random.uniform(5, 25, 100),
- 'درجة المخاطرة': np.random.choice(['منخفضة', 'متوسطة', 'عالية'], 100),
- 'حالة المناقصة': np.random.choice(['جارية', 'مغلقة', 'ملغاة', 'فائزة', 'خاسرة'], 100)
- }
-
- # إنشاء DataFrame
- df = pd.DataFrame(data)
-
- # إضافة بعض العلاقات المنطقية
- df['إجمالي التكلفة'] = df['تكلفة المواد'] + df['تكلفة العمالة'] + df['تكلفة المعدات']
- df['الربح المتوقع'] = df['سعر العرض'] - df['إجمالي التكلفة']
- df['نسبة التكلفة من العرض (%)'] = (df['إجمالي التكلفة'] / df['سعر العرض'] * 100).round(2)
-
- return df
+import streamlit as st
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.express as px
+import plotly.graph_objects as go
+import seaborn as sns
+from datetime import datetime
+import os
+import sys
+from pathlib import Path
+
+# إضافة المسار للوصول إلى الوحدات الأخرى
+current_dir = os.path.dirname(os.path.abspath(__file__))
+parent_dir = os.path.dirname(os.path.dirname(current_dir))
+if parent_dir not in sys.path:
+ sys.path.append(parent_dir)
+
+class DataAnalysisApp:
+ """تطبيق تحليل البيانات"""
+
+ def __init__(self):
+ """تهيئة تطبيق تحليل البيانات"""
+ self.data = None
+ self.file_path = None
+
+ def run(self):
+ """تشغيل تطبيق تحليل البيانات"""
+ # استيراد مدير التكوين
+ from config_manager import ConfigManager
+
+ # محاولة تعيين تكوين الصفحة (سيتم تجاهلها إذا كان التكوين معينًا بالفعل)
+ config_manager = ConfigManager()
+ config_manager.set_page_config_if_needed(
+ page_title="تحليل البيانات",
+ page_icon="📊",
+ layout="wide"
+ )
+
+ # عرض عنوان التطبيق
+ st.title("تحليل البيانات")
+ st.write("استخدم هذه الأداة لتحليل بيانات المناقصات والمشاريع")
+
+ # إنشاء علامات تبويب للتطبيق
+ tabs = st.tabs(["تحميل البيانات", "استكشاف البيانات", "تحليل متقدم", "التصور المرئي", "التقارير"])
+
+ with tabs[0]:
+ self._load_data_tab()
+
+ with tabs[1]:
+ self._explore_data_tab()
+
+ with tabs[2]:
+ self._advanced_analysis_tab()
+
+ with tabs[3]:
+ self._visualization_tab()
+
+ with tabs[4]:
+ self._reports_tab()
+
+ def _load_data_tab(self):
+ """علامة تبويب تحميل البيانات"""
+ st.header("تحميل البيانات")
+
+ # خيارات تحميل البيانات
+ data_source = st.radio(
+ "اختر مصدر البيانات:",
+ ["تحميل ملف", "استيراد من قاعدة البيانات", "استخدام بيانات نموذجية"]
+ )
+
+ if data_source == "تحميل ملف":
+ uploaded_file = st.file_uploader("اختر ملف CSV أو Excel", type=["csv", "xlsx", "xls"])
+
+ if uploaded_file is not None:
+ try:
+ if uploaded_file.name.endswith('.csv'):
+ self.data = pd.read_csv(uploaded_file)
+ else:
+ self.data = pd.read_excel(uploaded_file)
+
+ st.success(f"تم تحميل الملف بنجاح! عدد الصفوف: {self.data.shape[0]}, عدد الأعمدة: {self.data.shape[1]}")
+ st.write("معاينة البيانات:")
+ st.dataframe(self.data.head())
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء تحميل الملف: {str(e)}")
+
+ elif data_source == "استيراد من قاعدة البيانات":
+ st.info("هذه الميزة قيد التطوير")
+
+ # محاكاة الاتصال بقاعدة البيانات
+ if st.button("اتصال بقاعدة البيانات"):
+ with st.spinner("جاري الاتصال بقاعدة البيانات..."):
+ # محاكاة تأخير الاتصال
+ import time
+ time.sleep(2)
+
+ # إنشاء بيانات نموذجية
+ self.data = self._create_sample_data()
+
+ st.success("تم الاتصال بقاعدة البيانات بنجاح!")
+ st.write("معاينة البيانات:")
+ st.dataframe(self.data.head())
+
+ elif data_source == "استخدام بيانات نموذجية":
+ if st.button("تحميل بيانات نموذجية"):
+ self.data = self._create_sample_data()
+ st.success("تم تحميل البيانات النموذجية بنجاح!")
+ st.write("معاينة البيانات:")
+ st.dataframe(self.data.head())
+
+ def _explore_data_tab(self):
+ """علامة تبويب استكشاف البيانات"""
+ st.header("استكشاف البيانات")
+
+ if self.data is None:
+ st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
+ return
+
+ # عرض معلومات عامة عن البيانات
+ st.subheader("معلومات عامة")
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.write(f"عدد الصفوف: {self.data.shape[0]}")
+ st.write(f"عدد الأعمدة: {self.data.shape[1]}")
+ st.write(f"القيم المفقودة: {self.data.isna().sum().sum()}")
+
+ with col2:
+ st.write(f"أنواع البيانات:")
+ st.write(self.data.dtypes)
+
+ # عرض إحصاءات وصفية
+ st.subheader("إحصاءات وصفية")
+ st.dataframe(self.data.describe())
+
+ # عرض معلومات عن الأعمدة
+ st.subheader("معلومات الأعمدة")
+
+ selected_column = st.selectbox("اختر عمودًا لتحليله:", self.data.columns)
+
+ if selected_column:
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.write(f"نوع البيانات: {self.data[selected_column].dtype}")
+ st.write(f"القيم الفريدة: {self.data[selected_column].nunique()}")
+ st.write(f"القيم المفقودة: {self.data[selected_column].isna().sum()}")
+
+ with col2:
+ if pd.api.types.is_numeric_dtype(self.data[selected_column]):
+ st.write(f"الحد الأدنى: {self.data[selected_column].min()}")
+ st.write(f"الحد الأقصى: {self.data[selected_column].max()}")
+ st.write(f"المتوسط: {self.data[selected_column].mean()}")
+ st.write(f"الوسيط: {self.data[selected_column].median()}")
+ else:
+ st.write("القيم الأكثر تكرارًا:")
+ st.write(self.data[selected_column].value_counts().head())
+
+ # عرض رسم بياني للعمود المحدد
+ st.subheader(f"رسم بياني لـ {selected_column}")
+
+ if pd.api.types.is_numeric_dtype(self.data[selected_column]):
+ fig = px.histogram(self.data, x=selected_column, title=f"توزيع {selected_column}")
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ # الكود المعدل لحل مشكلة الرسم البياني
+ value_counts_df = self.data[selected_column].value_counts().reset_index()
+ value_counts_df.columns = ['القيمة', 'العدد'] # تسمية الأعمدة بأسماء واضحة
+ fig = px.bar(value_counts_df, x='القيمة', y='العدد', title=f"توزيع {selected_column}")
+ fig.update_layout(xaxis_title="القيمة", yaxis_title="العدد")
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _advanced_analysis_tab(self):
+ """علامة تبويب التحليل المتقدم"""
+ st.header("تحليل متقدم")
+
+ if self.data is None:
+ st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
+ return
+
+ # أنواع التحليل المتقدم
+ analysis_type = st.selectbox(
+ "اختر نوع التحليل:",
+ ["تحليل الارتباط", "تحليل الاتجاهات", "تحليل المجموعات", "تحليل التباين"]
+ )
+
+ if analysis_type == "تحليل الارتباط":
+ st.subheader("تحليل الارتباط")
+
+ # اختيار الأعمدة الرقمية فقط
+ numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
+
+ if len(numeric_columns) < 2:
+ st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإجراء تحليل الارتباط")
+ return
+
+ # حساب مصفوفة الارتباط
+ correlation_matrix = self.data[numeric_columns].corr()
+
+ # عرض مصفوفة الارتباط
+ st.write("مصفوفة الارتباط:")
+ st.dataframe(correlation_matrix)
+
+ # رسم خريطة حرارية للارتباط
+ st.write("خريطة حرارية للارتباط:")
+ fig = px.imshow(correlation_matrix, text_auto=True, aspect="auto",
+ title="خريطة حرارية لمصفوفة الارتباط")
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل الارتباط بين عمودين محددين
+ st.subheader("تحليل الارتباط بين عمودين محددين")
+
+ col1 = st.selectbox("اختر العمود الأول:", numeric_columns, key="corr_col1")
+ col2 = st.selectbox("اختر العمود الثاني:", numeric_columns, key="corr_col2")
+
+ if col1 != col2:
+ # حساب معامل الارتباط
+ correlation = self.data[col1].corr(self.data[col2])
+
+ st.write(f"معامل الارتباط بين {col1} و {col2}: {correlation:.4f}")
+
+ # رسم مخطط التشتت
+ fig = px.scatter(self.data, x=col1, y=col2, title=f"مخطط التشتت: {col1} مقابل {col2}")
+ fig.update_layout(xaxis_title=col1, yaxis_title=col2)
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.warning("الرجاء اختيار عمودين مختلفين")
+
+ elif analysis_type == "تحليل الاتجاهات":
+ st.subheader("تحليل الاتجاهات")
+ st.info("هذه الميزة قيد التطوير")
+
+ elif analysis_type == "تحليل المجموعات":
+ st.subheader("تحليل المجموعات")
+ st.info("هذه الميزة قيد التطوير")
+
+ elif analysis_type == "تحليل التباين":
+ st.subheader("تحليل التباين")
+ st.info("هذه الميزة قيد التطوير")
+
+ def _visualization_tab(self):
+ """علامة تبويب التصور المرئي"""
+ st.header("التصور المرئي")
+
+ if self.data is None:
+ st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
+ return
+
+ # أنواع الرسوم البيانية
+ chart_type = st.selectbox(
+ "اختر نوع الرسم البياني:",
+ ["مخطط شريطي", "مخطط خطي", "مخطط دائري", "مخطط تشتت", "مخطط صندوقي", "مخطط حراري"]
+ )
+
+ # اختيار الأعمدة حسب نوع الرسم البياني
+ if chart_type == "مخطط شريطي":
+ st.subheader("مخطط شريطي")
+
+ x_column = st.selectbox("اختر عمود المحور الأفقي (x):", self.data.columns, key="bar_x")
+ y_column = st.selectbox("اختر عمود المحور الرأسي (y):",
+ self.data.select_dtypes(include=['number']).columns.tolist(),
+ key="bar_y")
+
+ # خيارات إضافية
+ color_column = st.selectbox("اختر عمود اللون (اختياري):",
+ ["لا يوجد"] + self.data.columns.tolist(),
+ key="bar_color")
+
+ # إنشاء الرسم البياني
+ if color_column == "لا يوجد":
+ fig = px.bar(self.data, x=x_column, y=y_column, title=f"{y_column} حسب {x_column}")
+ else:
+ fig = px.bar(self.data, x=x_column, y=y_column, color=color_column,
+ title=f"{y_column} حسب {x_column} (مصنف حسب {color_column})")
+
+ fig.update_layout(xaxis_title=x_column, yaxis_title=y_column)
+ st.plotly_chart(fig, use_container_width=True)
+
+ elif chart_type == "مخطط خطي":
+ st.subheader("مخطط خطي")
+
+ x_column = st.selectbox("اختر عمود المحور الأفقي (x):", self.data.columns, key="line_x")
+ y_columns = st.multiselect("اختر أعمدة المحور الرأسي (y):",
+ self.data.select_dtypes(include=['number']).columns.tolist(),
+ key="line_y")
+
+ if y_columns:
+ # إنشاء الرسم البياني
+ fig = go.Figure()
+
+ for y_column in y_columns:
+ fig.add_trace(go.Scatter(x=self.data[x_column], y=self.data[y_column],
+ mode='lines+markers', name=y_column))
+
+ fig.update_layout(title=f"مخطط خطي", xaxis_title=x_column, yaxis_title="القيمة")
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.warning("الرجاء اختيار عمود واحد على الأقل للمحور الرأسي")
+
+ elif chart_type == "مخطط دائري":
+ st.subheader("مخطط دائري")
+
+ column = st.selectbox("اختر العمود:", self.data.columns, key="pie_column")
+
+ # إنشاء الرسم البياني
+ # تعديل لحل مشكلة مماثلة في مخطط دائري
+ value_counts_df = self.data[column].value_counts().reset_index()
+ value_counts_df.columns = ['القيمة', 'العدد']
+ fig = px.pie(value_counts_df, names='القيمة', values='العدد', title=f"توزيع {column}")
+ st.plotly_chart(fig, use_container_width=True)
+
+ elif chart_type == "مخطط تشتت":
+ st.subheader("مخطط تشتت")
+
+ numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
+
+ if len(numeric_columns) < 2:
+ st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإنشاء مخطط تشتت")
+ return
+
+ x_column = st.selectbox("اختر عمود المحور الأفقي (x):", numeric_columns, key="scatter_x")
+ y_column = st.selectbox("اختر عمود المحور الرأسي (y):", numeric_columns, key="scatter_y")
+
+ # خيارات إضافية
+ color_column = st.selectbox("اختر عمود اللون (اختياري):",
+ ["لا يوجد"] + self.data.columns.tolist(),
+ key="scatter_color")
+
+ size_column = st.selectbox("اختر عمود الحجم (اختياري):",
+ ["لا يوجد"] + numeric_columns,
+ key="scatter_size")
+
+ # إنشاء الرسم البياني
+ if color_column == "لا يوجد" and size_column == "لا يوجد":
+ fig = px.scatter(self.data, x=x_column, y=y_column,
+ title=f"{y_column} مقابل {x_column}")
+ elif color_column != "لا يوجد" and size_column == "لا يوجد":
+ fig = px.scatter(self.data, x=x_column, y=y_column, color=color_column,
+ title=f"{y_column} مقابل {x_column} (مصنف حسب {color_column})")
+ elif color_column == "لا يوجد" and size_column != "لا يوجد":
+ fig = px.scatter(self.data, x=x_column, y=y_column, size=size_column,
+ title=f"{y_column} مقابل {x_column} (الحجم حسب {size_column})")
+ else:
+ fig = px.scatter(self.data, x=x_column, y=y_column, color=color_column, size=size_column,
+ title=f"{y_column} مقابل {x_column} (مصنف حسب {color_column}, الحجم حسب {size_column})")
+
+ fig.update_layout(xaxis_title=x_column, yaxis_title=y_column)
+ st.plotly_chart(fig, use_container_width=True)
+
+ elif chart_type == "مخطط صندوقي":
+ st.subheader("مخطط صندوقي")
+
+ numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
+
+ if not numeric_columns:
+ st.warning("يجب أن يكون هناك عمود رقمي واحد على الأقل لإنشاء مخطط صندوقي")
+ return
+
+ y_column = st.selectbox("اختر عمود القيمة:", numeric_columns, key="box_y")
+
+ # خيارات إضافية
+ x_column = st.selectbox("اختر عمود التصنيف (اختياري):",
+ ["لا يوجد"] + self.data.columns.tolist(),
+ key="box_x")
+
+ # إنشاء الرسم البياني
+ if x_column == "لا يوجد":
+ fig = px.box(self.data, y=y_column, title=f"مخطط صندوقي لـ {y_column}")
+ else:
+ fig = px.box(self.data, x=x_column, y=y_column,
+ title=f"مخطط صندوقي لـ {y_column} حسب {x_column}")
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ elif chart_type == "مخطط حراري":
+ st.subheader("مخطط حراري")
+
+ numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
+
+ if len(numeric_columns) < 2:
+ st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإنشاء مخطط حراري")
+ return
+
+ # اختيار الأعمدة للمخطط الحراري
+ selected_columns = st.multiselect("اختر الأعمدة للمخطط الحراري:",
+ numeric_columns,
+ default=numeric_columns[:5] if len(numeric_columns) > 5 else numeric_columns)
+
+ if selected_columns:
+ # حساب مصفوفة الارتباط
+ correlation_matrix = self.data[selected_columns].corr()
+
+ # إنشاء الرسم البياني
+ fig = px.imshow(correlation_matrix, text_auto=True, aspect="auto",
+ title="مخطط حراري لمصفوفة الارتباط")
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.warning("الرجاء اختيار عمود واحد على الأقل")
+
+ def _reports_tab(self):
+ """علامة تبويب التقارير"""
+ st.header("التقارير")
+
+ if self.data is None:
+ st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
+ return
+
+ st.subheader("إنشاء تقرير")
+
+ # خيارات التقرير
+ report_type = st.selectbox(
+ "اختر نوع التقرير:",
+ ["تقرير ملخص", "تقرير تحليلي", "تقرير مقارنة"]
+ )
+
+ if report_type == "تقرير ملخص":
+ st.write("محتوى التقرير:")
+
+ # إنشاء ملخص للبيانات
+ st.write("### ملخص البيانات")
+ st.write(f"عدد الصفوف: {self.data.shape[0]}")
+ st.write(f"عدد الأعمدة: {self.data.shape[1]}")
+
+ # إحصاءات وصفية
+ st.write("### إحصاءات وصفية")
+ st.dataframe(self.data.describe())
+
+ # معلومات عن القيم المفقودة
+ st.write("### القيم المفقودة")
+ missing_data = pd.DataFrame({
+ 'العمود': self.data.columns,
+ 'عدد القيم المفقودة': self.data.isna().sum().values,
+ 'نسبة القيم المفقودة (%)': (self.data.isna().sum().values / len(self.data) * 100).round(2)
+ })
+ st.dataframe(missing_data)
+
+ # توزيع البيانات الرقمية
+ st.write("### توزيع البيانات الرقمية")
+ numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
+
+ if numeric_columns:
+ for i in range(0, len(numeric_columns), 2):
+ cols = st.columns(2)
+ for j in range(2):
+ if i + j < len(numeric_columns):
+ col = numeric_columns[i + j]
+ with cols[j]:
+ fig = px.histogram(self.data, x=col, title=f"توزيع {col}")
+ st.plotly_chart(fig, use_container_width=True)
+
+ # خيارات تصدير التقرير
+ st.subheader("تصدير التقرير")
+ export_format = st.radio("اختر صيغة التصدير:", ["PDF", "Excel", "HTML"])
+
+ if st.button("تصدير التقرير"):
+ st.success(f"تم تصدير التقرير بصيغة {export_format} بنجاح!")
+
+ elif report_type == "تقرير تحليلي":
+ st.info("هذه الميزة قيد التطوير")
+
+ elif report_type == "تقرير مقارنة":
+ st.info("هذه الميزة قيد التطوير")
+
+ def _create_sample_data(self):
+ """إنشاء بيانات نموذجية للمناقصات"""
+ # إنشاء تواريخ عشوائية
+ start_date = datetime(2023, 1, 1)
+ end_date = datetime(2025, 3, 31)
+ days = (end_date - start_date).days
+
+ # إنشاء بيانات نموذجية
+ data = {
+ 'رقم المناقصة': [f'T-{i:04d}' for i in range(1, 101)],
+ 'اسم المشروع': [f'مشروع {i}' for i in range(1, 101)],
+ 'نوع المشروع': np.random.choice(['بناء', 'صيانة', 'تطوير', 'توريد', 'خدمات'], 100),
+ 'الموقع': np.random.choice(['الرياض', 'جدة', 'الدمام', 'مكة', 'المدينة', 'تبوك', 'أبها'], 100),
+ 'تاريخ الإعلان': [start_date + pd.Timedelta(days=np.random.randint(0, days)) for _ in range(100)],
+ 'تاريخ الإغلاق': [start_date + pd.Timedelta(days=np.random.randint(30, days)) for _ in range(100)],
+ 'الميزانية التقديرية': np.random.uniform(1000000, 50000000, 100),
+ 'عدد المتقدمين': np.random.randint(1, 20, 100),
+ 'سعر العرض': np.random.uniform(900000, 55000000, 100),
+ 'نسبة الفوز (%)': np.random.uniform(0, 100, 100),
+ 'مدة التنفيذ (أشهر)': np.random.randint(3, 36, 100),
+ 'عدد العمال': np.random.randint(10, 500, 100),
+ 'تكلفة المواد': np.random.uniform(500000, 30000000, 100),
+ 'تكلفة العمالة': np.random.uniform(200000, 15000000, 100),
+ 'تكلفة المعدات': np.random.uniform(100000, 10000000, 100),
+ 'هامش الربح (%)': np.random.uniform(5, 25, 100),
+ 'درجة المخاطرة': np.random.choice(['منخفضة', 'متوسطة', 'عالية'], 100),
+ 'حالة المناقصة': np.random.choice(['جارية', 'مغلقة', 'ملغاة', 'فائزة', 'خاسرة'], 100)
+ }
+
+ # إنشاء DataFrame
+ df = pd.DataFrame(data)
+
+ # إضافة بعض العلاقات المنطقية
+ df['إجمالي التكلفة'] = df['تكلفة المواد'] + df['تكلفة العمالة'] + df['تكلفة المعدات']
+ df['الربح المتوقع'] = df['سعر العرض'] - df['إجمالي التكلفة']
+ df['نسبة التكلفة من العرض (%)'] = (df['إجمالي التكلفة'] / df['سعر العرض'] * 100).round(2)
+
+ return df
diff --git a/modules/document_analysis/analyzer.py b/modules/document_analysis/analyzer.py
index d8c7cc50380e00e391877451bf35983f08505b77..5127da34cc7e3bdd01adfc8644c6c2fd36430b87 100644
--- a/modules/document_analysis/analyzer.py
+++ b/modules/document_analysis/analyzer.py
@@ -1,281 +1,281 @@
-"""
-وحدة تحليل المستندات لنظام إدارة المناقصات - Hybrid Face
-"""
-
-import os
-import re
-import logging
-import threading
-from pathlib import Path
-import datetime
-import json
-
-# تهيئة السجل
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger('document_analysis')
-
-class DocumentAnalyzer:
- """فئة تحليل المستندات"""
-
- def __init__(self, config=None):
- """تهيئة محلل المستندات"""
- self.config = config
- self.analysis_in_progress = False
- self.current_document = None
- self.analysis_results = {}
-
- # إنشاء مجلد المستندات إذا لم يكن موجوداً
- if config and hasattr(config, 'DOCUMENTS_PATH'):
- self.documents_path = Path(config.DOCUMENTS_PATH)
- else:
- self.documents_path = Path('data/documents')
-
- if not self.documents_path.exists():
- self.documents_path.mkdir(parents=True, exist_ok=True)
-
- def analyze_document(self, document_path, document_type="tender", callback=None):
- """تحليل مستند"""
- if self.analysis_in_progress:
- logger.warning("هناك عملية تحليل جارية بالفعل")
- return False
-
- if not os.path.exists(document_path):
- logger.error(f"المستند غير موجود: {document_path}")
- return False
-
- self.analysis_in_progress = True
- self.current_document = document_path
- self.analysis_results = {
- "document_path": document_path,
- "document_type": document_type,
- "analysis_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- "status": "جاري التحليل",
- "items": [],
- "entities": [],
- "dates": [],
- "amounts": [],
- "risks": []
- }
-
- # بدء التحليل في خيط منفصل
- thread = threading.Thread(
- target=self._analyze_document_thread,
- args=(document_path, document_type, callback)
- )
- thread.daemon = True
- thread.start()
-
- return True
-
- def _analyze_document_thread(self, document_path, document_type, callback):
- """خيط تحليل المستند"""
- try:
- # تحديد نوع المستند
- file_extension = os.path.splitext(document_path)[1].lower()
-
- if file_extension == '.pdf':
- self._analyze_pdf(document_path, document_type)
- elif file_extension == '.docx':
- self._analyze_docx(document_path, document_type)
- elif file_extension == '.xlsx':
- self._analyze_xlsx(document_path, document_type)
- elif file_extension == '.txt':
- self._analyze_txt(document_path, document_type)
- else:
- logger.error(f"نوع المستند غير مدعوم: {file_extension}")
- self.analysis_results["status"] = "فشل التحليل"
- self.analysis_results["error"] = "نوع المستند غير مدعوم"
-
- # تحديث حالة التحليل
- if self.analysis_results["status"] != "فشل التحليل":
- self.analysis_results["status"] = "اكتمل التحليل"
- self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-
- logger.info(f"اكتمل تحليل المستند: {document_path}")
-
- except Exception as e:
- logger.error(f"خطأ في تحليل المستند: {str(e)}")
- self.analysis_results["status"] = "فشل التحليل"
- self.analysis_results["error"] = str(e)
-
- finally:
- self.analysis_in_progress = False
-
- # استدعاء دالة الاستجابة إذا تم توفيرها
- if callback and callable(callback):
- callback(self.analysis_results)
-
- def _analyze_pdf(self, document_path, document_type):
- """تحليل مستند PDF"""
- try:
- # محاكاة تحليل مستند PDF
- logger.info(f"تحليل مستند PDF: {document_path}")
-
- # في التطبيق الفعلي، سيتم استخدام مكتبة مثل PyPDF2 أو pdfplumber
- # لاستخراج النص من ملف PDF وتحليله
-
- # محاكاة استخراج البنود
- self.analysis_results["items"] = [
- {"id": 1, "name": "أعمال الحفر", "description": "حفر وإزالة التربة", "unit": "م³", "estimated_quantity": 1500},
- {"id": 2, "name": "أعمال الخرسانة", "description": "صب خرسانة مسلحة", "unit": "م³", "estimated_quantity": 750},
- {"id": 3, "name": "أعمال الأسفلت", "description": "تمهيد وفرش طبقة أسفلت", "unit": "م²", "estimated_quantity": 5000}
- ]
-
- # محاكاة استخراج الكيانات
- self.analysis_results["entities"] = [
- {"type": "client", "name": "وزارة النقل", "mentions": 5},
- {"type": "location", "name": "المنطقة الشرقية", "mentions": 3},
- {"type": "contractor", "name": "شركة المقاولات المتحدة", "mentions": 2}
- ]
-
- # محاكاة استخراج التواريخ
- self.analysis_results["dates"] = [
- {"type": "start_date", "date": "2025-05-01", "description": "تاريخ بدء المشروع"},
- {"type": "end_date", "date": "2025-11-30", "description": "تاريخ انتهاء المشروع"},
- {"type": "submission_date", "date": "2025-04-15", "description": "تاريخ تقديم العروض"}
- ]
-
- # محاكاة استخراج المبالغ
- self.analysis_results["amounts"] = [
- {"type": "estimated_cost", "amount": 5000000, "currency": "SAR", "description": "التكلفة التقديرية للمشروع"},
- {"type": "advance_payment", "amount": 500000, "currency": "SAR", "description": "الدفعة المقدمة (10%)"},
- {"type": "performance_bond", "amount": 250000, "currency": "SAR", "description": "ضمان حسن التنفيذ (5%)"}
- ]
-
- # محاكاة استخراج المخاطر
- self.analysis_results["risks"] = [
- {"type": "delay_risk", "description": "مخاطر التأخير في التنفيذ", "probability": "متوسط", "impact": "عالي"},
- {"type": "cost_risk", "description": "مخاطر زيادة التكاليف", "probability": "عالي", "impact": "عالي"},
- {"type": "quality_risk", "description": "مخاطر جودة التنفيذ", "probability": "منخفض", "impact": "متوسط"}
- ]
-
- except Exception as e:
- logger.error(f"خطأ في تحليل مستند PDF: {str(e)}")
- raise
-
- def _analyze_docx(self, document_path, document_type):
- """تحليل مستند Word"""
- try:
- # محاكاة تحليل مستند Word
- logger.info(f"تحليل مستند Word: {document_path}")
-
- # في التطبيق الفعلي، سيتم استخدام مكتبة مثل python-docx
- # لاستخراج النص من ملف Word وتحليله
-
- # محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
- # (مشابه لتحليل PDF)
- self.analysis_results["items"] = [
- {"id": 1, "name": "توريد معدات", "description": "توريد معدات المشروع", "unit": "مجموعة", "estimated_quantity": 10},
- {"id": 2, "name": "تركيب المعدات", "description": "تركيب وتشغيل المعدات", "unit": "مجموعة", "estimated_quantity": 10},
- {"id": 3, "name": "التدريب", "description": "تدريب الموظفين على استخدام المعدات", "unit": "يوم", "estimated_quantity": 20}
- ]
-
- # محاكاة استخراج الكيانات والتواريخ والمبالغ والمخاطر
- # (مشابه لتحليل PDF)
-
- except Exception as e:
- logger.error(f"خطأ في تحليل مستند Word: {str(e)}")
- raise
-
- def _analyze_xlsx(self, document_path, document_type):
- """تحليل مستند Excel"""
- try:
- # محاكاة تحليل مستند Excel
- logger.info(f"تحليل مستند Excel: {document_path}")
-
- # في التطبيق الفعلي، سيتم استخدام مكتبة مثل pandas أو openpyxl
- # لاستخراج البيانات من ملف Excel وتحليلها
-
- # محاكاة استخراج البنود
- self.analysis_results["items"] = [
- {"id": 1, "name": "بند 1", "description": "وصف البند 1", "unit": "وحدة", "estimated_quantity": 100},
- {"id": 2, "name": "بند 2", "description": "وصف البند 2", "unit": "وحدة", "estimated_quantity": 200},
- {"id": 3, "name": "بند 3", "description": "وصف البند 3", "unit": "وحدة", "estimated_quantity": 300}
- ]
-
- # محاكاة استخراج المبالغ
- self.analysis_results["amounts"] = [
- {"type": "item_cost", "amount": 10000, "currency": "SAR", "description": "تكلفة البند 1"},
- {"type": "item_cost", "amount": 20000, "currency": "SAR", "description": "تكلفة البند 2"},
- {"type": "item_cost", "amount": 30000, "currency": "SAR", "description": "تكلفة البند 3"}
- ]
-
- except Exception as e:
- logger.error(f"خطأ في تحليل مستند Excel: {str(e)}")
- raise
-
- def _analyze_txt(self, document_path, document_type):
- """تحليل مستند نصي"""
- try:
- # محاكاة تحليل مستند نصي
- logger.info(f"تحليل مستند نصي: {document_path}")
-
- # في التطبيق الفعلي، سيتم قراءة الملف النصي وتحليله
-
- # محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
- # (مشابه للتحليلات الأخرى)
-
- except Exception as e:
- logger.error(f"خطأ في تحليل مستند نصي: {str(e)}")
- raise
-
- def get_analysis_status(self):
- """الحصول على حالة التحليل الحالي"""
- if not self.analysis_in_progress:
- if not self.analysis_results:
- return {"status": "لا يوجد تحليل جارٍ"}
- else:
- return {"status": self.analysis_results.get("status", "غير معروف")}
-
- return {
- "status": "جاري التحليل",
- "document_path": self.current_document,
- "start_time": self.analysis_results.get("analysis_start_time")
- }
-
- def get_analysis_results(self):
- """الحصول على نتائج التحليل"""
- return self.analysis_results
-
- def export_analysis_results(self, output_path=None):
- """تصدير نتائج التحليل إلى ملف JSON"""
- if not self.analysis_results:
- logger.warning("لا توجد نتائج تحليل للتصدير")
- return None
-
- if not output_path:
- # إنشاء اسم ملف افتراضي
- timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
- filename = f"analysis_results_{timestamp}.json"
- output_path = os.path.join(self.documents_path, filename)
-
- try:
- with open(output_path, 'w', encoding='utf-8') as f:
- json.dump(self.analysis_results, f, ensure_ascii=False, indent=4)
-
- logger.info(f"تم تصدير نتائج التحليل إلى: {output_path}")
- return output_path
-
- except Exception as e:
- logger.error(f"خطأ في تصدير نتائج التحليل: {str(e)}")
- return None
-
- def import_analysis_results(self, input_path):
- """استيراد نتائج التحليل من ملف JSON"""
- if not os.path.exists(input_path):
- logger.error(f"ملف نتائج التحليل غير موجود: {input_path}")
- return False
-
- try:
- with open(input_path, 'r', encoding='utf-8') as f:
- self.analysis_results = json.load(f)
-
- logger.info(f"تم استيراد نتائج التحليل من: {input_path}")
- return True
-
- except Exception as e:
- logger.error(f"خطأ في استيراد نتائج التحليل: {str(e)}")
- return False
+"""
+وحدة تحليل المستندات لنظام إدارة المناقصات - Hybrid Face
+"""
+
+import os
+import re
+import logging
+import threading
+from pathlib import Path
+import datetime
+import json
+
+# تهيئة السجل
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger('document_analysis')
+
+class DocumentAnalyzer:
+ """فئة تحليل المستندات"""
+
+ def __init__(self, config=None):
+ """تهيئة محلل المستندات"""
+ self.config = config
+ self.analysis_in_progress = False
+ self.current_document = None
+ self.analysis_results = {}
+
+ # إنشاء مجلد المستندات إذا لم يكن موجوداً
+ if config and hasattr(config, 'DOCUMENTS_PATH'):
+ self.documents_path = Path(config.DOCUMENTS_PATH)
+ else:
+ self.documents_path = Path('data/documents')
+
+ if not self.documents_path.exists():
+ self.documents_path.mkdir(parents=True, exist_ok=True)
+
+ def analyze_document(self, document_path, document_type="tender", callback=None):
+ """تحليل مستند"""
+ if self.analysis_in_progress:
+ logger.warning("هناك عملية تحليل جارية بالفعل")
+ return False
+
+ if not os.path.exists(document_path):
+ logger.error(f"المستند غير موجود: {document_path}")
+ return False
+
+ self.analysis_in_progress = True
+ self.current_document = document_path
+ self.analysis_results = {
+ "document_path": document_path,
+ "document_type": document_type,
+ "analysis_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ "status": "جاري التحليل",
+ "items": [],
+ "entities": [],
+ "dates": [],
+ "amounts": [],
+ "risks": []
+ }
+
+ # بدء التحليل في خيط منفصل
+ thread = threading.Thread(
+ target=self._analyze_document_thread,
+ args=(document_path, document_type, callback)
+ )
+ thread.daemon = True
+ thread.start()
+
+ return True
+
+ def _analyze_document_thread(self, document_path, document_type, callback):
+ """خيط تحليل المستند"""
+ try:
+ # تحديد نوع المستند
+ file_extension = os.path.splitext(document_path)[1].lower()
+
+ if file_extension == '.pdf':
+ self._analyze_pdf(document_path, document_type)
+ elif file_extension == '.docx':
+ self._analyze_docx(document_path, document_type)
+ elif file_extension == '.xlsx':
+ self._analyze_xlsx(document_path, document_type)
+ elif file_extension == '.txt':
+ self._analyze_txt(document_path, document_type)
+ else:
+ logger.error(f"نوع المستند غير مدعوم: {file_extension}")
+ self.analysis_results["status"] = "فشل التحليل"
+ self.analysis_results["error"] = "نوع المستند غير مدعوم"
+
+ # تحديث حالة التحليل
+ if self.analysis_results["status"] != "فشل التحليل":
+ self.analysis_results["status"] = "اكتمل التحليل"
+ self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+
+ logger.info(f"اكتمل تحليل المستند: {document_path}")
+
+ except Exception as e:
+ logger.error(f"خطأ في تحليل المستند: {str(e)}")
+ self.analysis_results["status"] = "فشل التحليل"
+ self.analysis_results["error"] = str(e)
+
+ finally:
+ self.analysis_in_progress = False
+
+ # استدعاء دالة الاستجابة إذا تم توفيرها
+ if callback and callable(callback):
+ callback(self.analysis_results)
+
+ def _analyze_pdf(self, document_path, document_type):
+ """تحليل مستند PDF"""
+ try:
+ # محاكاة تحليل مستند PDF
+ logger.info(f"تحليل مستند PDF: {document_path}")
+
+ # في التطبيق الفعلي، سيتم استخدام مكتبة مثل PyPDF2 أو pdfplumber
+ # لاستخراج النص من ملف PDF وتحليله
+
+ # محاكاة استخراج البنود
+ self.analysis_results["items"] = [
+ {"id": 1, "name": "أعمال الحفر", "description": "حفر وإزالة التربة", "unit": "م³", "estimated_quantity": 1500},
+ {"id": 2, "name": "أعمال الخرسانة", "description": "صب خرسانة مسلحة", "unit": "م³", "estimated_quantity": 750},
+ {"id": 3, "name": "أعمال الأسفلت", "description": "تمهيد وفرش طبقة أسفلت", "unit": "م²", "estimated_quantity": 5000}
+ ]
+
+ # محاكاة استخراج الكيانات
+ self.analysis_results["entities"] = [
+ {"type": "client", "name": "وزارة النقل", "mentions": 5},
+ {"type": "location", "name": "المنطقة الشرقية", "mentions": 3},
+ {"type": "contractor", "name": "شركة المقاولات المتحدة", "mentions": 2}
+ ]
+
+ # محاكاة استخراج التواريخ
+ self.analysis_results["dates"] = [
+ {"type": "start_date", "date": "2025-05-01", "description": "تاريخ بدء المشروع"},
+ {"type": "end_date", "date": "2025-11-30", "description": "تاريخ انتهاء المشروع"},
+ {"type": "submission_date", "date": "2025-04-15", "description": "تاريخ تقديم العروض"}
+ ]
+
+ # محاكاة استخراج المبالغ
+ self.analysis_results["amounts"] = [
+ {"type": "estimated_cost", "amount": 5000000, "currency": "SAR", "description": "التكلفة التقديرية للمشروع"},
+ {"type": "advance_payment", "amount": 500000, "currency": "SAR", "description": "الدفعة المقدمة (10%)"},
+ {"type": "performance_bond", "amount": 250000, "currency": "SAR", "description": "ضمان حسن التنفيذ (5%)"}
+ ]
+
+ # محاكاة استخراج المخاطر
+ self.analysis_results["risks"] = [
+ {"type": "delay_risk", "description": "مخاطر التأخير في التنفيذ", "probability": "متوسط", "impact": "عالي"},
+ {"type": "cost_risk", "description": "مخاطر زيادة التكاليف", "probability": "عالي", "impact": "عالي"},
+ {"type": "quality_risk", "description": "مخاطر جودة التنفيذ", "probability": "منخفض", "impact": "متوسط"}
+ ]
+
+ except Exception as e:
+ logger.error(f"خطأ في تحليل مستند PDF: {str(e)}")
+ raise
+
+ def _analyze_docx(self, document_path, document_type):
+ """تحليل مستند Word"""
+ try:
+ # محاكاة تحليل مستند Word
+ logger.info(f"تحليل مستند Word: {document_path}")
+
+ # في التطبيق الفعلي، سيتم استخدام مكتبة مثل python-docx
+ # لاستخراج النص من ملف Word وتحليله
+
+ # محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
+ # (مشابه لتحليل PDF)
+ self.analysis_results["items"] = [
+ {"id": 1, "name": "توريد معدات", "description": "توريد معدات المشروع", "unit": "مجموعة", "estimated_quantity": 10},
+ {"id": 2, "name": "تركيب المعدات", "description": "تركيب وتشغيل المعدات", "unit": "مجموعة", "estimated_quantity": 10},
+ {"id": 3, "name": "التدريب", "description": "تدريب الموظفين على استخدام المعدات", "unit": "يوم", "estimated_quantity": 20}
+ ]
+
+ # محاكاة استخراج الكيانات والتواريخ والمبالغ والمخاطر
+ # (مشابه لتحليل PDF)
+
+ except Exception as e:
+ logger.error(f"خطأ في تحليل مستند Word: {str(e)}")
+ raise
+
+ def _analyze_xlsx(self, document_path, document_type):
+ """تحليل مستند Excel"""
+ try:
+ # محاكاة تحليل مستند Excel
+ logger.info(f"تحليل مستند Excel: {document_path}")
+
+ # في التطبيق الفعلي، سيتم استخدام مكتبة مثل pandas أو openpyxl
+ # لاستخراج البيانات من ملف Excel وتحليلها
+
+ # محاكاة استخراج البنود
+ self.analysis_results["items"] = [
+ {"id": 1, "name": "بند 1", "description": "وصف البند 1", "unit": "وحدة", "estimated_quantity": 100},
+ {"id": 2, "name": "بند 2", "description": "وصف البند 2", "unit": "وحدة", "estimated_quantity": 200},
+ {"id": 3, "name": "بند 3", "description": "وصف البند 3", "unit": "وحدة", "estimated_quantity": 300}
+ ]
+
+ # محاكاة استخراج المبالغ
+ self.analysis_results["amounts"] = [
+ {"type": "item_cost", "amount": 10000, "currency": "SAR", "description": "تكلفة البند 1"},
+ {"type": "item_cost", "amount": 20000, "currency": "SAR", "description": "تكلفة البند 2"},
+ {"type": "item_cost", "amount": 30000, "currency": "SAR", "description": "تكلفة البند 3"}
+ ]
+
+ except Exception as e:
+ logger.error(f"خطأ في تحليل مستند Excel: {str(e)}")
+ raise
+
+ def _analyze_txt(self, document_path, document_type):
+ """تحليل مستند نصي"""
+ try:
+ # محاكاة تحليل مستند نصي
+ logger.info(f"تحليل مستند نصي: {document_path}")
+
+ # في التطبيق الفعلي، سيتم قراءة الملف النصي وتحليله
+
+ # محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
+ # (مشابه للتحليلات الأخرى)
+
+ except Exception as e:
+ logger.error(f"خطأ في تحليل مستند نصي: {str(e)}")
+ raise
+
+ def get_analysis_status(self):
+ """الحصول على حالة التحليل الحالي"""
+ if not self.analysis_in_progress:
+ if not self.analysis_results:
+ return {"status": "لا يوجد تحليل جارٍ"}
+ else:
+ return {"status": self.analysis_results.get("status", "غير معروف")}
+
+ return {
+ "status": "جاري التحليل",
+ "document_path": self.current_document,
+ "start_time": self.analysis_results.get("analysis_start_time")
+ }
+
+ def get_analysis_results(self):
+ """الحصول على نتائج التحليل"""
+ return self.analysis_results
+
+ def export_analysis_results(self, output_path=None):
+ """تصدير نتائج التحليل إلى ملف JSON"""
+ if not self.analysis_results:
+ logger.warning("لا توجد نتائج تحليل للتصدير")
+ return None
+
+ if not output_path:
+ # إنشاء اسم ملف افتراضي
+ timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
+ filename = f"analysis_results_{timestamp}.json"
+ output_path = os.path.join(self.documents_path, filename)
+
+ try:
+ with open(output_path, 'w', encoding='utf-8') as f:
+ json.dump(self.analysis_results, f, ensure_ascii=False, indent=4)
+
+ logger.info(f"تم تصدير نتائج التحليل إلى: {output_path}")
+ return output_path
+
+ except Exception as e:
+ logger.error(f"خطأ في تصدير نتائج التحليل: {str(e)}")
+ return None
+
+ def import_analysis_results(self, input_path):
+ """استيراد نتائج التحليل من ملف JSON"""
+ if not os.path.exists(input_path):
+ logger.error(f"ملف نتائج التحليل غير موجود: {input_path}")
+ return False
+
+ try:
+ with open(input_path, 'r', encoding='utf-8') as f:
+ self.analysis_results = json.load(f)
+
+ logger.info(f"تم استيراد نتائج التحليل من: {input_path}")
+ return True
+
+ except Exception as e:
+ logger.error(f"خطأ في استيراد نتائج التحليل: {str(e)}")
+ return False
diff --git a/modules/document_analysis/document_app.py b/modules/document_analysis/document_app.py
index 823c75347d64e0493d3c564a050c12b155d82837..50ca57466f5f9c714091e326d792631f86d6d8dc 100644
--- a/modules/document_analysis/document_app.py
+++ b/modules/document_analysis/document_app.py
@@ -171,13 +171,15 @@ class DataAnalysisApp:
def run(self):
"""
تشغيل وحدة تحليل البيانات
-
+
هذه الدالة هي نقطة الدخول الرئيسية لوحدة تحليل البيانات.
تقوم بتهيئة واجهة المستخدم وعرض البيانات والتحليلات.
"""
try:
- # تعيين عنوان الصفحة
- st.set_page_config(
+ # استخدام مدير التكوين لضبط إعدادات الصفحة
+ from config_manager import ConfigManager
+ config_manager = ConfigManager()
+ config_manager.set_page_config_if_needed(
page_title="وحدة تحليل البيانات - نظام المناقصات",
page_icon="📊",
layout="wide",
@@ -216,7 +218,7 @@ class DataAnalysisApp:
# عرض الشريط الجانبي
with st.sidebar:
- st.image("/home/ubuntu/tender_system/tender_system/assets/images/logo.png", width=200)
+ st.image("assets/images/logo.png", width=200)
st.markdown("## نظام تحليل المناقصات")
st.markdown("### وحدة تحليل البيانات")
@@ -287,146 +289,152 @@ class DataAnalysisApp:
st.markdown("#### مؤشرات الأداء الرئيسية")
# استخراج البيانات اللازمة للمؤشرات
-def _render_dashboard_tab(self):
- """عرض تبويب لوحة المعلومات"""
-
- st.markdown("### لوحة المعلومات")
-
- # عرض مؤشرات الأداء الرئيسية
- st.markdown("#### مؤشرات الأداء الرئيسية")
-
- # استخراج البيانات اللازمة للمؤشرات
- tenders_df = st.session_state.sample_data["tenders"]
-
- # حساب المؤشرات
- total_tenders = len(tenders_df)
- won_tenders = len(tenders_df[tenders_df["الحالة"] == "فائز"])
- win_rate = won_tenders / total_tenders * 100
- avg_profit_margin = tenders_df["هامش الربح (%)"].mean()
- total_profit = tenders_df["الربح (ريال)"].sum()
-
- # عرض المؤشرات
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- st.metric("إجمالي المناقصات", f"{total_tenders}")
-
- with col2:
- st.metric("معدل الفوز", f"{win_rate:.1f}%")
-
- with col3:
- st.metric("متوسط هامش الربح", f"{avg_profit_margin:.1f}%")
-
- with col4:
- st.metric("إجمالي الربح", f"{total_profit:,.0f} ريال")
-
- # عرض توزيع المناقصات حسب الحالة
- st.markdown("#### توزيع المناقصات حسب الحالة")
-
- status_counts = tenders_df["الحالة"].value_counts().reset_index()
- status_counts.columns = ["الحالة", "العدد"]
-
- fig = px.pie(
- status_counts,
- values="العدد",
- names="الحالة",
- title="توزيع المناقصات حسب الحالة",
- color="الحالة",
- color_discrete_map={
- "فائز": "#2ecc71",
- "خاسر": "#e74c3c",
- "قيد التنفيذ": "#3498db",
- "منجز": "#f39c12"
- }
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع المناقصات حسب نوع المشروع
- st.markdown("#### توزيع المناقصات حسب نوع المشروع")
-
- type_counts = tenders_df["نوع المشروع"].value_counts().reset_index()
- type_counts.columns = ["نوع المشروع", "العدد"]
-
- fig = px.bar(
- type_counts,
- x="نوع المشروع",
- y="العدد",
- title="توزيع المناقصات حسب نوع المشروع",
- color="نوع المشروع",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض تطور هامش الربح عبر الزمن
- st.markdown("#### تطور هامش الربح عبر الزمن")
-
- # إضافة عمود السنة
- tenders_df["السنة"] = tenders_df["تاريخ التقديم"].str[:4]
-
- # حساب متوسط هامش الربح لكل سنة
- profit_margin_by_year = tenders_df.groupby("السنة")["هامش الربح (%)"].mean().reset_index()
-
- fig = px.line(
- profit_margin_by_year,
- x="السنة",
- y="هامش الربح (%)",
- title="تطور متوسط هامش الربح عبر السنوات",
- markers=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع المناقصات حسب الموقع
- st.markdown("#### توزيع المناقصات حسب الموقع")
-
- location_counts = tenders_df["الموقع"].value_counts().reset_index()
- location_counts.columns = ["الموقع", "العدد"]
-
- fig = px.bar(
- location_counts,
- x="الموقع",
- y="العدد",
- title="توزيع المناقصات حسب الموقع",
- color="الموقع",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض العلاقة بين الميزانية والتكلفة
- st.markdown("#### العلاقة بين الميزانية والتكلفة")
-
- fig = px.scatter(
- tenders_df,
- x="الميزانية (ريال)",
- y="التكلفة (ريال)",
- color="الحالة",
- size="المساحة (م2)",
- hover_name="رقم المناقصة",
- hover_data=["نوع المشروع", "الموقع", "هامش الربح (%)"],
- title="العلاقة بين الميزانية والتكلفة",
- color_discrete_map={
- "فائز": "#2ecc71",
- "خاسر": "#e74c3c",
- "قيد التنفيذ": "#3498db",
- "منجز": "#f39c12"
- }
- )
-
- # إضافة خط الميزانية = التكلفة
- max_value = max(tenders_df["الميزانية (ريال)"].max(), tenders_df["التكلفة (ريال)"].max())
- fig.add_trace(
- go.Scatter(
- x=[0, max_value],
- y=[0, max_value],
- mode="lines",
- line=dict(color="gray", dash="dash"),
- name="الميزانية = التكلفة"
+ tenders_df = st.session_state.sample_data["tenders"]
+
+ # حساب المؤشرات
+ total_tenders = len(tenders_df)
+ won_tenders = len(tenders_df[tenders_df["الحالة"] == "فائز"])
+ win_rate = won_tenders / total_tenders * 100
+ avg_profit_margin = tenders_df["هامش الربح (%)"].mean()
+ total_profit = tenders_df["الربح (ريال)"].sum()
+
+ # عرض المؤشرات
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ st.metric("إجمالي المناقصات", f"{total_tenders}")
+
+ with col2:
+ st.metric("معدل الفوز", f"{win_rate:.1f}%")
+
+ with col3:
+ st.metric("متوسط هامش الربح", f"{avg_profit_margin:.1f}%")
+
+ with col4:
+ st.metric("إجمالي الربح", f"{total_profit:,.0f} ريال")
+
+ # عرض توزيع المناقصات حسب الحالة
+ st.markdown("#### توزيع المناقصات حسب الحالة")
+
+ status_counts = tenders_df["الحالة"].value_counts().reset_index()
+ status_counts.columns = ["الحالة", "العدد"]
+
+ fig = px.pie(
+ status_counts,
+ values="العدد",
+ names="الحالة",
+ title="توزيع المناقصات حسب الحالة",
+ color="الحالة",
+ color_discrete_map={
+ "فائز": "#2ecc71",
+ "خاسر": "#e74c3c",
+ "قيد التنفيذ": "#3498db",
+ "منجز": "#f39c12"
+ }
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض توزيع المناقصات حسب نوع المشروع
+ st.markdown("#### توزيع المناقصات حسب نوع المشروع")
+
+ type_counts = tenders_df["نوع المشروع"].value_counts().reset_index()
+ type_counts.columns = ["نوع المشروع", "العدد"]
+
+ fig = px.bar(
+ type_counts,
+ x="نوع المشروع",
+ y="العدد",
+ title="توزيع المناقصات حسب نوع المشروع",
+ color="نوع المشروع",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض تطور هامش الربح عبر الزمن
+ st.markdown("#### تطور هامش الربح عبر الزمن")
+
+ # إضافة عمود السنة
+ tenders_df["السنة"] = tenders_df["تاريخ التقديم"].str[:4]
+
+ # حساب متوسط هامش الربح لكل سنة
+ profit_margin_by_year = tenders_df.groupby("السنة")["هامش الربح (%)"].mean().reset_index()
+
+ fig = px.line(
+ profit_margin_by_year,
+ x="السنة",
+ y="هامش الربح (%)",
+ title="تطور متوسط هامش الربح عبر السنوات",
+ markers=True
)
- )
- st.plotly_chart(fig, use_container_width=True)
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض توزيع المناقصات حسب الموقع
+ st.markdown("#### توزيع المناقصات حسب الموقع")
+
+ location_counts = tenders_df["الموقع"].value_counts().reset_index()
+ location_counts.columns = ["الموقع", "العدد"]
+
+ fig = px.bar(
+ location_counts,
+ x="الموقع",
+ y="العدد",
+ title="توزيع المناقصات حسب الموقع",
+ color="الموقع",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض العلاقة بين الميزانية والتكلفة
+ st.markdown("#### العلاقة بين الميزانية والتكلفة")
+
+ fig = px.scatter(
+ tenders_df,
+ x="الميزانية (ريال)",
+ y="التكلفة (ريال)",
+ color="الحالة",
+ size="المساحة (م2)",
+ hover_name="رقم المناقصة",
+ hover_data=["نوع المشروع", "الموقع", "هامش الربح (%)"],
+ title="العلاقة بين الميزانية والتكلفة",
+ color_discrete_map={
+ "فائز": "#2ecc71",
+ "خاسر": "#e74c3c",
+ "قيد التنفيذ": "#3498db",
+ "منجز": "#f39c12"
+ }
+ )
+
+ # إضافة خط الميزانية = التكلفة
+ max_value = max(tenders_df["الميزانية (ريال)"].max(), tenders_df["التكلفة (ريال)"].max())
+ fig.add_trace(
+ go.Scatter(
+ x=[0, max_value],
+ y=[0, max_value],
+ mode="lines",
+ line=dict(color="gray", dash="dash"),
+ name="الميزانية = التكلفة"
+ )
+ )
+ st.plotly_chart(fig, use_container_width=True)
-DocumentAnalysisApp = DataAnalysisApp
+ def _render_tenders_analysis_tab(self):
+ """عرض تبويب تحليل المناقصات"""
+ st.markdown("### تحليل المناقصات")
+
+ def _render_price_analysis_tab(self):
+ """عرض تبويب تحليل الأسعار"""
+ st.markdown("### تحليل الأسعار")
+
+ def _render_competitors_analysis_tab(self):
+ """عرض تبويب تحليل المنافسين"""
+ st.markdown("### تحليل المنافسين")
+
+ def _render_import_export_tab(self):
+ """عرض تبويب استيراد وتصدير البيانات"""
+ st.markdown("### استيراد وتصدير البيانات")
+DocumentAnalysisApp = DataAnalysisApp
\ No newline at end of file
diff --git a/modules/document_comparison/document_comparison_app.py b/modules/document_comparison/document_comparison_app.py
index 9921479e4b89392338e1c3486f5dd5e1f99a6a24..a5b997fb738828e4a9adbef445da58354a2ae732 100644
--- a/modules/document_comparison/document_comparison_app.py
+++ b/modules/document_comparison/document_comparison_app.py
@@ -1,1003 +1,1003 @@
-"""
-وحدة مقارنة المستندات - نظام تحليل المناقصات
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import os
-import sys
-from pathlib import Path
-import difflib
-import re
-import datetime
-
-# إضافة مسار المشروع للنظام
-sys.path.append(str(Path(__file__).parent.parent))
-
-# استيراد محسن واجهة المستخدم
-from styling.enhanced_ui import UIEnhancer
-
-class DocumentComparisonApp:
- """تطبيق مقارنة المستندات"""
-
- def __init__(self):
- """تهيئة تطبيق مقارنة المستندات"""
- self.ui = UIEnhancer(page_title="مقارنة المستندات - نظام تحليل المناقصات", page_icon="📄")
- self.ui.apply_theme_colors()
-
- # بيانات المستندات (نموذجية)
- self.documents_data = [
- {
- "id": "DOC001",
- "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
- "type": "كراسة شروط",
- "version": "1.0",
- "date": "2025-01-15",
- "size": 2.4,
- "pages": 45,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/specs_v1.pdf"
- },
- {
- "id": "DOC002",
- "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
- "type": "كراسة شروط",
- "version": "1.1",
- "date": "2025-02-10",
- "size": 2.6,
- "pages": 48,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/specs_v1.1.pdf"
- },
- {
- "id": "DOC003",
- "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
- "type": "كراسة شروط",
- "version": "2.0",
- "date": "2025-03-05",
- "size": 2.8,
- "pages": 52,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/specs_v2.0.pdf"
- },
- {
- "id": "DOC004",
- "name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
- "type": "جدول كميات",
- "version": "1.0",
- "date": "2025-01-15",
- "size": 1.2,
- "pages": 20,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/boq_v1.0.xlsx"
- },
- {
- "id": "DOC005",
- "name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
- "type": "جدول كميات",
- "version": "1.1",
- "date": "2025-02-20",
- "size": 1.3,
- "pages": 22,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/boq_v1.1.xlsx"
- },
- {
- "id": "DOC006",
- "name": "المخططات - مناقصة إنشاء مبنى إداري",
- "type": "مخططات",
- "version": "1.0",
- "date": "2025-01-15",
- "size": 15.6,
- "pages": 30,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/drawings_v1.0.pdf"
- },
- {
- "id": "DOC007",
- "name": "المخططات - مناقصة إنشاء مبنى إداري",
- "type": "مخططات",
- "version": "2.0",
- "date": "2025-03-10",
- "size": 18.2,
- "pages": 35,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/drawings_v2.0.pdf"
- },
- {
- "id": "DOC008",
- "name": "كراسة الشروط - مناقصة صيانة طرق",
- "type": "كراسة شروط",
- "version": "1.0",
- "date": "2025-02-05",
- "size": 1.8,
- "pages": 32,
- "related_entity": "T-2025-002",
- "path": "/documents/T-2025-002/specs_v1.0.pdf"
- },
- {
- "id": "DOC009",
- "name": "كراسة الشروط - مناقصة صيانة طرق",
- "type": "كراسة شروط",
- "version": "1.1",
- "date": "2025-03-15",
- "size": 1.9,
- "pages": 34,
- "related_entity": "T-2025-002",
- "path": "/documents/T-2025-002/specs_v1.1.pdf"
- },
- {
- "id": "DOC010",
- "name": "جدول الكميات - مناقصة صيانة طرق",
- "type": "جدول كميات",
- "version": "1.0",
- "date": "2025-02-05",
- "size": 0.9,
- "pages": 15,
- "related_entity": "T-2025-002",
- "path": "/documents/T-2025-002/boq_v1.0.xlsx"
- }
- ]
-
- # بيانات نموذجية لمحتوى المستندات (للعرض فقط)
- self.sample_document_content = {
- "DOC001": """
- # كراسة الشروط والمواصفات
- ## مناقصة إنشاء مبنى إداري
-
- ### 1. مقدمة
- تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
-
- ### 2. نطاق العمل
- يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع، ويشمل ذلك:
- - أعمال الهيكل الإنشائي
- - أعمال التشطيبات الداخلية والخارجية
- - أعمال الكهرباء والميكانيكا
- - أعمال تنسيق الموقع
-
- ### 3. المواصفات الفنية
- #### 3.1 أعمال الخرسانة
- - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 30 نيوتن/مم²
- - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
-
- #### 3.2 أعمال التشطيبات
- - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
- - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
-
- ### 4. الشروط العامة
- - مدة التنفيذ: 18 شهراً من تاريخ استلام الموقع
- - غرامة التأخير: 0.1% من قيمة العقد عن كل يوم تأخير
- - ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
- """,
-
- "DOC002": """
- # كراسة الشروط والمواصفات
- ## مناقصة إنشاء مبنى إداري
-
- ### 1. مقدمة
- تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
-
- ### 2. نطاق العمل
- يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5500 متر مربع، ويشمل ذلك:
- - أعمال الهيكل الإنشائي
- - أعمال التشطيبات الداخلية والخارجية
- - أعمال الكهرباء والميكانيكا
- - أعمال تنسيق الموقع
- - أعمال أنظمة الأمن والسلامة
-
- ### 3. المواصفات الفنية
- #### 3.1 أعمال الخرسانة
- - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 35 نيوتن/مم²
- - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
-
- #### 3.2 أعمال التشطيبات
- - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
- - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
- - يجب استخدام زجاج عاكس للحرارة للواجهات
-
- ### 4. الشروط العامة
- - مدة التنفيذ: 16 شهراً من تاريخ استلام الموقع
- - غرامة التأخير: 0.15% من قيمة العقد عن كل يوم تأخير
- - ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
- """,
-
- "DOC003": """
- # كراسة الشروط والمواصفات
- ## مناقصة إنشاء مبنى إداري
-
- ### 1. مقدمة
- تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض وفقاً للمواصفات المعتمدة من الهيئة السعودية للمواصفات والمقاييس.
-
- ### 2. نطاق العمل
- يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
- - أعمال الهيكل الإنشائي
- - أعمال التشطيبات الداخلية والخارجية
- - أعمال الكهرباء والميكانيكا
- - أعمال تنسيق الموقع
- - أعمال أنظمة الأمن والسلامة
- - أعمال أنظمة المباني الذكية
-
- ### 3. المواصفات الفنية
- #### 3.1 أعمال الخرسانة
- - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
- - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
- - يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
-
- #### 3.2 أعمال التشطيبات
- - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
- - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
- - يجب استخدام زجاج عاكس للحرارة للواجهات
- - يجب استخدام مواد صديقة للبيئة
-
- ### 4. الشروط العامة
- - مدة التنفيذ: 15 شهراً من تاريخ استلام الموقع
- - غرامة التأخير: 0.2% من قيمة العقد عن كل يوم تأخير
- - ضمان الأعمال: 15 سنوات للهيكل الإنشائي، 7 سنوات للأعمال الميكانيكية والكهربائية
-
- ### 5. متطلبات الاستدامة
- - يجب أن يحقق المبنى متطلبات الاستدامة وفقاً لمعايير LEED
- - يجب توفير أنظمة لترشيد استهلاك الطاقة والمياه
- """
- }
-
- def run(self):
- """تشغيل تطبيق مقارنة المستندات"""
- # إنشاء قائمة العناصر
- menu_items = [
- {"name": "لوحة المعلومات", "icon": "house"},
- {"name": "المناقصات والعقود", "icon": "file-text"},
- {"name": "تحليل المستندات", "icon": "file-earmark-text"},
- {"name": "نظام التسعير", "icon": "calculator"},
- {"name": "حاسبة تكاليف البناء", "icon": "building"},
- {"name": "الموارد والتكاليف", "icon": "people"},
- {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
- {"name": "إدارة المشاريع", "icon": "kanban"},
- {"name": "الخرائط والمواقع", "icon": "geo-alt"},
- {"name": "الجدول الزمني", "icon": "calendar3"},
- {"name": "الإشعارات", "icon": "bell"},
- {"name": "مقارنة المستندات", "icon": "files"},
- {"name": "المساعد الذكي", "icon": "robot"},
- {"name": "التقارير", "icon": "bar-chart"},
- {"name": "الإعدادات", "icon": "gear"}
- ]
-
- # إنشاء الشريط الجانبي
- selected = self.ui.create_sidebar(menu_items)
-
- # إنشاء ترويسة الصفحة
- self.ui.create_header("مقارنة المستندات", "أدوات متقدمة لمقارنة وتحليل المستندات")
-
- # إنشاء علامات تبويب للوظائف المختلفة
- tabs = st.tabs(["مقارنة الإصدارات", "مقارنة المستندات", "تحليل التغييرات", "سجل التغييرات"])
-
- # علامة تبويب مقارنة الإصدارات
- with tabs[0]:
- self.compare_versions()
-
- # علامة تبويب مقارنة المستندات
- with tabs[1]:
- self.compare_documents()
-
- # علامة تبويب تحليل التغييرات
- with tabs[2]:
- self.analyze_changes()
-
- # علامة تبويب سجل التغييرات
- with tabs[3]:
- self.show_change_history()
-
- def compare_versions(self):
- """مقارنة إصدارات المستندات"""
- st.markdown("### مقارنة إصدارات المستندات")
-
- # اختيار المناقصة
- tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
- selected_tender = st.selectbox(
- "اختر المناقصة",
- options=tender_options
- )
-
- # فلترة المستندات حسب المناقصة المختارة
- filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
-
- # اختيار نوع المستند
- doc_types = list(set([doc["type"] for doc in filtered_docs]))
- selected_type = st.selectbox(
- "اختر نوع المستند",
- options=doc_types
- )
-
- # فلترة المستندات حسب النوع المختار
- type_filtered_docs = [doc for doc in filtered_docs if doc["type"] == selected_type]
-
- # ترتيب المستندات حسب الإصدار
- type_filtered_docs = sorted(type_filtered_docs, key=lambda x: x["version"])
-
- if len(type_filtered_docs) < 2:
- st.warning("يجب توفر إصدارين على الأقل للمقارنة")
- else:
- # اختيار الإصدارات للمقارنة
- col1, col2 = st.columns(2)
-
- with col1:
- version_options = [f"{doc['name']} (الإصدار {doc['version']})" for doc in type_filtered_docs]
- selected_version1_index = st.selectbox(
- "الإصدار الأول",
- options=range(len(version_options)),
- format_func=lambda x: version_options[x]
- )
- selected_doc1 = type_filtered_docs[selected_version1_index]
-
- with col2:
- remaining_indices = [i for i in range(len(type_filtered_docs)) if i != selected_version1_index]
- selected_version2_index = st.selectbox(
- "الإصدار الثاني",
- options=remaining_indices,
- format_func=lambda x: version_options[x]
- )
- selected_doc2 = type_filtered_docs[selected_version2_index]
-
- # زر بدء المقارنة
- if st.button("بدء المقارنة", use_container_width=True):
- # عرض معلومات المستندات المختارة
- st.markdown("### معلومات المستندات المختارة")
-
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown(f"**الإصدار الأول:** {selected_doc1['version']}")
- st.markdown(f"**التاريخ:** {selected_doc1['date']}")
- st.markdown(f"**عدد الصفحات:** {selected_doc1['pages']}")
- st.markdown(f"**الحجم:** {selected_doc1['size']} ميجابايت")
-
- with col2:
- st.markdown(f"**الإصدار الثاني:** {selected_doc2['version']}")
- st.markdown(f"**التاريخ:** {selected_doc2['date']}")
- st.markdown(f"**عدد الصفحات:** {selected_doc2['pages']}")
- st.markdown(f"**الحجم:** {selected_doc2['size']} ميجابايت")
-
- # الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
- doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
- doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
-
- # إجراء المقارنة
- self.display_comparison(doc1_content, doc2_content)
-
- def display_comparison(self, text1, text2):
- """عرض نتائج المقارنة بين نصين"""
- st.markdown("### نتائج المقارنة")
-
- # تقسيم النصوص إلى أسطر
- lines1 = text1.splitlines()
- lines2 = text2.splitlines()
-
- # إجراء المقارنة باستخدام difflib
- d = difflib.Differ()
- diff = list(d.compare(lines1, lines2))
-
- # عرض ملخص التغييرات
- added = len([line for line in diff if line.startswith('+ ')])
- removed = len([line for line in diff if line.startswith('- ')])
- changed = len([line for line in diff if line.startswith('? ')])
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- self.ui.create_metric_card(
- "الإضافات",
- str(added),
- None,
- self.ui.COLORS['success']
- )
-
- with col2:
- self.ui.create_metric_card(
- "الحذف",
- str(removed),
- None,
- self.ui.COLORS['danger']
- )
-
- with col3:
- self.ui.create_metric_card(
- "التغييرات",
- str(changed // 2), # تقسيم على 2 لأن كل تغيير يظهر مرتين
- None,
- self.ui.COLORS['warning']
- )
-
- # عرض التغييرات بالتفصيل
- st.markdown("### التغييرات بالتفصيل")
-
- # إنشاء عرض HTML للتغييرات
- html_diff = []
- for line in diff:
- if line.startswith('+ '):
- html_diff.append(f'{line[2:]}
')
- elif line.startswith('- '):
- html_diff.append(f'{line[2:]}
')
- elif line.startswith('? '):
- # تجاهل أسطر التفاصيل
- continue
- else:
- html_diff.append(f'{line[2:]}
')
-
- # عرض التغييرات
- st.markdown(''.join(html_diff), unsafe_allow_html=True)
-
- # خيارات إضافية
- st.markdown("### خيارات إضافية")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- if st.button("تصدير التغييرات", use_container_width=True):
- st.success("تم تصدير التغييرات بنجاح")
-
- with col2:
- if st.button("إنشاء تقرير", use_container_width=True):
- st.success("تم إنشاء التقرير بنجاح")
-
- with col3:
- if st.button("حفظ المقارنة", use_container_width=True):
- st.success("تم حفظ المقارنة بنجاح")
-
- def compare_documents(self):
- """مقارنة مستندات مختلفة"""
- st.markdown("### مقارنة مستندات مختلفة")
-
- # اختيار المستند الأول
- col1, col2 = st.columns(2)
-
- with col1:
- tender1_options = list(set([doc["related_entity"] for doc in self.documents_data]))
- selected_tender1 = st.selectbox(
- "اختر المناقصة الأولى",
- options=tender1_options,
- key="tender1"
- )
-
- # فلترة المستندات حسب المناقصة المختارة
- filtered_docs1 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender1]
-
- # اختيار المستند
- doc_options1 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs1]
- selected_doc1_index = st.selectbox(
- "اختر المستند الأول",
- options=range(len(doc_options1)),
- format_func=lambda x: doc_options1[x],
- key="doc1"
- )
- selected_doc1 = filtered_docs1[selected_doc1_index]
-
- with col2:
- tender2_options = list(set([doc["related_entity"] for doc in self.documents_data]))
- selected_tender2 = st.selectbox(
- "اختر المناقصة الثانية",
- options=tender2_options,
- key="tender2"
- )
-
- # فلترة المستندات حسب المناقصة المختارة
- filtered_docs2 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender2]
-
- # اختيار المستند
- doc_options2 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs2]
- selected_doc2_index = st.selectbox(
- "اختر المستند الثاني",
- options=range(len(doc_options2)),
- format_func=lambda x: doc_options2[x],
- key="doc2"
- )
- selected_doc2 = filtered_docs2[selected_doc2_index]
-
- # خيارات المقارنة
- st.markdown("### خيارات المقارنة")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- comparison_type = st.radio(
- "نوع المقارنة",
- options=["مقارنة كاملة", "مقارنة الأقسام المتطابقة فقط", "مقارنة الاختلافات فقط"]
- )
-
- with col2:
- ignore_options = st.multiselect(
- "تجاهل",
- options=["المسافات", "علامات الترقيم", "حالة الأحرف", "الأرقام"],
- default=["المسافات"]
- )
-
- with col3:
- similarity_threshold = st.slider(
- "عتبة التشابه",
- min_value=0.0,
- max_value=1.0,
- value=0.7,
- step=0.05
- )
-
- # زر بدء المقارنة
- if st.button("بدء المقارنة بين المستندات", use_container_width=True):
- # عرض معلومات المستندات المختارة
- st.markdown("### معلومات المستندات المختارة")
-
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown(f"**المستند الأول:** {selected_doc1['name']}")
- st.markdown(f"**الإصدار:** {selected_doc1['version']}")
- st.markdown(f"**التاريخ:** {selected_doc1['date']}")
- st.markdown(f"**المناقصة:** {selected_doc1['related_entity']}")
-
- with col2:
- st.markdown(f"**المستند الثاني:** {selected_doc2['name']}")
- st.markdown(f"**الإصدار:** {selected_doc2['version']}")
- st.markdown(f"**التاريخ:** {selected_doc2['date']}")
- st.markdown(f"**المناقصة:** {selected_doc2['related_entity']}")
-
- # الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
- doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
- doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
-
- # إجراء المقارنة
- self.display_document_comparison(doc1_content, doc2_content, comparison_type, ignore_options, similarity_threshold)
-
- def display_document_comparison(self, text1, text2, comparison_type, ignore_options, similarity_threshold):
- """عرض نتائج المقارنة بين مستندين"""
- st.markdown("### نتائج المقارنة بين المستندين")
-
- # تقسيم النصوص إلى أقسام (في هذا المثال، نستخدم العناوين كفواصل للأقسام)
- sections1 = self.split_into_sections(text1)
- sections2 = self.split_into_sections(text2)
-
- # حساب نسبة التشابه الإجمالية
- similarity = difflib.SequenceMatcher(None, text1, text2).ratio()
-
- # عرض نسبة التشابه
- st.markdown(f"**نسبة التشابه الإجمالية:** {similarity:.2%}")
-
- # عرض مقارنة الأقسام
- st.markdown("### مقارنة الأقسام")
-
- # إنشاء جدول لمقارنة الأقسام
- section_comparisons = []
-
- for section1_title, section1_content in sections1.items():
- best_match = None
- best_similarity = 0
-
- for section2_title, section2_content in sections2.items():
- # حساب نسبة التشابه بين عناوين الأقسام
- title_similarity = difflib.SequenceMatcher(None, section1_title, section2_title).ratio()
-
- # حساب نسبة التشابه بين محتوى الأقسام
- content_similarity = difflib.SequenceMatcher(None, section1_content, section2_content).ratio()
-
- # حساب متوسط نسبة التشابه
- avg_similarity = (title_similarity + content_similarity) / 2
-
- if avg_similarity > best_similarity:
- best_similarity = avg_similarity
- best_match = {
- "title": section2_title,
- "content": section2_content,
- "similarity": avg_similarity
- }
-
- # إضافة المقارنة إلى القائمة
- if best_match and best_similarity >= similarity_threshold:
- section_comparisons.append({
- "section1_title": section1_title,
- "section2_title": best_match["title"],
- "similarity": best_similarity
- })
- else:
- section_comparisons.append({
- "section1_title": section1_title,
- "section2_title": "غير موجود",
- "similarity": 0
- })
-
- # إضافة الأقسام الموجودة في المستند الثاني فقط
- for section2_title, section2_content in sections2.items():
- if not any(comp["section2_title"] == section2_title for comp in section_comparisons):
- section_comparisons.append({
- "section1_title": "غير موجود",
- "section2_title": section2_title,
- "similarity": 0
- })
-
- # عرض جدول المقارنة
- section_df = pd.DataFrame(section_comparisons)
- section_df = section_df.rename(columns={
- "section1_title": "القسم في المستند الأول",
- "section2_title": "القسم في المستند الثاني",
- "similarity": "نسبة التشابه"
- })
-
- # تنسيق نسبة التشابه
- section_df["نسبة التشابه"] = section_df["نسبة التشابه"].apply(lambda x: f"{x:.2%}")
-
- st.dataframe(
- section_df,
- use_container_width=True,
- hide_index=True
- )
-
- # عرض تفاصيل المقارنة
- st.markdown("### تفاصيل المقارنة")
-
- # اختيار قسم للمقارنة التفصيلية
- selected_section = st.selectbox(
- "اختر قسماً للمقارنة التفصيلية",
- options=[comp["section1_title"] for comp in section_comparisons if comp["section1_title"] != "غير موجود"]
- )
-
- # العثور على القسم المقابل في المستند الثاني
- matching_comparison = next((comp for comp in section_comparisons if comp["section1_title"] == selected_section), None)
-
- if matching_comparison and matching_comparison["section2_title"] != "غير موجود":
- # الحصول على محتوى القسمين
- section1_content = sections1[selected_section]
- section2_content = sections2[matching_comparison["section2_title"]]
-
- # عرض المقارنة التفصيلية
- self.display_comparison(section1_content, section2_content)
- else:
- st.warning("القسم المحدد غير موجود في المستند الثاني")
-
- def split_into_sections(self, text):
- """تقسيم النص إلى أقسام باستخدام العناوين"""
- sections = {}
- current_section = None
- current_content = []
-
- for line in text.splitlines():
- # البحث عن العناوين (الأسطر التي تبدأ بـ #)
- if line.strip().startswith('#'):
- # حفظ القسم السابق إذا وجد
- if current_section:
- sections[current_section] = '\n'.join(current_content)
-
- # بدء قسم جديد
- current_section = line.strip()
- current_content = []
- elif current_section:
- # إضافة السطر إلى محتوى القسم الحالي
- current_content.append(line)
-
- # حفظ القسم الأخير
- if current_section:
- sections[current_section] = '\n'.join(current_content)
-
- return sections
-
- def analyze_changes(self):
- """تحليل التغييرات في المستندات"""
- st.markdown("### تحليل التغييرات في المستندات")
-
- # اختيار المناقصة
- tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
- selected_tender = st.selectbox(
- "اختر المناقصة",
- options=tender_options,
- key="analyze_tender"
- )
-
- # فلترة المستندات حسب المناقصة المختارة
- filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
-
- # تجميع المستندات حسب النوع
- doc_types = {}
- for doc in filtered_docs:
- if doc["type"] not in doc_types:
- doc_types[doc["type"]] = []
- doc_types[doc["type"]].append(doc)
-
- # عرض تحليل التغييرات لكل نوع مستند
- for doc_type, docs in doc_types.items():
- if len(docs) > 1:
- with st.expander(f"تحليل التغييرات في {doc_type}"):
- # ترتيب المستندات حسب الإصدار
- sorted_docs = sorted(docs, key=lambda x: x["version"])
-
- # عرض معلومات الإصدارات
- st.markdown(f"**عدد الإصدارات:** {len(sorted_docs)}")
- st.markdown(f"**أول إصدار:** {sorted_docs[0]['version']} ({sorted_docs[0]['date']})")
- st.markdown(f"**آخر إصدار:** {sorted_docs[-1]['version']} ({sorted_docs[-1]['date']})")
-
- # حساب التغييرات بين الإصدارات
- changes = []
- for i in range(1, len(sorted_docs)):
- prev_doc = sorted_docs[i-1]
- curr_doc = sorted_docs[i]
-
- # حساب التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
- page_diff = curr_doc["pages"] - prev_doc["pages"]
- size_diff = curr_doc["size"] - prev_doc["size"]
-
- changes.append({
- "from_version": prev_doc["version"],
- "to_version": curr_doc["version"],
- "date": curr_doc["date"],
- "page_diff": page_diff,
- "size_diff": size_diff
- })
-
- # عرض جدول التغييرات
- changes_df = pd.DataFrame(changes)
- changes_df = changes_df.rename(columns={
- "from_version": "من الإصدار",
- "to_version": "إلى الإصدار",
- "date": "تاريخ التغيير",
- "page_diff": "التغيير في عدد الصفحات",
- "size_diff": "التغيير في الحجم (ميجابايت)"
- })
-
- st.dataframe(
- changes_df,
- use_container_width=True,
- hide_index=True
- )
-
- # عرض رسم بياني للتغييرات
- st.markdown("#### تطور حجم المستند عبر الإصدارات")
-
- versions = [doc["version"] for doc in sorted_docs]
- sizes = [doc["size"] for doc in sorted_docs]
-
- chart_data = pd.DataFrame({
- "الإصدار": versions,
- "الحجم (ميجابايت)": sizes
- })
-
- st.line_chart(chart_data.set_index("الإصدار"))
-
- # عرض رسم بياني لعدد الصفحات
- st.markdown("#### تطور عدد الصفحات عبر الإصدارات")
-
- pages = [doc["pages"] for doc in sorted_docs]
-
- chart_data = pd.DataFrame({
- "الإصدار": versions,
- "عدد الصفحات": pages
- })
-
- st.line_chart(chart_data.set_index("الإصدار"))
-
- # تحليل التغييرات الشاملة
- st.markdown("### تحليل التغييرات الشاملة")
-
- # حساب إجمالي التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
- total_docs = len(filtered_docs)
- total_versions = sum(len(docs) for docs in doc_types.values())
- avg_versions = total_versions / len(doc_types) if doc_types else 0
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- self.ui.create_metric_card(
- "إجمالي المستندات",
- str(total_docs),
- None,
- self.ui.COLORS['primary']
- )
-
- with col2:
- self.ui.create_metric_card(
- "إجمالي الإصدارات",
- str(total_versions),
- None,
- self.ui.COLORS['secondary']
- )
-
- with col3:
- self.ui.create_metric_card(
- "متوسط الإصدارات لكل نوع",
- f"{avg_versions:.1f}",
- None,
- self.ui.COLORS['accent']
- )
-
- # عرض توزيع التغييرات حسب النوع
- st.markdown("#### توزيع الإصدارات حسب نوع المستند")
-
- type_counts = {doc_type: len(docs) for doc_type, docs in doc_types.items()}
-
- chart_data = pd.DataFrame({
- "نوع المستند": list(type_counts.keys()),
- "عدد الإصدارات": list(type_counts.values())
- })
-
- st.bar_chart(chart_data.set_index("نوع المستند"))
-
- def show_change_history(self):
- """عرض سجل التغييرات"""
- st.markdown("### سجل التغييرات")
-
- # إنشاء بيانات نموذجية لسجل التغييرات
- change_history = [
- {
- "id": "CH001",
- "document_id": "DOC001",
- "document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
- "from_version": "1.0",
- "to_version": "1.1",
- "change_date": "2025-02-10",
- "change_type": "تحديث",
- "changed_by": "أحمد محمد",
- "description": "تحديث المواصفات الفنية وشروط التنفيذ",
- "sections_changed": ["نطاق العمل", "المواصفات الفنية", "الشروط العامة"]
- },
- {
- "id": "CH002",
- "document_id": "DOC002",
- "document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
- "from_version": "1.1",
- "to_version": "2.0",
- "change_date": "2025-03-05",
- "change_type": "تحديث رئيسي",
- "changed_by": "سارة عبدالله",
- "description": "إضافة متطلبات الاستدامة وتحديث المواصفات الفنية",
- "sections_changed": ["المواصفات الفنية", "الشروط العامة", "متطلبات الاستدامة"]
- },
- {
- "id": "CH003",
- "document_id": "DOC004",
- "document_name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
- "from_version": "1.0",
- "to_version": "1.1",
- "change_date": "2025-02-20",
- "change_type": "تحديث",
- "changed_by": "خالد عمر",
- "description": "تحديث الكميات وإضافة بنود جديدة",
- "sections_changed": ["أعمال الهيكل الإنشائي", "أعمال التشطيبات", "أعمال الكهرباء"]
- },
- {
- "id": "CH004",
- "document_id": "DOC006",
- "document_name": "المخططات - مناقصة إنشاء مبنى إداري",
- "from_version": "1.0",
- "to_version": "2.0",
- "change_date": "2025-03-10",
- "change_type": "تحديث رئيسي",
- "changed_by": "محمد علي",
- "description": "تحديث المخططات المعمارية والإنشائية",
- "sections_changed": ["المخططات المعمارية", "المخططات الإنشائية", "مخططات الكهرباء"]
- },
- {
- "id": "CH005",
- "document_id": "DOC008",
- "document_name": "كراسة الشروط - مناقصة صيانة طرق",
- "from_version": "1.0",
- "to_version": "1.1",
- "change_date": "2025-03-15",
- "change_type": "تحديث",
- "changed_by": "فاطمة أحمد",
- "description": "تحديث المواصفات الفنية وشروط التنفيذ",
- "sections_changed": ["نطاق العمل", "المواصفات الفنية", "الشروط العامة"]
- }
- ]
-
- # إنشاء فلاتر للسجل
- col1, col2, col3 = st.columns(3)
-
- with col1:
- document_filter = st.selectbox(
- "المستند",
- options=["الكل"] + list(set([ch["document_name"] for ch in change_history]))
- )
-
- with col2:
- change_type_filter = st.selectbox(
- "نوع التغيير",
- options=["الكل"] + list(set([ch["change_type"] for ch in change_history]))
- )
-
- with col3:
- date_range = st.date_input(
- "نطاق التاريخ",
- value=(
- datetime.datetime.strptime("2025-01-01", "%Y-%m-%d").date(),
- datetime.datetime.strptime("2025-12-31", "%Y-%m-%d").date()
- )
- )
-
- # تطبيق الفلاتر
- filtered_history = change_history
-
- if document_filter != "الكل":
- filtered_history = [ch for ch in filtered_history if ch["document_name"] == document_filter]
-
- if change_type_filter != "الكل":
- filtered_history = [ch for ch in filtered_history if ch["change_type"] == change_type_filter]
-
- if len(date_range) == 2:
- start_date, end_date = date_range
- filtered_history = [
- ch for ch in filtered_history
- if start_date <= datetime.datetime.strptime(ch["change_date"], "%Y-%m-%d").date() <= end_date
- ]
-
- # عرض سجل التغييرات
- if not filtered_history:
- st.info("لا توجد تغييرات تطابق الفلاتر المحددة")
- else:
- # تحويل البيانات إلى DataFrame
- history_df = pd.DataFrame(filtered_history)
-
- # إعادة ترتيب الأعمدة وتغيير أسمائها
- display_df = history_df[[
- "id", "document_name", "from_version", "to_version", "change_date", "change_type", "changed_by", "description"
- ]].rename(columns={
- "id": "الرقم",
- "document_name": "اسم المستند",
- "from_version": "من الإصدار",
- "to_version": "إلى الإصدار",
- "change_date": "تاريخ التغيير",
- "change_type": "نوع التغيير",
- "changed_by": "بواسطة",
- "description": "الوصف"
- })
-
- # عرض الجدول
- st.dataframe(
- display_df,
- use_container_width=True,
- hide_index=True
- )
-
- # عرض تفاصيل التغيير المحدد
- st.markdown("### تفاصيل التغيير")
-
- selected_change_id = st.selectbox(
- "اختر تغييراً لعرض التفاصيل",
- options=[ch["id"] for ch in filtered_history],
- 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), "")
- )
-
- # العثور على التغيير المحدد
- selected_change = next((ch for ch in filtered_history if ch["id"] == selected_change_id), None)
-
- if selected_change:
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown(f"**المستند:** {selected_change['document_name']}")
- st.markdown(f"**من الإصدار:** {selected_change['from_version']}")
- st.markdown(f"**إلى الإصدار:** {selected_change['to_version']}")
- st.markdown(f"**تاريخ التغيير:** {selected_change['change_date']}")
-
- with col2:
- st.markdown(f"**نوع التغيير:** {selected_change['change_type']}")
- st.markdown(f"**بواسطة:** {selected_change['changed_by']}")
- st.markdown(f"**الوصف:** {selected_change['description']}")
-
- # عرض الأقسام التي تم تغييرها
- st.markdown("#### الأقسام التي تم تغييرها")
-
- for section in selected_change["sections_changed"]:
- st.markdown(f"- {section}")
-
- # أزرار الإجراءات
- col1, col2, col3 = st.columns(3)
-
- with col1:
- if st.button("عرض التغييرات بالتفصيل", use_container_width=True):
- st.success("تم فتح التغييرات بالتفصيل")
-
- with col2:
- if st.button("إنشاء تقرير", use_container_width=True):
- st.success("تم إنشاء التقرير بنجاح")
-
- with col3:
- if st.button("تصدير التغييرات", use_container_width=True):
- st.success("تم تصدير التغييرات بنجاح")
-
-# تشغيل التطبيق
-if __name__ == "__main__":
- doc_comparison_app = DocumentComparisonApp()
- doc_comparison_app.run()
+"""
+وحدة مقارنة المستندات - نظام تحليل المناقصات
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import os
+import sys
+from pathlib import Path
+import difflib
+import re
+import datetime
+
+# إضافة مسار المشروع للنظام
+sys.path.append(str(Path(__file__).parent.parent))
+
+# استيراد محسن واجهة المستخدم
+from styling.enhanced_ui import UIEnhancer
+
+class DocumentComparisonApp:
+ """تطبيق مقارنة المستندات"""
+
+ def __init__(self):
+ """تهيئة تطبيق مقارنة المستندات"""
+ self.ui = UIEnhancer(page_title="مقارنة المستندات - نظام تحليل المناقصات", page_icon="📄")
+ self.ui.apply_theme_colors()
+
+ # بيانات المستندات (نموذجية)
+ self.documents_data = [
+ {
+ "id": "DOC001",
+ "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
+ "type": "كراسة شروط",
+ "version": "1.0",
+ "date": "2025-01-15",
+ "size": 2.4,
+ "pages": 45,
+ "related_entity": "T-2025-001",
+ "path": "/documents/T-2025-001/specs_v1.pdf"
+ },
+ {
+ "id": "DOC002",
+ "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
+ "type": "كراسة شروط",
+ "version": "1.1",
+ "date": "2025-02-10",
+ "size": 2.6,
+ "pages": 48,
+ "related_entity": "T-2025-001",
+ "path": "/documents/T-2025-001/specs_v1.1.pdf"
+ },
+ {
+ "id": "DOC003",
+ "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
+ "type": "كراسة شروط",
+ "version": "2.0",
+ "date": "2025-03-05",
+ "size": 2.8,
+ "pages": 52,
+ "related_entity": "T-2025-001",
+ "path": "/documents/T-2025-001/specs_v2.0.pdf"
+ },
+ {
+ "id": "DOC004",
+ "name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
+ "type": "جدول كميات",
+ "version": "1.0",
+ "date": "2025-01-15",
+ "size": 1.2,
+ "pages": 20,
+ "related_entity": "T-2025-001",
+ "path": "/documents/T-2025-001/boq_v1.0.xlsx"
+ },
+ {
+ "id": "DOC005",
+ "name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
+ "type": "جدول كميات",
+ "version": "1.1",
+ "date": "2025-02-20",
+ "size": 1.3,
+ "pages": 22,
+ "related_entity": "T-2025-001",
+ "path": "/documents/T-2025-001/boq_v1.1.xlsx"
+ },
+ {
+ "id": "DOC006",
+ "name": "المخططات - مناقصة إنشاء مبنى إداري",
+ "type": "مخططات",
+ "version": "1.0",
+ "date": "2025-01-15",
+ "size": 15.6,
+ "pages": 30,
+ "related_entity": "T-2025-001",
+ "path": "/documents/T-2025-001/drawings_v1.0.pdf"
+ },
+ {
+ "id": "DOC007",
+ "name": "المخططات - مناقصة إنشاء مبنى إداري",
+ "type": "مخططات",
+ "version": "2.0",
+ "date": "2025-03-10",
+ "size": 18.2,
+ "pages": 35,
+ "related_entity": "T-2025-001",
+ "path": "/documents/T-2025-001/drawings_v2.0.pdf"
+ },
+ {
+ "id": "DOC008",
+ "name": "كراسة الشروط - مناقصة صيانة طرق",
+ "type": "كراسة شروط",
+ "version": "1.0",
+ "date": "2025-02-05",
+ "size": 1.8,
+ "pages": 32,
+ "related_entity": "T-2025-002",
+ "path": "/documents/T-2025-002/specs_v1.0.pdf"
+ },
+ {
+ "id": "DOC009",
+ "name": "كراسة الشروط - مناقصة صيانة طرق",
+ "type": "كراسة شروط",
+ "version": "1.1",
+ "date": "2025-03-15",
+ "size": 1.9,
+ "pages": 34,
+ "related_entity": "T-2025-002",
+ "path": "/documents/T-2025-002/specs_v1.1.pdf"
+ },
+ {
+ "id": "DOC010",
+ "name": "جدول الكميات - مناقصة صيانة طرق",
+ "type": "جدول كميات",
+ "version": "1.0",
+ "date": "2025-02-05",
+ "size": 0.9,
+ "pages": 15,
+ "related_entity": "T-2025-002",
+ "path": "/documents/T-2025-002/boq_v1.0.xlsx"
+ }
+ ]
+
+ # بيانات نموذجية لمحتوى المستندات (للعرض فقط)
+ self.sample_document_content = {
+ "DOC001": """
+ # كراسة الشروط والمواصفات
+ ## مناقصة إنشاء مبنى إداري
+
+ ### 1. مقدمة
+ تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
+
+ ### 2. نطاق العمل
+ يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع، ويشمل ذلك:
+ - أعمال الهيكل الإنشائي
+ - أعمال التشطيبات الداخلية والخارجية
+ - أعمال الكهرباء والميكانيكا
+ - أعمال تنسيق الموقع
+
+ ### 3. المواصفات الفنية
+ #### 3.1 أعمال الخرسانة
+ - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 30 نيوتن/مم²
+ - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
+
+ #### 3.2 أعمال التشطيبات
+ - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
+ - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
+
+ ### 4. الشروط العامة
+ - مدة التنفيذ: 18 شهراً من تاريخ استلام الموقع
+ - غرامة التأخير: 0.1% من قيمة العقد عن كل يوم تأخير
+ - ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
+ """,
+
+ "DOC002": """
+ # كراسة الشروط والمواصفات
+ ## مناقصة إنشاء مبنى إداري
+
+ ### 1. مقدمة
+ تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
+
+ ### 2. نطاق العمل
+ يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5500 متر مربع، ويشمل ذلك:
+ - أعمال الهيكل الإنشائي
+ - أعمال التشطيبات الداخلية والخارجية
+ - أعمال الكهرباء والميكانيكا
+ - أعمال تنسيق الموقع
+ - أعمال أنظمة الأمن والسلامة
+
+ ### 3. المواصفات الفنية
+ #### 3.1 أعمال الخرسانة
+ - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 35 نيوتن/مم²
+ - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
+
+ #### 3.2 أعمال التشطيبات
+ - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
+ - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
+ - يجب استخدام زجاج عاكس للحرارة للواجهات
+
+ ### 4. الشروط العامة
+ - مدة التنفيذ: 16 شهراً من تاريخ استلام الموقع
+ - غرامة التأخير: 0.15% من قيمة العقد عن كل يوم تأخير
+ - ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
+ """,
+
+ "DOC003": """
+ # كراسة الشروط والمواصفات
+ ## مناقصة إنشاء مبنى إداري
+
+ ### 1. مقدمة
+ تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض وفقاً للمواصفات المعتمدة من الهيئة السعودية للمواصفات والمقاييس.
+
+ ### 2. نطاق العمل
+ يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
+ - أعمال الهيكل الإنشائي
+ - أعمال التشطيبات الداخلية والخارجية
+ - أعمال الكهرباء والميكانيكا
+ - أعمال تنسيق الموقع
+ - أعمال أنظمة الأمن والسلامة
+ - أعمال أنظمة المباني الذكية
+
+ ### 3. المواصفات الفنية
+ #### 3.1 أعمال الخرسانة
+ - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
+ - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
+ - يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
+
+ #### 3.2 أعمال التشطيبات
+ - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
+ - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
+ - يجب استخدام زجاج عاكس للحرارة للواجهات
+ - يجب استخدام مواد صديقة للبيئة
+
+ ### 4. الشروط العامة
+ - مدة التنفيذ: 15 شهراً من تاريخ استلام الموقع
+ - غرامة التأخير: 0.2% من قيمة العقد عن كل يوم تأخير
+ - ضمان الأعمال: 15 سنوات للهيكل الإنشائي، 7 سنوات للأعمال الميكانيكية والكهربائية
+
+ ### 5. متطلبات الاستدامة
+ - يجب أن يحقق المبنى متطلبات الاستدامة وفقاً لمعايير LEED
+ - يجب توفير أنظمة لترشيد استهلاك الطاقة والمياه
+ """
+ }
+
+ def run(self):
+ """تشغيل تطبيق مقارنة المستندات"""
+ # إنشاء قائمة العناصر
+ menu_items = [
+ {"name": "لوحة المعلومات", "icon": "house"},
+ {"name": "المناقصات والعقود", "icon": "file-text"},
+ {"name": "تحليل المستندات", "icon": "file-earmark-text"},
+ {"name": "نظام التسعير", "icon": "calculator"},
+ {"name": "حاسبة تكاليف البناء", "icon": "building"},
+ {"name": "الموارد والتكاليف", "icon": "people"},
+ {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
+ {"name": "إدارة المشاريع", "icon": "kanban"},
+ {"name": "الخرائط والمواقع", "icon": "geo-alt"},
+ {"name": "الجدول الزمني", "icon": "calendar3"},
+ {"name": "الإشعارات", "icon": "bell"},
+ {"name": "مقارنة المستندات", "icon": "files"},
+ {"name": "المساعد الذكي", "icon": "robot"},
+ {"name": "التقارير", "icon": "bar-chart"},
+ {"name": "الإعدادات", "icon": "gear"}
+ ]
+
+ # إنشاء الشريط الجانبي
+ selected = self.ui.create_sidebar(menu_items)
+
+ # إنشاء ترويسة الصفحة
+ self.ui.create_header("مقارنة المستندات", "أدوات متقدمة لمقارنة وتحليل المستندات")
+
+ # إنشاء علامات تبويب للوظائف المختلفة
+ tabs = st.tabs(["مقارنة الإصدارات", "مقارنة المستندات", "تحليل التغييرات", "سجل التغييرات"])
+
+ # علامة تبويب مقارنة الإصدارات
+ with tabs[0]:
+ self.compare_versions()
+
+ # علامة تبويب مقارنة المستندات
+ with tabs[1]:
+ self.compare_documents()
+
+ # علامة تبويب تحليل التغييرات
+ with tabs[2]:
+ self.analyze_changes()
+
+ # علامة تبويب سجل التغييرات
+ with tabs[3]:
+ self.show_change_history()
+
+ def compare_versions(self):
+ """مقارنة إصدارات المستندات"""
+ st.markdown("### مقارنة إصدارات المستندات")
+
+ # اختيار المناقصة
+ tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
+ selected_tender = st.selectbox(
+ "اختر المناقصة",
+ options=tender_options
+ )
+
+ # فلترة المستندات حسب المناقصة المختارة
+ filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
+
+ # اختيار نوع المستند
+ doc_types = list(set([doc["type"] for doc in filtered_docs]))
+ selected_type = st.selectbox(
+ "اختر نوع المستند",
+ options=doc_types
+ )
+
+ # فلترة المستندات حسب النوع المختار
+ type_filtered_docs = [doc for doc in filtered_docs if doc["type"] == selected_type]
+
+ # ترتيب المستندات حسب الإصدار
+ type_filtered_docs = sorted(type_filtered_docs, key=lambda x: x["version"])
+
+ if len(type_filtered_docs) < 2:
+ st.warning("يجب توفر إصدارين على الأقل للمقارنة")
+ else:
+ # اختيار الإصدارات للمقارنة
+ col1, col2 = st.columns(2)
+
+ with col1:
+ version_options = [f"{doc['name']} (الإصدار {doc['version']})" for doc in type_filtered_docs]
+ selected_version1_index = st.selectbox(
+ "الإصدار الأول",
+ options=range(len(version_options)),
+ format_func=lambda x: version_options[x]
+ )
+ selected_doc1 = type_filtered_docs[selected_version1_index]
+
+ with col2:
+ remaining_indices = [i for i in range(len(type_filtered_docs)) if i != selected_version1_index]
+ selected_version2_index = st.selectbox(
+ "الإصدار الثاني",
+ options=remaining_indices,
+ format_func=lambda x: version_options[x]
+ )
+ selected_doc2 = type_filtered_docs[selected_version2_index]
+
+ # زر بدء المقارنة
+ if st.button("بدء المقارنة", use_container_width=True):
+ # عرض معلومات المستندات المختارة
+ st.markdown("### معلومات المستندات المختارة")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown(f"**الإصدار الأول:** {selected_doc1['version']}")
+ st.markdown(f"**التاريخ:** {selected_doc1['date']}")
+ st.markdown(f"**عدد الصفحات:** {selected_doc1['pages']}")
+ st.markdown(f"**الحجم:** {selected_doc1['size']} ميجابايت")
+
+ with col2:
+ st.markdown(f"**الإصدار الثاني:** {selected_doc2['version']}")
+ st.markdown(f"**التاريخ:** {selected_doc2['date']}")
+ st.markdown(f"**عدد الصفحات:** {selected_doc2['pages']}")
+ st.markdown(f"**الحجم:** {selected_doc2['size']} ميجابايت")
+
+ # الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
+ doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
+ doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
+
+ # إجراء المقارنة
+ self.display_comparison(doc1_content, doc2_content)
+
+ def display_comparison(self, text1, text2):
+ """عرض نتائج المقارنة بين نصين"""
+ st.markdown("### نتائج المقارنة")
+
+ # تقسيم النصوص إلى أسطر
+ lines1 = text1.splitlines()
+ lines2 = text2.splitlines()
+
+ # إجراء المقارنة باستخدام difflib
+ d = difflib.Differ()
+ diff = list(d.compare(lines1, lines2))
+
+ # عرض ملخص التغييرات
+ added = len([line for line in diff if line.startswith('+ ')])
+ removed = len([line for line in diff if line.startswith('- ')])
+ changed = len([line for line in diff if line.startswith('? ')])
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ self.ui.create_metric_card(
+ "الإضافات",
+ str(added),
+ None,
+ self.ui.COLORS['success']
+ )
+
+ with col2:
+ self.ui.create_metric_card(
+ "الحذف",
+ str(removed),
+ None,
+ self.ui.COLORS['danger']
+ )
+
+ with col3:
+ self.ui.create_metric_card(
+ "التغييرات",
+ str(changed // 2), # تقسيم على 2 لأن كل تغيير يظهر مرتين
+ None,
+ self.ui.COLORS['warning']
+ )
+
+ # عرض التغييرات بالتفصيل
+ st.markdown("### التغييرات بالتفصيل")
+
+ # إنشاء عرض HTML للتغييرات
+ html_diff = []
+ for line in diff:
+ if line.startswith('+ '):
+ html_diff.append(f'{line[2:]}
')
+ elif line.startswith('- '):
+ html_diff.append(f'{line[2:]}
')
+ elif line.startswith('? '):
+ # تجاهل أسطر التفاصيل
+ continue
+ else:
+ html_diff.append(f'{line[2:]}
')
+
+ # عرض التغييرات
+ st.markdown(''.join(html_diff), unsafe_allow_html=True)
+
+ # خيارات إضافية
+ st.markdown("### خيارات إضافية")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ if st.button("تصدير التغييرات", use_container_width=True):
+ st.success("تم تصدير التغييرات بنجاح")
+
+ with col2:
+ if st.button("إنشاء تقرير", use_container_width=True):
+ st.success("تم إنشاء التقرير بنجاح")
+
+ with col3:
+ if st.button("حفظ المقارنة", use_container_width=True):
+ st.success("تم حفظ المقارنة بنجاح")
+
+ def compare_documents(self):
+ """مقارنة مستندات مختلفة"""
+ st.markdown("### مقارنة مستندات مختلفة")
+
+ # اختيار المستند الأول
+ col1, col2 = st.columns(2)
+
+ with col1:
+ tender1_options = list(set([doc["related_entity"] for doc in self.documents_data]))
+ selected_tender1 = st.selectbox(
+ "اختر المناقصة الأولى",
+ options=tender1_options,
+ key="tender1"
+ )
+
+ # فلترة المستندات حسب المناقصة المختارة
+ filtered_docs1 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender1]
+
+ # اختيار المستند
+ doc_options1 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs1]
+ selected_doc1_index = st.selectbox(
+ "اختر المستند الأول",
+ options=range(len(doc_options1)),
+ format_func=lambda x: doc_options1[x],
+ key="doc1"
+ )
+ selected_doc1 = filtered_docs1[selected_doc1_index]
+
+ with col2:
+ tender2_options = list(set([doc["related_entity"] for doc in self.documents_data]))
+ selected_tender2 = st.selectbox(
+ "اختر المناقصة الثانية",
+ options=tender2_options,
+ key="tender2"
+ )
+
+ # فلترة المستندات حسب المناقصة المختارة
+ filtered_docs2 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender2]
+
+ # اختيار المستند
+ doc_options2 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs2]
+ selected_doc2_index = st.selectbox(
+ "اختر المستند الثاني",
+ options=range(len(doc_options2)),
+ format_func=lambda x: doc_options2[x],
+ key="doc2"
+ )
+ selected_doc2 = filtered_docs2[selected_doc2_index]
+
+ # خيارات المقارنة
+ st.markdown("### خيارات المقارنة")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ comparison_type = st.radio(
+ "نوع المقارنة",
+ options=["مقارنة كاملة", "مقارنة الأقسام المتطابقة فقط", "مقارنة الاختلافات فقط"]
+ )
+
+ with col2:
+ ignore_options = st.multiselect(
+ "تجاهل",
+ options=["المسافات", "علامات الترقيم", "حالة الأحرف", "الأرقام"],
+ default=["المسافات"]
+ )
+
+ with col3:
+ similarity_threshold = st.slider(
+ "عتبة التشابه",
+ min_value=0.0,
+ max_value=1.0,
+ value=0.7,
+ step=0.05
+ )
+
+ # زر بدء المقارنة
+ if st.button("بدء المقارنة بين المستندات", use_container_width=True):
+ # عرض معلومات المستندات المختارة
+ st.markdown("### معلومات المستندات المختارة")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown(f"**المستند الأول:** {selected_doc1['name']}")
+ st.markdown(f"**الإصدار:** {selected_doc1['version']}")
+ st.markdown(f"**التاريخ:** {selected_doc1['date']}")
+ st.markdown(f"**المناقصة:** {selected_doc1['related_entity']}")
+
+ with col2:
+ st.markdown(f"**المستند الثاني:** {selected_doc2['name']}")
+ st.markdown(f"**الإصدار:** {selected_doc2['version']}")
+ st.markdown(f"**التاريخ:** {selected_doc2['date']}")
+ st.markdown(f"**المناقصة:** {selected_doc2['related_entity']}")
+
+ # الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
+ doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
+ doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
+
+ # إجراء المقارنة
+ self.display_document_comparison(doc1_content, doc2_content, comparison_type, ignore_options, similarity_threshold)
+
+ def display_document_comparison(self, text1, text2, comparison_type, ignore_options, similarity_threshold):
+ """عرض نتائج المقارنة بين مستندين"""
+ st.markdown("### نتائج المقارنة بين المستندين")
+
+ # تقسيم النصوص إلى أقسام (في هذا المثال، نستخدم العناوين كفواصل للأقسام)
+ sections1 = self.split_into_sections(text1)
+ sections2 = self.split_into_sections(text2)
+
+ # حساب نسبة التشابه الإجمالية
+ similarity = difflib.SequenceMatcher(None, text1, text2).ratio()
+
+ # عرض نسبة التشابه
+ st.markdown(f"**نسبة التشابه الإجمالية:** {similarity:.2%}")
+
+ # عرض مقارنة الأقسام
+ st.markdown("### مقارنة الأقسام")
+
+ # إنشاء جدول لمقارنة الأقسام
+ section_comparisons = []
+
+ for section1_title, section1_content in sections1.items():
+ best_match = None
+ best_similarity = 0
+
+ for section2_title, section2_content in sections2.items():
+ # حساب نسبة التشابه بين عناوين الأقسام
+ title_similarity = difflib.SequenceMatcher(None, section1_title, section2_title).ratio()
+
+ # حساب نسبة التشابه بين محتوى الأقسام
+ content_similarity = difflib.SequenceMatcher(None, section1_content, section2_content).ratio()
+
+ # حساب متوسط نسبة التشابه
+ avg_similarity = (title_similarity + content_similarity) / 2
+
+ if avg_similarity > best_similarity:
+ best_similarity = avg_similarity
+ best_match = {
+ "title": section2_title,
+ "content": section2_content,
+ "similarity": avg_similarity
+ }
+
+ # إضافة المقارنة إلى القائمة
+ if best_match and best_similarity >= similarity_threshold:
+ section_comparisons.append({
+ "section1_title": section1_title,
+ "section2_title": best_match["title"],
+ "similarity": best_similarity
+ })
+ else:
+ section_comparisons.append({
+ "section1_title": section1_title,
+ "section2_title": "غير موجود",
+ "similarity": 0
+ })
+
+ # إضافة الأقسام الموجودة في المستند الثاني فقط
+ for section2_title, section2_content in sections2.items():
+ if not any(comp["section2_title"] == section2_title for comp in section_comparisons):
+ section_comparisons.append({
+ "section1_title": "غير موجود",
+ "section2_title": section2_title,
+ "similarity": 0
+ })
+
+ # عرض جدول المقارنة
+ section_df = pd.DataFrame(section_comparisons)
+ section_df = section_df.rename(columns={
+ "section1_title": "القسم في المستند الأول",
+ "section2_title": "القسم في المستند الثاني",
+ "similarity": "نسبة التشابه"
+ })
+
+ # تنسيق نسبة التشابه
+ section_df["نسبة التشابه"] = section_df["نسبة التشابه"].apply(lambda x: f"{x:.2%}")
+
+ st.dataframe(
+ section_df,
+ use_container_width=True,
+ hide_index=True
+ )
+
+ # عرض تفاصيل المقارنة
+ st.markdown("### تفاصيل المقارنة")
+
+ # اختيار قسم للمقارنة التفصيلية
+ selected_section = st.selectbox(
+ "اختر قسماً للمقارنة التفصيلية",
+ options=[comp["section1_title"] for comp in section_comparisons if comp["section1_title"] != "غير موجود"]
+ )
+
+ # العثور على القسم المقابل في المستند الثاني
+ matching_comparison = next((comp for comp in section_comparisons if comp["section1_title"] == selected_section), None)
+
+ if matching_comparison and matching_comparison["section2_title"] != "غير موجود":
+ # الحصول على محتوى القسمين
+ section1_content = sections1[selected_section]
+ section2_content = sections2[matching_comparison["section2_title"]]
+
+ # عرض المقارنة التفصيلية
+ self.display_comparison(section1_content, section2_content)
+ else:
+ st.warning("القسم المحدد غير موجود في المستند الثاني")
+
+ def split_into_sections(self, text):
+ """تقسيم النص إلى أقسام باستخدام العناوين"""
+ sections = {}
+ current_section = None
+ current_content = []
+
+ for line in text.splitlines():
+ # البحث عن العناوين (الأسطر التي تبدأ بـ #)
+ if line.strip().startswith('#'):
+ # حفظ القسم السابق إذا وجد
+ if current_section:
+ sections[current_section] = '\n'.join(current_content)
+
+ # بدء قسم جديد
+ current_section = line.strip()
+ current_content = []
+ elif current_section:
+ # إضافة السطر إلى محتوى القسم الحالي
+ current_content.append(line)
+
+ # حفظ القسم الأخير
+ if current_section:
+ sections[current_section] = '\n'.join(current_content)
+
+ return sections
+
+ def analyze_changes(self):
+ """تحليل التغييرات في المستندات"""
+ st.markdown("### تحليل التغييرات في المستندات")
+
+ # اختيار المناقصة
+ tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
+ selected_tender = st.selectbox(
+ "اختر المناقصة",
+ options=tender_options,
+ key="analyze_tender"
+ )
+
+ # فلترة المستندات حسب المناقصة المختارة
+ filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
+
+ # تجميع المستندات حسب النوع
+ doc_types = {}
+ for doc in filtered_docs:
+ if doc["type"] not in doc_types:
+ doc_types[doc["type"]] = []
+ doc_types[doc["type"]].append(doc)
+
+ # عرض تحليل التغييرات لكل نوع مستند
+ for doc_type, docs in doc_types.items():
+ if len(docs) > 1:
+ with st.expander(f"تحليل التغييرات في {doc_type}"):
+ # ترتيب المستندات حسب الإصدار
+ sorted_docs = sorted(docs, key=lambda x: x["version"])
+
+ # عرض معلومات الإصدارات
+ st.markdown(f"**عدد الإصدارات:** {len(sorted_docs)}")
+ st.markdown(f"**أول إصدار:** {sorted_docs[0]['version']} ({sorted_docs[0]['date']})")
+ st.markdown(f"**آخر إصدار:** {sorted_docs[-1]['version']} ({sorted_docs[-1]['date']})")
+
+ # حساب التغييرات بين الإصدارات
+ changes = []
+ for i in range(1, len(sorted_docs)):
+ prev_doc = sorted_docs[i-1]
+ curr_doc = sorted_docs[i]
+
+ # حساب التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
+ page_diff = curr_doc["pages"] - prev_doc["pages"]
+ size_diff = curr_doc["size"] - prev_doc["size"]
+
+ changes.append({
+ "from_version": prev_doc["version"],
+ "to_version": curr_doc["version"],
+ "date": curr_doc["date"],
+ "page_diff": page_diff,
+ "size_diff": size_diff
+ })
+
+ # عرض جدول التغييرات
+ changes_df = pd.DataFrame(changes)
+ changes_df = changes_df.rename(columns={
+ "from_version": "من الإصدار",
+ "to_version": "إلى الإصدار",
+ "date": "تاريخ التغيير",
+ "page_diff": "التغيير في عدد الصفحات",
+ "size_diff": "التغيير في الحجم (ميجابايت)"
+ })
+
+ st.dataframe(
+ changes_df,
+ use_container_width=True,
+ hide_index=True
+ )
+
+ # عرض رسم بياني للتغييرات
+ st.markdown("#### تطور حجم المستند عبر الإصدارات")
+
+ versions = [doc["version"] for doc in sorted_docs]
+ sizes = [doc["size"] for doc in sorted_docs]
+
+ chart_data = pd.DataFrame({
+ "الإصدار": versions,
+ "الحجم (ميجابايت)": sizes
+ })
+
+ st.line_chart(chart_data.set_index("الإصدار"))
+
+ # عرض رسم بياني لعدد الصفحات
+ st.markdown("#### تطور عدد الصفحات عبر الإصدارات")
+
+ pages = [doc["pages"] for doc in sorted_docs]
+
+ chart_data = pd.DataFrame({
+ "الإصدار": versions,
+ "عدد الصفحات": pages
+ })
+
+ st.line_chart(chart_data.set_index("الإصدار"))
+
+ # تحليل التغييرات الشاملة
+ st.markdown("### تحليل التغييرات الشاملة")
+
+ # حساب إجمالي التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
+ total_docs = len(filtered_docs)
+ total_versions = sum(len(docs) for docs in doc_types.values())
+ avg_versions = total_versions / len(doc_types) if doc_types else 0
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ self.ui.create_metric_card(
+ "إجمالي المستندات",
+ str(total_docs),
+ None,
+ self.ui.COLORS['primary']
+ )
+
+ with col2:
+ self.ui.create_metric_card(
+ "إجمالي الإصدارات",
+ str(total_versions),
+ None,
+ self.ui.COLORS['secondary']
+ )
+
+ with col3:
+ self.ui.create_metric_card(
+ "متوسط الإصدارات لكل نوع",
+ f"{avg_versions:.1f}",
+ None,
+ self.ui.COLORS['accent']
+ )
+
+ # عرض توزيع التغييرات حسب النوع
+ st.markdown("#### توزيع الإصدارات حسب نوع المستند")
+
+ type_counts = {doc_type: len(docs) for doc_type, docs in doc_types.items()}
+
+ chart_data = pd.DataFrame({
+ "نوع المستند": list(type_counts.keys()),
+ "عدد الإصدارات": list(type_counts.values())
+ })
+
+ st.bar_chart(chart_data.set_index("نوع المستند"))
+
+ def show_change_history(self):
+ """عرض سجل التغييرات"""
+ st.markdown("### سجل التغييرات")
+
+ # إنشاء بيانات نموذجية لسجل التغييرات
+ change_history = [
+ {
+ "id": "CH001",
+ "document_id": "DOC001",
+ "document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
+ "from_version": "1.0",
+ "to_version": "1.1",
+ "change_date": "2025-02-10",
+ "change_type": "تحديث",
+ "changed_by": "أحمد محمد",
+ "description": "تحديث المواصفات الفنية وشروط التنفيذ",
+ "sections_changed": ["نطاق العمل", "المواصفات الفنية", "الشروط العامة"]
+ },
+ {
+ "id": "CH002",
+ "document_id": "DOC002",
+ "document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
+ "from_version": "1.1",
+ "to_version": "2.0",
+ "change_date": "2025-03-05",
+ "change_type": "تحديث رئيسي",
+ "changed_by": "سارة عبدالله",
+ "description": "إضافة متطلبات الاستدامة وتحديث المواصفات الفنية",
+ "sections_changed": ["المواصفات الفنية", "الشروط العامة", "متطلبات الاستدامة"]
+ },
+ {
+ "id": "CH003",
+ "document_id": "DOC004",
+ "document_name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
+ "from_version": "1.0",
+ "to_version": "1.1",
+ "change_date": "2025-02-20",
+ "change_type": "تحديث",
+ "changed_by": "خالد عمر",
+ "description": "تحديث الكميات وإضافة بنود جديدة",
+ "sections_changed": ["أعمال الهيكل الإنشائي", "أعمال التشطيبات", "أعمال الكهرباء"]
+ },
+ {
+ "id": "CH004",
+ "document_id": "DOC006",
+ "document_name": "المخططات - مناقصة إنشاء مبنى إداري",
+ "from_version": "1.0",
+ "to_version": "2.0",
+ "change_date": "2025-03-10",
+ "change_type": "تحديث رئيسي",
+ "changed_by": "محمد علي",
+ "description": "تحديث المخططات المعمارية والإنشائية",
+ "sections_changed": ["المخططات المعمارية", "المخططات الإنشائية", "مخططات الكهرباء"]
+ },
+ {
+ "id": "CH005",
+ "document_id": "DOC008",
+ "document_name": "كراسة الشروط - مناقصة صيانة طرق",
+ "from_version": "1.0",
+ "to_version": "1.1",
+ "change_date": "2025-03-15",
+ "change_type": "تحديث",
+ "changed_by": "فاطمة أحمد",
+ "description": "تحديث المواصفات الفنية وشروط التنفيذ",
+ "sections_changed": ["نطاق العمل", "المواصفات الفنية", "الشروط العامة"]
+ }
+ ]
+
+ # إنشاء فلاتر للسجل
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ document_filter = st.selectbox(
+ "المستند",
+ options=["الكل"] + list(set([ch["document_name"] for ch in change_history]))
+ )
+
+ with col2:
+ change_type_filter = st.selectbox(
+ "نوع التغيير",
+ options=["الكل"] + list(set([ch["change_type"] for ch in change_history]))
+ )
+
+ with col3:
+ date_range = st.date_input(
+ "نطاق التاريخ",
+ value=(
+ datetime.datetime.strptime("2025-01-01", "%Y-%m-%d").date(),
+ datetime.datetime.strptime("2025-12-31", "%Y-%m-%d").date()
+ )
+ )
+
+ # تطبيق الفلاتر
+ filtered_history = change_history
+
+ if document_filter != "الكل":
+ filtered_history = [ch for ch in filtered_history if ch["document_name"] == document_filter]
+
+ if change_type_filter != "الكل":
+ filtered_history = [ch for ch in filtered_history if ch["change_type"] == change_type_filter]
+
+ if len(date_range) == 2:
+ start_date, end_date = date_range
+ filtered_history = [
+ ch for ch in filtered_history
+ if start_date <= datetime.datetime.strptime(ch["change_date"], "%Y-%m-%d").date() <= end_date
+ ]
+
+ # عرض سجل التغييرات
+ if not filtered_history:
+ st.info("لا توجد تغييرات تطابق الفلاتر المحددة")
+ else:
+ # تحويل البيانات إلى DataFrame
+ history_df = pd.DataFrame(filtered_history)
+
+ # إعادة ترتيب الأعمدة وتغيير أسمائها
+ display_df = history_df[[
+ "id", "document_name", "from_version", "to_version", "change_date", "change_type", "changed_by", "description"
+ ]].rename(columns={
+ "id": "الرقم",
+ "document_name": "اسم المستند",
+ "from_version": "من الإصدار",
+ "to_version": "إلى الإصدار",
+ "change_date": "تاريخ التغيير",
+ "change_type": "نوع التغيير",
+ "changed_by": "بواسطة",
+ "description": "الوصف"
+ })
+
+ # عرض الجدول
+ st.dataframe(
+ display_df,
+ use_container_width=True,
+ hide_index=True
+ )
+
+ # عرض تفاصيل التغيير المحدد
+ st.markdown("### تفاصيل التغيير")
+
+ selected_change_id = st.selectbox(
+ "اختر تغييراً لعرض التفاصيل",
+ options=[ch["id"] for ch in filtered_history],
+ 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), "")
+ )
+
+ # العثور على التغيير المحدد
+ selected_change = next((ch for ch in filtered_history if ch["id"] == selected_change_id), None)
+
+ if selected_change:
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown(f"**المستند:** {selected_change['document_name']}")
+ st.markdown(f"**من الإصدار:** {selected_change['from_version']}")
+ st.markdown(f"**إلى الإصدار:** {selected_change['to_version']}")
+ st.markdown(f"**تاريخ التغيير:** {selected_change['change_date']}")
+
+ with col2:
+ st.markdown(f"**نوع التغيير:** {selected_change['change_type']}")
+ st.markdown(f"**بواسطة:** {selected_change['changed_by']}")
+ st.markdown(f"**الوصف:** {selected_change['description']}")
+
+ # عرض الأقسام التي تم تغييرها
+ st.markdown("#### الأقسام التي تم تغييرها")
+
+ for section in selected_change["sections_changed"]:
+ st.markdown(f"- {section}")
+
+ # أزرار الإجراءات
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ if st.button("عرض التغييرات بالتفصيل", use_container_width=True):
+ st.success("تم فتح التغييرات بالتفصيل")
+
+ with col2:
+ if st.button("إنشاء تقرير", use_container_width=True):
+ st.success("تم إنشاء التقرير بنجاح")
+
+ with col3:
+ if st.button("تصدير التغييرات", use_container_width=True):
+ st.success("تم تصدير التغييرات بنجاح")
+
+# تشغيل التطبيق
+if __name__ == "__main__":
+ doc_comparison_app = DocumentComparisonApp()
+ doc_comparison_app.run()
diff --git a/modules/maps/maps_app.py b/modules/maps/maps_app.py
index 663eb4d46abb04361c81bac89ea0f8cfa9b8ead8..43ce6335bfd09d8fc5d192240d5d73669af04475 100644
--- a/modules/maps/maps_app.py
+++ b/modules/maps/maps_app.py
@@ -1,456 +1,456 @@
-"""
-وحدة الخرائط والمواقع - نظام تحليل المناقصات
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import folium
-from streamlit_folium import folium_static
-import json
-import os
-import sys
-from pathlib import Path
-
-# إضافة مسار المشروع للنظام
-sys.path.append(str(Path(__file__).parent.parent))
-
-# استيراد محسن واجهة المستخدم
-from styling.enhanced_ui import UIEnhancer
-
-class MapsApp:
- """تطبيق الخرائط والمواقع"""
-
- def __init__(self):
- """تهيئة تطبيق الخرائط والمواقع"""
- self.ui = UIEnhancer(page_title="الخرائط والمواقع - نظام تحليل المناقصات", page_icon="🗺️")
- self.ui.apply_theme_colors()
-
- # بيانات المشاريع (نموذجية)
- self.projects_data = [
- {
- "id": "P001",
- "name": "إنشاء مبنى إداري - الرياض",
- "location": "الرياض",
- "coordinates": [24.7136, 46.6753],
- "status": "جاري التنفيذ",
- "budget": 15000000,
- "completion": 45,
- "client": "وزارة الإسكان",
- "start_date": "2024-10-15",
- "end_date": "2025-12-30"
- },
- {
- "id": "P002",
- "name": "تطوير طريق الملك فهد - جدة",
- "location": "جدة",
- "coordinates": [21.5433, 39.1728],
- "status": "قيد الدراسة",
- "budget": 8500000,
- "completion": 0,
- "client": "أمانة جدة",
- "start_date": "2025-05-01",
- "end_date": "2026-02-28"
- },
- {
- "id": "P003",
- "name": "إنشاء مجمع سكني - الدمام",
- "location": "الدمام",
- "coordinates": [26.4207, 50.0888],
- "status": "مكتمل",
- "budget": 22000000,
- "completion": 100,
- "client": "شركة الإسكان للتطوير",
- "start_date": "2023-08-10",
- "end_date": "2025-01-15"
- },
- {
- "id": "P004",
- "name": "بناء مدرسة - أبها",
- "location": "أبها",
- "coordinates": [18.2164, 42.5053],
- "status": "جاري التنفيذ",
- "budget": 5200000,
- "completion": 75,
- "client": "وزارة التعليم",
- "start_date": "2024-06-20",
- "end_date": "2025-07-30"
- },
- {
- "id": "P005",
- "name": "تطوير شبكة مياه - المدينة المنورة",
- "location": "المدينة المنورة",
- "coordinates": [24.5247, 39.5692],
- "status": "جاري التنفيذ",
- "budget": 12800000,
- "completion": 30,
- "client": "شركة المياه الوطنية",
- "start_date": "2024-11-05",
- "end_date": "2026-03-15"
- }
- ]
-
- def run(self):
- """تشغيل تطبيق الخرائط والمواقع"""
- # إنشاء قائمة العناصر
- menu_items = [
- {"name": "لوحة المعلومات", "icon": "house"},
- {"name": "المناقصات والعقود", "icon": "file-text"},
- {"name": "تحليل المستندات", "icon": "file-earmark-text"},
- {"name": "نظام التسعير", "icon": "calculator"},
- {"name": "حاسبة تكاليف البناء", "icon": "building"},
- {"name": "الموارد والتكاليف", "icon": "people"},
- {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
- {"name": "إدارة المشاريع", "icon": "kanban"},
- {"name": "الخرائط والمواقع", "icon": "geo-alt"},
- {"name": "الجدول الزمني", "icon": "calendar3"},
- {"name": "الإشعارات", "icon": "bell"},
- {"name": "مقارنة المستندات", "icon": "files"},
- {"name": "المساعد الذكي", "icon": "robot"},
- {"name": "التقارير", "icon": "bar-chart"},
- {"name": "الإعدادات", "icon": "gear"}
- ]
-
- # إنشاء الشريط الجانبي
- selected = self.ui.create_sidebar(menu_items)
-
- # إنشاء ترويسة الصفحة
- self.ui.create_header("الخرائط والمواقع", "عرض وإدارة مواقع المشاريع")
-
- # إنشاء علامات تبويب للوظائف المختلفة
- tabs = st.tabs(["خريطة المشاريع", "تفاصيل المواقع", "إضافة موقع جديد", "تحليل المناطق"])
-
- # علامة تبويب خريطة المشاريع
- with tabs[0]:
- self.show_projects_map()
-
- # علامة تبويب تفاصيل المواقع
- with tabs[1]:
- self.show_location_details()
-
- # علامة تبويب إضافة موقع جديد
- with tabs[2]:
- self.add_new_location()
-
- # علامة تبويب تحليل المناطق
- with tabs[3]:
- self.analyze_regions()
-
- def show_projects_map(self):
- """عرض خريطة المشاريع"""
- # إنشاء فلاتر للخريطة
- col1, col2, col3 = st.columns(3)
-
- with col1:
- status_filter = st.multiselect(
- "حالة المشروع",
- options=["الكل", "جاري التنفيذ", "قيد الدراسة", "مكتمل"],
- default=["الكل"]
- )
-
- with col2:
- location_filter = st.multiselect(
- "الموقع",
- options=["الكل"] + list(set([p["location"] for p in self.projects_data])),
- default=["الكل"]
- )
-
- with col3:
- budget_range = st.slider(
- "نطاق الميزانية (مليون ريال)",
- 0.0, 25.0, (0.0, 25.0),
- step=0.5
- )
-
- # تطبيق الفلاتر
- filtered_projects = self.projects_data
-
- if "الكل" not in status_filter and status_filter:
- filtered_projects = [p for p in filtered_projects if p["status"] in status_filter]
-
- if "الكل" not in location_filter and location_filter:
- filtered_projects = [p for p in filtered_projects if p["location"] in location_filter]
-
- filtered_projects = [p for p in filtered_projects if budget_range[0] * 1000000 <= p["budget"] <= budget_range[1] * 1000000]
-
- # إنشاء الخريطة
- st.markdown("### خريطة المشاريع")
-
- # تحديد مركز الخريطة (وسط المملكة العربية السعودية تقريباً)
- center = [24.0, 45.0]
-
- # إنشاء خريطة folium
- m = folium.Map(location=center, zoom_start=5, tiles="OpenStreetMap")
-
- # إضافة المشاريع إلى الخريطة
- for project in filtered_projects:
- # تحديد لون العلامة بناءً على حالة المشروع
- if project["status"] == "جاري التنفيذ":
- color = "blue"
- elif project["status"] == "قيد الدراسة":
- color = "orange"
- elif project["status"] == "مكتمل":
- color = "green"
- else:
- color = "gray"
-
- # إنشاء نص النافذة المنبثقة
- popup_text = f"""
-
-
{project['name']}
-
الحالة: {project['status']}
-
الميزانية: {project['budget']:,} ريال
-
نسبة الإنجاز: {project['completion']}%
-
العميل: {project['client']}
-
تاريخ البدء: {project['start_date']}
-
تاريخ الانتهاء: {project['end_date']}
-
عرض التفاصيل
-
- """
-
- # إضافة علامة للمشروع
- folium.Marker(
- location=project["coordinates"],
- popup=folium.Popup(popup_text, max_width=300),
- tooltip=project["name"],
- icon=folium.Icon(color=color, icon="info-sign")
- ).add_to(m)
-
- # عرض الخريطة
- folium_static(m, width=1000, height=500)
-
- # عرض إحصائيات المشاريع
- st.markdown("### إحصائيات المشاريع")
-
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- self.ui.create_metric_card(
- "إجمالي المشاريع",
- str(len(filtered_projects)),
- None,
- self.ui.COLORS['primary']
- )
-
- with col2:
- projects_in_progress = len([p for p in filtered_projects if p["status"] == "جاري التنفيذ"])
- self.ui.create_metric_card(
- "مشاريع جارية",
- str(projects_in_progress),
- None,
- self.ui.COLORS['secondary']
- )
-
- with col3:
- total_budget = sum([p["budget"] for p in filtered_projects])
- self.ui.create_metric_card(
- "إجمالي الميزانية",
- f"{total_budget/1000000:.1f} مليون ريال",
- None,
- self.ui.COLORS['accent']
- )
-
- with col4:
- avg_completion = np.mean([p["completion"] for p in filtered_projects])
- self.ui.create_metric_card(
- "متوسط نسبة الإنجاز",
- f"{avg_completion:.1f}%",
- None,
- self.ui.COLORS['success']
- )
-
- def show_location_details(self):
- """عرض تفاصيل المواقع"""
- st.markdown("### تفاصيل مواقع المشاريع")
-
- # إنشاء جدول بيانات المشاريع
- projects_df = pd.DataFrame(self.projects_data)
- projects_df = projects_df.rename(columns={
- "id": "رقم المشروع",
- "name": "اسم المشروع",
- "location": "الموقع",
- "status": "الحالة",
- "budget": "الميزانية (ريال)",
- "completion": "نسبة الإنجاز (%)",
- "client": "العميل",
- "start_date": "تاريخ البدء",
- "end_date": "تاريخ الانتهاء"
- })
-
- # حذف عمود الإحداثيات من العرض
- projects_df = projects_df.drop(columns=["coordinates"])
-
- # عرض الجدول
- st.dataframe(
- projects_df,
- use_container_width=True,
- hide_index=True
- )
-
- # إضافة خيار تصدير البيانات
- col1, col2 = st.columns([1, 5])
- with col1:
- self.ui.create_button("تصدير البيانات", "primary")
-
- # عرض تفاصيل مشروع محدد
- st.markdown("### تفاصيل مشروع محدد")
-
- selected_project = st.selectbox(
- "اختر مشروعاً لعرض التفاصيل",
- options=[p["name"] for p in self.projects_data]
- )
-
- # العثور على المشروع المحدد
- project = next((p for p in self.projects_data if p["name"] == selected_project), None)
-
- if project:
- col1, col2 = st.columns([2, 1])
-
- with col1:
- # عرض تفاصيل المشروع
- st.markdown(f"#### {project['name']}")
- st.markdown(f"**الموقع:** {project['location']}")
- st.markdown(f"**الحالة:** {project['status']}")
- st.markdown(f"**الميزانية:** {project['budget']:,} ريال")
- st.markdown(f"**نسبة الإنجاز:** {project['completion']}%")
- st.markdown(f"**العميل:** {project['client']}")
- st.markdown(f"**تاريخ البدء:** {project['start_date']}")
- st.markdown(f"**تاريخ الانتهاء:** {project['end_date']}")
-
- # أزرار الإجراءات
- col1, col2, col3 = st.columns(3)
- with col1:
- self.ui.create_button("تعديل البيانات", "primary")
- with col2:
- self.ui.create_button("عرض المستندات", "secondary")
- with col3:
- self.ui.create_button("تقرير الموقع", "accent")
-
- with col2:
- # عرض خريطة مصغرة للمشروع
- m = folium.Map(location=project["coordinates"], zoom_start=12)
- folium.Marker(
- location=project["coordinates"],
- tooltip=project["name"],
- icon=folium.Icon(color="red", icon="info-sign")
- ).add_to(m)
- folium_static(m, width=300, height=300)
-
- def add_new_location(self):
- """إضافة موقع جديد"""
- st.markdown("### إضافة موقع مشروع جديد")
-
- # نموذج إضافة موقع جديد
- with st.form("new_location_form"):
- col1, col2 = st.columns(2)
-
- with col1:
- project_id = st.text_input("رقم المشروع", value="P00" + str(len(self.projects_data) + 1))
- project_name = st.text_input("اسم المشروع")
- location = st.text_input("الموقع")
- status = st.selectbox(
- "الحالة",
- options=["جاري التنفيذ", "قيد الدراسة", "مكتمل"]
- )
- budget = st.number_input("الميزانية (ريال)", min_value=0, step=100000)
-
- with col2:
- completion = st.slider("نسبة الإنجاز (%)", 0, 100, 0)
- client = st.text_input("العميل")
- start_date = st.date_input("تاريخ البدء")
- end_date = st.date_input("تاريخ الانتهاء")
-
- st.markdown("### تحديد الموقع على الخريطة")
- st.markdown("انقر على الخريطة لتحديد موقع المشروع أو أدخل الإحداثيات يدوياً")
-
- col1, col2 = st.columns(2)
-
- with col1:
- latitude = st.number_input("خط العرض", value=24.0, format="%.4f")
-
- with col2:
- longitude = st.number_input("خط الطول", value=45.0, format="%.4f")
-
- # عرض الخريطة لتحديد الموقع
- m = folium.Map(location=[latitude, longitude], zoom_start=5)
- folium.Marker(
- location=[latitude, longitude],
- tooltip="موقع المشروع الجديد",
- icon=folium.Icon(color="red", icon="info-sign")
- ).add_to(m)
- folium_static(m, width=700, height=300)
-
- # زر الإرسال
- submit_button = st.form_submit_button("إضافة المشروع")
-
- if submit_button:
- # إضافة المشروع الجديد (في تطبيق حقيقي، سيتم حفظ البيانات في قاعدة البيانات)
- st.success("تم إضافة المشروع بنجاح!")
-
- # إعادة تعيين النموذج
- st.experimental_rerun()
-
- def analyze_regions(self):
- """تحليل المناطق"""
- st.markdown("### تحليل المناطق")
-
- # إنشاء بيانات المناطق (نموذجية)
- regions_data = {
- "المنطقة": ["الرياض", "مكة المكرمة", "المدينة المنورة", "القصيم", "المنطقة الشرقية", "عسير", "تبوك", "حائل", "الحدود الشمالية", "جازان", "نجران", "الباحة", "الجوف"],
- "عدد المشاريع": [15, 12, 8, 5, 18, 7, 4, 3, 2, 6, 3, 2, 3],
- "إجمالي الميزانية (مليون ريال)": [120, 95, 45, 30, 150, 40, 25, 18, 12, 35, 20, 15, 22],
- "متوسط مدة المشروع (شهر)": [18, 16, 14, 12, 20, 15, 12, 10, 9, 14, 12, 10, 11]
- }
-
- regions_df = pd.DataFrame(regions_data)
-
- # عرض خريطة حرارية للمناطق
- st.markdown("#### توزيع المشاريع حسب المناطق")
-
- # في تطبيق حقيقي، يمكن استخدام خريطة حرارية حقيقية للمملكة
- st.image("https://via.placeholder.com/800x400?text=خريطة+حرارية+للمشاريع+حسب+المناطق", use_column_width=True)
-
- # عرض إحصائيات المناطق
- st.markdown("#### إحصائيات المناطق")
-
- # عرض الجدول
- st.dataframe(
- regions_df,
- use_container_width=True,
- hide_index=True
- )
-
- # عرض رسوم بيانية للمقارنة
- st.markdown("#### مقارنة المناطق")
-
- chart_type = st.radio(
- "نوع الرسم البياني",
- options=["عدد المشاريع", "إجمالي الميزانية", "متوسط مدة المشروع"],
- horizontal=True
- )
-
- if chart_type == "عدد المشاريع":
- chart_data = regions_df[["المنطقة", "عدد المشاريع"]].sort_values(by="عدد المشاريع", ascending=False)
- st.bar_chart(chart_data.set_index("المنطقة"))
- elif chart_type == "إجمالي الميزانية":
- chart_data = regions_df[["المنطقة", "إجمالي الميزانية (مليون ريال)"]].sort_values(by="إجمالي الميزانية (مليون ريال)", ascending=False)
- st.bar_chart(chart_data.set_index("المنطقة"))
- else:
- chart_data = regions_df[["المنطقة", "متوسط مدة المشروع (شهر)"]].sort_values(by="متوسط مدة المشروع (شهر)", ascending=False)
- st.bar_chart(chart_data.set_index("المنطقة"))
-
- # تحليل الكثافة
- st.markdown("#### تحليل كثافة المشاريع")
- st.markdown("""
- يوضح هذا التحليل توزيع المشاريع حسب المناطق الجغرافية، مما يساعد في:
- - تحديد المناطق ذات النشاط العالي
- - تحديد فرص النمو في المناطق الأقل نشاطاً
- - تخطيط الموارد بناءً على التوزيع الجغرافي
- """)
-
- # في تطبيق حقيقي، يمكن إضافة تحليلات أكثر تفصيلاً
-
-# تشغيل التطبيق
-if __name__ == "__main__":
- maps_app = MapsApp()
- maps_app.run()
+"""
+وحدة الخرائط والمواقع - نظام تحليل المناقصات
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import folium
+from streamlit_folium import folium_static
+import json
+import os
+import sys
+from pathlib import Path
+
+# إضافة مسار المشروع للنظام
+sys.path.append(str(Path(__file__).parent.parent))
+
+# استيراد محسن واجهة المستخدم
+from styling.enhanced_ui import UIEnhancer
+
+class MapsApp:
+ """تطبيق الخرائط والمواقع"""
+
+ def __init__(self):
+ """تهيئة تطبيق الخرائط والمواقع"""
+ self.ui = UIEnhancer(page_title="الخرائط والمواقع - نظام تحليل المناقصات", page_icon="🗺️")
+ self.ui.apply_theme_colors()
+
+ # بيانات المشاريع (نموذجية)
+ self.projects_data = [
+ {
+ "id": "P001",
+ "name": "إنشاء مبنى إداري - الرياض",
+ "location": "الرياض",
+ "coordinates": [24.7136, 46.6753],
+ "status": "جاري التنفيذ",
+ "budget": 15000000,
+ "completion": 45,
+ "client": "وزارة الإسكان",
+ "start_date": "2024-10-15",
+ "end_date": "2025-12-30"
+ },
+ {
+ "id": "P002",
+ "name": "تطوير طريق الملك فهد - جدة",
+ "location": "جدة",
+ "coordinates": [21.5433, 39.1728],
+ "status": "قيد الدراسة",
+ "budget": 8500000,
+ "completion": 0,
+ "client": "أمانة جدة",
+ "start_date": "2025-05-01",
+ "end_date": "2026-02-28"
+ },
+ {
+ "id": "P003",
+ "name": "إنشاء مجمع سكني - الدمام",
+ "location": "الدمام",
+ "coordinates": [26.4207, 50.0888],
+ "status": "مكتمل",
+ "budget": 22000000,
+ "completion": 100,
+ "client": "شركة الإسكان للتطوير",
+ "start_date": "2023-08-10",
+ "end_date": "2025-01-15"
+ },
+ {
+ "id": "P004",
+ "name": "بناء مدرسة - أبها",
+ "location": "أبها",
+ "coordinates": [18.2164, 42.5053],
+ "status": "جاري التنفيذ",
+ "budget": 5200000,
+ "completion": 75,
+ "client": "وزارة التعليم",
+ "start_date": "2024-06-20",
+ "end_date": "2025-07-30"
+ },
+ {
+ "id": "P005",
+ "name": "تطوير شبكة مياه - المدينة المنورة",
+ "location": "المدينة المنورة",
+ "coordinates": [24.5247, 39.5692],
+ "status": "جاري التنفيذ",
+ "budget": 12800000,
+ "completion": 30,
+ "client": "شركة المياه الوطنية",
+ "start_date": "2024-11-05",
+ "end_date": "2026-03-15"
+ }
+ ]
+
+ def run(self):
+ """تشغيل تطبيق الخرائط والمواقع"""
+ # إنشاء قائمة العناصر
+ menu_items = [
+ {"name": "لوحة المعلومات", "icon": "house"},
+ {"name": "المناقصات والعقود", "icon": "file-text"},
+ {"name": "تحليل المستندات", "icon": "file-earmark-text"},
+ {"name": "نظام التسعير", "icon": "calculator"},
+ {"name": "حاسبة تكاليف البناء", "icon": "building"},
+ {"name": "الموارد والتكاليف", "icon": "people"},
+ {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
+ {"name": "إدارة المشاريع", "icon": "kanban"},
+ {"name": "الخرائط والمواقع", "icon": "geo-alt"},
+ {"name": "الجدول الزمني", "icon": "calendar3"},
+ {"name": "الإشعارات", "icon": "bell"},
+ {"name": "مقارنة المستندات", "icon": "files"},
+ {"name": "المساعد الذكي", "icon": "robot"},
+ {"name": "التقارير", "icon": "bar-chart"},
+ {"name": "الإعدادات", "icon": "gear"}
+ ]
+
+ # إنشاء الشريط الجانبي
+ selected = self.ui.create_sidebar(menu_items)
+
+ # إنشاء ترويسة الصفحة
+ self.ui.create_header("الخرائط والمواقع", "عرض وإدارة مواقع المشاريع")
+
+ # إنشاء علامات تبويب للوظائف المختلفة
+ tabs = st.tabs(["خريطة المشاريع", "تفاصيل المواقع", "إضافة موقع جديد", "تحليل المناطق"])
+
+ # علامة تبويب خريطة المشاريع
+ with tabs[0]:
+ self.show_projects_map()
+
+ # علامة تبويب تفاصيل المواقع
+ with tabs[1]:
+ self.show_location_details()
+
+ # علامة تبويب إضافة موقع جديد
+ with tabs[2]:
+ self.add_new_location()
+
+ # علامة تبويب تحليل المناطق
+ with tabs[3]:
+ self.analyze_regions()
+
+ def show_projects_map(self):
+ """عرض خريطة المشاريع"""
+ # إنشاء فلاتر للخريطة
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ status_filter = st.multiselect(
+ "حالة المشروع",
+ options=["الكل", "جاري التنفيذ", "قيد الدراسة", "مكتمل"],
+ default=["الكل"]
+ )
+
+ with col2:
+ location_filter = st.multiselect(
+ "الموقع",
+ options=["الكل"] + list(set([p["location"] for p in self.projects_data])),
+ default=["الكل"]
+ )
+
+ with col3:
+ budget_range = st.slider(
+ "نطاق الميزانية (مليون ريال)",
+ 0.0, 25.0, (0.0, 25.0),
+ step=0.5
+ )
+
+ # تطبيق الفلاتر
+ filtered_projects = self.projects_data
+
+ if "الكل" not in status_filter and status_filter:
+ filtered_projects = [p for p in filtered_projects if p["status"] in status_filter]
+
+ if "الكل" not in location_filter and location_filter:
+ filtered_projects = [p for p in filtered_projects if p["location"] in location_filter]
+
+ filtered_projects = [p for p in filtered_projects if budget_range[0] * 1000000 <= p["budget"] <= budget_range[1] * 1000000]
+
+ # إنشاء الخريطة
+ st.markdown("### خريطة المشاريع")
+
+ # تحديد مركز الخريطة (وسط المملكة العربية السعودية تقريباً)
+ center = [24.0, 45.0]
+
+ # إنشاء خريطة folium
+ m = folium.Map(location=center, zoom_start=5, tiles="OpenStreetMap")
+
+ # إضافة المشاريع إلى الخريطة
+ for project in filtered_projects:
+ # تحديد لون العلامة بناءً على حالة المشروع
+ if project["status"] == "جاري التنفيذ":
+ color = "blue"
+ elif project["status"] == "قيد الدراسة":
+ color = "orange"
+ elif project["status"] == "مكتمل":
+ color = "green"
+ else:
+ color = "gray"
+
+ # إنشاء نص النافذة المنبثقة
+ popup_text = f"""
+
+
{project['name']}
+
الحالة: {project['status']}
+
الميزانية: {project['budget']:,} ريال
+
نسبة الإنجاز: {project['completion']}%
+
العميل: {project['client']}
+
تاريخ البدء: {project['start_date']}
+
تاريخ الانتهاء: {project['end_date']}
+
عرض التفاصيل
+
+ """
+
+ # إضافة علامة للمشروع
+ folium.Marker(
+ location=project["coordinates"],
+ popup=folium.Popup(popup_text, max_width=300),
+ tooltip=project["name"],
+ icon=folium.Icon(color=color, icon="info-sign")
+ ).add_to(m)
+
+ # عرض الخريطة
+ folium_static(m, width=1000, height=500)
+
+ # عرض إحصائيات المشاريع
+ st.markdown("### إحصائيات المشاريع")
+
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ self.ui.create_metric_card(
+ "إجمالي المشاريع",
+ str(len(filtered_projects)),
+ None,
+ self.ui.COLORS['primary']
+ )
+
+ with col2:
+ projects_in_progress = len([p for p in filtered_projects if p["status"] == "جاري التنفيذ"])
+ self.ui.create_metric_card(
+ "مشاريع جارية",
+ str(projects_in_progress),
+ None,
+ self.ui.COLORS['secondary']
+ )
+
+ with col3:
+ total_budget = sum([p["budget"] for p in filtered_projects])
+ self.ui.create_metric_card(
+ "إجمالي الميزانية",
+ f"{total_budget/1000000:.1f} مليون ريال",
+ None,
+ self.ui.COLORS['accent']
+ )
+
+ with col4:
+ avg_completion = np.mean([p["completion"] for p in filtered_projects])
+ self.ui.create_metric_card(
+ "متوسط نسبة الإنجاز",
+ f"{avg_completion:.1f}%",
+ None,
+ self.ui.COLORS['success']
+ )
+
+ def show_location_details(self):
+ """عرض تفاصيل المواقع"""
+ st.markdown("### تفاصيل مواقع المشاريع")
+
+ # إنشاء جدول بيانات المشاريع
+ projects_df = pd.DataFrame(self.projects_data)
+ projects_df = projects_df.rename(columns={
+ "id": "رقم المشروع",
+ "name": "اسم المشروع",
+ "location": "الموقع",
+ "status": "الحالة",
+ "budget": "الميزانية (ريال)",
+ "completion": "نسبة الإنجاز (%)",
+ "client": "العميل",
+ "start_date": "تاريخ البدء",
+ "end_date": "تاريخ الانتهاء"
+ })
+
+ # حذف عمود الإحداثيات من العرض
+ projects_df = projects_df.drop(columns=["coordinates"])
+
+ # عرض الجدول
+ st.dataframe(
+ projects_df,
+ use_container_width=True,
+ hide_index=True
+ )
+
+ # إضافة خيار تصدير البيانات
+ col1, col2 = st.columns([1, 5])
+ with col1:
+ self.ui.create_button("تصدير البيانات", "primary")
+
+ # عرض تفاصيل مشروع محدد
+ st.markdown("### تفاصيل مشروع محدد")
+
+ selected_project = st.selectbox(
+ "اختر مشروعاً لعرض التفاصيل",
+ options=[p["name"] for p in self.projects_data]
+ )
+
+ # العثور على المشروع المحدد
+ project = next((p for p in self.projects_data if p["name"] == selected_project), None)
+
+ if project:
+ col1, col2 = st.columns([2, 1])
+
+ with col1:
+ # عرض تفاصيل المشروع
+ st.markdown(f"#### {project['name']}")
+ st.markdown(f"**الموقع:** {project['location']}")
+ st.markdown(f"**الحالة:** {project['status']}")
+ st.markdown(f"**الميزانية:** {project['budget']:,} ريال")
+ st.markdown(f"**نسبة الإنجاز:** {project['completion']}%")
+ st.markdown(f"**العميل:** {project['client']}")
+ st.markdown(f"**تاريخ البدء:** {project['start_date']}")
+ st.markdown(f"**تاريخ الانتهاء:** {project['end_date']}")
+
+ # أزرار الإجراءات
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ self.ui.create_button("تعديل البيانات", "primary")
+ with col2:
+ self.ui.create_button("عرض المستندات", "secondary")
+ with col3:
+ self.ui.create_button("تقرير الموقع", "accent")
+
+ with col2:
+ # عرض خريطة مصغرة للمشروع
+ m = folium.Map(location=project["coordinates"], zoom_start=12)
+ folium.Marker(
+ location=project["coordinates"],
+ tooltip=project["name"],
+ icon=folium.Icon(color="red", icon="info-sign")
+ ).add_to(m)
+ folium_static(m, width=300, height=300)
+
+ def add_new_location(self):
+ """إضافة موقع جديد"""
+ st.markdown("### إضافة موقع مشروع جديد")
+
+ # نموذج إضافة موقع جديد
+ with st.form("new_location_form"):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_id = st.text_input("رقم المشروع", value="P00" + str(len(self.projects_data) + 1))
+ project_name = st.text_input("اسم المشروع")
+ location = st.text_input("الموقع")
+ status = st.selectbox(
+ "الحالة",
+ options=["جاري التنفيذ", "قيد الدراسة", "مكتمل"]
+ )
+ budget = st.number_input("الميزانية (ريال)", min_value=0, step=100000)
+
+ with col2:
+ completion = st.slider("نسبة الإنجاز (%)", 0, 100, 0)
+ client = st.text_input("العميل")
+ start_date = st.date_input("تاريخ البدء")
+ end_date = st.date_input("تاريخ الانتهاء")
+
+ st.markdown("### تحديد الموقع على الخريطة")
+ st.markdown("انقر على الخريطة لتحديد موقع المشروع أو أدخل الإحداثيات يدوياً")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ latitude = st.number_input("خط العرض", value=24.0, format="%.4f")
+
+ with col2:
+ longitude = st.number_input("خط الطول", value=45.0, format="%.4f")
+
+ # عرض الخريطة لتحديد الموقع
+ m = folium.Map(location=[latitude, longitude], zoom_start=5)
+ folium.Marker(
+ location=[latitude, longitude],
+ tooltip="موقع المشروع الجديد",
+ icon=folium.Icon(color="red", icon="info-sign")
+ ).add_to(m)
+ folium_static(m, width=700, height=300)
+
+ # زر الإرسال
+ submit_button = st.form_submit_button("إضافة المشروع")
+
+ if submit_button:
+ # إضافة المشروع الجديد (في تطبيق حقيقي، سيتم حفظ البيانات في قاعدة البيانات)
+ st.success("تم إضافة المشروع بنجاح!")
+
+ # إعادة تعيين النموذج
+ st.experimental_rerun()
+
+ def analyze_regions(self):
+ """تحليل المناطق"""
+ st.markdown("### تحليل المناطق")
+
+ # إنشاء بيانات المناطق (نموذجية)
+ regions_data = {
+ "المنطقة": ["الرياض", "مكة المكرمة", "المدينة المنورة", "القصيم", "المنطقة الشرقية", "عسير", "تبوك", "حائل", "الحدود الشمالية", "جازان", "نجران", "الباحة", "الجوف"],
+ "عدد المشاريع": [15, 12, 8, 5, 18, 7, 4, 3, 2, 6, 3, 2, 3],
+ "إجمالي الميزانية (مليون ريال)": [120, 95, 45, 30, 150, 40, 25, 18, 12, 35, 20, 15, 22],
+ "متوسط مدة المشروع (شهر)": [18, 16, 14, 12, 20, 15, 12, 10, 9, 14, 12, 10, 11]
+ }
+
+ regions_df = pd.DataFrame(regions_data)
+
+ # عرض خريطة حرارية للمناطق
+ st.markdown("#### توزيع المشاريع حسب المناطق")
+
+ # في تطبيق حقيقي، يمكن استخدام خريطة حرارية حقيقية للمملكة
+ st.image("https://via.placeholder.com/800x400?text=خريطة+حرارية+للمشاريع+حسب+المناطق", use_column_width=True)
+
+ # عرض إحصائيات المناطق
+ st.markdown("#### إحصائيات المناطق")
+
+ # عرض الجدول
+ st.dataframe(
+ regions_df,
+ use_container_width=True,
+ hide_index=True
+ )
+
+ # عرض رسوم بيانية للمقارنة
+ st.markdown("#### مقارنة المناطق")
+
+ chart_type = st.radio(
+ "نوع الرسم البياني",
+ options=["عدد المشاريع", "إجمالي الميزانية", "متوسط مدة المشروع"],
+ horizontal=True
+ )
+
+ if chart_type == "عدد المشاريع":
+ chart_data = regions_df[["المنطقة", "عدد المشاريع"]].sort_values(by="عدد المشاريع", ascending=False)
+ st.bar_chart(chart_data.set_index("المنطقة"))
+ elif chart_type == "إجمالي الميزانية":
+ chart_data = regions_df[["المنطقة", "إجمالي الميزانية (مليون ريال)"]].sort_values(by="إجمالي الميزانية (مليون ريال)", ascending=False)
+ st.bar_chart(chart_data.set_index("المنطقة"))
+ else:
+ chart_data = regions_df[["المنطقة", "متوسط مدة المشروع (شهر)"]].sort_values(by="متوسط مدة المشروع (شهر)", ascending=False)
+ st.bar_chart(chart_data.set_index("المنطقة"))
+
+ # تحليل الكثافة
+ st.markdown("#### تحليل كثافة المشاريع")
+ st.markdown("""
+ يوضح هذا التحليل توزيع المشاريع حسب المناطق الجغرافية، مما يساعد في:
+ - تحديد المناطق ذات النشاط العالي
+ - تحديد فرص النمو في المناطق الأقل نشاطاً
+ - تخطيط الموارد بناءً على التوزيع الجغرافي
+ """)
+
+ # في تطبيق حقيقي، يمكن إضافة تحليلات أكثر تفصيلاً
+
+# تشغيل التطبيق
+if __name__ == "__main__":
+ maps_app = MapsApp()
+ maps_app.run()
diff --git a/modules/notifications/notifications_app.py b/modules/notifications/notifications_app.py
index ae2a8f393ee30c4dd29441bc473c5a4d102c1b16..07e3ac4b74d1f3154e5c2411274cad4e55eab313 100644
--- a/modules/notifications/notifications_app.py
+++ b/modules/notifications/notifications_app.py
@@ -1,707 +1,707 @@
-"""
-وحدة الإشعارات الذكية - نظام تحليل المناقصات
-"""
-
-import streamlit as st
-import pandas as pd
-import datetime
-import json
-import os
-import sys
-from pathlib import Path
-
-# إضافة مسار المشروع للنظام
-sys.path.append(str(Path(__file__).parent.parent))
-
-# استيراد محسن واجهة المستخدم
-from styling.enhanced_ui import UIEnhancer
-
-class NotificationsApp:
- """تطبيق الإشعارات الذكية"""
-
- def __init__(self):
- """تهيئة تطبيق الإشعارات الذكية"""
- self.ui = UIEnhancer(page_title="الإشعارات الذكية - نظام تحليل المناقصات", page_icon="🔔")
-
- # تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
- if 'theme' not in st.session_state:
- st.session_state.theme = 'light'
-
- self.ui.apply_theme_colors()
-
- # بيانات الإشعارات (نموذجية)
- self.notifications_data = [
- {
- "id": "N001",
- "title": "موعد تسليم مناقصة",
- "message": "موعد تسليم مناقصة T-2025-001 (إنشاء مبنى إداري) بعد 5 أيام",
- "type": "deadline",
- "priority": "high",
- "related_entity": "T-2025-001",
- "created_at": "2025-03-25T10:30:00",
- "is_read": False
- },
- {
- "id": "N002",
- "title": "ترسية مناقصة",
- "message": "تم ترسية مناقصة T-2025-003 (توريد معدات) بنجاح",
- "type": "award",
- "priority": "medium",
- "related_entity": "T-2025-003",
- "created_at": "2025-03-28T14:15:00",
- "is_read": True
- },
- {
- "id": "N003",
- "title": "تحديث مستندات",
- "message": "تم تحديث مستندات مناقصة T-2025-002 (صيانة طرق)",
- "type": "document",
- "priority": "medium",
- "related_entity": "T-2025-002",
- "created_at": "2025-03-29T09:45:00",
- "is_read": False
- },
- {
- "id": "N004",
- "title": "تغيير في المواصفات",
- "message": "تم تغيير المواصفات الفنية لمناقصة T-2025-001 (إنشاء مبنى إداري)",
- "type": "change",
- "priority": "high",
- "related_entity": "T-2025-001",
- "created_at": "2025-03-27T11:20:00",
- "is_read": False
- },
- {
- "id": "N005",
- "title": "تأخير في المشروع",
- "message": "تأخير في تنفيذ مشروع P002 (تطوير طريق الملك فهد - جدة)",
- "type": "delay",
- "priority": "high",
- "related_entity": "P002",
- "created_at": "2025-03-26T16:10:00",
- "is_read": True
- },
- {
- "id": "N006",
- "title": "اكتمال مرحلة",
- "message": "اكتمال مرحلة الأساسات في مشروع P001 (إنشاء مبنى إداري - الرياض)",
- "type": "milestone",
- "priority": "low",
- "related_entity": "P001",
- "created_at": "2025-03-24T13:30:00",
- "is_read": True
- },
- {
- "id": "N007",
- "title": "طلب معلومات إضافية",
- "message": "طلب معلومات إضافية لمناقصة T-2025-004 (تجهيز مختبرات)",
- "type": "request",
- "priority": "medium",
- "related_entity": "T-2025-004",
- "created_at": "2025-03-30T08:15:00",
- "is_read": False
- },
- {
- "id": "N008",
- "title": "تحديث أسعار المواد",
- "message": "تم تحديث أسعار مواد البناء في قاعدة البيانات",
- "type": "update",
- "priority": "low",
- "related_entity": "DB-MATERIALS",
- "created_at": "2025-03-29T15:40:00",
- "is_read": False
- },
- {
- "id": "N009",
- "title": "اجتماع فريق العمل",
- "message": "اجتماع فريق العمل لمناقشة مناقصة T-2025-001 غداً الساعة 10:00 صباحاً",
- "type": "meeting",
- "priority": "medium",
- "related_entity": "T-2025-001",
- "created_at": "2025-03-28T16:20:00",
- "is_read": True
- },
- {
- "id": "N010",
- "title": "تغيير في الميزانية",
- "message": "تم تغيير الميزانية المخصصة لمشروع P004 (بناء مدرسة - أبها)",
- "type": "budget",
- "priority": "high",
- "related_entity": "P004",
- "created_at": "2025-03-25T14:50:00",
- "is_read": False
- }
- ]
-
- # إعدادات الإشعارات (نموذجية)
- self.notification_settings = {
- "deadline": True,
- "award": True,
- "document": True,
- "change": True,
- "delay": True,
- "milestone": True,
- "request": True,
- "update": True,
- "meeting": True,
- "budget": True,
- "email_notifications": True,
- "sms_notifications": False,
- "push_notifications": True,
- "notification_frequency": "realtime"
- }
-
- def run(self):
- """تشغيل تطبيق الإشعارات الذكية"""
- # إضافة زر تبديل السمة في أعلى الصفحة
- col1, col2, col3 = st.columns([1, 8, 1])
- with col3:
- if st.button("🌓 تبديل السمة"):
- # تبديل السمة
- if st.session_state.theme == "light":
- st.session_state.theme = "dark"
- else:
- st.session_state.theme = "light"
-
- # تطبيق السمة الجديدة
- self.ui.theme_mode = st.session_state.theme
- self.ui.apply_theme_colors()
- st.rerun()
-
- # إنشاء ترويسة الصفحة
- self.ui.create_header("الإشعارات الذكية", "إدارة ومتابعة الإشعارات والتنبيهات")
-
- # إنشاء علامات تبويب للوظائف المختلفة
- tabs = st.tabs(["الإشعارات الحالية", "إعدادات الإشعارات", "إنشاء إشعار", "سجل الإشعارات"])
-
- # علامة تبويب الإشعارات الحالية
- with tabs[0]:
- self.show_current_notifications()
-
- # علامة تبويب إعدادات الإشعارات
- with tabs[1]:
- self.show_notification_settings()
-
- # علامة تبويب إنشاء إشعار
- with tabs[2]:
- self.create_notification()
-
- # علامة تبويب سجل الإشعارات
- with tabs[3]:
- self.show_notification_history()
-
- def show_current_notifications(self):
- """عرض الإشعارات الحالية"""
- st.markdown("### الإشعارات الحالية")
-
- # إنشاء فلاتر للإشعارات
- col1, col2, col3 = st.columns(3)
-
- with col1:
- type_filter = st.multiselect(
- "نوع الإشعار",
- options=["الكل", "موعد نهائي", "ترسية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"],
- default=["الكل"]
- )
-
- with col2:
- priority_filter = st.multiselect(
- "الأولوية",
- options=["الكل", "عالية", "متوسطة", "منخفضة"],
- default=["الكل"]
- )
-
- with col3:
- read_filter = st.radio(
- "الحالة",
- options=["الكل", "غير مقروءة", "مقروءة"],
- horizontal=True
- )
-
- # تطبيق الفلاتر
- filtered_notifications = self.notifications_data
-
- # تحويل أنواع الإشعارات من الإنجليزية إلى العربية للفلترة
- type_mapping = {
- "موعد نهائي": "deadline",
- "ترسية": "award",
- "مستند": "document",
- "تغيير": "change",
- "تأخير": "delay",
- "مرحلة": "milestone",
- "طلب": "request",
- "تحديث": "update",
- "اجتماع": "meeting",
- "ميزانية": "budget"
- }
-
- # تحويل الأولويات من العربية إلى الإنجليزية للفلترة
- priority_mapping = {
- "عالية": "high",
- "متوسطة": "medium",
- "منخفضة": "low"
- }
-
- if "الكل" not in type_filter and type_filter:
- filtered_types = [type_mapping[t] for t in type_filter if t in type_mapping]
- filtered_notifications = [n for n in filtered_notifications if n["type"] in filtered_types]
-
- if "الكل" not in priority_filter and priority_filter:
- filtered_priorities = [priority_mapping[p] for p in priority_filter if p in priority_mapping]
- filtered_notifications = [n for n in filtered_notifications if n["priority"] in filtered_priorities]
-
- if read_filter == "غير مقروءة":
- filtered_notifications = [n for n in filtered_notifications if not n["is_read"]]
- elif read_filter == "مقروءة":
- filtered_notifications = [n for n in filtered_notifications if n["is_read"]]
-
- # عرض عدد الإشعارات غير المقروءة
- unread_count = len([n for n in filtered_notifications if not n["is_read"]])
-
- st.markdown(f"**عدد الإشعارات غير المقروءة:** {unread_count}")
-
- # زر تحديث وتعليم الكل كمقروء
- col1, col2 = st.columns([1, 1])
- with col1:
- if st.button("تحديث الإشعارات", use_container_width=True):
- st.success("تم تحديث الإشعارات بنجاح")
-
- with col2:
- if st.button("تعليم الكل كمقروء", use_container_width=True):
- st.success("تم تعليم جميع الإشعارات كمقروءة")
-
- # عرض الإشعارات
- if not filtered_notifications:
- st.info("لا توجد إشعارات تطابق الفلاتر المحددة")
- else:
- for notification in filtered_notifications:
- self.display_notification(notification)
-
- def display_notification(self, notification):
- """عرض إشعار واحد"""
- # تحديد لون الإشعار بناءً على الأولوية
- if notification["priority"] == "high":
- color = self.ui.COLORS['danger']
- priority_text = "عالية"
- elif notification["priority"] == "medium":
- color = self.ui.COLORS['warning']
- priority_text = "متوسطة"
- else:
- color = self.ui.COLORS['secondary']
- priority_text = "منخفضة"
-
- # تحويل نوع الإشعار إلى العربية
- type_mapping = {
- "deadline": "موعد نهائي",
- "award": "ترسية",
- "document": "مستند",
- "change": "تغيير",
- "delay": "تأخير",
- "milestone": "مرحلة",
- "request": "طلب",
- "update": "تحديث",
- "meeting": "اجتماع",
- "budget": "ميزانية"
- }
-
- notification_type = type_mapping.get(notification["type"], notification["type"])
-
- # تحويل التاريخ إلى تنسيق مناسب
- created_at = datetime.datetime.fromisoformat(notification["created_at"])
- formatted_date = created_at.strftime("%Y-%m-%d %H:%M")
-
- # تحديد أيقونة الإشعار
- icon_mapping = {
- "deadline": "⏰",
- "award": "🏆",
- "document": "📄",
- "change": "🔄",
- "delay": "⚠️",
- "milestone": "🏁",
- "request": "❓",
- "update": "🔄",
- "meeting": "👥",
- "budget": "💰"
- }
-
- icon = icon_mapping.get(notification["type"], "📌")
-
- # إنشاء بطاقة الإشعار
- st.markdown(
- f"""
-
-
-
-
{icon} {notification['title']}
-
{notification['message']}
-
- النوع: {notification_type}
- الأولوية: {priority_text}
- التاريخ: {formatted_date}
-
-
-
- ✓
- 🗑️
-
-
-
- """,
- unsafe_allow_html=True
- )
-
- def show_notification_settings(self):
- """عرض إعدادات الإشعارات"""
- st.markdown("### إعدادات الإشعارات")
-
- # إنشاء نموذج الإعدادات
- with st.form("notification_settings_form"):
- st.markdown("#### أنواع الإشعارات")
-
- col1, col2 = st.columns(2)
-
- with col1:
- deadline = st.checkbox("المواعيد النهائية", value=self.notification_settings["deadline"])
- award = st.checkbox("ترسية المناقصات", value=self.notification_settings["award"])
- document = st.checkbox("تحديثات المستندات", value=self.notification_settings["document"])
- change = st.checkbox("التغييرات في المواصفات", value=self.notification_settings["change"])
- delay = st.checkbox("التأخيرات في المشاريع", value=self.notification_settings["delay"])
-
- with col2:
- milestone = st.checkbox("اكتمال المراحل", value=self.notification_settings["milestone"])
- request = st.checkbox("طلبات المعلومات", value=self.notification_settings["request"])
- update = st.checkbox("تحديثات النظام", value=self.notification_settings["update"])
- meeting = st.checkbox("الاجتماعات", value=self.notification_settings["meeting"])
- budget = st.checkbox("تغييرات الميزانية", value=self.notification_settings["budget"])
-
- st.markdown("#### طرق الإشعار")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- email_notifications = st.checkbox("البريد الإلكتروني", value=self.notification_settings["email_notifications"])
-
- with col2:
- sms_notifications = st.checkbox("الرسائل النصية", value=self.notification_settings["sms_notifications"])
-
- with col3:
- push_notifications = st.checkbox("إشعارات الويب", value=self.notification_settings["push_notifications"])
-
- st.markdown("#### تكرار الإشعارات")
-
- notification_frequency = st.radio(
- "تكرار الإشعارات",
- options=["في الوقت الحقيقي", "مرة واحدة يومياً", "مرة واحدة أسبوعياً"],
- index=0 if self.notification_settings["notification_frequency"] == "realtime" else 1 if self.notification_settings["notification_frequency"] == "daily" else 2,
- horizontal=True
- )
-
- # زر حفظ الإعدادات
- submit_button = st.form_submit_button("حفظ الإعدادات")
-
- if submit_button:
- # تحديث الإعدادات (في تطبيق حقيقي، سيتم حفظ الإعدادات في قاعدة البيانات)
- self.notification_settings.update({
- "deadline": deadline,
- "award": award,
- "document": document,
- "change": change,
- "delay": delay,
- "milestone": milestone,
- "request": request,
- "update": update,
- "meeting": meeting,
- "budget": budget,
- "email_notifications": email_notifications,
- "sms_notifications": sms_notifications,
- "push_notifications": push_notifications,
- "notification_frequency": "realtime" if notification_frequency == "في الوقت الحقيقي" else "daily" if notification_frequency == "مرة واحدة يومياً" else "weekly"
- })
-
- st.success("تم حفظ الإعدادات بنجاح")
-
- # إعدادات متقدمة
- st.markdown("### إعدادات متقدمة")
-
- with st.expander("إعدادات متقدمة"):
- st.markdown("#### جدولة الإشعارات")
-
- col1, col2 = st.columns(2)
-
- with col1:
- st.time_input("وقت الإشعارات اليومية", datetime.time(9, 0))
-
- with col2:
- st.selectbox(
- "يوم الإشعارات الأسبوعية",
- options=["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"],
- index=0
- )
-
- st.markdown("#### فلترة الإشعارات")
-
- min_priority = st.select_slider(
- "الحد الأدنى للأولوية",
- options=["منخفضة", "متوسطة", "عالية"],
- value="منخفضة"
- )
-
- st.markdown("#### حفظ الإشعارات")
-
- retention_period = st.slider(
- "فترة الاحتفاظ بالإشعارات (بالأيام)",
- min_value=7,
- max_value=365,
- value=90,
- step=1
- )
-
- if st.button("حفظ الإعدادات المتقدمة"):
- st.success("تم حفظ الإعدادات المتقدمة بنجاح")
-
- def create_notification(self):
- """إنشاء إشعار جديد"""
- st.markdown("### إنشاء إشعار جديد")
-
- # إنشاء نموذج إشعار جديد
- with st.form("new_notification_form"):
- title = st.text_input("عنوان الإشعار")
- message = st.text_area("نص الإشعار")
-
- col1, col2 = st.columns(2)
-
- with col1:
- notification_type = st.selectbox(
- "نوع الإشعار",
- options=["موعد نهائي", "ترسية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"]
- )
-
- # تحويل نوع الإشعار إلى الإنجليزية
- type_mapping = {
- "موعد نهائي": "deadline",
- "ترسية": "award",
- "مستند": "document",
- "تغيير": "change",
- "تأخير": "delay",
- "مرحلة": "milestone",
- "طلب": "request",
- "تحديث": "update",
- "اجتماع": "meeting",
- "ميزانية": "budget"
- }
-
- notification_type_en = type_mapping.get(notification_type, "deadline")
-
- priority = st.selectbox(
- "الأولوية",
- options=["عالية", "متوسطة", "منخفضة"]
- )
-
- # تحويل الأولوية إلى الإنجليزية
- priority_mapping = {
- "عالية": "high",
- "متوسطة": "medium",
- "منخفضة": "low"
- }
-
- priority_en = priority_mapping.get(priority, "medium")
-
- with col2:
- related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة، رقم المشروع)")
-
- notification_date = st.date_input(
- "تاريخ الإشعار",
- value=datetime.datetime.now().date()
- )
-
- notification_time = st.time_input(
- "وقت الإشعار",
- value=datetime.datetime.now().time()
- )
-
- # زر إنشاء الإشعار
- submit_button = st.form_submit_button("إنشاء الإشعار")
-
- if submit_button and title and message:
- # إنشاء إشعار جديد (في تطبيق حقيقي، سيتم حفظ الإشعار في قاعدة البيانات)
- new_id = f"N{len(self.notifications_data) + 1:03d}"
-
- # تحويل التاريخ والوقت إلى تنسيق ISO
- notification_datetime = datetime.datetime.combine(notification_date, notification_time)
- notification_datetime_iso = notification_datetime.isoformat()
-
- # إضافة الإشعار الجديد إلى قائمة الإشعارات
- self.notifications_data.append({
- "id": new_id,
- "title": title,
- "message": message,
- "type": notification_type_en,
- "priority": priority_en,
- "related_entity": related_entity,
- "created_at": notification_datetime_iso,
- "is_read": False
- })
-
- st.success("تم إنشاء الإشعار بنجاح")
-
- # عرض الإشعار الجديد
- st.markdown("### الإشعار الجديد")
- self.display_notification(self.notifications_data[-1])
-
- # إنشاء إشعارات متعددة
- st.markdown("### إنشاء إشعارات متعددة")
-
- with st.expander("إنشاء إشعارات متعددة"):
- st.markdown("#### تحميل ملف إشعارات")
-
- uploaded_file = st.file_uploader("قم بتحميل ملف إشعارات (CSV, JSON)", type=["csv", "json"])
-
- if uploaded_file is not None:
- if st.button("استيراد الإشعارات"):
- st.success("تم استيراد الإشعارات بنجاح")
-
- st.markdown("#### إنشاء إشعارات من قالب")
-
- template_type = st.selectbox(
- "نوع القالب",
- options=["إشعارات المواعيد النهائية", "إشعارات الاجتماعات", "إشعارات التحديثات"]
- )
-
- if st.button("إنشاء إشعارات من القالب"):
- st.success("تم إنشاء الإشعارات من القالب بنجاح")
-
- def show_notification_history(self):
- """عرض سجل الإشعارات"""
- st.markdown("### سجل الإشعارات")
-
- # إنشاء فلاتر للسجل
- col1, col2 = st.columns(2)
-
- with col1:
- date_range = st.date_input(
- "نطاق التاريخ",
- value=(
- datetime.datetime.now().date() - datetime.timedelta(days=30),
- datetime.datetime.now().date()
- )
- )
-
- with col2:
- entity_filter = st.text_input("الكيان المرتبط")
-
- # تحويل البيانات إلى DataFrame
- notifications_df = pd.DataFrame(self.notifications_data)
-
- # تحويل عمود التاريخ إلى نوع datetime
- notifications_df["created_at"] = pd.to_datetime(notifications_df["created_at"])
-
- # تطبيق فلتر التاريخ
- if len(date_range) == 2:
- start_date, end_date = date_range
- start_date = pd.to_datetime(start_date)
- end_date = pd.to_datetime(end_date) + pd.Timedelta(days=1) - pd.Timedelta(seconds=1)
-
- notifications_df = notifications_df[
- (notifications_df["created_at"] >= start_date) &
- (notifications_df["created_at"] <= end_date)
- ]
-
- # تطبيق فلتر الكيان المرتبط
- if entity_filter:
- notifications_df = notifications_df[
- notifications_df["related_entity"].str.contains(entity_filter, case=False)
- ]
-
- # تحويل أنواع الإشعارات من الإنجليزية إلى العربية
- type_mapping = {
- "deadline": "موعد نهائي",
- "award": "ترسية",
- "document": "مستند",
- "change": "تغيير",
- "delay": "تأخير",
- "milestone": "مرحلة",
- "request": "طلب",
- "update": "تحديث",
- "meeting": "اجتماع",
- "budget": "ميزانية"
- }
-
- notifications_df["type_ar"] = notifications_df["type"].map(type_mapping)
-
- # تحويل الأولويات من الإنجليزية إلى العربية
- priority_mapping = {
- "high": "عالية",
- "medium": "متوسطة",
- "low": "منخفضة"
- }
-
- notifications_df["priority_ar"] = notifications_df["priority"].map(priority_mapping)
-
- # تحويل حالة القراءة إلى نص
- notifications_df["is_read_text"] = notifications_df["is_read"].map({True: "مقروءة", False: "غير مقروءة"})
-
- # تنسيق عمود التاريخ
- notifications_df["created_at_formatted"] = notifications_df["created_at"].dt.strftime("%Y-%m-%d %H:%M")
-
- # إنشاء DataFrame للعرض
- display_df = notifications_df[[
- "id", "title", "type_ar", "priority_ar", "related_entity",
- "created_at_formatted", "is_read_text"
- ]].rename(columns={
- "id": "الرقم",
- "title": "العنوان",
- "type_ar": "النوع",
- "priority_ar": "الأولوية",
- "related_entity": "الكيان المرتبط",
- "created_at_formatted": "التاريخ",
- "is_read_text": "الحالة"
- })
-
- # عرض الجدول
- st.dataframe(display_df, use_container_width=True, hide_index=True)
-
- # إحصائيات الإشعارات
- st.markdown("### إحصائيات الإشعارات")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- total_count = len(notifications_df)
- st.metric("إجمالي الإشعارات", total_count)
-
- with col2:
- read_count = len(notifications_df[notifications_df["is_read"] == True])
- st.metric("الإشعارات المقروءة", read_count)
-
- with col3:
- unread_count = len(notifications_df[notifications_df["is_read"] == False])
- st.metric("الإشعارات غير المقروءة", unread_count)
-
- # رسم بياني لتوزيع الإشعارات حسب النوع
- st.markdown("#### توزيع الإشعارات حسب النوع")
-
- type_counts = notifications_df["type_ar"].value_counts().reset_index()
- type_counts.columns = ["النوع", "العدد"]
-
- st.bar_chart(type_counts, x="النوع", y="العدد")
-
- # رسم بياني لتوزيع الإشعارات حسب الأولوية
- st.markdown("#### توزيع الإشعارات حسب الأولوية")
-
- priority_counts = notifications_df["priority_ar"].value_counts().reset_index()
- priority_counts.columns = ["الأولوية", "العدد"]
-
- st.bar_chart(priority_counts, x="الأولوية", y="العدد")
-
- # خيارات التصدير
- st.markdown("### تصدير البيانات")
-
- col1, col2 = st.columns(2)
-
- with col1:
- if st.button("تصدير إلى CSV"):
- st.success("تم تصدير البيانات إلى CSV بنجاح")
-
- with col2:
- if st.button("تصدير إلى Excel"):
- st.success("تم تصدير البيانات إلى Excel بنجاح")
+"""
+وحدة الإشعارات الذكية - نظام تحليل المناقصات
+"""
+
+import streamlit as st
+import pandas as pd
+import datetime
+import json
+import os
+import sys
+from pathlib import Path
+
+# إضافة مسار المشروع للنظام
+sys.path.append(str(Path(__file__).parent.parent))
+
+# استيراد محسن واجهة المستخدم
+from styling.enhanced_ui import UIEnhancer
+
+class NotificationsApp:
+ """تطبيق الإشعارات الذكية"""
+
+ def __init__(self):
+ """تهيئة تطبيق الإشعارات الذكية"""
+ self.ui = UIEnhancer(page_title="الإشعارات الذكية - نظام تحليل المناقصات", page_icon="🔔")
+
+ # تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
+ if 'theme' not in st.session_state:
+ st.session_state.theme = 'light'
+
+ self.ui.apply_theme_colors()
+
+ # بيانات الإشعارات (نموذجية)
+ self.notifications_data = [
+ {
+ "id": "N001",
+ "title": "موعد تسليم مناقصة",
+ "message": "موعد تسليم مناقصة T-2025-001 (إنشاء مبنى إداري) بعد 5 أيام",
+ "type": "deadline",
+ "priority": "high",
+ "related_entity": "T-2025-001",
+ "created_at": "2025-03-25T10:30:00",
+ "is_read": False
+ },
+ {
+ "id": "N002",
+ "title": "ترسية مناقصة",
+ "message": "تم ترسية مناقصة T-2025-003 (توريد معدات) بنجاح",
+ "type": "award",
+ "priority": "medium",
+ "related_entity": "T-2025-003",
+ "created_at": "2025-03-28T14:15:00",
+ "is_read": True
+ },
+ {
+ "id": "N003",
+ "title": "تحديث مستندات",
+ "message": "تم تحديث مستندات مناقصة T-2025-002 (صيانة طرق)",
+ "type": "document",
+ "priority": "medium",
+ "related_entity": "T-2025-002",
+ "created_at": "2025-03-29T09:45:00",
+ "is_read": False
+ },
+ {
+ "id": "N004",
+ "title": "تغيير في المواصفات",
+ "message": "تم تغيير المواصفات الفنية لمناقصة T-2025-001 (إنشاء مبنى إداري)",
+ "type": "change",
+ "priority": "high",
+ "related_entity": "T-2025-001",
+ "created_at": "2025-03-27T11:20:00",
+ "is_read": False
+ },
+ {
+ "id": "N005",
+ "title": "تأخير في المشروع",
+ "message": "تأخير في تنفيذ مشروع P002 (تطوير طريق الملك فهد - جدة)",
+ "type": "delay",
+ "priority": "high",
+ "related_entity": "P002",
+ "created_at": "2025-03-26T16:10:00",
+ "is_read": True
+ },
+ {
+ "id": "N006",
+ "title": "اكتمال مرحلة",
+ "message": "اكتمال مرحلة الأساسات في مشروع P001 (إنشاء مبنى إداري - الرياض)",
+ "type": "milestone",
+ "priority": "low",
+ "related_entity": "P001",
+ "created_at": "2025-03-24T13:30:00",
+ "is_read": True
+ },
+ {
+ "id": "N007",
+ "title": "طلب معلومات إضافية",
+ "message": "طلب معلومات إضافية لمناقصة T-2025-004 (تجهيز مختبرات)",
+ "type": "request",
+ "priority": "medium",
+ "related_entity": "T-2025-004",
+ "created_at": "2025-03-30T08:15:00",
+ "is_read": False
+ },
+ {
+ "id": "N008",
+ "title": "تحديث أسعار المواد",
+ "message": "تم تحديث أسعار مواد البناء في قاعدة البيانات",
+ "type": "update",
+ "priority": "low",
+ "related_entity": "DB-MATERIALS",
+ "created_at": "2025-03-29T15:40:00",
+ "is_read": False
+ },
+ {
+ "id": "N009",
+ "title": "اجتماع فريق العمل",
+ "message": "اجتماع فريق العمل لمناقشة مناقصة T-2025-001 غداً الساعة 10:00 صباحاً",
+ "type": "meeting",
+ "priority": "medium",
+ "related_entity": "T-2025-001",
+ "created_at": "2025-03-28T16:20:00",
+ "is_read": True
+ },
+ {
+ "id": "N010",
+ "title": "تغيير في الميزانية",
+ "message": "تم تغيير الميزانية المخصصة لمشروع P004 (بناء مدرسة - أبها)",
+ "type": "budget",
+ "priority": "high",
+ "related_entity": "P004",
+ "created_at": "2025-03-25T14:50:00",
+ "is_read": False
+ }
+ ]
+
+ # إعدادات الإشعارات (نموذجية)
+ self.notification_settings = {
+ "deadline": True,
+ "award": True,
+ "document": True,
+ "change": True,
+ "delay": True,
+ "milestone": True,
+ "request": True,
+ "update": True,
+ "meeting": True,
+ "budget": True,
+ "email_notifications": True,
+ "sms_notifications": False,
+ "push_notifications": True,
+ "notification_frequency": "realtime"
+ }
+
+ def run(self):
+ """تشغيل تطبيق الإشعارات الذكية"""
+ # إضافة زر تبديل السمة في أعلى الصفحة
+ col1, col2, col3 = st.columns([1, 8, 1])
+ with col3:
+ if st.button("🌓 تبديل السمة"):
+ # تبديل السمة
+ if st.session_state.theme == "light":
+ st.session_state.theme = "dark"
+ else:
+ st.session_state.theme = "light"
+
+ # تطبيق السمة الجديدة
+ self.ui.theme_mode = st.session_state.theme
+ self.ui.apply_theme_colors()
+ st.rerun()
+
+ # إنشاء ترويسة الصفحة
+ self.ui.create_header("الإشعارات الذكية", "إدارة ومتابعة الإشعارات والتنبيهات")
+
+ # إنشاء علامات تبويب للوظائف المختلفة
+ tabs = st.tabs(["الإشعارات الحالية", "إعدادات الإشعارات", "إنشاء إشعار", "سجل الإشعارات"])
+
+ # علامة تبويب الإشعارات الحالية
+ with tabs[0]:
+ self.show_current_notifications()
+
+ # علامة تبويب إعدادات الإشعارات
+ with tabs[1]:
+ self.show_notification_settings()
+
+ # علامة تبويب إنشاء إشعار
+ with tabs[2]:
+ self.create_notification()
+
+ # علامة تبويب سجل الإشعارات
+ with tabs[3]:
+ self.show_notification_history()
+
+ def show_current_notifications(self):
+ """عرض الإشعارات الحالية"""
+ st.markdown("### الإشعارات الحالية")
+
+ # إنشاء فلاتر للإشعارات
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ type_filter = st.multiselect(
+ "نوع الإشعار",
+ options=["الكل", "موعد نهائي", "ترسية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"],
+ default=["الكل"]
+ )
+
+ with col2:
+ priority_filter = st.multiselect(
+ "الأولوية",
+ options=["الكل", "عالية", "متوسطة", "منخفضة"],
+ default=["الكل"]
+ )
+
+ with col3:
+ read_filter = st.radio(
+ "الحالة",
+ options=["الكل", "غير مقروءة", "مقروءة"],
+ horizontal=True
+ )
+
+ # تطبيق الفلاتر
+ filtered_notifications = self.notifications_data
+
+ # تحويل أنواع الإشعارات من الإنجليزية إلى العربية للفلترة
+ type_mapping = {
+ "موعد نهائي": "deadline",
+ "ترسية": "award",
+ "مستند": "document",
+ "تغيير": "change",
+ "تأخير": "delay",
+ "مرحلة": "milestone",
+ "طلب": "request",
+ "تحديث": "update",
+ "اجتماع": "meeting",
+ "ميزانية": "budget"
+ }
+
+ # تحويل الأولويات من العربية إلى الإنجليزية للفلترة
+ priority_mapping = {
+ "عالية": "high",
+ "متوسطة": "medium",
+ "منخفضة": "low"
+ }
+
+ if "الكل" not in type_filter and type_filter:
+ filtered_types = [type_mapping[t] for t in type_filter if t in type_mapping]
+ filtered_notifications = [n for n in filtered_notifications if n["type"] in filtered_types]
+
+ if "الكل" not in priority_filter and priority_filter:
+ filtered_priorities = [priority_mapping[p] for p in priority_filter if p in priority_mapping]
+ filtered_notifications = [n for n in filtered_notifications if n["priority"] in filtered_priorities]
+
+ if read_filter == "غير مقروءة":
+ filtered_notifications = [n for n in filtered_notifications if not n["is_read"]]
+ elif read_filter == "مقروءة":
+ filtered_notifications = [n for n in filtered_notifications if n["is_read"]]
+
+ # عرض عدد الإشعارات غير المقروءة
+ unread_count = len([n for n in filtered_notifications if not n["is_read"]])
+
+ st.markdown(f"**عدد الإشعارات غير المقروءة:** {unread_count}")
+
+ # زر تحديث وتعليم الكل كمقروء
+ col1, col2 = st.columns([1, 1])
+ with col1:
+ if st.button("تحديث الإشعارات", use_container_width=True):
+ st.success("تم تحديث الإشعارات بنجاح")
+
+ with col2:
+ if st.button("تعليم الكل كمقروء", use_container_width=True):
+ st.success("تم تعليم جميع الإشعارات كمقروءة")
+
+ # عرض الإشعارات
+ if not filtered_notifications:
+ st.info("لا توجد إشعارات تطابق الفلاتر المحددة")
+ else:
+ for notification in filtered_notifications:
+ self.display_notification(notification)
+
+ def display_notification(self, notification):
+ """عرض إشعار واحد"""
+ # تحديد لون الإشعار بناءً على الأولوية
+ if notification["priority"] == "high":
+ color = self.ui.COLORS['danger']
+ priority_text = "عالية"
+ elif notification["priority"] == "medium":
+ color = self.ui.COLORS['warning']
+ priority_text = "متوسطة"
+ else:
+ color = self.ui.COLORS['secondary']
+ priority_text = "منخفضة"
+
+ # تحويل نوع الإشعار إلى العربية
+ type_mapping = {
+ "deadline": "موعد نهائي",
+ "award": "ترسية",
+ "document": "مستند",
+ "change": "تغيير",
+ "delay": "تأخير",
+ "milestone": "مرحلة",
+ "request": "طلب",
+ "update": "تحديث",
+ "meeting": "اجتماع",
+ "budget": "ميزانية"
+ }
+
+ notification_type = type_mapping.get(notification["type"], notification["type"])
+
+ # تحويل التاريخ إلى تنسيق مناسب
+ created_at = datetime.datetime.fromisoformat(notification["created_at"])
+ formatted_date = created_at.strftime("%Y-%m-%d %H:%M")
+
+ # تحديد أيقونة الإشعار
+ icon_mapping = {
+ "deadline": "⏰",
+ "award": "🏆",
+ "document": "📄",
+ "change": "🔄",
+ "delay": "⚠️",
+ "milestone": "🏁",
+ "request": "❓",
+ "update": "🔄",
+ "meeting": "👥",
+ "budget": "💰"
+ }
+
+ icon = icon_mapping.get(notification["type"], "📌")
+
+ # إنشاء بطاقة الإشعار
+ st.markdown(
+ f"""
+
+
+
+
{icon} {notification['title']}
+
{notification['message']}
+
+ النوع: {notification_type}
+ الأولوية: {priority_text}
+ التاريخ: {formatted_date}
+
+
+
+ ✓
+ 🗑️
+
+
+
+ """,
+ unsafe_allow_html=True
+ )
+
+ def show_notification_settings(self):
+ """عرض إعدادات الإشعارات"""
+ st.markdown("### إعدادات الإشعارات")
+
+ # إنشاء نموذج الإعدادات
+ with st.form("notification_settings_form"):
+ st.markdown("#### أنواع الإشعارات")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ deadline = st.checkbox("المواعيد النهائية", value=self.notification_settings["deadline"])
+ award = st.checkbox("ترسية المناقصات", value=self.notification_settings["award"])
+ document = st.checkbox("تحديثات المستندات", value=self.notification_settings["document"])
+ change = st.checkbox("التغييرات في المواصفات", value=self.notification_settings["change"])
+ delay = st.checkbox("التأخيرات في المشاريع", value=self.notification_settings["delay"])
+
+ with col2:
+ milestone = st.checkbox("اكتمال المراحل", value=self.notification_settings["milestone"])
+ request = st.checkbox("طلبات المعلومات", value=self.notification_settings["request"])
+ update = st.checkbox("تحديثات النظام", value=self.notification_settings["update"])
+ meeting = st.checkbox("الاجتماعات", value=self.notification_settings["meeting"])
+ budget = st.checkbox("تغييرات الميزانية", value=self.notification_settings["budget"])
+
+ st.markdown("#### طرق الإشعار")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ email_notifications = st.checkbox("البريد الإلكتروني", value=self.notification_settings["email_notifications"])
+
+ with col2:
+ sms_notifications = st.checkbox("الرسائل النصية", value=self.notification_settings["sms_notifications"])
+
+ with col3:
+ push_notifications = st.checkbox("إشعارات الويب", value=self.notification_settings["push_notifications"])
+
+ st.markdown("#### تكرار الإشعارات")
+
+ notification_frequency = st.radio(
+ "تكرار الإشعارات",
+ options=["في الوقت الحقيقي", "مرة واحدة يومياً", "مرة واحدة أسبوعياً"],
+ index=0 if self.notification_settings["notification_frequency"] == "realtime" else 1 if self.notification_settings["notification_frequency"] == "daily" else 2,
+ horizontal=True
+ )
+
+ # زر حفظ الإعدادات
+ submit_button = st.form_submit_button("حفظ الإعدادات")
+
+ if submit_button:
+ # تحديث الإعدادات (في تطبيق حقيقي، سيتم حفظ الإعدادات في قاعدة البيانات)
+ self.notification_settings.update({
+ "deadline": deadline,
+ "award": award,
+ "document": document,
+ "change": change,
+ "delay": delay,
+ "milestone": milestone,
+ "request": request,
+ "update": update,
+ "meeting": meeting,
+ "budget": budget,
+ "email_notifications": email_notifications,
+ "sms_notifications": sms_notifications,
+ "push_notifications": push_notifications,
+ "notification_frequency": "realtime" if notification_frequency == "في الوقت الحقيقي" else "daily" if notification_frequency == "مرة واحدة يومياً" else "weekly"
+ })
+
+ st.success("تم حفظ الإعدادات بنجاح")
+
+ # إعدادات متقدمة
+ st.markdown("### إعدادات متقدمة")
+
+ with st.expander("إعدادات متقدمة"):
+ st.markdown("#### جدولة الإشعارات")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.time_input("وقت الإشعارات اليومية", datetime.time(9, 0))
+
+ with col2:
+ st.selectbox(
+ "يوم الإشعارات الأسبوعية",
+ options=["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"],
+ index=0
+ )
+
+ st.markdown("#### فلترة الإشعارات")
+
+ min_priority = st.select_slider(
+ "الحد الأدنى للأولوية",
+ options=["منخفضة", "متوسطة", "عالية"],
+ value="منخفضة"
+ )
+
+ st.markdown("#### حفظ الإشعارات")
+
+ retention_period = st.slider(
+ "فترة الاحتفاظ بالإشعارات (بالأيام)",
+ min_value=7,
+ max_value=365,
+ value=90,
+ step=1
+ )
+
+ if st.button("حفظ الإعدادات المتقدمة"):
+ st.success("تم حفظ الإعدادات المتقدمة بنجاح")
+
+ def create_notification(self):
+ """إنشاء إشعار جديد"""
+ st.markdown("### إنشاء إشعار جديد")
+
+ # إنشاء نموذج إشعار جديد
+ with st.form("new_notification_form"):
+ title = st.text_input("عنوان الإشعار")
+ message = st.text_area("نص الإشعار")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ notification_type = st.selectbox(
+ "نوع الإشعار",
+ options=["موعد نهائي", "ترسية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"]
+ )
+
+ # تحويل نوع الإشعار إلى الإنجليزية
+ type_mapping = {
+ "موعد نهائي": "deadline",
+ "ترسية": "award",
+ "مستند": "document",
+ "تغيير": "change",
+ "تأخير": "delay",
+ "مرحلة": "milestone",
+ "طلب": "request",
+ "تحديث": "update",
+ "اجتماع": "meeting",
+ "ميزانية": "budget"
+ }
+
+ notification_type_en = type_mapping.get(notification_type, "deadline")
+
+ priority = st.selectbox(
+ "الأولوية",
+ options=["عالية", "متوسطة", "منخفضة"]
+ )
+
+ # تحويل الأولوية إلى الإنجليزية
+ priority_mapping = {
+ "عالية": "high",
+ "متوسطة": "medium",
+ "منخفضة": "low"
+ }
+
+ priority_en = priority_mapping.get(priority, "medium")
+
+ with col2:
+ related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة، رقم المشروع)")
+
+ notification_date = st.date_input(
+ "تاريخ الإشعار",
+ value=datetime.datetime.now().date()
+ )
+
+ notification_time = st.time_input(
+ "وقت الإشعار",
+ value=datetime.datetime.now().time()
+ )
+
+ # زر إنشاء الإشعار
+ submit_button = st.form_submit_button("إنشاء الإشعار")
+
+ if submit_button and title and message:
+ # إنشاء إشعار جديد (في تطبيق حقيقي، سيتم حفظ الإشعار في قاعدة البيانات)
+ new_id = f"N{len(self.notifications_data) + 1:03d}"
+
+ # تحويل التاريخ والوقت إلى تنسيق ISO
+ notification_datetime = datetime.datetime.combine(notification_date, notification_time)
+ notification_datetime_iso = notification_datetime.isoformat()
+
+ # إضافة الإشعار الجديد إلى قائمة الإشعارات
+ self.notifications_data.append({
+ "id": new_id,
+ "title": title,
+ "message": message,
+ "type": notification_type_en,
+ "priority": priority_en,
+ "related_entity": related_entity,
+ "created_at": notification_datetime_iso,
+ "is_read": False
+ })
+
+ st.success("تم إنشاء الإشعار بنجاح")
+
+ # عرض الإشعار الجديد
+ st.markdown("### الإشعار الجديد")
+ self.display_notification(self.notifications_data[-1])
+
+ # إنشاء إشعارات متعددة
+ st.markdown("### إنشاء إشعارات متعددة")
+
+ with st.expander("إنشاء إشعارات متعددة"):
+ st.markdown("#### تحميل ملف إشعارات")
+
+ uploaded_file = st.file_uploader("قم بتحميل ملف إشعارات (CSV, JSON)", type=["csv", "json"])
+
+ if uploaded_file is not None:
+ if st.button("استيراد الإشعارات"):
+ st.success("تم استيراد الإشعارات بنجاح")
+
+ st.markdown("#### إنشاء إشعارات من قالب")
+
+ template_type = st.selectbox(
+ "نوع القالب",
+ options=["إشعارات المواعيد النهائية", "إشعارات الاجتماعات", "إشعارات التحديثات"]
+ )
+
+ if st.button("إنشاء إشعارات من القالب"):
+ st.success("تم إنشاء الإشعارات من القالب بنجاح")
+
+ def show_notification_history(self):
+ """عرض سجل الإشعارات"""
+ st.markdown("### سجل الإشعارات")
+
+ # إنشاء فلاتر للسجل
+ col1, col2 = st.columns(2)
+
+ with col1:
+ date_range = st.date_input(
+ "نطاق التاريخ",
+ value=(
+ datetime.datetime.now().date() - datetime.timedelta(days=30),
+ datetime.datetime.now().date()
+ )
+ )
+
+ with col2:
+ entity_filter = st.text_input("الكيان المرتبط")
+
+ # تحويل البيانات إلى DataFrame
+ notifications_df = pd.DataFrame(self.notifications_data)
+
+ # تحويل عمود التاريخ إلى نوع datetime
+ notifications_df["created_at"] = pd.to_datetime(notifications_df["created_at"])
+
+ # تطبيق فلتر التاريخ
+ if len(date_range) == 2:
+ start_date, end_date = date_range
+ start_date = pd.to_datetime(start_date)
+ end_date = pd.to_datetime(end_date) + pd.Timedelta(days=1) - pd.Timedelta(seconds=1)
+
+ notifications_df = notifications_df[
+ (notifications_df["created_at"] >= start_date) &
+ (notifications_df["created_at"] <= end_date)
+ ]
+
+ # تطبيق فلتر الكيان المرتبط
+ if entity_filter:
+ notifications_df = notifications_df[
+ notifications_df["related_entity"].str.contains(entity_filter, case=False)
+ ]
+
+ # تحويل أنواع الإشعارات من الإنجليزية إلى العربية
+ type_mapping = {
+ "deadline": "موعد نهائي",
+ "award": "ترسية",
+ "document": "مستند",
+ "change": "تغيير",
+ "delay": "تأخير",
+ "milestone": "مرحلة",
+ "request": "طلب",
+ "update": "تحديث",
+ "meeting": "اجتماع",
+ "budget": "ميزانية"
+ }
+
+ notifications_df["type_ar"] = notifications_df["type"].map(type_mapping)
+
+ # تحويل الأولويات من الإنجليزية إلى العربية
+ priority_mapping = {
+ "high": "عالية",
+ "medium": "متوسطة",
+ "low": "منخفضة"
+ }
+
+ notifications_df["priority_ar"] = notifications_df["priority"].map(priority_mapping)
+
+ # تحويل حالة القراءة إلى نص
+ notifications_df["is_read_text"] = notifications_df["is_read"].map({True: "مقروءة", False: "غير مقروءة"})
+
+ # تنسيق عمود التاريخ
+ notifications_df["created_at_formatted"] = notifications_df["created_at"].dt.strftime("%Y-%m-%d %H:%M")
+
+ # إنشاء DataFrame للعرض
+ display_df = notifications_df[[
+ "id", "title", "type_ar", "priority_ar", "related_entity",
+ "created_at_formatted", "is_read_text"
+ ]].rename(columns={
+ "id": "الرقم",
+ "title": "العنوان",
+ "type_ar": "النوع",
+ "priority_ar": "الأولوية",
+ "related_entity": "الكيان المرتبط",
+ "created_at_formatted": "التاريخ",
+ "is_read_text": "الحالة"
+ })
+
+ # عرض الجدول
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
+
+ # إحصائيات الإشعارات
+ st.markdown("### إحصائيات الإشعارات")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ total_count = len(notifications_df)
+ st.metric("إجمالي الإشعارات", total_count)
+
+ with col2:
+ read_count = len(notifications_df[notifications_df["is_read"] == True])
+ st.metric("الإشعارات المقروءة", read_count)
+
+ with col3:
+ unread_count = len(notifications_df[notifications_df["is_read"] == False])
+ st.metric("الإشعارات غير المقروءة", unread_count)
+
+ # رسم بياني لتوزيع الإشعارات حسب النوع
+ st.markdown("#### توزيع الإشعارات حسب النوع")
+
+ type_counts = notifications_df["type_ar"].value_counts().reset_index()
+ type_counts.columns = ["النوع", "العدد"]
+
+ st.bar_chart(type_counts, x="النوع", y="العدد")
+
+ # رسم بياني لتوزيع الإشعارات حسب الأولوية
+ st.markdown("#### توزيع الإشعارات حسب الأولوية")
+
+ priority_counts = notifications_df["priority_ar"].value_counts().reset_index()
+ priority_counts.columns = ["الأولوية", "العدد"]
+
+ st.bar_chart(priority_counts, x="الأولوية", y="العدد")
+
+ # خيارات التصدير
+ st.markdown("### تصدير البيانات")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if st.button("تصدير إلى CSV"):
+ st.success("تم تصدير البيانات إلى CSV بنجاح")
+
+ with col2:
+ if st.button("تصدير إلى Excel"):
+ st.success("تم تصدير البيانات إلى Excel بنجاح")
diff --git a/modules/pricing/price_analyzer.py b/modules/pricing/price_analyzer.py
index ecdbf1a08b00585ac39627249913839a0d5b5a65..84574d9ffc56de6c1f7c223049c31ee2c6b7f4f9 100644
--- a/modules/pricing/price_analyzer.py
+++ b/modules/pricing/price_analyzer.py
@@ -1,1695 +1,1695 @@
-"""
-محلل الأسعار لنظام إدارة المناقصات
-"""
-
-import os
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import seaborn as sns
-from datetime import datetime, timedelta
-from scipy import stats
-import logging
-
-logger = logging.getLogger('tender_system.pricing.analyzer')
-
-class PriceAnalyzer:
- """فئة تحليل الأسعار"""
-
- def __init__(self, db_connector):
- """تهيئة محلل الأسعار"""
- self.db = db_connector
- self.charts_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "data", "charts")
-
- # إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
- os.makedirs(self.charts_dir, exist_ok=True)
-
- def get_price_history(self, item_id, start_date=None, end_date=None):
- """الحصول على تاريخ الأسعار لبند معين
-
- المعلمات:
- item_id (int): معرف البند
- start_date (str, optional): تاريخ البداية بتنسيق 'YYYY-MM-DD'
- end_date (str, optional): تاريخ النهاية بتنسيق 'YYYY-MM-DD'
-
- العائد:
- pandas.DataFrame: إطار بيانات يحتوي على تاريخ الأسعار
- """
- try:
- query = """
- SELECT
- pih.id,
- pih.price,
- pih.price_date,
- pih.price_source,
- pih.notes,
- pib.code,
- pib.name,
- mu.name as unit_name,
- mu.symbol as unit_symbol
- FROM
- pricing_items_history pih
- JOIN
- pricing_items_base pib ON pih.base_item_id = pib.id
- LEFT JOIN
- measurement_units mu ON pib.unit_id = mu.id
- WHERE
- pih.base_item_id = ?
- """
-
- params = [item_id]
-
- if start_date:
- query += " AND pih.price_date >= ?"
- params.append(start_date)
-
- if end_date:
- query += " AND pih.price_date <= ?"
- params.append(end_date)
-
- query += " ORDER BY pih.price_date ASC"
-
- results = self.db.fetch_all(query, params)
-
- if not results:
- logger.warning(f"لا توجد بيانات تاريخية للسعر للبند رقم {item_id}")
- return pd.DataFrame()
-
- # تحويل النتائج إلى إطار بيانات
- df = pd.DataFrame(results, columns=[
- 'id', 'price', 'price_date', 'price_source', 'notes',
- 'code', 'name', 'unit_name', 'unit_symbol'
- ])
-
- # تحويل تاريخ السعر إلى نوع datetime
- df['price_date'] = pd.to_datetime(df['price_date'])
-
- return df
-
- except Exception as e:
- logger.error(f"خطأ في الحصول على تاريخ الأسعار: {str(e)}")
- return pd.DataFrame()
-
- def analyze_price_trends(self, item_id, start_date=None, end_date=None):
- """تحليل اتجاهات الأسعار
-
- المعلمات:
- item_id (int): معرف البند
- start_date (str, optional): تاريخ البداية بتنسيق 'YYYY-MM-DD'
- end_date (str, optional): تاريخ النهاية بتنسيق 'YYYY-MM-DD'
-
- العائد:
- dict: قاموس يحتوي على نتائج تحليل اتجاهات الأسعار
- """
- try:
- # الحصول على تاريخ الأسعار
- df = self.get_price_history(item_id, start_date, end_date)
-
- if df.empty:
- return {
- 'status': 'error',
- 'message': 'لا توجد بيانات كافية لتحليل اتجاهات الأسعار'
- }
-
- # حساب الإحصاءات الأساسية
- stats_data = {
- 'min_price': df['price'].min(),
- 'max_price': df['price'].max(),
- 'avg_price': df['price'].mean(),
- 'median_price': df['price'].median(),
- 'std_dev': df['price'].std(),
- 'price_range': df['price'].max() - df['price'].min(),
- 'count': len(df),
- 'start_date': df['price_date'].min().strftime('%Y-%m-%d'),
- 'end_date': df['price_date'].max().strftime('%Y-%m-%d'),
- 'duration_days': (df['price_date'].max() - df['price_date'].min()).days,
- 'item_name': df['name'].iloc[0],
- 'item_code': df['code'].iloc[0],
- 'unit': df['unit_symbol'].iloc[0] if not pd.isna(df['unit_symbol'].iloc[0]) else ''
- }
-
- # حساب التغير المطلق والنسبي
- if len(df) >= 2:
- first_price = df['price'].iloc[0]
- last_price = df['price'].iloc[-1]
-
- stats_data['absolute_change'] = last_price - first_price
- stats_data['percentage_change'] = ((last_price - first_price) / first_price) * 100
-
- # حساب معدل التغير السنوي
- years = stats_data['duration_days'] / 365.0
- if years > 0:
- stats_data['annual_change_rate'] = (((last_price / first_price) ** (1 / years)) - 1) * 100
- else:
- stats_data['annual_change_rate'] = 0
- else:
- stats_data['absolute_change'] = 0
- stats_data['percentage_change'] = 0
- stats_data['annual_change_rate'] = 0
-
- # تحليل الاتجاه باستخدام الانحدار الخطي
- if len(df) >= 3:
- # إنشاء متغير مستقل (الأيام منذ أول تاريخ)
- df['days'] = (df['price_date'] - df['price_date'].min()).dt.days
-
- # حساب الانحدار الخطي
- slope, intercept, r_value, p_value, std_err = stats.linregress(df['days'], df['price'])
-
- stats_data['trend_slope'] = slope
- stats_data['trend_intercept'] = intercept
- stats_data['trend_r_squared'] = r_value ** 2
- stats_data['trend_p_value'] = p_value
- stats_data['trend_std_err'] = std_err
-
- # تحديد اتجاه السعر
- if p_value < 0.05: # إذا كان الاتجاه ذو دلالة إحصائية
- if slope > 0:
- stats_data['trend_direction'] = 'upward'
- stats_data['trend_description'] = 'اتجاه تصاعدي'
- elif slope < 0:
- stats_data['trend_direction'] = 'downward'
- stats_data['trend_description'] = 'اتجاه تنازلي'
- else:
- stats_data['trend_direction'] = 'stable'
- stats_data['trend_description'] = 'مستقر'
- else:
- stats_data['trend_direction'] = 'no_significant_trend'
- stats_data['trend_description'] = 'لا يوجد اتجاه واضح'
-
- # حساب التقلب (معامل الاختلاف)
- stats_data['volatility'] = (df['price'].std() / df['price'].mean()) * 100
-
- # تصنيف التقلب
- if stats_data['volatility'] < 5:
- stats_data['volatility_level'] = 'low'
- stats_data['volatility_description'] = 'منخفض'
- elif stats_data['volatility'] < 15:
- stats_data['volatility_level'] = 'medium'
- stats_data['volatility_description'] = 'متوسط'
- else:
- stats_data['volatility_level'] = 'high'
- stats_data['volatility_description'] = 'مرتفع'
- else:
- stats_data['trend_direction'] = 'insufficient_data'
- stats_data['trend_description'] = 'بيانات غير كافية'
- stats_data['volatility'] = 0
- stats_data['volatility_level'] = 'unknown'
- stats_data['volatility_description'] = 'غير معروف'
-
- # إنشاء رسم بياني للاتجاه
- chart_path = self._create_trend_chart(df, stats_data, item_id)
- stats_data['chart_path'] = chart_path
-
- return {
- 'status': 'success',
- 'data': stats_data
- }
-
- except Exception as e:
- logger.error(f"خطأ في تحليل اتجاهات الأسعار: {str(e)}")
- return {
- 'status': 'error',
- 'message': f'حدث خطأ أثناء تحليل اتجاهات الأسعار: {str(e)}'
- }
-
- def _create_trend_chart(self, df, stats_data, item_id):
- """إنشاء رسم بياني للاتجاه
-
- المعلمات:
- df (pandas.DataFrame): إطار البيانات
- stats_data (dict): بيانات الإحصاءات
- item_id (int): معرف البند
-
- العائد:
- str: مسار ملف الرسم البياني
- """
- try:
- # إنشاء رسم بياني جديد
- plt.figure(figsize=(10, 6))
-
- # رسم نقاط البيانات
- plt.scatter(df['price_date'], df['price'], color='blue', alpha=0.6, label='أسعار فعلية')
-
- # رسم خط الاتجاه إذا كان هناك بيانات كافية
- if len(df) >= 3 and 'trend_slope' in stats_data:
- # إنشاء خط الاتجاه
- x_trend = pd.date_range(start=df['price_date'].min(), end=df['price_date'].max(), periods=100)
- days_trend = [(date - df['price_date'].min()).days for date in x_trend]
- y_trend = stats_data['trend_slope'] * np.array(days_trend) + stats_data['trend_intercept']
-
- # رسم خط الاتجاه
- plt.plot(x_trend, y_trend, color='red', linestyle='--', label='خط الاتجاه')
-
- # رسم خط متوسط السعر
- plt.axhline(y=stats_data['avg_price'], color='green', linestyle='-', alpha=0.5, label='متوسط السعر')
-
- # إضافة عنوان ومحاور
- plt.title(f"تحليل اتجاه السعر - {stats_data['item_name']} ({stats_data['item_code']})")
- plt.xlabel('التاريخ')
- plt.ylabel(f"السعر ({stats_data['unit']})")
-
- # إضافة شبكة
- plt.grid(True, linestyle='--', alpha=0.7)
-
- # إضافة وسيلة إيضاح
- plt.legend()
-
- # تنسيق التاريخ على المحور السيني
- plt.gcf().autofmt_xdate()
-
- # إضافة معلومات إحصائية
- info_text = (
- f"التغير: {stats_data['percentage_change']:.2f}%\n"
- f"التقلب: {stats_data['volatility']:.2f}%\n"
- )
-
- if 'trend_r_squared' in stats_data:
- info_text += f"R²: {stats_data['trend_r_squared']:.3f}"
-
- plt.annotate(info_text, xy=(0.02, 0.95), xycoords='axes fraction',
- bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="gray", alpha=0.8))
-
- # حفظ الرسم البياني
- chart_filename = f"price_trend_{item_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
- chart_path = os.path.join(self.charts_dir, chart_filename)
- plt.savefig(chart_path, dpi=100, bbox_inches='tight')
- plt.close()
-
- return chart_path
-
- except Exception as e:
- logger.error(f"خطأ في إنشاء رسم بياني للاتجاه: {str(e)}")
- return None
-
- def compare_prices(self, items, date=None):
- """مقارنة الأسعار بين عدة بنود
-
- المعلمات:
- items (list): قائمة بمعرفات البنود
- date (str, optional): تاريخ المقارنة بتنسيق 'YYYY-MM-DD'
-
- العائد:
- dict: قاموس يحتوي على نتائج مقارنة الأسعار
- """
- try:
- if not items:
- return {
- 'status': 'error',
- 'message': 'لم يتم تحديد أي بنود للمقارنة'
- }
-
- comparison_data = []
-
- for item_id in items:
- # الحصول على معلومات البند الأساسية
- item_query = """
- SELECT
- id, code, name, description,
- (SELECT name FROM measurement_units WHERE id = unit_id) as unit_name,
- (SELECT symbol FROM measurement_units WHERE id = unit_id) as unit_symbol,
- base_price, last_updated_date
- FROM
- pricing_items_base
- WHERE
- id = ?
- """
-
- item_result = self.db.fetch_one(item_query, [item_id])
-
- if not item_result:
- logger.warning(f"البند رقم {item_id} غير موجود")
- continue
-
- item_data = {
- 'id': item_result[0],
- 'code': item_result[1],
- 'name': item_result[2],
- 'description': item_result[3],
- 'unit_name': item_result[4],
- 'unit_symbol': item_result[5],
- 'base_price': item_result[6],
- 'last_updated_date': item_result[7]
- }
-
- # إذا تم تحديد تاريخ، نبحث عن السعر في ذلك التاريخ
- if date:
- price_query = """
- SELECT price, price_date, price_source
- FROM pricing_items_history
- WHERE base_item_id = ?
- AND price_date <= ?
- ORDER BY price_date DESC
- LIMIT 1
- """
-
- price_result = self.db.fetch_one(price_query, [item_id, date])
-
- if price_result:
- item_data['price'] = price_result[0]
- item_data['price_date'] = price_result[1]
- item_data['price_source'] = price_result[2]
- else:
- # إذا لم يتم العثور على سعر في التاريخ المحدد، نستخدم السعر الأساسي
- item_data['price'] = item_data['base_price']
- item_data['price_date'] = item_data['last_updated_date']
- item_data['price_source'] = 'base_price'
- else:
- # إذا لم يتم تحديد تاريخ، نستخدم أحدث سعر
- price_query = """
- SELECT price, price_date, price_source
- FROM pricing_items_history
- WHERE base_item_id = ?
- ORDER BY price_date DESC
- LIMIT 1
- """
-
- price_result = self.db.fetch_one(price_query, [item_id])
-
- if price_result:
- item_data['price'] = price_result[0]
- item_data['price_date'] = price_result[1]
- item_data['price_source'] = price_result[2]
- else:
- # إذا لم يتم العثور على سعر، نستخدم السعر الأساسي
- item_data['price'] = item_data['base_price']
- item_data['price_date'] = item_data['last_updated_date']
- item_data['price_source'] = 'base_price'
-
- comparison_data.append(item_data)
-
- if not comparison_data:
- return {
- 'status': 'error',
- 'message': 'لم يتم العثور على أي بنود للمقارنة'
- }
-
- # إنشاء رسم بياني للمقارنة
- chart_path = self._create_comparison_chart(comparison_data, date)
-
- return {
- 'status': 'success',
- 'data': {
- 'items': comparison_data,
- 'comparison_date': date if date else 'latest',
- 'chart_path': chart_path
- }
- }
-
- except Exception as e:
- logger.error(f"خطأ في مقارنة الأسعار: {str(e)}")
- return {
- 'status': 'error',
- 'message': f'حدث خطأ أثناء مقارنة الأسعار: {str(e)}'
- }
-
- def _create_comparison_chart(self, comparison_data, date=None):
- """إنشاء رسم بياني للمقارنة
-
- المعلمات:
- comparison_data (list): بيانات المقارنة
- date (str, optional): تاريخ المقارنة
-
- العائد:
- str: مسار ملف الرسم البياني
- """
- try:
- # إنشاء رسم بياني جديد
- plt.figure(figsize=(12, 6))
-
- # إعداد البيانات للرسم
- names = [f"{item['code']} - {item['name']}" for item in comparison_data]
- prices = [item['price'] for item in comparison_data]
-
- # رسم الأعمدة
- bars = plt.bar(names, prices, color='skyblue', edgecolor='navy')
-
- # إضافة القيم فوق الأعمدة
- for bar in bars:
- height = bar.get_height()
- plt.text(bar.get_x() + bar.get_width()/2., height + 0.1,
- f'{height:.2f}', ha='center', va='bottom')
-
- # إضافة عنوان ومحاور
- title = "مقارنة الأسعار"
- if date:
- title += f" (بتاريخ {date})"
-
- plt.title(title)
- plt.xlabel('البنود')
- plt.ylabel('السعر')
-
- # تدوير تسميات المحور السيني لتجنب التداخل
- plt.xticks(rotation=45, ha='right')
-
- # إضافة شبكة
- plt.grid(True, linestyle='--', alpha=0.7, axis='y')
-
- # ضبط التخطيط
- plt.tight_layout()
-
- # حفظ الرسم البياني
- chart_filename = f"price_comparison_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
- chart_path = os.path.join(self.charts_dir, chart_filename)
- plt.savefig(chart_path, dpi=100, bbox_inches='tight')
- plt.close()
-
- return chart_path
-
- except Exception as e:
- logger.error(f"خطأ في إنشاء رسم بياني للمقارنة: {str(e)}")
- return None
-
- def calculate_price_volatility(self, item_id, period='1y'):
- """حساب تقلب الأسعار
-
- المعلمات:
- item_id (int): معرف البند
- period (str): الفترة الزمنية ('1m', '3m', '6m', '1y', '2y', '5y', 'all')
-
- العائد:
- dict: قاموس يحتوي على نتائج حساب تقلب الأسعار
- """
- try:
- # تحديد تاريخ البداية بناءً على الفترة
- end_date = datetime.now().strftime('%Y-%m-%d')
-
- if period == '1m':
- start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
- elif period == '3m':
- start_date = (datetime.now() - timedelta(days=90)).strftime('%Y-%m-%d')
- elif period == '6m':
- start_date = (datetime.now() - timedelta(days=180)).strftime('%Y-%m-%d')
- elif period == '1y':
- start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
- elif period == '2y':
- start_date = (datetime.now() - timedelta(days=730)).strftime('%Y-%m-%d')
- elif period == '5y':
- start_date = (datetime.now() - timedelta(days=1825)).strftime('%Y-%m-%d')
- else: # 'all'
- start_date = None
-
- # الحصول على تاريخ الأسعار
- df = self.get_price_history(item_id, start_date, end_date)
-
- if df.empty or len(df) < 2:
- return {
- 'status': 'error',
- 'message': 'لا توجد بيانات كافية لحساب تقلب الأسعار'
- }
-
- # حساب التقلب (معامل الاختلاف)
- mean_price = df['price'].mean()
- std_dev = df['price'].std()
- volatility = (std_dev / mean_price) * 100
-
- # حساب التغيرات النسبية
- df['price_shift'] = df['price'].shift(1)
- df = df.dropna()
-
- if not df.empty:
- df['price_change_pct'] = ((df['price'] - df['price_shift']) / df['price_shift']) * 100
-
- # حساب إحصاءات التغيرات
- max_increase = df['price_change_pct'].max()
- max_decrease = df['price_change_pct'].min()
- avg_change = df['price_change_pct'].mean()
- median_change = df['price_change_pct'].median()
-
- # حساب عدد التغيرات الإيجابية والسلبية
- positive_changes = (df['price_change_pct'] > 0).sum()
- negative_changes = (df['price_change_pct'] < 0).sum()
- no_changes = (df['price_change_pct'] == 0).sum()
-
- # تصنيف التقلب
- if volatility < 5:
- volatility_level = 'low'
- volatility_description = 'منخفض'
- elif volatility < 15:
- volatility_level = 'medium'
- volatility_description = 'متوسط'
- else:
- volatility_level = 'high'
- volatility_description = 'مرتفع'
-
- # إنشاء رسم بياني للتقلب
- chart_path = self._create_volatility_chart(df, item_id, period)
-
- return {
- 'status': 'success',
- 'data': {
- 'item_id': item_id,
- 'item_name': df['name'].iloc[0],
- 'item_code': df['code'].iloc[0],
- 'period': period,
- 'start_date': df['price_date'].min().strftime('%Y-%m-%d'),
- 'end_date': df['price_date'].max().strftime('%Y-%m-%d'),
- 'data_points': len(df),
- 'mean_price': mean_price,
- 'std_dev': std_dev,
- 'volatility': volatility,
- 'volatility_level': volatility_level,
- 'volatility_description': volatility_description,
- 'max_increase': max_increase,
- 'max_decrease': max_decrease,
- 'avg_change': avg_change,
- 'median_change': median_change,
- 'positive_changes': positive_changes,
- 'negative_changes': negative_changes,
- 'no_changes': no_changes,
- 'chart_path': chart_path
- }
- }
- else:
- return {
- 'status': 'error',
- 'message': 'لا توجد بيانات كافية لحساب تقلب الأسعار بعد معالجة البيانات'
- }
-
- except Exception as e:
- logger.error(f"خطأ في حساب تقلب الأسعار: {str(e)}")
- return {
- 'status': 'error',
- 'message': f'حدث خطأ أثناء حساب تقلب الأسعار: {str(e)}'
- }
-
- def _create_volatility_chart(self, df, item_id, period):
- """إنشاء رسم بياني للتقلب
-
- المعلمات:
- df (pandas.DataFrame): إطار البيانات
- item_id (int): معرف البند
- period (str): الفترة الزمنية
-
- العائد:
- str: مسار ملف الرسم البياني
- """
- try:
- # إنشاء رسم بياني بمحورين
- fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), gridspec_kw={'height_ratios': [2, 1]})
-
- # الرسم البياني العلوي: سعر البند عبر الزمن
- ax1.plot(df['price_date'], df['price'], 'b-', linewidth=2)
- ax1.set_title(f"سعر البند عبر الزمن - {df['name'].iloc[0]} ({df['code'].iloc[0]})")
- ax1.set_xlabel('التاريخ')
- ax1.set_ylabel('السعر')
- ax1.grid(True, linestyle='--', alpha=0.7)
-
- # إضافة نطاق الانحراف المعياري
- mean_price = df['price'].mean()
- std_dev = df['price'].std()
-
- ax1.axhline(y=mean_price, color='g', linestyle='-', alpha=0.8, label='متوسط السعر')
- ax1.axhline(y=mean_price + std_dev, color='r', linestyle='--', alpha=0.5, label='انحراف معياري +1')
- ax1.axhline(y=mean_price - std_dev, color='r', linestyle='--', alpha=0.5, label='انحراف معياري -1')
-
- ax1.fill_between(df['price_date'], mean_price - std_dev, mean_price + std_dev, color='gray', alpha=0.2)
- ax1.legend()
-
- # الرسم البياني السفلي: التغيرات النسبية
- ax2.bar(df['price_date'], df['price_change_pct'], color='skyblue', edgecolor='navy', alpha=0.7)
- ax2.set_title('التغيرات النسبية في السعر (%)')
- ax2.set_xlabel('التاريخ')
- ax2.set_ylabel('التغير النسبي (%)')
- ax2.grid(True, linestyle='--', alpha=0.7)
-
- # إضافة خط الصفر
- ax2.axhline(y=0, color='k', linestyle='-', alpha=0.3)
-
- # تنسيق التاريخ على المحور السيني
- fig.autofmt_xdate()
-
- # ضبط التخطيط
- plt.tight_layout()
-
- # حفظ الرسم البياني
- chart_filename = f"price_volatility_{item_id}_{period}_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
- chart_path = os.path.join(self.charts_dir, chart_filename)
- plt.savefig(chart_path, dpi=100, bbox_inches='tight')
- plt.close()
-
- return chart_path
-
- except Exception as e:
- logger.error(f"خطأ في إنشاء رسم بياني للتقلب: {str(e)}")
- return None
-
- def perform_sensitivity_analysis(self, project_id, variable_items, ranges):
- """إجراء تحليل الحساسية
-
- المعلمات:
- project_id (int): معرف المشروع
- variable_items (list): قائمة بمعرفات البنود المتغيرة
- ranges (dict): نطاقات التغيير لكل بند
-
- العائد:
- dict: قاموس يحتوي على نتائج تحليل الحساسية
- """
- try:
- # الحصول على بنود المشروع
- query = """
- SELECT
- id, item_number, description, quantity, unit_price, total_price
- FROM
- project_pricing_items
- WHERE
- project_id = ?
- """
-
- results = self.db.fetch_all(query, [project_id])
-
- if not results:
- return {
- 'status': 'error',
- 'message': 'لا توجد بنود للمشروع المحدد'
- }
-
- # تحويل النتائج إلى إطار بيانات
- project_items = pd.DataFrame(results, columns=[
- 'id', 'item_number', 'description', 'quantity', 'unit_price', 'total_price'
- ])
-
- # حساب إجمالي المشروع الأصلي
- original_total = project_items['total_price'].sum()
-
- # تحضير بيانات تحليل الحساسية
- sensitivity_data = []
-
- for item_id in variable_items:
- if item_id not in project_items['id'].values:
- logger.warning(f"البند رقم {item_id} غير موجود في المشروع")
- continue
-
- # الحصول على معلومات البند
- item_info = project_items[project_items['id'] == item_id].iloc[0]
-
- # الحصول على نطاق التغيير للبند
- if str(item_id) in ranges:
- item_range = ranges[str(item_id)]
- else:
- # استخدام نطاق افتراضي إذا لم يتم تحديد نطاق
- item_range = {'min': -20, 'max': 20, 'step': 10}
-
- # إنشاء قائمة بنسب التغيير
- change_percentages = list(range(
- item_range['min'],
- item_range['max'] + item_range['step'],
- item_range['step']
- ))
-
- item_sensitivity = {
- 'item_id': item_id,
- 'item_number': item_info['item_number'],
- 'description': item_info['description'],
- 'original_price': item_info['unit_price'],
- 'original_total': item_info['total_price'],
- 'changes': []
- }
-
- # حساب تأثير كل نسبة تغيير
- for percentage in change_percentages:
- # حساب السعر الجديد
- new_price = item_info['unit_price'] * (1 + percentage / 100)
- new_total = new_price * item_info['quantity']
-
- # حساب إجمالي المشروع الجديد
- project_total = original_total - item_info['total_price'] + new_total
-
- # حساب التغير في إجمالي المشروع
- project_change = ((project_total - original_total) / original_total) * 100
-
- item_sensitivity['changes'].append({
- 'percentage': percentage,
- 'new_price': new_price,
- 'new_total': new_total,
- 'project_total': project_total,
- 'project_change': project_change
- })
-
- sensitivity_data.append(item_sensitivity)
-
- if not sensitivity_data:
- return {
- 'status': 'error',
- 'message': 'لا توجد بنود صالحة لتحليل الحساسية'
- }
-
- # إنشاء رسم بياني لتحليل الحساسية
- chart_path = self._create_sensitivity_chart(sensitivity_data, original_total, project_id)
-
- return {
- 'status': 'success',
- 'data': {
- 'project_id': project_id,
- 'original_total': original_total,
- 'sensitivity_data': sensitivity_data,
- 'chart_path': chart_path
- }
- }
-
- except Exception as e:
- logger.error(f"خطأ في إجراء تحليل الحساسية: {str(e)}")
- return {
- 'status': 'error',
- 'message': f'حدث خطأ أثناء إجراء تحليل الحساسية: {str(e)}'
- }
-
- def _create_sensitivity_chart(self, sensitivity_data, original_total, project_id):
- """إنشاء رسم بياني لتحليل الحساسية
-
- المعلمات:
- sensitivity_data (list): بيانات تحليل الحساسية
- original_total (float): إجمالي المشروع الأصلي
- project_id (int): معرف المشروع
-
- العائد:
- str: مسار ملف الرسم البياني
- """
- try:
- # إنشاء رسم بياني جديد
- plt.figure(figsize=(12, 8))
-
- # رسم خطوط الحساسية لكل بند
- for item in sensitivity_data:
- percentages = [change['percentage'] for change in item['changes']]
- project_changes = [change['project_change'] for change in item['changes']]
-
- plt.plot(percentages, project_changes, marker='o', linewidth=2,
- label=f"{item['item_number']} - {item['description'][:30]}...")
-
- # إضافة عنوان ومحاور
- plt.title(f"تحليل الحساسية للمشروع رقم {project_id}")
- plt.xlabel('نسبة التغيير في سعر البند (%)')
- plt.ylabel('نسبة التغيير في إجمالي المشروع (%)')
-
- # إضافة خط الصفر
- plt.axhline(y=0, color='k', linestyle='-', alpha=0.3)
- plt.axvline(x=0, color='k', linestyle='-', alpha=0.3)
-
- # إضافة شبكة
- plt.grid(True, linestyle='--', alpha=0.7)
-
- # إضافة وسيلة إيضاح
- plt.legend(loc='best')
-
- # إضافة معلومات إضافية
- info_text = f"إجمالي المشروع الأصلي: {original_total:,.2f}"
- plt.annotate(info_text, xy=(0.02, 0.02), xycoords='axes fraction',
- bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="gray", alpha=0.8))
-
- # ضبط التخطيط
- plt.tight_layout()
-
- # حفظ الرسم البياني
- chart_filename = f"sensitivity_analysis_{project_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
- chart_path = os.path.join(self.charts_dir, chart_filename)
- plt.savefig(chart_path, dpi=100, bbox_inches='tight')
- plt.close()
-
- return chart_path
-
- except Exception as e:
- logger.error(f"خطأ في إنشاء رسم بياني لتحليل الحساسية: {str(e)}")
- return None
-
- def analyze_price_correlations(self, items):
- """تحليل ارتباطات الأسعار بين عدة بنود
-
- المعلمات:
- items (list): قائمة بمعرفات البنود
-
- العائد:
- dict: قاموس يحتوي على نتائج تحليل الارتباطات
- """
- try:
- if not items or len(items) < 2:
- return {
- 'status': 'error',
- 'message': 'يجب تحديد بندين على الأقل لتحليل الارتباطات'
- }
-
- # جمع بيانات الأسعار لجميع البنود
- all_prices = {}
- item_names = {}
-
- for item_id in items:
- # الحصول على تاريخ الأسعار
- df = self.get_price_history(item_id)
-
- if df.empty:
- logger.warning(f"لا توجد بيانات تاريخية للسعر للبند رقم {item_id}")
- continue
-
- # تخزين بيانات الأسعار
- all_prices[item_id] = df[['price_date', 'price']].copy()
- item_names[item_id] = f"{df['code'].iloc[0]} - {df['name'].iloc[0]}"
-
- if len(all_prices) < 2:
- return {
- 'status': 'error',
- 'message': 'لا توجد بيانات كافية لتحليل الارتباطات'
- }
-
- # إنشاء إطار بيانات موحد بتواريخ مشتركة
- # أولاً، نجمع جميع التواريخ الفريدة
- all_dates = set()
- for item_id, df in all_prices.items():
- all_dates.update(df['price_date'].dt.strftime('%Y-%m-%d').tolist())
-
- # إنشاء إطار بيانات جديد بجميع التواريخ
- unified_df = pd.DataFrame({'price_date': sorted(list(all_dates))})
- unified_df['price_date'] = pd.to_datetime(unified_df['price_date'])
-
- # إضافة أسعار كل بند
- for item_id, df in all_prices.items():
- # تحويل إطار البيانات إلى سلسلة زمنية مفهرسة بالتاريخ
- price_series = df.set_index('price_date')['price']
-
- # إعادة فهرسة السلسلة الزمنية لتتوافق مع التواريخ الموحدة
- unified_df[f'price_{item_id}'] = unified_df['price_date'].map(
- lambda x: price_series.get(x, None)
- )
-
- # ملء القيم المفقودة باستخدام الاستيفاء الخطي
- price_columns = [col for col in unified_df.columns if col.startswith('price_')]
- unified_df[price_columns] = unified_df[price_columns].interpolate(method='linear')
-
- # حذف الصفوف التي لا تزال تحتوي على قيم مفقودة
- unified_df = unified_df.dropna()
-
- if len(unified_df) < 3:
- return {
- 'status': 'error',
- 'message': 'لا توجد بيانات كافية بعد معالجة التواريخ المشتركة'
- }
-
- # حساب مصفوفة الارتباط
- correlation_matrix = unified_df[price_columns].corr()
-
- # تحويل مصفوفة الارتباط إلى تنسيق أكثر قابلية للقراءة
- correlation_data = []
-
- for i, item1_id in enumerate(items):
- if f'price_{item1_id}' not in correlation_matrix.columns:
- continue
-
- for j, item2_id in enumerate(items):
- if f'price_{item2_id}' not in correlation_matrix.columns or i >= j:
- continue
-
- correlation = correlation_matrix.loc[f'price_{item1_id}', f'price_{item2_id}']
-
- # تحديد قوة واتجاه الارتباط
- if abs(correlation) < 0.3:
- strength = 'weak'
- strength_description = 'ضعيف'
- elif abs(correlation) < 0.7:
- strength = 'moderate'
- strength_description = 'متوسط'
- else:
- strength = 'strong'
- strength_description = 'قوي'
-
- if correlation > 0:
- direction = 'positive'
- direction_description = 'طردي'
- else:
- direction = 'negative'
- direction_description = 'عكسي'
-
- correlation_data.append({
- 'item1_id': item1_id,
- 'item1_name': item_names.get(item1_id, f'البند {item1_id}'),
- 'item2_id': item2_id,
- 'item2_name': item_names.get(item2_id, f'البند {item2_id}'),
- 'correlation': correlation,
- 'strength': strength,
- 'strength_description': strength_description,
- 'direction': direction,
- 'direction_description': direction_description
- })
-
- if not correlation_data:
- return {
- 'status': 'error',
- 'message': 'لم يتم العثور على ارتباطات بين البنود المحددة'
- }
-
- # إنشاء رسم بياني للارتباطات
- chart_path = self._create_correlation_chart(correlation_matrix, item_names)
-
- # إنشاء رسم بياني لتطور الأسعار
- trends_chart_path = self._create_price_trends_chart(unified_df, price_columns, item_names)
-
- return {
- 'status': 'success',
- 'data': {
- 'correlation_data': correlation_data,
- 'chart_path': chart_path,
- 'trends_chart_path': trends_chart_path
- }
- }
-
- except Exception as e:
- logger.error(f"خطأ في تحليل ارتباطات الأسعار: {str(e)}")
- return {
- 'status': 'error',
- 'message': f'حدث خطأ أثناء تحليل ارتباطات الأسعار: {str(e)}'
- }
-
- def _create_correlation_chart(self, correlation_matrix, item_names):
- """إنشاء رسم بياني لمصفوفة الارتباط
-
- المعلمات:
- correlation_matrix (pandas.DataFrame): مصفوفة الارتباط
- item_names (dict): قاموس بأسماء البنود
-
- العائد:
- str: مسار ملف الرسم البياني
- """
- try:
- # إنشاء رسم بياني جديد
- plt.figure(figsize=(10, 8))
-
- # إنشاء خريطة حرارية للارتباطات
- mask = np.triu(np.ones_like(correlation_matrix, dtype=bool))
- cmap = sns.diverging_palette(230, 20, as_cmap=True)
-
- # تعديل تسميات المحاور
- labels = [item_names.get(int(col.split('_')[1]), col) for col in correlation_matrix.columns]
-
- # رسم الخريطة الحرارية
- sns.heatmap(correlation_matrix, mask=mask, cmap=cmap, vmax=1, vmin=-1, center=0,
- square=True, linewidths=.5, cbar_kws={"shrink": .5}, annot=True,
- xticklabels=labels, yticklabels=labels)
-
- # إضافة عنوان
- plt.title('مصفوفة ارتباط الأسعار بين البنود')
-
- # ضبط التخطيط
- plt.tight_layout()
-
- # حفظ الرسم البياني
- chart_filename = f"price_correlation_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
- chart_path = os.path.join(self.charts_dir, chart_filename)
- plt.savefig(chart_path, dpi=100, bbox_inches='tight')
- plt.close()
-
- return chart_path
-
- except Exception as e:
- logger.error(f"خطأ في إنشاء رسم بياني لمصفوفة الارتباط: {str(e)}")
- return None
-
- def _create_price_trends_chart(self, unified_df, price_columns, item_names):
- """إنشاء رسم بياني لتطور الأسعار
-
- المعلمات:
- unified_df (pandas.DataFrame): إطار البيانات الموحد
- price_columns (list): أسماء أعمدة الأسعار
- item_names (dict): قاموس بأسماء البنود
-
- العائد:
- str: مسار ملف الرسم البياني
- """
- try:
- # إنشاء رسم بياني جديد
- plt.figure(figsize=(12, 6))
-
- # رسم تطور الأسعار لكل بند
- for col in price_columns:
- item_id = int(col.split('_')[1])
- item_name = item_names.get(item_id, f'البند {item_id}')
-
- # تطبيع الأسعار للمقارنة (القيمة الأولى = 100)
- first_price = unified_df[col].iloc[0]
- normalized_prices = (unified_df[col] / first_price) * 100
-
- plt.plot(unified_df['price_date'], normalized_prices, linewidth=2, label=item_name)
-
- # إضافة عنوان ومحاور
- plt.title('تطور الأسعار النسبية للبنود (القيمة الأولى = 100)')
- plt.xlabel('التاريخ')
- plt.ylabel('السعر النسبي')
-
- # إضافة شبكة
- plt.grid(True, linestyle='--', alpha=0.7)
-
- # إضافة وسيلة إيضاح
- plt.legend(loc='best')
-
- # تنسيق التاريخ على المحور السيني
- plt.gcf().autofmt_xdate()
-
- # ضبط التخطيط
- plt.tight_layout()
-
- # حفظ الرسم البياني
- chart_filename = f"price_trends_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
- chart_path = os.path.join(self.charts_dir, chart_filename)
- plt.savefig(chart_path, dpi=100, bbox_inches='tight')
- plt.close()
-
- return chart_path
-
- except Exception as e:
- logger.error(f"خطأ في إنشاء رسم بياني لتطور الأسعار: {str(e)}")
- return None
-
- def compare_with_market_prices(self, items):
- """مقارنة أسعار البنود مع أسعار السوق
-
- المعلمات:
- items (list): قائمة بمعرفات البنود
-
- العائد:
- dict: قاموس يحتوي على نتائج المقارنة
- """
- try:
- if not items:
- return {
- 'status': 'error',
- 'message': 'لم يتم تحديد أي بنود للمقارنة'
- }
-
- comparison_data = []
-
- for item_id in items:
- # الحصول على معلومات البند الأساسية
- item_query = """
- SELECT
- id, code, name, description,
- (SELECT name FROM measurement_units WHERE id = unit_id) as unit_name,
- (SELECT symbol FROM measurement_units WHERE id = unit_id) as unit_symbol,
- base_price, last_updated_date
- FROM
- pricing_items_base
- WHERE
- id = ?
- """
-
- item_result = self.db.fetch_one(item_query, [item_id])
-
- if not item_result:
- logger.warning(f"البند رقم {item_id} غير موجود")
- continue
-
- item_data = {
- 'id': item_result[0],
- 'code': item_result[1],
- 'name': item_result[2],
- 'description': item_result[3],
- 'unit_name': item_result[4],
- 'unit_symbol': item_result[5],
- 'base_price': item_result[6],
- 'last_updated_date': item_result[7]
- }
-
- # الحصول على أحدث سعر للبند
- price_query = """
- SELECT price, price_date, price_source
- FROM pricing_items_history
- WHERE base_item_id = ?
- ORDER BY price_date DESC
- LIMIT 1
- """
-
- price_result = self.db.fetch_one(price_query, [item_id])
-
- if price_result:
- item_data['current_price'] = price_result[0]
- item_data['price_date'] = price_result[1]
- item_data['price_source'] = price_result[2]
- else:
- # إذا لم يتم العثور على سعر، نستخدم السعر الأساسي
- item_data['current_price'] = item_data['base_price']
- item_data['price_date'] = item_data['last_updated_date']
- item_data['price_source'] = 'base_price'
-
- # الحصول على متوسط سعر السوق (من مصادر مختلفة)
- market_query = """
- SELECT AVG(price) as avg_price
- FROM pricing_items_history
- WHERE base_item_id = ? AND price_source != 'internal'
- AND price_date >= date('now', '-6 months')
- """
-
- market_result = self.db.fetch_one(market_query, [item_id])
-
- if market_result and market_result[0]:
- item_data['market_price'] = market_result[0]
-
- # حساب الفرق بين السعر الحالي وسعر السوق
- item_data['price_difference'] = item_data['current_price'] - item_data['market_price']
- item_data['price_difference_percentage'] = (item_data['price_difference'] / item_data['market_price']) * 100
-
- # تحديد حالة السعر
- if abs(item_data['price_difference_percentage']) < 5:
- item_data['price_status'] = 'competitive'
- item_data['price_status_description'] = 'تنافسي'
- elif item_data['price_difference_percentage'] < 0:
- item_data['price_status'] = 'below_market'
- item_data['price_status_description'] = 'أقل من السوق'
- else:
- item_data['price_status'] = 'above_market'
- item_data['price_status_description'] = 'أعلى من السوق'
- else:
- # إذا لم يتم العثور على سعر سوق، نستخدم متوسط الأسعار الداخلية
- internal_query = """
- SELECT AVG(price) as avg_price
- FROM pricing_items_history
- WHERE base_item_id = ?
- AND price_date >= date('now', '-6 months')
- """
-
- internal_result = self.db.fetch_one(internal_query, [item_id])
-
- if internal_result and internal_result[0]:
- item_data['market_price'] = internal_result[0]
- item_data['price_difference'] = item_data['current_price'] - item_data['market_price']
- item_data['price_difference_percentage'] = (item_data['price_difference'] / item_data['market_price']) * 100
-
- # تحديد حالة السعر
- if abs(item_data['price_difference_percentage']) < 5:
- item_data['price_status'] = 'competitive'
- item_data['price_status_description'] = 'تنافسي'
- elif item_data['price_difference_percentage'] < 0:
- item_data['price_status'] = 'below_average'
- item_data['price_status_description'] = 'أقل من المتوسط'
- else:
- item_data['price_status'] = 'above_average'
- item_data['price_status_description'] = 'أعلى من المتوسط'
- else:
- item_data['market_price'] = None
- item_data['price_difference'] = None
- item_data['price_difference_percentage'] = None
- item_data['price_status'] = 'unknown'
- item_data['price_status_description'] = 'غير معروف'
-
- comparison_data.append(item_data)
-
- if not comparison_data:
- return {
- 'status': 'error',
- 'message': 'لم يتم العثور على أي بنود للمقارنة'
- }
-
- # إنشاء رسم بياني للمقارنة
- chart_path = self._create_market_comparison_chart(comparison_data)
-
- return {
- 'status': 'success',
- 'data': {
- 'items': comparison_data,
- 'chart_path': chart_path
- }
- }
-
- except Exception as e:
- logger.error(f"خطأ في مقارنة الأسعار مع أسعار السوق: {str(e)}")
- return {
- 'status': 'error',
- 'message': f'حدث خطأ أثناء مقارنة الأسعار مع أسعار السوق: {str(e)}'
- }
-
- def _create_market_comparison_chart(self, comparison_data):
- """إنشاء رسم بياني لمقارنة الأسعار مع أسعار السوق
-
- المعلمات:
- comparison_data (list): بيانات المقارنة
-
- العائد:
- str: مسار ملف الرسم البياني
- """
- try:
- # تصفية البنود التي لها أسعار سوق
- valid_items = [item for item in comparison_data if item.get('market_price') is not None]
-
- if not valid_items:
- return None
-
- # إنشاء رسم بياني جديد
- plt.figure(figsize=(12, 6))
-
- # إعداد البيانات للرسم
- names = [f"{item['code']} - {item['name'][:20]}..." for item in valid_items]
- current_prices = [item['current_price'] for item in valid_items]
- market_prices = [item['market_price'] for item in valid_items]
-
- # إنشاء مواقع الأعمدة
- x = np.arange(len(names))
- width = 0.35
-
- # رسم الأعمدة
- plt.bar(x - width/2, current_prices, width, label='السعر الحالي', color='skyblue')
- plt.bar(x + width/2, market_prices, width, label='سعر السوق', color='lightgreen')
-
- # إضافة تسميات وعنوان
- plt.xlabel('البنود')
- plt.ylabel('السعر')
- plt.title('مقارنة الأسعار الحالية مع أسعار السوق')
- plt.xticks(x, names, rotation=45, ha='right')
- plt.legend()
-
- # إضافة شبكة
- plt.grid(True, linestyle='--', alpha=0.7, axis='y')
-
- # إضافة قيم الفروق النسبية
- for i, item in enumerate(valid_items):
- if 'price_difference_percentage' in item and item['price_difference_percentage'] is not None:
- percentage = item['price_difference_percentage']
- color = 'green' if percentage < 0 else 'red' if percentage > 0 else 'black'
- plt.annotate(f"{percentage:.1f}%",
- xy=(x[i], max(current_prices[i], market_prices[i]) * 1.05),
- ha='center', va='bottom', color=color,
- bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="gray", alpha=0.8))
-
- # ضبط التخطيط
- plt.tight_layout()
-
- # حفظ الرسم البياني
- chart_filename = f"market_comparison_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
- chart_path = os.path.join(self.charts_dir, chart_filename)
- plt.savefig(chart_path, dpi=100, bbox_inches='tight')
- plt.close()
-
- return chart_path
-
- except Exception as e:
- logger.error(f"خطأ في إنشاء رسم بياني لمقارنة الأسعار مع أسعار السوق: {str(e)}")
- return None
-
- def analyze_cost_drivers(self, project_id):
- """تحليل محركات التكلفة للمشروع
-
- المعلمات:
- project_id (int): معرف المشروع
-
- العائد:
- dict: قاموس يحتوي على نتائج تحليل محركات التكلفة
- """
- try:
- # الحصول على بنود المشروع
- query = """
- SELECT
- id, item_number, description, quantity, unit_price, total_price,
- (SELECT name FROM pricing_categories WHERE id =
- (SELECT category_id FROM pricing_items_base WHERE id = base_item_id)
- ) as category_name
- FROM
- project_pricing_items
- WHERE
- project_id = ?
- """
-
- results = self.db.fetch_all(query, [project_id])
-
- if not results:
- return {
- 'status': 'error',
- 'message': 'لا توجد بنود للمشروع المحدد'
- }
-
- # تحويل النتائج إلى إطار بيانات
- df = pd.DataFrame(results, columns=[
- 'id', 'item_number', 'description', 'quantity', 'unit_price',
- 'total_price', 'category_name'
- ])
-
- # معالجة القيم المفقودة في عمود الفئة
- df['category_name'] = df['category_name'].fillna('أخرى')
-
- # حساب إجمالي المشروع
- project_total = df['total_price'].sum()
-
- # تحليل البنود حسب الفئة
- category_analysis = df.groupby('category_name').agg({
- 'total_price': 'sum'
- }).reset_index()
-
- # إضافة النسبة المئوية
- category_analysis['percentage'] = (category_analysis['total_price'] / project_total) * 100
-
- # ترتيب الفئات حسب التكلفة
- category_analysis = category_analysis.sort_values('total_price', ascending=False)
-
- # تحليل البنود الأعلى تكلفة
- top_items = df.sort_values('total_price', ascending=False).head(10)
- top_items['percentage'] = (top_items['total_price'] / project_total) * 100
-
- # حساب تركيز التكلفة (نسبة باريتو)
- df_sorted = df.sort_values('total_price', ascending=False)
- df_sorted['cumulative_cost'] = df_sorted['total_price'].cumsum()
- df_sorted['cumulative_percentage'] = (df_sorted['cumulative_cost'] / project_total) * 100
-
- # تحديد عدد البنود التي تشكل 80% من التكلفة
- items_80_percent = len(df_sorted[df_sorted['cumulative_percentage'] <= 80])
- if items_80_percent == 0:
- items_80_percent = 1
-
- pareto_ratio = items_80_percent / len(df)
-
- # إنشاء رسوم بيانية
- category_chart_path = self._create_category_chart(category_analysis)
- top_items_chart_path = self._create_top_items_chart(top_items)
- pareto_chart_path = self._create_pareto_chart(df_sorted)
-
- return {
- 'status': 'success',
- 'data': {
- 'project_id': project_id,
- 'project_total': project_total,
- 'category_analysis': category_analysis.to_dict('records'),
- 'top_items': top_items.to_dict('records'),
- 'pareto_ratio': pareto_ratio,
- 'items_80_percent': items_80_percent,
- 'total_items': len(df),
- 'category_chart_path': category_chart_path,
- 'top_items_chart_path': top_items_chart_path,
- 'pareto_chart_path': pareto_chart_path
- }
- }
-
- except Exception as e:
- logger.error(f"خطأ في تحليل محركات التكلفة: {str(e)}")
- return {
- 'status': 'error',
- 'message': f'حدث خطأ أثناء تحليل محركات التكلفة: {str(e)}'
- }
-
- def _create_category_chart(self, category_analysis):
- """إنشاء رسم بياني للتكاليف حسب الفئة
-
- المعلمات:
- category_analysis (pandas.DataFrame): تحليل الفئات
-
- العائد:
- str: مسار ملف الرسم البياني
- """
- try:
- # إنشاء رسم بياني جديد
- plt.figure(figsize=(10, 6))
-
- # رسم مخطط دائري
- plt.pie(
- category_analysis['total_price'],
- labels=category_analysis['category_name'],
- autopct='%1.1f%%',
- startangle=90,
- shadow=False,
- wedgeprops={'edgecolor': 'white', 'linewidth': 1}
- )
-
- # إضافة عنوان
- plt.title('توزيع التكاليف حسب الفئة')
-
- # جعل الرسم البياني دائريًا
- plt.axis('equal')
-
- # ضبط التخطيط
- plt.tight_layout()
-
- # حفظ الرسم البياني
- chart_filename = f"cost_category_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
- chart_path = os.path.join(self.charts_dir, chart_filename)
- plt.savefig(chart_path, dpi=100, bbox_inches='tight')
- plt.close()
-
- return chart_path
-
- except Exception as e:
- logger.error(f"خطأ في إنشاء رسم بياني للتكاليف حسب الفئة: {str(e)}")
- return None
-
- def _create_top_items_chart(self, top_items):
- """إنشاء رسم بياني للبنود الأعلى تكلفة
-
- المعلمات:
- top_items (pandas.DataFrame): البنود الأعلى تكلفة
-
- العائد:
- str: مسار ملف الرسم البياني
- """
- try:
- # إنشاء رسم بياني جديد
- plt.figure(figsize=(12, 6))
-
- # إعداد البيانات للرسم
- items = [f"{row['item_number']} - {row['description'][:20]}..." for _, row in top_items.iterrows()]
- costs = top_items['total_price'].tolist()
-
- # رسم الأعمدة
- bars = plt.barh(items, costs, color='skyblue', edgecolor='navy')
-
- # إضافة القيم على الأعمدة
- for i, bar in enumerate(bars):
- width = bar.get_width()
- plt.text(width * 1.01, bar.get_y() + bar.get_height()/2,
- f'{width:,.0f} ({top_items["percentage"].iloc[i]:.1f}%)',
- va='center')
-
- # إضافة عنوان ومحاور
- plt.title('البنود الأعلى تكلفة')
- plt.xlabel('التكلفة')
- plt.ylabel('البنود')
-
- # إضافة شبكة
- plt.grid(True, linestyle='--', alpha=0.7, axis='x')
-
- # ضبط التخطيط
- plt.tight_layout()
-
- # حفظ الرسم البياني
- chart_filename = f"top_cost_items_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
- chart_path = os.path.join(self.charts_dir, chart_filename)
- plt.savefig(chart_path, dpi=100, bbox_inches='tight')
- plt.close()
-
- return chart_path
-
- except Exception as e:
- logger.error(f"خطأ في إنشاء رسم بياني للبنود الأعلى تكلفة: {str(e)}")
- return None
-
- def _create_pareto_chart(self, df_sorted):
- """إنشاء رسم بياني لتحليل باريتو
-
- المعلمات:
- df_sorted (pandas.DataFrame): إطار البيانات المرتب
-
- العائد:
- str: مسار ملف الرسم البياني
- """
- try:
- # إنشاء رسم بياني جديد
- fig, ax1 = plt.subplots(figsize=(12, 6))
-
- # إعداد البيانات للرسم
- x = range(1, len(df_sorted) + 1)
- y1 = df_sorted['total_price'].tolist()
- y2 = df_sorted['cumulative_percentage'].tolist()
-
- # رسم الأعمدة (التكلفة)
- ax1.bar(x, y1, color='skyblue', alpha=0.7)
- ax1.set_xlabel('عدد البنود')
- ax1.set_ylabel('التكلفة', color='navy')
- ax1.tick_params(axis='y', labelcolor='navy')
-
- # إنشاء محور ثانوي
- ax2 = ax1.twinx()
-
- # رسم الخط (النسبة التراكمية)
- ax2.plot(x, y2, 'r-', linewidth=2, marker='o', markersize=4)
- ax2.set_ylabel('النسبة التراكمية (%)', color='red')
- ax2.tick_params(axis='y', labelcolor='red')
-
- # إضافة خط 80%
- ax2.axhline(y=80, color='green', linestyle='--', alpha=0.7)
-
- # إضافة عنوان
- plt.title('تحليل باريتو للتكاليف')
-
- # إضافة شبكة
- ax1.grid(True, linestyle='--', alpha=0.7)
-
- # ضبط التخطيط
- fig.tight_layout()
-
- # حفظ الرسم البياني
- chart_filename = f"pareto_analysis_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
- chart_path = os.path.join(self.charts_dir, chart_filename)
- plt.savefig(chart_path, dpi=100, bbox_inches='tight')
- plt.close()
-
- return chart_path
-
- except Exception as e:
- logger.error(f"خطأ في إنشاء رسم بياني لتحليل باريتو: {str(e)}")
- return None
-
- def generate_price_analysis_charts(self, analysis_type, params):
- """إنشاء رسوم بيانية لتحليل الأسعار
-
- المعلمات:
- analysis_type (str): نوع التحليل
- params (dict): معلمات التحليل
-
- العائد:
- dict: قاموس يحتوي على مسارات الرسوم البيانية
- """
- try:
- if analysis_type == 'trend':
- # تحليل اتجاه السعر
- if 'item_id' not in params:
- return {
- 'status': 'error',
- 'message': 'لم يتم تحديد معرف البند'
- }
-
- result = self.analyze_price_trends(
- params['item_id'],
- params.get('start_date'),
- params.get('end_date')
- )
-
- if result['status'] == 'success':
- return {
- 'status': 'success',
- 'charts': [result['data']['chart_path']]
- }
- else:
- return result
-
- elif analysis_type == 'comparison':
- # مقارنة الأسعار
- if 'items' not in params:
- return {
- 'status': 'error',
- 'message': 'لم يتم تحديد البنود للمقارنة'
- }
-
- result = self.compare_prices(
- params['items'],
- params.get('date')
- )
-
- if result['status'] == 'success':
- return {
- 'status': 'success',
- 'charts': [result['data']['chart_path']]
- }
- else:
- return result
-
- elif analysis_type == 'volatility':
- # تحليل تقلب الأسعار
- if 'item_id' not in params:
- return {
- 'status': 'error',
- 'message': 'لم يتم تحديد معرف البند'
- }
-
- result = self.calculate_price_volatility(
- params['item_id'],
- params.get('period', '1y')
- )
-
- if result['status'] == 'success':
- return {
- 'status': 'success',
- 'charts': [result['data']['chart_path']]
- }
- else:
- return result
-
- elif analysis_type == 'sensitivity':
- # تحليل الحساسية
- if 'project_id' not in params or 'variable_items' not in params:
- return {
- 'status': 'error',
- 'message': 'لم يتم تحديد معرف المشروع أو البنود المتغيرة'
- }
-
- result = self.perform_sensitivity_analysis(
- params['project_id'],
- params['variable_items'],
- params.get('ranges', {})
- )
-
- if result['status'] == 'success':
- return {
- 'status': 'success',
- 'charts': [result['data']['chart_path']]
- }
- else:
- return result
-
- elif analysis_type == 'correlation':
- # تحليل الارتباطات
- if 'items' not in params:
- return {
- 'status': 'error',
- 'message': 'لم يتم تحديد البنود للتحليل'
- }
-
- result = self.analyze_price_correlations(params['items'])
-
- if result['status'] == 'success':
- return {
- 'status': 'success',
- 'charts': [result['data']['chart_path'], result['data']['trends_chart_path']]
- }
- else:
- return result
-
- elif analysis_type == 'market_comparison':
- # مقارنة مع أسعار السوق
- if 'items' not in params:
- return {
- 'status': 'error',
- 'message': 'لم يتم تحديد البنود للمقارنة'
- }
-
- result = self.compare_with_market_prices(params['items'])
-
- if result['status'] == 'success':
- return {
- 'status': 'success',
- 'charts': [result['data']['chart_path']]
- }
- else:
- return result
-
- elif analysis_type == 'cost_drivers':
- # تحليل محركات التكلفة
- if 'project_id' not in params:
- return {
- 'status': 'error',
- 'message': 'لم يتم تحديد معرف المشروع'
- }
-
- result = self.analyze_cost_drivers(params['project_id'])
-
- if result['status'] == 'success':
- return {
- 'status': 'success',
- 'charts': [
- result['data']['category_chart_path'],
- result['data']['top_items_chart_path'],
- result['data']['pareto_chart_path']
- ]
- }
- else:
- return result
-
- else:
- return {
- 'status': 'error',
- 'message': f'نوع التحليل غير معروف: {analysis_type}'
- }
-
- except Exception as e:
- logger.error(f"خطأ في إنشاء رسوم بيانية لتحليل الأسعار: {str(e)}")
- return {
- 'status': 'error',
- 'message': f'حدث خطأ أثناء إنشاء رسوم بيانية لتحليل الأسعار: {str(e)}'
- }
+"""
+محلل الأسعار لنظام إدارة المناقصات
+"""
+
+import os
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import seaborn as sns
+from datetime import datetime, timedelta
+from scipy import stats
+import logging
+
+logger = logging.getLogger('tender_system.pricing.analyzer')
+
+class PriceAnalyzer:
+ """فئة تحليل الأسعار"""
+
+ def __init__(self, db_connector):
+ """تهيئة محلل الأسعار"""
+ self.db = db_connector
+ self.charts_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "data", "charts")
+
+ # إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
+ os.makedirs(self.charts_dir, exist_ok=True)
+
+ def get_price_history(self, item_id, start_date=None, end_date=None):
+ """الحصول على تاريخ الأسعار لبند معين
+
+ المعلمات:
+ item_id (int): معرف البند
+ start_date (str, optional): تاريخ البداية بتنسيق 'YYYY-MM-DD'
+ end_date (str, optional): تاريخ النهاية بتنسيق 'YYYY-MM-DD'
+
+ العائد:
+ pandas.DataFrame: إطار بيانات يحتوي على تاريخ الأسعار
+ """
+ try:
+ query = """
+ SELECT
+ pih.id,
+ pih.price,
+ pih.price_date,
+ pih.price_source,
+ pih.notes,
+ pib.code,
+ pib.name,
+ mu.name as unit_name,
+ mu.symbol as unit_symbol
+ FROM
+ pricing_items_history pih
+ JOIN
+ pricing_items_base pib ON pih.base_item_id = pib.id
+ LEFT JOIN
+ measurement_units mu ON pib.unit_id = mu.id
+ WHERE
+ pih.base_item_id = ?
+ """
+
+ params = [item_id]
+
+ if start_date:
+ query += " AND pih.price_date >= ?"
+ params.append(start_date)
+
+ if end_date:
+ query += " AND pih.price_date <= ?"
+ params.append(end_date)
+
+ query += " ORDER BY pih.price_date ASC"
+
+ results = self.db.fetch_all(query, params)
+
+ if not results:
+ logger.warning(f"لا توجد بيانات تاريخية للسعر للبند رقم {item_id}")
+ return pd.DataFrame()
+
+ # تحويل النتائج إلى إطار بيانات
+ df = pd.DataFrame(results, columns=[
+ 'id', 'price', 'price_date', 'price_source', 'notes',
+ 'code', 'name', 'unit_name', 'unit_symbol'
+ ])
+
+ # تحويل تاريخ السعر إلى نوع datetime
+ df['price_date'] = pd.to_datetime(df['price_date'])
+
+ return df
+
+ except Exception as e:
+ logger.error(f"خطأ في الحصول على تاريخ الأسعار: {str(e)}")
+ return pd.DataFrame()
+
+ def analyze_price_trends(self, item_id, start_date=None, end_date=None):
+ """تحليل اتجاهات الأسعار
+
+ المعلمات:
+ item_id (int): معرف البند
+ start_date (str, optional): تاريخ البداية بتنسيق 'YYYY-MM-DD'
+ end_date (str, optional): تاريخ النهاية بتنسيق 'YYYY-MM-DD'
+
+ العائد:
+ dict: قاموس يحتوي على نتائج تحليل اتجاهات الأسعار
+ """
+ try:
+ # الحصول على تاريخ الأسعار
+ df = self.get_price_history(item_id, start_date, end_date)
+
+ if df.empty:
+ return {
+ 'status': 'error',
+ 'message': 'لا توجد بيانات كافية لتحليل اتجاهات الأسعار'
+ }
+
+ # حساب الإحصاءات الأساسية
+ stats_data = {
+ 'min_price': df['price'].min(),
+ 'max_price': df['price'].max(),
+ 'avg_price': df['price'].mean(),
+ 'median_price': df['price'].median(),
+ 'std_dev': df['price'].std(),
+ 'price_range': df['price'].max() - df['price'].min(),
+ 'count': len(df),
+ 'start_date': df['price_date'].min().strftime('%Y-%m-%d'),
+ 'end_date': df['price_date'].max().strftime('%Y-%m-%d'),
+ 'duration_days': (df['price_date'].max() - df['price_date'].min()).days,
+ 'item_name': df['name'].iloc[0],
+ 'item_code': df['code'].iloc[0],
+ 'unit': df['unit_symbol'].iloc[0] if not pd.isna(df['unit_symbol'].iloc[0]) else ''
+ }
+
+ # حساب التغير المطلق والنسبي
+ if len(df) >= 2:
+ first_price = df['price'].iloc[0]
+ last_price = df['price'].iloc[-1]
+
+ stats_data['absolute_change'] = last_price - first_price
+ stats_data['percentage_change'] = ((last_price - first_price) / first_price) * 100
+
+ # حساب معدل التغير السنوي
+ years = stats_data['duration_days'] / 365.0
+ if years > 0:
+ stats_data['annual_change_rate'] = (((last_price / first_price) ** (1 / years)) - 1) * 100
+ else:
+ stats_data['annual_change_rate'] = 0
+ else:
+ stats_data['absolute_change'] = 0
+ stats_data['percentage_change'] = 0
+ stats_data['annual_change_rate'] = 0
+
+ # تحليل الاتجاه باستخدام الانحدار الخطي
+ if len(df) >= 3:
+ # إنشاء متغير مستقل (الأيام منذ أول تاريخ)
+ df['days'] = (df['price_date'] - df['price_date'].min()).dt.days
+
+ # حساب الانحدار الخطي
+ slope, intercept, r_value, p_value, std_err = stats.linregress(df['days'], df['price'])
+
+ stats_data['trend_slope'] = slope
+ stats_data['trend_intercept'] = intercept
+ stats_data['trend_r_squared'] = r_value ** 2
+ stats_data['trend_p_value'] = p_value
+ stats_data['trend_std_err'] = std_err
+
+ # تحديد اتجاه السعر
+ if p_value < 0.05: # إذا كان الاتجاه ذو دلالة إحصائية
+ if slope > 0:
+ stats_data['trend_direction'] = 'upward'
+ stats_data['trend_description'] = 'اتجاه تصاعدي'
+ elif slope < 0:
+ stats_data['trend_direction'] = 'downward'
+ stats_data['trend_description'] = 'اتجاه تنازلي'
+ else:
+ stats_data['trend_direction'] = 'stable'
+ stats_data['trend_description'] = 'مستقر'
+ else:
+ stats_data['trend_direction'] = 'no_significant_trend'
+ stats_data['trend_description'] = 'لا يوجد اتجاه واضح'
+
+ # حساب التقلب (معامل الاختلاف)
+ stats_data['volatility'] = (df['price'].std() / df['price'].mean()) * 100
+
+ # تصنيف التقلب
+ if stats_data['volatility'] < 5:
+ stats_data['volatility_level'] = 'low'
+ stats_data['volatility_description'] = 'منخفض'
+ elif stats_data['volatility'] < 15:
+ stats_data['volatility_level'] = 'medium'
+ stats_data['volatility_description'] = 'متوسط'
+ else:
+ stats_data['volatility_level'] = 'high'
+ stats_data['volatility_description'] = 'مرتفع'
+ else:
+ stats_data['trend_direction'] = 'insufficient_data'
+ stats_data['trend_description'] = 'بيانات غير كافية'
+ stats_data['volatility'] = 0
+ stats_data['volatility_level'] = 'unknown'
+ stats_data['volatility_description'] = 'غير معروف'
+
+ # إنشاء رسم بياني للاتجاه
+ chart_path = self._create_trend_chart(df, stats_data, item_id)
+ stats_data['chart_path'] = chart_path
+
+ return {
+ 'status': 'success',
+ 'data': stats_data
+ }
+
+ except Exception as e:
+ logger.error(f"خطأ في تحليل اتجاهات الأسعار: {str(e)}")
+ return {
+ 'status': 'error',
+ 'message': f'حدث خطأ أثناء تحليل اتجاهات الأسعار: {str(e)}'
+ }
+
+ def _create_trend_chart(self, df, stats_data, item_id):
+ """إنشاء رسم بياني للاتجاه
+
+ المعلمات:
+ df (pandas.DataFrame): إطار البيانات
+ stats_data (dict): بيانات الإحصاءات
+ item_id (int): معرف البند
+
+ العائد:
+ str: مسار ملف الرسم البياني
+ """
+ try:
+ # إنشاء رسم بياني جديد
+ plt.figure(figsize=(10, 6))
+
+ # رسم نقاط البيانات
+ plt.scatter(df['price_date'], df['price'], color='blue', alpha=0.6, label='أسعار فعلية')
+
+ # رسم خط الاتجاه إذا كان هناك بيانات كافية
+ if len(df) >= 3 and 'trend_slope' in stats_data:
+ # إنشاء خط الاتجاه
+ x_trend = pd.date_range(start=df['price_date'].min(), end=df['price_date'].max(), periods=100)
+ days_trend = [(date - df['price_date'].min()).days for date in x_trend]
+ y_trend = stats_data['trend_slope'] * np.array(days_trend) + stats_data['trend_intercept']
+
+ # رسم خط الاتجاه
+ plt.plot(x_trend, y_trend, color='red', linestyle='--', label='خط الاتجاه')
+
+ # رسم خط متوسط السعر
+ plt.axhline(y=stats_data['avg_price'], color='green', linestyle='-', alpha=0.5, label='متوسط السعر')
+
+ # إضافة عنوان ومحاور
+ plt.title(f"تحليل اتجاه السعر - {stats_data['item_name']} ({stats_data['item_code']})")
+ plt.xlabel('التاريخ')
+ plt.ylabel(f"السعر ({stats_data['unit']})")
+
+ # إضافة شبكة
+ plt.grid(True, linestyle='--', alpha=0.7)
+
+ # إضافة وسيلة إيضاح
+ plt.legend()
+
+ # تنسيق التاريخ على المحور السيني
+ plt.gcf().autofmt_xdate()
+
+ # إضافة معلومات إحصائية
+ info_text = (
+ f"التغير: {stats_data['percentage_change']:.2f}%\n"
+ f"التقلب: {stats_data['volatility']:.2f}%\n"
+ )
+
+ if 'trend_r_squared' in stats_data:
+ info_text += f"R²: {stats_data['trend_r_squared']:.3f}"
+
+ plt.annotate(info_text, xy=(0.02, 0.95), xycoords='axes fraction',
+ bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="gray", alpha=0.8))
+
+ # حفظ الرسم البياني
+ chart_filename = f"price_trend_{item_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
+ chart_path = os.path.join(self.charts_dir, chart_filename)
+ plt.savefig(chart_path, dpi=100, bbox_inches='tight')
+ plt.close()
+
+ return chart_path
+
+ except Exception as e:
+ logger.error(f"خطأ في إنشاء رسم بياني للاتجاه: {str(e)}")
+ return None
+
+ def compare_prices(self, items, date=None):
+ """مقارنة الأسعار بين عدة بنود
+
+ المعلمات:
+ items (list): قائمة بمعرفات البنود
+ date (str, optional): تاريخ المقارنة بتنسيق 'YYYY-MM-DD'
+
+ العائد:
+ dict: قاموس يحتوي على نتائج مقارنة الأسعار
+ """
+ try:
+ if not items:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم تحديد أي بنود للمقارنة'
+ }
+
+ comparison_data = []
+
+ for item_id in items:
+ # الحصول على معلومات البند الأساسية
+ item_query = """
+ SELECT
+ id, code, name, description,
+ (SELECT name FROM measurement_units WHERE id = unit_id) as unit_name,
+ (SELECT symbol FROM measurement_units WHERE id = unit_id) as unit_symbol,
+ base_price, last_updated_date
+ FROM
+ pricing_items_base
+ WHERE
+ id = ?
+ """
+
+ item_result = self.db.fetch_one(item_query, [item_id])
+
+ if not item_result:
+ logger.warning(f"البند رقم {item_id} غير موجود")
+ continue
+
+ item_data = {
+ 'id': item_result[0],
+ 'code': item_result[1],
+ 'name': item_result[2],
+ 'description': item_result[3],
+ 'unit_name': item_result[4],
+ 'unit_symbol': item_result[5],
+ 'base_price': item_result[6],
+ 'last_updated_date': item_result[7]
+ }
+
+ # إذا تم تحديد تاريخ، نبحث عن السعر في ذلك التاريخ
+ if date:
+ price_query = """
+ SELECT price, price_date, price_source
+ FROM pricing_items_history
+ WHERE base_item_id = ?
+ AND price_date <= ?
+ ORDER BY price_date DESC
+ LIMIT 1
+ """
+
+ price_result = self.db.fetch_one(price_query, [item_id, date])
+
+ if price_result:
+ item_data['price'] = price_result[0]
+ item_data['price_date'] = price_result[1]
+ item_data['price_source'] = price_result[2]
+ else:
+ # إذا لم يتم العثور على سعر في التاريخ المحدد، نستخدم السعر الأساسي
+ item_data['price'] = item_data['base_price']
+ item_data['price_date'] = item_data['last_updated_date']
+ item_data['price_source'] = 'base_price'
+ else:
+ # إذا لم يتم تحديد تاريخ، نستخدم أحدث سعر
+ price_query = """
+ SELECT price, price_date, price_source
+ FROM pricing_items_history
+ WHERE base_item_id = ?
+ ORDER BY price_date DESC
+ LIMIT 1
+ """
+
+ price_result = self.db.fetch_one(price_query, [item_id])
+
+ if price_result:
+ item_data['price'] = price_result[0]
+ item_data['price_date'] = price_result[1]
+ item_data['price_source'] = price_result[2]
+ else:
+ # إذا لم يتم العثور على سعر، نستخدم السعر الأساسي
+ item_data['price'] = item_data['base_price']
+ item_data['price_date'] = item_data['last_updated_date']
+ item_data['price_source'] = 'base_price'
+
+ comparison_data.append(item_data)
+
+ if not comparison_data:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم العثور على أي بنود للمقارنة'
+ }
+
+ # إنشاء رسم بياني للمقارنة
+ chart_path = self._create_comparison_chart(comparison_data, date)
+
+ return {
+ 'status': 'success',
+ 'data': {
+ 'items': comparison_data,
+ 'comparison_date': date if date else 'latest',
+ 'chart_path': chart_path
+ }
+ }
+
+ except Exception as e:
+ logger.error(f"خطأ في مقارنة الأسعار: {str(e)}")
+ return {
+ 'status': 'error',
+ 'message': f'حدث خطأ أثناء مقارنة الأسعار: {str(e)}'
+ }
+
+ def _create_comparison_chart(self, comparison_data, date=None):
+ """إنشاء رسم بياني للمقارنة
+
+ المعلمات:
+ comparison_data (list): بيانات المقارنة
+ date (str, optional): تاريخ المقارنة
+
+ العائد:
+ str: مسار ملف الرسم البياني
+ """
+ try:
+ # إنشاء رسم بياني جديد
+ plt.figure(figsize=(12, 6))
+
+ # إعداد البيانات للرسم
+ names = [f"{item['code']} - {item['name']}" for item in comparison_data]
+ prices = [item['price'] for item in comparison_data]
+
+ # رسم الأعمدة
+ bars = plt.bar(names, prices, color='skyblue', edgecolor='navy')
+
+ # إضافة القيم فوق الأعمدة
+ for bar in bars:
+ height = bar.get_height()
+ plt.text(bar.get_x() + bar.get_width()/2., height + 0.1,
+ f'{height:.2f}', ha='center', va='bottom')
+
+ # إضافة عنوان ومحاور
+ title = "مقارنة الأسعار"
+ if date:
+ title += f" (بتاريخ {date})"
+
+ plt.title(title)
+ plt.xlabel('البنود')
+ plt.ylabel('السعر')
+
+ # تدوير تسميات المحور السيني لتجنب التداخل
+ plt.xticks(rotation=45, ha='right')
+
+ # إضافة شبكة
+ plt.grid(True, linestyle='--', alpha=0.7, axis='y')
+
+ # ضبط التخطيط
+ plt.tight_layout()
+
+ # حفظ الرسم البياني
+ chart_filename = f"price_comparison_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
+ chart_path = os.path.join(self.charts_dir, chart_filename)
+ plt.savefig(chart_path, dpi=100, bbox_inches='tight')
+ plt.close()
+
+ return chart_path
+
+ except Exception as e:
+ logger.error(f"خطأ في إنشاء رسم بياني للمقارنة: {str(e)}")
+ return None
+
+ def calculate_price_volatility(self, item_id, period='1y'):
+ """حساب تقلب الأسعار
+
+ المعلمات:
+ item_id (int): معرف البند
+ period (str): الفترة الزمنية ('1m', '3m', '6m', '1y', '2y', '5y', 'all')
+
+ العائد:
+ dict: قاموس يحتوي على نتائج حساب تقلب الأسعار
+ """
+ try:
+ # تحديد تاريخ البداية بناءً على الفترة
+ end_date = datetime.now().strftime('%Y-%m-%d')
+
+ if period == '1m':
+ start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
+ elif period == '3m':
+ start_date = (datetime.now() - timedelta(days=90)).strftime('%Y-%m-%d')
+ elif period == '6m':
+ start_date = (datetime.now() - timedelta(days=180)).strftime('%Y-%m-%d')
+ elif period == '1y':
+ start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
+ elif period == '2y':
+ start_date = (datetime.now() - timedelta(days=730)).strftime('%Y-%m-%d')
+ elif period == '5y':
+ start_date = (datetime.now() - timedelta(days=1825)).strftime('%Y-%m-%d')
+ else: # 'all'
+ start_date = None
+
+ # الحصول على تاريخ الأسعار
+ df = self.get_price_history(item_id, start_date, end_date)
+
+ if df.empty or len(df) < 2:
+ return {
+ 'status': 'error',
+ 'message': 'لا توجد بيانات كافية لحساب تقلب الأسعار'
+ }
+
+ # حساب التقلب (معامل الاختلاف)
+ mean_price = df['price'].mean()
+ std_dev = df['price'].std()
+ volatility = (std_dev / mean_price) * 100
+
+ # حساب التغيرات النسبية
+ df['price_shift'] = df['price'].shift(1)
+ df = df.dropna()
+
+ if not df.empty:
+ df['price_change_pct'] = ((df['price'] - df['price_shift']) / df['price_shift']) * 100
+
+ # حساب إحصاءات التغيرات
+ max_increase = df['price_change_pct'].max()
+ max_decrease = df['price_change_pct'].min()
+ avg_change = df['price_change_pct'].mean()
+ median_change = df['price_change_pct'].median()
+
+ # حساب عدد التغيرات الإيجابية والسلبية
+ positive_changes = (df['price_change_pct'] > 0).sum()
+ negative_changes = (df['price_change_pct'] < 0).sum()
+ no_changes = (df['price_change_pct'] == 0).sum()
+
+ # تصنيف التقلب
+ if volatility < 5:
+ volatility_level = 'low'
+ volatility_description = 'منخفض'
+ elif volatility < 15:
+ volatility_level = 'medium'
+ volatility_description = 'متوسط'
+ else:
+ volatility_level = 'high'
+ volatility_description = 'مرتفع'
+
+ # إنشاء رسم بياني للتقلب
+ chart_path = self._create_volatility_chart(df, item_id, period)
+
+ return {
+ 'status': 'success',
+ 'data': {
+ 'item_id': item_id,
+ 'item_name': df['name'].iloc[0],
+ 'item_code': df['code'].iloc[0],
+ 'period': period,
+ 'start_date': df['price_date'].min().strftime('%Y-%m-%d'),
+ 'end_date': df['price_date'].max().strftime('%Y-%m-%d'),
+ 'data_points': len(df),
+ 'mean_price': mean_price,
+ 'std_dev': std_dev,
+ 'volatility': volatility,
+ 'volatility_level': volatility_level,
+ 'volatility_description': volatility_description,
+ 'max_increase': max_increase,
+ 'max_decrease': max_decrease,
+ 'avg_change': avg_change,
+ 'median_change': median_change,
+ 'positive_changes': positive_changes,
+ 'negative_changes': negative_changes,
+ 'no_changes': no_changes,
+ 'chart_path': chart_path
+ }
+ }
+ else:
+ return {
+ 'status': 'error',
+ 'message': 'لا توجد بيانات كافية لحساب تقلب الأسعار بعد معالجة البيانات'
+ }
+
+ except Exception as e:
+ logger.error(f"خطأ في حساب تقلب الأسعار: {str(e)}")
+ return {
+ 'status': 'error',
+ 'message': f'حدث خطأ أثناء حساب تقلب الأسعار: {str(e)}'
+ }
+
+ def _create_volatility_chart(self, df, item_id, period):
+ """إنشاء رسم بياني للتقلب
+
+ المعلمات:
+ df (pandas.DataFrame): إطار البيانات
+ item_id (int): معرف البند
+ period (str): الفترة الزمنية
+
+ العائد:
+ str: مسار ملف الرسم البياني
+ """
+ try:
+ # إنشاء رسم بياني بمحورين
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), gridspec_kw={'height_ratios': [2, 1]})
+
+ # الرسم البياني العلوي: سعر البند عبر الزمن
+ ax1.plot(df['price_date'], df['price'], 'b-', linewidth=2)
+ ax1.set_title(f"سعر البند عبر الزمن - {df['name'].iloc[0]} ({df['code'].iloc[0]})")
+ ax1.set_xlabel('التاريخ')
+ ax1.set_ylabel('السعر')
+ ax1.grid(True, linestyle='--', alpha=0.7)
+
+ # إضافة نطاق الانحراف المعياري
+ mean_price = df['price'].mean()
+ std_dev = df['price'].std()
+
+ ax1.axhline(y=mean_price, color='g', linestyle='-', alpha=0.8, label='متوسط السعر')
+ ax1.axhline(y=mean_price + std_dev, color='r', linestyle='--', alpha=0.5, label='انحراف معياري +1')
+ ax1.axhline(y=mean_price - std_dev, color='r', linestyle='--', alpha=0.5, label='انحراف معياري -1')
+
+ ax1.fill_between(df['price_date'], mean_price - std_dev, mean_price + std_dev, color='gray', alpha=0.2)
+ ax1.legend()
+
+ # الرسم البياني السفلي: التغيرات النسبية
+ ax2.bar(df['price_date'], df['price_change_pct'], color='skyblue', edgecolor='navy', alpha=0.7)
+ ax2.set_title('التغيرات النسبية في السعر (%)')
+ ax2.set_xlabel('التاريخ')
+ ax2.set_ylabel('التغير النسبي (%)')
+ ax2.grid(True, linestyle='--', alpha=0.7)
+
+ # إضافة خط الصفر
+ ax2.axhline(y=0, color='k', linestyle='-', alpha=0.3)
+
+ # تنسيق التاريخ على المحور السيني
+ fig.autofmt_xdate()
+
+ # ضبط التخطيط
+ plt.tight_layout()
+
+ # حفظ الرسم البياني
+ chart_filename = f"price_volatility_{item_id}_{period}_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
+ chart_path = os.path.join(self.charts_dir, chart_filename)
+ plt.savefig(chart_path, dpi=100, bbox_inches='tight')
+ plt.close()
+
+ return chart_path
+
+ except Exception as e:
+ logger.error(f"خطأ في إنشاء رسم بياني للتقلب: {str(e)}")
+ return None
+
+ def perform_sensitivity_analysis(self, project_id, variable_items, ranges):
+ """إجراء تحليل الحساسية
+
+ المعلمات:
+ project_id (int): معرف المشروع
+ variable_items (list): قائمة بمعرفات البنود المتغيرة
+ ranges (dict): نطاقات التغيير لكل بند
+
+ العائد:
+ dict: قاموس يحتوي على نتائج تحليل الحساسية
+ """
+ try:
+ # الحصول على بنود المشروع
+ query = """
+ SELECT
+ id, item_number, description, quantity, unit_price, total_price
+ FROM
+ project_pricing_items
+ WHERE
+ project_id = ?
+ """
+
+ results = self.db.fetch_all(query, [project_id])
+
+ if not results:
+ return {
+ 'status': 'error',
+ 'message': 'لا توجد بنود للمشروع المحدد'
+ }
+
+ # تحويل النتائج إلى إطار بيانات
+ project_items = pd.DataFrame(results, columns=[
+ 'id', 'item_number', 'description', 'quantity', 'unit_price', 'total_price'
+ ])
+
+ # حساب إجمالي المشروع الأصلي
+ original_total = project_items['total_price'].sum()
+
+ # تحضير بيانات تحليل الحساسية
+ sensitivity_data = []
+
+ for item_id in variable_items:
+ if item_id not in project_items['id'].values:
+ logger.warning(f"البند رقم {item_id} غير موجود في المشروع")
+ continue
+
+ # الحصول على معلومات البند
+ item_info = project_items[project_items['id'] == item_id].iloc[0]
+
+ # الحصول على نطاق التغيير للبند
+ if str(item_id) in ranges:
+ item_range = ranges[str(item_id)]
+ else:
+ # استخدام نطاق افتراضي إذا لم يتم تحديد نطاق
+ item_range = {'min': -20, 'max': 20, 'step': 10}
+
+ # إنشاء قائمة بنسب التغيير
+ change_percentages = list(range(
+ item_range['min'],
+ item_range['max'] + item_range['step'],
+ item_range['step']
+ ))
+
+ item_sensitivity = {
+ 'item_id': item_id,
+ 'item_number': item_info['item_number'],
+ 'description': item_info['description'],
+ 'original_price': item_info['unit_price'],
+ 'original_total': item_info['total_price'],
+ 'changes': []
+ }
+
+ # حساب تأثير كل نسبة تغيير
+ for percentage in change_percentages:
+ # حساب السعر الجديد
+ new_price = item_info['unit_price'] * (1 + percentage / 100)
+ new_total = new_price * item_info['quantity']
+
+ # حساب إجمالي المشروع الجديد
+ project_total = original_total - item_info['total_price'] + new_total
+
+ # حساب التغير في إجمالي المشروع
+ project_change = ((project_total - original_total) / original_total) * 100
+
+ item_sensitivity['changes'].append({
+ 'percentage': percentage,
+ 'new_price': new_price,
+ 'new_total': new_total,
+ 'project_total': project_total,
+ 'project_change': project_change
+ })
+
+ sensitivity_data.append(item_sensitivity)
+
+ if not sensitivity_data:
+ return {
+ 'status': 'error',
+ 'message': 'لا توجد بنود صالحة لتحليل الحساسية'
+ }
+
+ # إنشاء رسم بياني لتحليل الحساسية
+ chart_path = self._create_sensitivity_chart(sensitivity_data, original_total, project_id)
+
+ return {
+ 'status': 'success',
+ 'data': {
+ 'project_id': project_id,
+ 'original_total': original_total,
+ 'sensitivity_data': sensitivity_data,
+ 'chart_path': chart_path
+ }
+ }
+
+ except Exception as e:
+ logger.error(f"خطأ في إجراء تحليل الحساسية: {str(e)}")
+ return {
+ 'status': 'error',
+ 'message': f'حدث خطأ أثناء إجراء تحليل الحساسية: {str(e)}'
+ }
+
+ def _create_sensitivity_chart(self, sensitivity_data, original_total, project_id):
+ """إنشاء رسم بياني لتحليل الحساسية
+
+ المعلمات:
+ sensitivity_data (list): بيانات تحليل الحساسية
+ original_total (float): إجمالي المشروع الأصلي
+ project_id (int): معرف المشروع
+
+ العائد:
+ str: مسار ملف الرسم البياني
+ """
+ try:
+ # إنشاء رسم بياني جديد
+ plt.figure(figsize=(12, 8))
+
+ # رسم خطوط الحساسية لكل بند
+ for item in sensitivity_data:
+ percentages = [change['percentage'] for change in item['changes']]
+ project_changes = [change['project_change'] for change in item['changes']]
+
+ plt.plot(percentages, project_changes, marker='o', linewidth=2,
+ label=f"{item['item_number']} - {item['description'][:30]}...")
+
+ # إضافة عنوان ومحاور
+ plt.title(f"تحليل الحساسية للمشروع رقم {project_id}")
+ plt.xlabel('نسبة التغيير في سعر البند (%)')
+ plt.ylabel('نسبة التغيير في إجمالي المشروع (%)')
+
+ # إضافة خط الصفر
+ plt.axhline(y=0, color='k', linestyle='-', alpha=0.3)
+ plt.axvline(x=0, color='k', linestyle='-', alpha=0.3)
+
+ # إضافة شبكة
+ plt.grid(True, linestyle='--', alpha=0.7)
+
+ # إضافة وسيلة إيضاح
+ plt.legend(loc='best')
+
+ # إضافة معلومات إضافية
+ info_text = f"إجمالي المشروع الأصلي: {original_total:,.2f}"
+ plt.annotate(info_text, xy=(0.02, 0.02), xycoords='axes fraction',
+ bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="gray", alpha=0.8))
+
+ # ضبط التخطيط
+ plt.tight_layout()
+
+ # حفظ الرسم البياني
+ chart_filename = f"sensitivity_analysis_{project_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
+ chart_path = os.path.join(self.charts_dir, chart_filename)
+ plt.savefig(chart_path, dpi=100, bbox_inches='tight')
+ plt.close()
+
+ return chart_path
+
+ except Exception as e:
+ logger.error(f"خطأ في إنشاء رسم بياني لتحليل الحساسية: {str(e)}")
+ return None
+
+ def analyze_price_correlations(self, items):
+ """تحليل ارتباطات الأسعار بين عدة بنود
+
+ المعلمات:
+ items (list): قائمة بمعرفات البنود
+
+ العائد:
+ dict: قاموس يحتوي على نتائج تحليل الارتباطات
+ """
+ try:
+ if not items or len(items) < 2:
+ return {
+ 'status': 'error',
+ 'message': 'يجب تحديد بندين على الأقل لتحليل الارتباطات'
+ }
+
+ # جمع بيانات الأسعار لجميع البنود
+ all_prices = {}
+ item_names = {}
+
+ for item_id in items:
+ # الحصول على تاريخ الأسعار
+ df = self.get_price_history(item_id)
+
+ if df.empty:
+ logger.warning(f"لا توجد بيانات تاريخية للسعر للبند رقم {item_id}")
+ continue
+
+ # تخزين بيانات الأسعار
+ all_prices[item_id] = df[['price_date', 'price']].copy()
+ item_names[item_id] = f"{df['code'].iloc[0]} - {df['name'].iloc[0]}"
+
+ if len(all_prices) < 2:
+ return {
+ 'status': 'error',
+ 'message': 'لا توجد بيانات كافية لتحليل الارتباطات'
+ }
+
+ # إنشاء إطار بيانات موحد بتواريخ مشتركة
+ # أولاً، نجمع جميع التواريخ الفريدة
+ all_dates = set()
+ for item_id, df in all_prices.items():
+ all_dates.update(df['price_date'].dt.strftime('%Y-%m-%d').tolist())
+
+ # إنشاء إطار بيانات جديد بجميع التواريخ
+ unified_df = pd.DataFrame({'price_date': sorted(list(all_dates))})
+ unified_df['price_date'] = pd.to_datetime(unified_df['price_date'])
+
+ # إضافة أسعار كل بند
+ for item_id, df in all_prices.items():
+ # تحويل إطار البيانات إلى سلسلة زمنية مفهرسة بالتاريخ
+ price_series = df.set_index('price_date')['price']
+
+ # إعادة فهرسة السلسلة الزمنية لتتوافق مع التواريخ الموحدة
+ unified_df[f'price_{item_id}'] = unified_df['price_date'].map(
+ lambda x: price_series.get(x, None)
+ )
+
+ # ملء القيم المفقودة باستخدام الاستيفاء الخطي
+ price_columns = [col for col in unified_df.columns if col.startswith('price_')]
+ unified_df[price_columns] = unified_df[price_columns].interpolate(method='linear')
+
+ # حذف الصفوف التي لا تزال تحتوي على قيم مفقودة
+ unified_df = unified_df.dropna()
+
+ if len(unified_df) < 3:
+ return {
+ 'status': 'error',
+ 'message': 'لا توجد بيانات كافية بعد معالجة التواريخ المشتركة'
+ }
+
+ # حساب مصفوفة الارتباط
+ correlation_matrix = unified_df[price_columns].corr()
+
+ # تحويل مصفوفة الارتباط إلى تنسيق أكثر قابلية للقراءة
+ correlation_data = []
+
+ for i, item1_id in enumerate(items):
+ if f'price_{item1_id}' not in correlation_matrix.columns:
+ continue
+
+ for j, item2_id in enumerate(items):
+ if f'price_{item2_id}' not in correlation_matrix.columns or i >= j:
+ continue
+
+ correlation = correlation_matrix.loc[f'price_{item1_id}', f'price_{item2_id}']
+
+ # تحديد قوة واتجاه الارتباط
+ if abs(correlation) < 0.3:
+ strength = 'weak'
+ strength_description = 'ضعيف'
+ elif abs(correlation) < 0.7:
+ strength = 'moderate'
+ strength_description = 'متوسط'
+ else:
+ strength = 'strong'
+ strength_description = 'قوي'
+
+ if correlation > 0:
+ direction = 'positive'
+ direction_description = 'طردي'
+ else:
+ direction = 'negative'
+ direction_description = 'عكسي'
+
+ correlation_data.append({
+ 'item1_id': item1_id,
+ 'item1_name': item_names.get(item1_id, f'البند {item1_id}'),
+ 'item2_id': item2_id,
+ 'item2_name': item_names.get(item2_id, f'البند {item2_id}'),
+ 'correlation': correlation,
+ 'strength': strength,
+ 'strength_description': strength_description,
+ 'direction': direction,
+ 'direction_description': direction_description
+ })
+
+ if not correlation_data:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم العثور على ارتباطات بين البنود المحددة'
+ }
+
+ # إنشاء رسم بياني للارتباطات
+ chart_path = self._create_correlation_chart(correlation_matrix, item_names)
+
+ # إنشاء رسم بياني لتطور الأسعار
+ trends_chart_path = self._create_price_trends_chart(unified_df, price_columns, item_names)
+
+ return {
+ 'status': 'success',
+ 'data': {
+ 'correlation_data': correlation_data,
+ 'chart_path': chart_path,
+ 'trends_chart_path': trends_chart_path
+ }
+ }
+
+ except Exception as e:
+ logger.error(f"خطأ في تحليل ارتباطات الأسعار: {str(e)}")
+ return {
+ 'status': 'error',
+ 'message': f'حدث خطأ أثناء تحليل ارتباطات الأسعار: {str(e)}'
+ }
+
+ def _create_correlation_chart(self, correlation_matrix, item_names):
+ """إنشاء رسم بياني لمصفوفة الارتباط
+
+ المعلمات:
+ correlation_matrix (pandas.DataFrame): مصفوفة الارتباط
+ item_names (dict): قاموس بأسماء البنود
+
+ العائد:
+ str: مسار ملف الرسم البياني
+ """
+ try:
+ # إنشاء رسم بياني جديد
+ plt.figure(figsize=(10, 8))
+
+ # إنشاء خريطة حرارية للارتباطات
+ mask = np.triu(np.ones_like(correlation_matrix, dtype=bool))
+ cmap = sns.diverging_palette(230, 20, as_cmap=True)
+
+ # تعديل تسميات المحاور
+ labels = [item_names.get(int(col.split('_')[1]), col) for col in correlation_matrix.columns]
+
+ # رسم الخريطة الحرارية
+ sns.heatmap(correlation_matrix, mask=mask, cmap=cmap, vmax=1, vmin=-1, center=0,
+ square=True, linewidths=.5, cbar_kws={"shrink": .5}, annot=True,
+ xticklabels=labels, yticklabels=labels)
+
+ # إضافة عنوان
+ plt.title('مصفوفة ارتباط الأسعار بين البنود')
+
+ # ضبط التخطيط
+ plt.tight_layout()
+
+ # حفظ الرسم البياني
+ chart_filename = f"price_correlation_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
+ chart_path = os.path.join(self.charts_dir, chart_filename)
+ plt.savefig(chart_path, dpi=100, bbox_inches='tight')
+ plt.close()
+
+ return chart_path
+
+ except Exception as e:
+ logger.error(f"خطأ في إنشاء رسم بياني لمصفوفة الارتباط: {str(e)}")
+ return None
+
+ def _create_price_trends_chart(self, unified_df, price_columns, item_names):
+ """إنشاء رسم بياني لتطور الأسعار
+
+ المعلمات:
+ unified_df (pandas.DataFrame): إطار البيانات الموحد
+ price_columns (list): أسماء أعمدة الأسعار
+ item_names (dict): قاموس بأسماء البنود
+
+ العائد:
+ str: مسار ملف الرسم البياني
+ """
+ try:
+ # إنشاء رسم بياني جديد
+ plt.figure(figsize=(12, 6))
+
+ # رسم تطور الأسعار لكل بند
+ for col in price_columns:
+ item_id = int(col.split('_')[1])
+ item_name = item_names.get(item_id, f'البند {item_id}')
+
+ # تطبيع الأسعار للمقارنة (القيمة الأولى = 100)
+ first_price = unified_df[col].iloc[0]
+ normalized_prices = (unified_df[col] / first_price) * 100
+
+ plt.plot(unified_df['price_date'], normalized_prices, linewidth=2, label=item_name)
+
+ # إضافة عنوان ومحاور
+ plt.title('تطور الأسعار النسبية للبنود (القيمة الأولى = 100)')
+ plt.xlabel('التاريخ')
+ plt.ylabel('السعر النسبي')
+
+ # إضافة شبكة
+ plt.grid(True, linestyle='--', alpha=0.7)
+
+ # إضافة وسيلة إيضاح
+ plt.legend(loc='best')
+
+ # تنسيق التاريخ على المحور السيني
+ plt.gcf().autofmt_xdate()
+
+ # ضبط التخطيط
+ plt.tight_layout()
+
+ # حفظ الرسم البياني
+ chart_filename = f"price_trends_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
+ chart_path = os.path.join(self.charts_dir, chart_filename)
+ plt.savefig(chart_path, dpi=100, bbox_inches='tight')
+ plt.close()
+
+ return chart_path
+
+ except Exception as e:
+ logger.error(f"خطأ في إنشاء رسم بياني لتطور الأسعار: {str(e)}")
+ return None
+
+ def compare_with_market_prices(self, items):
+ """مقارنة أسعار البنود مع أسعار السوق
+
+ المعلمات:
+ items (list): قائمة بمعرفات البنود
+
+ العائد:
+ dict: قاموس يحتوي على نتائج المقارنة
+ """
+ try:
+ if not items:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم تحديد أي بنود للمقارنة'
+ }
+
+ comparison_data = []
+
+ for item_id in items:
+ # الحصول على معلومات البند الأساسية
+ item_query = """
+ SELECT
+ id, code, name, description,
+ (SELECT name FROM measurement_units WHERE id = unit_id) as unit_name,
+ (SELECT symbol FROM measurement_units WHERE id = unit_id) as unit_symbol,
+ base_price, last_updated_date
+ FROM
+ pricing_items_base
+ WHERE
+ id = ?
+ """
+
+ item_result = self.db.fetch_one(item_query, [item_id])
+
+ if not item_result:
+ logger.warning(f"البند رقم {item_id} غير موجود")
+ continue
+
+ item_data = {
+ 'id': item_result[0],
+ 'code': item_result[1],
+ 'name': item_result[2],
+ 'description': item_result[3],
+ 'unit_name': item_result[4],
+ 'unit_symbol': item_result[5],
+ 'base_price': item_result[6],
+ 'last_updated_date': item_result[7]
+ }
+
+ # الحصول على أحدث سعر للبند
+ price_query = """
+ SELECT price, price_date, price_source
+ FROM pricing_items_history
+ WHERE base_item_id = ?
+ ORDER BY price_date DESC
+ LIMIT 1
+ """
+
+ price_result = self.db.fetch_one(price_query, [item_id])
+
+ if price_result:
+ item_data['current_price'] = price_result[0]
+ item_data['price_date'] = price_result[1]
+ item_data['price_source'] = price_result[2]
+ else:
+ # إذا لم يتم العثور على سعر، نستخدم السعر الأساسي
+ item_data['current_price'] = item_data['base_price']
+ item_data['price_date'] = item_data['last_updated_date']
+ item_data['price_source'] = 'base_price'
+
+ # الحصول على متوسط سعر السوق (من مصادر مختلفة)
+ market_query = """
+ SELECT AVG(price) as avg_price
+ FROM pricing_items_history
+ WHERE base_item_id = ? AND price_source != 'internal'
+ AND price_date >= date('now', '-6 months')
+ """
+
+ market_result = self.db.fetch_one(market_query, [item_id])
+
+ if market_result and market_result[0]:
+ item_data['market_price'] = market_result[0]
+
+ # حساب الفرق بين السعر الحالي وسعر السوق
+ item_data['price_difference'] = item_data['current_price'] - item_data['market_price']
+ item_data['price_difference_percentage'] = (item_data['price_difference'] / item_data['market_price']) * 100
+
+ # تحديد حالة السعر
+ if abs(item_data['price_difference_percentage']) < 5:
+ item_data['price_status'] = 'competitive'
+ item_data['price_status_description'] = 'تنافسي'
+ elif item_data['price_difference_percentage'] < 0:
+ item_data['price_status'] = 'below_market'
+ item_data['price_status_description'] = 'أقل من السوق'
+ else:
+ item_data['price_status'] = 'above_market'
+ item_data['price_status_description'] = 'أعلى من السوق'
+ else:
+ # إذا لم يتم العثور على سعر سوق، نستخدم متوسط الأسعار الداخلية
+ internal_query = """
+ SELECT AVG(price) as avg_price
+ FROM pricing_items_history
+ WHERE base_item_id = ?
+ AND price_date >= date('now', '-6 months')
+ """
+
+ internal_result = self.db.fetch_one(internal_query, [item_id])
+
+ if internal_result and internal_result[0]:
+ item_data['market_price'] = internal_result[0]
+ item_data['price_difference'] = item_data['current_price'] - item_data['market_price']
+ item_data['price_difference_percentage'] = (item_data['price_difference'] / item_data['market_price']) * 100
+
+ # تحديد حالة السعر
+ if abs(item_data['price_difference_percentage']) < 5:
+ item_data['price_status'] = 'competitive'
+ item_data['price_status_description'] = 'تنافسي'
+ elif item_data['price_difference_percentage'] < 0:
+ item_data['price_status'] = 'below_average'
+ item_data['price_status_description'] = 'أقل من المتوسط'
+ else:
+ item_data['price_status'] = 'above_average'
+ item_data['price_status_description'] = 'أعلى من المتوسط'
+ else:
+ item_data['market_price'] = None
+ item_data['price_difference'] = None
+ item_data['price_difference_percentage'] = None
+ item_data['price_status'] = 'unknown'
+ item_data['price_status_description'] = 'غير معروف'
+
+ comparison_data.append(item_data)
+
+ if not comparison_data:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم العثور على أي بنود للمقارنة'
+ }
+
+ # إنشاء رسم بياني للمقارنة
+ chart_path = self._create_market_comparison_chart(comparison_data)
+
+ return {
+ 'status': 'success',
+ 'data': {
+ 'items': comparison_data,
+ 'chart_path': chart_path
+ }
+ }
+
+ except Exception as e:
+ logger.error(f"خطأ في مقارنة الأسعار مع أسعار السوق: {str(e)}")
+ return {
+ 'status': 'error',
+ 'message': f'حدث خطأ أثناء مقارنة الأسعار مع أسعار السوق: {str(e)}'
+ }
+
+ def _create_market_comparison_chart(self, comparison_data):
+ """إنشاء رسم بياني لمقارنة الأسعار مع أسعار السوق
+
+ المعلمات:
+ comparison_data (list): بيانات المقارنة
+
+ العائد:
+ str: مسار ملف الرسم البياني
+ """
+ try:
+ # تصفية البنود التي لها أسعار سوق
+ valid_items = [item for item in comparison_data if item.get('market_price') is not None]
+
+ if not valid_items:
+ return None
+
+ # إنشاء رسم بياني جديد
+ plt.figure(figsize=(12, 6))
+
+ # إعداد البيانات للرسم
+ names = [f"{item['code']} - {item['name'][:20]}..." for item in valid_items]
+ current_prices = [item['current_price'] for item in valid_items]
+ market_prices = [item['market_price'] for item in valid_items]
+
+ # إنشاء مواقع الأعمدة
+ x = np.arange(len(names))
+ width = 0.35
+
+ # رسم الأعمدة
+ plt.bar(x - width/2, current_prices, width, label='السعر الحالي', color='skyblue')
+ plt.bar(x + width/2, market_prices, width, label='سعر السوق', color='lightgreen')
+
+ # إضافة تسميات وعنوان
+ plt.xlabel('البنود')
+ plt.ylabel('السعر')
+ plt.title('مقارنة الأسعار الحالية مع أسعار السوق')
+ plt.xticks(x, names, rotation=45, ha='right')
+ plt.legend()
+
+ # إضافة شبكة
+ plt.grid(True, linestyle='--', alpha=0.7, axis='y')
+
+ # إضافة قيم الفروق النسبية
+ for i, item in enumerate(valid_items):
+ if 'price_difference_percentage' in item and item['price_difference_percentage'] is not None:
+ percentage = item['price_difference_percentage']
+ color = 'green' if percentage < 0 else 'red' if percentage > 0 else 'black'
+ plt.annotate(f"{percentage:.1f}%",
+ xy=(x[i], max(current_prices[i], market_prices[i]) * 1.05),
+ ha='center', va='bottom', color=color,
+ bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="gray", alpha=0.8))
+
+ # ضبط التخطيط
+ plt.tight_layout()
+
+ # حفظ الرسم البياني
+ chart_filename = f"market_comparison_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
+ chart_path = os.path.join(self.charts_dir, chart_filename)
+ plt.savefig(chart_path, dpi=100, bbox_inches='tight')
+ plt.close()
+
+ return chart_path
+
+ except Exception as e:
+ logger.error(f"خطأ في إنشاء رسم بياني لمقارنة الأسعار مع أسعار السوق: {str(e)}")
+ return None
+
+ def analyze_cost_drivers(self, project_id):
+ """تحليل محركات التكلفة للمشروع
+
+ المعلمات:
+ project_id (int): معرف المشروع
+
+ العائد:
+ dict: قاموس يحتوي على نتائج تحليل محركات التكلفة
+ """
+ try:
+ # الحصول على بنود المشروع
+ query = """
+ SELECT
+ id, item_number, description, quantity, unit_price, total_price,
+ (SELECT name FROM pricing_categories WHERE id =
+ (SELECT category_id FROM pricing_items_base WHERE id = base_item_id)
+ ) as category_name
+ FROM
+ project_pricing_items
+ WHERE
+ project_id = ?
+ """
+
+ results = self.db.fetch_all(query, [project_id])
+
+ if not results:
+ return {
+ 'status': 'error',
+ 'message': 'لا توجد بنود للمشروع المحدد'
+ }
+
+ # تحويل النتائج إلى إطار بيانات
+ df = pd.DataFrame(results, columns=[
+ 'id', 'item_number', 'description', 'quantity', 'unit_price',
+ 'total_price', 'category_name'
+ ])
+
+ # معالجة القيم المفقودة في عمود الفئة
+ df['category_name'] = df['category_name'].fillna('أخرى')
+
+ # حساب إجمالي المشروع
+ project_total = df['total_price'].sum()
+
+ # تحليل البنود حسب الفئة
+ category_analysis = df.groupby('category_name').agg({
+ 'total_price': 'sum'
+ }).reset_index()
+
+ # إضافة النسبة المئوية
+ category_analysis['percentage'] = (category_analysis['total_price'] / project_total) * 100
+
+ # ترتيب الفئات حسب التكلفة
+ category_analysis = category_analysis.sort_values('total_price', ascending=False)
+
+ # تحليل البنود الأعلى تكلفة
+ top_items = df.sort_values('total_price', ascending=False).head(10)
+ top_items['percentage'] = (top_items['total_price'] / project_total) * 100
+
+ # حساب تركيز التكلفة (نسبة باريتو)
+ df_sorted = df.sort_values('total_price', ascending=False)
+ df_sorted['cumulative_cost'] = df_sorted['total_price'].cumsum()
+ df_sorted['cumulative_percentage'] = (df_sorted['cumulative_cost'] / project_total) * 100
+
+ # تحديد عدد البنود التي تشكل 80% من التكلفة
+ items_80_percent = len(df_sorted[df_sorted['cumulative_percentage'] <= 80])
+ if items_80_percent == 0:
+ items_80_percent = 1
+
+ pareto_ratio = items_80_percent / len(df)
+
+ # إنشاء رسوم بيانية
+ category_chart_path = self._create_category_chart(category_analysis)
+ top_items_chart_path = self._create_top_items_chart(top_items)
+ pareto_chart_path = self._create_pareto_chart(df_sorted)
+
+ return {
+ 'status': 'success',
+ 'data': {
+ 'project_id': project_id,
+ 'project_total': project_total,
+ 'category_analysis': category_analysis.to_dict('records'),
+ 'top_items': top_items.to_dict('records'),
+ 'pareto_ratio': pareto_ratio,
+ 'items_80_percent': items_80_percent,
+ 'total_items': len(df),
+ 'category_chart_path': category_chart_path,
+ 'top_items_chart_path': top_items_chart_path,
+ 'pareto_chart_path': pareto_chart_path
+ }
+ }
+
+ except Exception as e:
+ logger.error(f"خطأ في تحليل محركات التكلفة: {str(e)}")
+ return {
+ 'status': 'error',
+ 'message': f'حدث خطأ أثناء تحليل محركات التكلفة: {str(e)}'
+ }
+
+ def _create_category_chart(self, category_analysis):
+ """إنشاء رسم بياني للتكاليف حسب الفئة
+
+ المعلمات:
+ category_analysis (pandas.DataFrame): تحليل الفئات
+
+ العائد:
+ str: مسار ملف الرسم البياني
+ """
+ try:
+ # إنشاء رسم بياني جديد
+ plt.figure(figsize=(10, 6))
+
+ # رسم مخطط دائري
+ plt.pie(
+ category_analysis['total_price'],
+ labels=category_analysis['category_name'],
+ autopct='%1.1f%%',
+ startangle=90,
+ shadow=False,
+ wedgeprops={'edgecolor': 'white', 'linewidth': 1}
+ )
+
+ # إضافة عنوان
+ plt.title('توزيع التكاليف حسب الفئة')
+
+ # جعل الرسم البياني دائريًا
+ plt.axis('equal')
+
+ # ضبط التخطيط
+ plt.tight_layout()
+
+ # حفظ الرسم البياني
+ chart_filename = f"cost_category_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
+ chart_path = os.path.join(self.charts_dir, chart_filename)
+ plt.savefig(chart_path, dpi=100, bbox_inches='tight')
+ plt.close()
+
+ return chart_path
+
+ except Exception as e:
+ logger.error(f"خطأ في إنشاء رسم بياني للتكاليف حسب الفئة: {str(e)}")
+ return None
+
+ def _create_top_items_chart(self, top_items):
+ """إنشاء رسم بياني للبنود الأعلى تكلفة
+
+ المعلمات:
+ top_items (pandas.DataFrame): البنود الأعلى تكلفة
+
+ العائد:
+ str: مسار ملف الرسم البياني
+ """
+ try:
+ # إنشاء رسم بياني جديد
+ plt.figure(figsize=(12, 6))
+
+ # إعداد البيانات للرسم
+ items = [f"{row['item_number']} - {row['description'][:20]}..." for _, row in top_items.iterrows()]
+ costs = top_items['total_price'].tolist()
+
+ # رسم الأعمدة
+ bars = plt.barh(items, costs, color='skyblue', edgecolor='navy')
+
+ # إضافة القيم على الأعمدة
+ for i, bar in enumerate(bars):
+ width = bar.get_width()
+ plt.text(width * 1.01, bar.get_y() + bar.get_height()/2,
+ f'{width:,.0f} ({top_items["percentage"].iloc[i]:.1f}%)',
+ va='center')
+
+ # إضافة عنوان ومحاور
+ plt.title('البنود الأعلى تكلفة')
+ plt.xlabel('التكلفة')
+ plt.ylabel('البنود')
+
+ # إضافة شبكة
+ plt.grid(True, linestyle='--', alpha=0.7, axis='x')
+
+ # ضبط التخطيط
+ plt.tight_layout()
+
+ # حفظ الرسم البياني
+ chart_filename = f"top_cost_items_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
+ chart_path = os.path.join(self.charts_dir, chart_filename)
+ plt.savefig(chart_path, dpi=100, bbox_inches='tight')
+ plt.close()
+
+ return chart_path
+
+ except Exception as e:
+ logger.error(f"خطأ في إنشاء رسم بياني للبنود الأعلى تكلفة: {str(e)}")
+ return None
+
+ def _create_pareto_chart(self, df_sorted):
+ """إنشاء رسم بياني لتحليل باريتو
+
+ المعلمات:
+ df_sorted (pandas.DataFrame): إطار البيانات المرتب
+
+ العائد:
+ str: مسار ملف الرسم البياني
+ """
+ try:
+ # إنشاء رسم بياني جديد
+ fig, ax1 = plt.subplots(figsize=(12, 6))
+
+ # إعداد البيانات للرسم
+ x = range(1, len(df_sorted) + 1)
+ y1 = df_sorted['total_price'].tolist()
+ y2 = df_sorted['cumulative_percentage'].tolist()
+
+ # رسم الأعمدة (التكلفة)
+ ax1.bar(x, y1, color='skyblue', alpha=0.7)
+ ax1.set_xlabel('عدد البنود')
+ ax1.set_ylabel('التكلفة', color='navy')
+ ax1.tick_params(axis='y', labelcolor='navy')
+
+ # إنشاء محور ثانوي
+ ax2 = ax1.twinx()
+
+ # رسم الخط (النسبة التراكمية)
+ ax2.plot(x, y2, 'r-', linewidth=2, marker='o', markersize=4)
+ ax2.set_ylabel('النسبة التراكمية (%)', color='red')
+ ax2.tick_params(axis='y', labelcolor='red')
+
+ # إضافة خط 80%
+ ax2.axhline(y=80, color='green', linestyle='--', alpha=0.7)
+
+ # إضافة عنوان
+ plt.title('تحليل باريتو للتكاليف')
+
+ # إضافة شبكة
+ ax1.grid(True, linestyle='--', alpha=0.7)
+
+ # ضبط التخطيط
+ fig.tight_layout()
+
+ # حفظ الرسم البياني
+ chart_filename = f"pareto_analysis_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
+ chart_path = os.path.join(self.charts_dir, chart_filename)
+ plt.savefig(chart_path, dpi=100, bbox_inches='tight')
+ plt.close()
+
+ return chart_path
+
+ except Exception as e:
+ logger.error(f"خطأ في إنشاء رسم بياني لتحليل باريتو: {str(e)}")
+ return None
+
+ def generate_price_analysis_charts(self, analysis_type, params):
+ """إنشاء رسوم بيانية لتحليل الأسعار
+
+ المعلمات:
+ analysis_type (str): نوع التحليل
+ params (dict): معلمات التحليل
+
+ العائد:
+ dict: قاموس يحتوي على مسارات الرسوم البيانية
+ """
+ try:
+ if analysis_type == 'trend':
+ # تحليل اتجاه السعر
+ if 'item_id' not in params:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم تحديد معرف البند'
+ }
+
+ result = self.analyze_price_trends(
+ params['item_id'],
+ params.get('start_date'),
+ params.get('end_date')
+ )
+
+ if result['status'] == 'success':
+ return {
+ 'status': 'success',
+ 'charts': [result['data']['chart_path']]
+ }
+ else:
+ return result
+
+ elif analysis_type == 'comparison':
+ # مقارنة الأسعار
+ if 'items' not in params:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم تحديد البنود للمقارنة'
+ }
+
+ result = self.compare_prices(
+ params['items'],
+ params.get('date')
+ )
+
+ if result['status'] == 'success':
+ return {
+ 'status': 'success',
+ 'charts': [result['data']['chart_path']]
+ }
+ else:
+ return result
+
+ elif analysis_type == 'volatility':
+ # تحليل تقلب الأسعار
+ if 'item_id' not in params:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم تحديد معرف البند'
+ }
+
+ result = self.calculate_price_volatility(
+ params['item_id'],
+ params.get('period', '1y')
+ )
+
+ if result['status'] == 'success':
+ return {
+ 'status': 'success',
+ 'charts': [result['data']['chart_path']]
+ }
+ else:
+ return result
+
+ elif analysis_type == 'sensitivity':
+ # تحليل الحساسية
+ if 'project_id' not in params or 'variable_items' not in params:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم تحديد معرف المشروع أو البنود المتغيرة'
+ }
+
+ result = self.perform_sensitivity_analysis(
+ params['project_id'],
+ params['variable_items'],
+ params.get('ranges', {})
+ )
+
+ if result['status'] == 'success':
+ return {
+ 'status': 'success',
+ 'charts': [result['data']['chart_path']]
+ }
+ else:
+ return result
+
+ elif analysis_type == 'correlation':
+ # تحليل الارتباطات
+ if 'items' not in params:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم تحديد البنود للتحليل'
+ }
+
+ result = self.analyze_price_correlations(params['items'])
+
+ if result['status'] == 'success':
+ return {
+ 'status': 'success',
+ 'charts': [result['data']['chart_path'], result['data']['trends_chart_path']]
+ }
+ else:
+ return result
+
+ elif analysis_type == 'market_comparison':
+ # مقارنة مع أسعار السوق
+ if 'items' not in params:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم تحديد البنود للمقارنة'
+ }
+
+ result = self.compare_with_market_prices(params['items'])
+
+ if result['status'] == 'success':
+ return {
+ 'status': 'success',
+ 'charts': [result['data']['chart_path']]
+ }
+ else:
+ return result
+
+ elif analysis_type == 'cost_drivers':
+ # تحليل محركات التكلفة
+ if 'project_id' not in params:
+ return {
+ 'status': 'error',
+ 'message': 'لم يتم تحديد معرف المشروع'
+ }
+
+ result = self.analyze_cost_drivers(params['project_id'])
+
+ if result['status'] == 'success':
+ return {
+ 'status': 'success',
+ 'charts': [
+ result['data']['category_chart_path'],
+ result['data']['top_items_chart_path'],
+ result['data']['pareto_chart_path']
+ ]
+ }
+ else:
+ return result
+
+ else:
+ return {
+ 'status': 'error',
+ 'message': f'نوع التحليل غير معروف: {analysis_type}'
+ }
+
+ except Exception as e:
+ logger.error(f"خطأ في إنشاء رسوم بيانية لتحليل الأسعار: {str(e)}")
+ return {
+ 'status': 'error',
+ 'message': f'حدث خطأ أثناء إنشاء رسوم بيانية لتحليل الأسعار: {str(e)}'
+ }
diff --git a/modules/pricing/pricing_app.py b/modules/pricing/pricing_app.py
index 552876c60563a284ee8d4c4944406f7a714340f6..91fbabf796f7616ff20b15b5862e4def3d63335e 100644
--- a/modules/pricing/pricing_app.py
+++ b/modules/pricing/pricing_app.py
@@ -1,6 +1,7 @@
from pathlib import Path
-
-# استيراد الوحدات الجديدة من pricing_system
+import streamlit as st
+import pandas as pd
+from datetime import datetime
from pricing_system.modules.analysis import smart_price_analysis as analysis_utils
from pricing_system.modules.catalogs import materials_catalog, equipment_catalog
from pricing_system.modules.indirect_support import overheads
@@ -8,152 +9,166 @@ from pricing_system.modules.pricing_strategies import balanced_pricing, profit_o
class PricingApp:
"""وحدة التسعير"""
- if 'local_content_data' not in st.session_state:
- st.session_state.local_content_data = {'target_percent': 30, 'actual_percent': 0, 'items': []}
-
-
-
- def run(self):
- self.render()
-
-
- def _render_pricing_scenarios_tab(self):
- st.markdown("### سيناريوهات التسعير")
- balanced_pricing.render_balanced_strategy()
+ def __init__(self):
+ """تهيئة وحدة التسعير"""
+ if 'project_data' not in st.session_state:
+ st.session_state.project_data = {}
- def _render_competitive_analysis_tab(self):
- st.markdown("### المقارنة التنافسية")
+ if 'bill_of_quantities' not in st.session_state:
+ st.session_state.bill_of_quantities = []
- def _render_unbalanced_pricing_tab(self):
- st.markdown("### التسعير غير المتزن")
- profit_oriented.render_profit_driven_strategy()
+ # Maintain existing session state for indirect costs and risks if available.
+ if 'indirect_costs' not in st.session_state:
+ st.session_state.indirect_costs = {
+ 'overhead': 0.10, # نسبة المصاريف العمومية والإدارية
+ 'profit': 0.15, # نسبة الربح
+ 'contingency': 0.05, # نسبة الطوارئ
+ 'bonds': 0.02, # نسبة الضمانات
+ 'insurance': 0.03 # نسبة التأمين
+ }
- def _render_reports_tab(self):
- st.markdown("### التقارير")
+ if 'risks' not in st.session_state:
+ st.session_state.risks = []
def run(self):
- self.render()
+ """تشغيل وحدة التسعير"""
+ st.title("وحدة التسعير")
- def render(self):
- st.markdown("وحدة التسعير ", unsafe_allow_html=True)
+ # اختيار المشروع
+ self._select_project()
tabs = st.tabs([
- "لوحة التحكم",
"جدول الكميات",
"تحليل التكاليف",
"سيناريوهات التسعير",
- "المقارنة التنافسية",
- "المحتوى المحلي",
- "تسعير غير متزن",
- "التقارير"
+ "المحتوى المحلي"
])
with tabs[0]:
- self._render_dashboard_tab()
- with tabs[1]:
self._render_bill_of_quantities_tab()
- with tabs[2]:
+ with tabs[1]:
self._render_cost_analysis_tab()
- with tabs[3]:
+ with tabs[2]:
self._render_pricing_scenarios_tab()
- with tabs[4]:
- self._render_competitive_analysis_tab()
- with tabs[5]:
+ with tabs[3]:
self._render_local_content_tab()
- with tabs[6]:
- self._render_unbalanced_pricing_tab()
- with tabs[7]:
- self._render_reports_tab()
- def _render_dashboard_tab(self):
- st.markdown("### لوحة تحكم التسعير")
- st.info("سيتم عرض بيانات ملخص التسعير والتحليلات هنا.")
+ def _select_project(self):
+ """اختيار المشروع"""
+ st.sidebar.markdown("### اختيار المشروع")
- def _render_bill_of_quantities_tab(self):
- st.markdown("### جدول الكميات")
+ # جلب المشاريع من قاعدة البيانات
+ projects = self._get_projects_from_db()
- boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
-
- if not boq_df.empty:
- st.dataframe(
- boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']],
- column_config={
- 'code': 'الكود',
- 'description': 'الوصف',
- 'unit': 'الوحدة',
- 'quantity': 'الكمية',
- 'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
- 'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
- 'category': 'الفئة'
- },
- hide_index=True,
- use_container_width=True
+ if projects:
+ project_names = [p['name'] for p in projects]
+ selected_project = st.sidebar.selectbox(
+ "اختر المشروع",
+ project_names
)
+
+ # تحديث بيانات المشروع المحدد
+ project = next((p for p in projects if p['name'] == selected_project), None)
+ if project:
+ st.session_state.current_project = project
+ st.session_state.bill_of_quantities = project.get('boq_items', [])
else:
- st.warning("لا توجد بيانات في جدول الكميات.")
+ st.sidebar.warning("لا توجد مشاريع متاحة")
+
+ def _get_projects_from_db(self):
+ """جلب المشاريع من قاعدة البيانات"""
+ # هنا يتم جلب المشاريع من قاعدة البيانات
+ # هذه بيانات تجريبية للتوضيح
+ return [
+ {
+ 'id': 1,
+ 'name': 'مشروع تطوير الطريق',
+ 'client': 'وزارة النقل',
+ 'boq_items': [
+ {
+ 'code': 'A-001',
+ 'description': 'أعمال الحفر',
+ 'unit': 'م3',
+ 'quantity': 1000,
+ 'unit_price': 50,
+ 'total_price': 50000
+ }
+ ]
+ }
+ ]
+ def _render_bill_of_quantities_tab(self):
+ """عرض تبويب جدول الكميات"""
+ st.markdown("### جدول الكميات")
+
+ # عرض البنود الحالية
+ if st.session_state.bill_of_quantities:
+ df = pd.DataFrame(st.session_state.bill_of_quantities)
+ st.dataframe(df, use_container_width=True)
+
+ # إضافة بند جديد
st.markdown("### إضافة بند جديد")
+
col1, col2 = st.columns(2)
with col1:
- new_code = st.text_input("الكود", key="new_boq_code")
- new_description = st.text_input("الوصف", key="new_boq_description")
- new_unit = st.selectbox("الوحدة", ["م3", "م2", "طن", "عدد", "متر طولي"], key="new_boq_unit")
+ code = st.text_input("كود البند")
+ description = st.text_area("وصف البند")
+
with col2:
- new_quantity = st.number_input("الكمية", min_value=0.0, step=1.0, key="new_boq_quantity")
- new_unit_price = st.number_input("سعر الوحدة", min_value=0.0, step=10.0, key="new_boq_unit_price")
- new_category = st.selectbox("الفئة", [
- "أعمال ترابية",
- "أعمال خرسانية",
- "أعمال حديد",
- "أعمال بناء",
- "أعمال تشطيبات",
- "أعمال نجارة",
- "أعمال ألمنيوم",
- "أعمال كهربائية",
- "أعمال ميكانيكية",
- "أعمال صحية"
- ], key="new_boq_category")
-
- if st.button("إضافة البند", key="add_boq_item"):
- if new_code and new_description and new_quantity > 0 and new_unit_price > 0:
- new_total_price = new_quantity * new_unit_price
- new_id = max([item['id'] for item in st.session_state.bill_of_quantities], default=0) + 1
- st.session_state.bill_of_quantities.append({
- 'id': new_id,
- 'code': new_code,
- 'description': new_description,
- 'unit': new_unit,
- 'quantity': new_quantity,
- 'unit_price': new_unit_price,
- 'total_price': new_total_price,
- 'category': new_category
- })
- st.success(f"تمت إضافة البند بنجاح: {new_description}")
+ unit = st.selectbox("الوحدة", ["م3", "م2", "متر طولي", "عدد"])
+ quantity = st.number_input("الكمية", min_value=0.0)
+ unit_price = st.number_input("سعر الوحدة", min_value=0.0)
+
+ if st.button("إضافة البند"):
+ if code and description and quantity > 0 and unit_price > 0:
+ new_item = {
+ 'code': code,
+ 'description': description,
+ 'unit': unit,
+ 'quantity': quantity,
+ 'unit_price': unit_price,
+ 'total_price': quantity * unit_price
+ }
+ st.session_state.bill_of_quantities.append(new_item)
+ st.success("تم إضافة البند بنجاح")
st.rerun()
- else:
- st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
+
def _render_cost_analysis_tab(self):
st.markdown("### تحليل التكاليف")
- analysis_utils.render_cost_breakdown()
+
+ if len(st.session_state.bill_of_quantities) > 0:
+ # تحليل التكاليف حسب الفئة
+ category_costs = {}
+ total_cost = 0
+
+ for item in st.session_state.bill_of_quantities:
+ category = item.get('category', 'غير مصنف') # Handle missing category gracefully
+ cost = item['total_price']
+
+ if category in category_costs:
+ category_costs[category] += cost
+ else:
+ category_costs[category] = cost
+
+ total_cost += cost
+
+ # عرض إجمالي التكاليف
+ st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال")
+
+ # عرض التكاليف حسب الفئة
+ st.markdown("#### التكاليف حسب الفئة")
+ for category, cost in category_costs.items():
+ percentage = (cost / total_cost) * 100
+ st.write(f"- {category}: {cost:,.2f} ريال ({percentage:.1f}%)")
+ else:
+ st.warning("لا توجد بنود في جدول الكميات")
def _render_pricing_scenarios_tab(self):
st.markdown("### سيناريوهات التسعير")
- self.strategies.render()
-
- def _render_competitive_analysis_tab(self):
- st.markdown("### المقارنة التنافسية")
- st.info("سيتم عرض مقارنة الأسعار والمنافسين هنا.")
+ balanced_pricing.render_balanced_strategy()
def _render_local_content_tab(self):
st.markdown("### المحتوى المحلي")
- overheads.render_local_content_ui()
-
- def _render_unbalanced_pricing_tab(self):
- st.markdown("### التسعير غير المتزن")
- st.info("سيتم عرض استراتيجية التسعير غير المتزن هنا لاحقًا.")
-
- def _render_reports_tab(self):
- st.markdown("### التقارير")
- st.info("يمكنك هنا تنزيل التقارير الخاصة بالتسعير وجدول الكميات.")
+ overheads.render_local_content_ui()
\ No newline at end of file
diff --git a/modules/pricing/pricing_engine.py b/modules/pricing/pricing_engine.py
index 5bf6307ec13ede246bf6173c576e33cfe6acd644..f80c9a29dd50f18802bf00d061ff76f5d493d889 100644
--- a/modules/pricing/pricing_engine.py
+++ b/modules/pricing/pricing_engine.py
@@ -1,430 +1,430 @@
-"""
-وحدة التسعير المتكامل لنظام إدارة المناقصات - Hybrid Face
-"""
-
-import os
-import logging
-import threading
-import datetime
-import json
-import math
-from pathlib import Path
-
-# تهيئة السجل
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger('pricing')
-
-class PricingEngine:
- """محرك التسعير المتكامل"""
-
- def __init__(self, config=None, db=None):
- """تهيئة محرك التسعير"""
- self.config = config
- self.db = db
- self.pricing_in_progress = False
- self.current_project = None
- self.pricing_results = {}
-
- # إنشاء مجلد التسعير إذا لم يكن موجوداً
- if config and hasattr(config, 'EXPORTS_PATH'):
- self.exports_path = Path(config.EXPORTS_PATH)
- else:
- self.exports_path = Path('data/exports')
-
- if not self.exports_path.exists():
- self.exports_path.mkdir(parents=True, exist_ok=True)
-
- def calculate_pricing(self, project_id, strategy="comprehensive", callback=None):
- """حساب التسعير للمشروع"""
- if self.pricing_in_progress:
- logger.warning("هناك عملية تسعير جارية بالفعل")
- return False
-
- self.pricing_in_progress = True
- self.current_project = project_id
- self.pricing_results = {
- "project_id": project_id,
- "strategy": strategy,
- "pricing_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- "status": "جاري التسعير",
- "direct_costs": {},
- "indirect_costs": {},
- "risk_costs": {},
- "summary": {}
- }
-
- # بدء التسعير في خيط منفصل
- thread = threading.Thread(
- target=self._calculate_pricing_thread,
- args=(project_id, strategy, callback)
- )
- thread.daemon = True
- thread.start()
-
- return True
-
- def _calculate_pricing_thread(self, project_id, strategy, callback):
- """خيط حساب التسعير"""
- try:
- # محاكاة جلب بيانات المشروع من قاعدة البيانات
- project_data = self._get_project_data(project_id)
-
- if not project_data:
- logger.error(f"لم يتم العثور على بيانات المشروع: {project_id}")
- self.pricing_results["status"] = "فشل التسعير"
- self.pricing_results["error"] = "لم يتم العثور على بيانات المشروع"
- return
-
- # حساب التكاليف المباشرة
- self._calculate_direct_costs(project_data)
-
- # حساب التكاليف غير المباشرة
- self._calculate_indirect_costs(project_data, strategy)
-
- # حساب تكاليف المخاطر
- self._calculate_risk_costs(project_data, strategy)
-
- # حساب ملخص التسعير
- self._calculate_pricing_summary(strategy)
-
- # تحديث حالة التسعير
- self.pricing_results["status"] = "اكتمل التسعير"
- self.pricing_results["pricing_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-
- logger.info(f"اكتمل تسعير المشروع: {project_id}")
-
- except Exception as e:
- logger.error(f"خطأ في تسعير المشروع: {str(e)}")
- self.pricing_results["status"] = "فشل التسعير"
- self.pricing_results["error"] = str(e)
-
- finally:
- self.pricing_in_progress = False
-
- # استدعاء دالة الاستجابة إذا تم توفيرها
- if callback and callable(callback):
- callback(self.pricing_results)
-
- def _get_project_data(self, project_id):
- """الحصول على بيانات المشروع"""
- # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
- # هنا نقوم بمحاكاة البيانات للتوضيح
-
- return {
- "id": project_id,
- "name": "مشروع الطرق السريعة",
- "client": "وزارة النقل",
- "items": [
- {"id": 1, "name": "أعمال الحفر", "unit": "م³", "quantity": 1500, "unit_cost": 45},
- {"id": 2, "name": "أعمال الخرسانة", "unit": "م³", "quantity": 750, "unit_cost": 1200},
- {"id": 3, "name": "أعمال الأسفلت", "unit": "م²", "quantity": 5000, "unit_cost": 120},
- {"id": 4, "name": "أعمال الإنارة", "unit": "عدد", "quantity": 50, "unit_cost": 3500}
- ],
- "resources": {
- "materials": [
- {"id": 1, "name": "أسمنت", "unit": "طن", "quantity": 300, "unit_cost": 950},
- {"id": 2, "name": "حديد تسليح", "unit": "طن", "quantity": 120, "unit_cost": 3200},
- {"id": 3, "name": "رمل", "unit": "م³", "quantity": 450, "unit_cost": 75},
- {"id": 4, "name": "أسفلت", "unit": "طن", "quantity": 200, "unit_cost": 1800}
- ],
- "equipment": [
- {"id": 1, "name": "حفارة", "unit": "يوم", "quantity": 45, "unit_cost": 1500},
- {"id": 2, "name": "لودر", "unit": "يوم", "quantity": 30, "unit_cost": 1200},
- {"id": 3, "name": "شاحنة نقل", "unit": "يوم", "quantity": 60, "unit_cost": 800},
- {"id": 4, "name": "خلاطة خرسانة", "unit": "يوم", "quantity": 40, "unit_cost": 600}
- ],
- "labor": [
- {"id": 1, "name": "عمال", "unit": "يوم", "quantity": 1200, "unit_cost": 150},
- {"id": 2, "name": "فنيون", "unit": "يوم", "quantity": 600, "unit_cost": 300},
- {"id": 3, "name": "مهندسون", "unit": "يوم", "quantity": 180, "unit_cost": 800}
- ]
- },
- "risks": [
- {"id": 1, "name": "تأخر توريد المواد", "probability": "متوسط", "impact": "عالي", "cost_impact": 0.05},
- {"id": 2, "name": "تغير أسعار المواد", "probability": "عالي", "impact": "عالي", "cost_impact": 0.08},
- {"id": 3, "name": "ظروف جوية غير مواتية", "probability": "منخفض", "impact": "متوسط", "cost_impact": 0.03},
- {"id": 4, "name": "نقص العمالة", "probability": "متوسط", "impact": "متوسط", "cost_impact": 0.04}
- ],
- "project_duration": 180, # بالأيام
- "location": "المنطقة الشرقية"
- }
-
- def _calculate_direct_costs(self, project_data):
- """حساب التكاليف المباشرة"""
- # حساب تكاليف البنود
- items_cost = 0
- items_details = []
-
- for item in project_data["items"]:
- total_cost = item["quantity"] * item["unit_cost"]
- items_cost += total_cost
-
- items_details.append({
- "id": item["id"],
- "name": item["name"],
- "unit": item["unit"],
- "quantity": item["quantity"],
- "unit_cost": item["unit_cost"],
- "total_cost": total_cost
- })
-
- # حساب تكاليف الموارد
- materials_cost = 0
- equipment_cost = 0
- labor_cost = 0
-
- for material in project_data["resources"]["materials"]:
- materials_cost += material["quantity"] * material["unit_cost"]
-
- for equipment in project_data["resources"]["equipment"]:
- equipment_cost += equipment["quantity"] * equipment["unit_cost"]
-
- for labor in project_data["resources"]["labor"]:
- labor_cost += labor["quantity"] * labor["unit_cost"]
-
- resources_cost = materials_cost + equipment_cost + labor_cost
-
- # تخزين نتائج التكاليف المباشرة
- self.pricing_results["direct_costs"] = {
- "items": {
- "total": items_cost,
- "details": items_details
- },
- "resources": {
- "total": resources_cost,
- "materials": materials_cost,
- "equipment": equipment_cost,
- "labor": labor_cost
- },
- "total_direct_costs": items_cost
- }
-
- def _calculate_indirect_costs(self, project_data, strategy):
- """حساب التكاليف غير المباشرة"""
- direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
-
- # تحديد نسب التكاليف غير المباشرة بناءً على استراتيجية التسعير
- if strategy == "comprehensive":
- overhead_rate = 0.15 # 15% نفقات عامة
- profit_rate = 0.10 # 10% ربح
- admin_rate = 0.05 # 5% تكاليف إدارية
- elif strategy == "competitive":
- overhead_rate = 0.12 # 12% نفقات عامة
- profit_rate = 0.07 # 7% ربح
- admin_rate = 0.04 # 4% تكاليف إدارية
- else: # balanced
- overhead_rate = 0.13 # 13% نفقات عامة
- profit_rate = 0.08 # 8% ربح
- admin_rate = 0.045 # 4.5% تكاليف إدارية
-
- # حساب التكاليف غير المباشرة
- overhead_cost = direct_costs * overhead_rate
- profit_cost = direct_costs * profit_rate
- admin_cost = direct_costs * admin_rate
-
- # تكاليف إضافية
- mobilization_cost = direct_costs * 0.03 # 3% تكاليف التجهيز
- bonds_insurance_cost = direct_costs * 0.02 # 2% تكاليف الضمانات والتأمين
-
- # إجمالي التكاليف غير المباشرة
- total_indirect_costs = overhead_cost + profit_cost + admin_cost + mobilization_cost + bonds_insurance_cost
-
- # تخزين نتائج التكاليف غير المباشرة
- self.pricing_results["indirect_costs"] = {
- "overhead": {
- "rate": overhead_rate,
- "cost": overhead_cost
- },
- "profit": {
- "rate": profit_rate,
- "cost": profit_cost
- },
- "administrative": {
- "rate": admin_rate,
- "cost": admin_cost
- },
- "mobilization": {
- "rate": 0.03,
- "cost": mobilization_cost
- },
- "bonds_insurance": {
- "rate": 0.02,
- "cost": bonds_insurance_cost
- },
- "total_indirect_costs": total_indirect_costs
- }
-
- def _calculate_risk_costs(self, project_data, strategy):
- """حساب تكاليف المخاطر"""
- direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
-
- # تحويل احتمالية وتأثير المخاطر إلى قيم رقمية
- probability_map = {
- "منخفض": 0.3,
- "متوسط": 0.5,
- "عالي": 0.7
- }
-
- impact_map = {
- "منخفض": 0.3,
- "متوسط": 0.5,
- "عالي": 0.7
- }
-
- # حساب تكاليف المخاطر
- risk_costs = []
- total_risk_cost = 0
-
- for risk in project_data["risks"]:
- probability = probability_map.get(risk["probability"], 0.5)
- impact = impact_map.get(risk["impact"], 0.5)
-
- # حساب درجة المخاطرة
- risk_score = probability * impact
-
- # حساب تكلفة المخاطرة
- risk_cost = direct_costs * risk["cost_impact"] * risk_score
-
- # تعديل تكلفة المخاطرة بناءً على استراتيجية التسعير
- if strategy == "comprehensive":
- risk_cost_factor = 1.0 # تغطية كاملة للمخاطر
- elif strategy == "competitive":
- risk_cost_factor = 0.7 # تغطية جزئية للمخاطر
- else: # balanced
- risk_cost_factor = 0.85 # تغطية متوازنة للمخاطر
-
- adjusted_risk_cost = risk_cost * risk_cost_factor
- total_risk_cost += adjusted_risk_cost
-
- risk_costs.append({
- "id": risk["id"],
- "name": risk["name"],
- "probability": risk["probability"],
- "impact": risk["impact"],
- "risk_score": risk_score,
- "cost_impact": risk["cost_impact"],
- "risk_cost": risk_cost,
- "adjusted_risk_cost": adjusted_risk_cost
- })
-
- # تخزين نتائج تكاليف المخاطر
- self.pricing_results["risk_costs"] = {
- "risks": risk_costs,
- "total_risk_cost": total_risk_cost,
- "strategy_factor": 1.0 if strategy == "comprehensive" else (0.7 if strategy == "competitive" else 0.85)
- }
-
- def _calculate_pricing_summary(self, strategy):
- """حساب ملخص التسعير"""
- direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
- indirect_costs = self.pricing_results["indirect_costs"]["total_indirect_costs"]
- risk_costs = self.pricing_results["risk_costs"]["total_risk_cost"]
-
- # حساب إجمالي التكاليف
- total_costs = direct_costs + indirect_costs + risk_costs
-
- # حساب ضريبة القيمة المضافة (15%)
- vat = total_costs * 0.15
-
- # حساب السعر النهائي
- final_price = total_costs + vat
-
- # تخزين ملخص التسعير
- self.pricing_results["summary"] = {
- "direct_costs": direct_costs,
- "indirect_costs": indirect_costs,
- "risk_costs": risk_costs,
- "total_costs": total_costs,
- "vat": {
- "rate": 0.15,
- "amount": vat
- },
- "final_price": final_price,
- "strategy": strategy,
- "pricing_notes": self._generate_pricing_notes(strategy)
- }
-
- def _generate_pricing_notes(self, strategy):
- """توليد ملاحظات التسعير"""
- if strategy == "comprehensive":
- return [
- "تم تطبيق استراتيجية التسعير الشاملة التي تغطي جميع التكاليف والمخاطر",
- "تم تضمين هامش ربح مناسب (10%) لضمان ربحية المشروع",
- "تم تغطية جميع المخاطر المحتملة بشكل كامل",
- "يوصى بمراجعة أسعار المواد قبل تقديم العرض النهائي"
- ]
- elif strategy == "competitive":
- return [
- "تم تطبيق استراتيجية التسعير التنافسية لزيادة فرص الفوز بالمناقصة",
- "تم تخفيض هامش الربح (7%) لتقديم سعر تنافسي",
- "تم تغطية المخاطر بشكل جزئي، مما يتطلب إدارة مخاطر فعالة أثناء التنفيذ",
- "يجب مراقبة التكاليف بدقة أثناء تنفيذ المشروع لضمان الربحية"
- ]
- else: # balanced
- return [
- "تم تطبيق استراتيجية التسعير المتوازنة التي توازن بين الربحية والتنافسية",
- "تم تضمين هامش ربح معقول (8%) يوازن بين الربحية والتنافسية",
- "تم تغطية المخاطر الرئيسية بشكل مناسب",
- "يوصى بمراجعة بنود التكلفة العالية قبل تقديم العرض النهائي"
- ]
-
- def get_pricing_status(self):
- """الحصول على حالة التسعير الحالي"""
- if not self.pricing_in_progress:
- if not self.pricing_results:
- return {"status": "لا يوجد تسعير جارٍ"}
- else:
- return {"status": self.pricing_results.get("status", "غير معروف")}
-
- return {
- "status": "جاري التسعير",
- "project_id": self.current_project,
- "start_time": self.pricing_results.get("pricing_start_time")
- }
-
- def get_pricing_results(self):
- """الحصول على نتائج التسعير"""
- return self.pricing_results
-
- def export_pricing_results(self, output_path=None):
- """تصدير نتائج التسعير إلى ملف JSON"""
- if not self.pricing_results:
- logger.warning("لا توجد نتائج تسعير للتصدير")
- return None
-
- if not output_path:
- # إنشاء اسم ملف افتراضي
- timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
- filename = f"pricing_results_{timestamp}.json"
- output_path = os.path.join(self.exports_path, filename)
-
- try:
- with open(output_path, 'w', encoding='utf-8') as f:
- json.dump(self.pricing_results, f, ensure_ascii=False, indent=4)
-
- logger.info(f"تم تصدير نتائج التسعير إلى: {output_path}")
- return output_path
-
- except Exception as e:
- logger.error(f"خطأ في تصدير نتائج التسعير: {str(e)}")
- return None
-
- def import_pricing_results(self, input_path):
- """استيراد نتائج التسعير من ملف JSON"""
- if not os.path.exists(input_path):
- logger.error(f"ملف نتائج التسعير غير موجود: {input_path}")
- return False
-
- try:
- with open(input_path, 'r', encoding='utf-8') as f:
- self.pricing_results = json.load(f)
-
- logger.info(f"تم استيراد نتائج التسعير من: {input_path}")
- return True
-
- except Exception as e:
- logger.error(f"خطأ في استيراد نتائج التسعير: {str(e)}")
- return False
+"""
+وحدة التسعير المتكامل لنظام إدارة المناقصات - Hybrid Face
+"""
+
+import os
+import logging
+import threading
+import datetime
+import json
+import math
+from pathlib import Path
+
+# تهيئة السجل
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger('pricing')
+
+class PricingEngine:
+ """محرك التسعير المتكامل"""
+
+ def __init__(self, config=None, db=None):
+ """تهيئة محرك التسعير"""
+ self.config = config
+ self.db = db
+ self.pricing_in_progress = False
+ self.current_project = None
+ self.pricing_results = {}
+
+ # إنشاء مجلد التسعير إذا لم يكن موجوداً
+ if config and hasattr(config, 'EXPORTS_PATH'):
+ self.exports_path = Path(config.EXPORTS_PATH)
+ else:
+ self.exports_path = Path('data/exports')
+
+ if not self.exports_path.exists():
+ self.exports_path.mkdir(parents=True, exist_ok=True)
+
+ def calculate_pricing(self, project_id, strategy="comprehensive", callback=None):
+ """حساب التسعير للمشروع"""
+ if self.pricing_in_progress:
+ logger.warning("هناك عملية تسعير جارية بالفعل")
+ return False
+
+ self.pricing_in_progress = True
+ self.current_project = project_id
+ self.pricing_results = {
+ "project_id": project_id,
+ "strategy": strategy,
+ "pricing_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ "status": "جاري التسعير",
+ "direct_costs": {},
+ "indirect_costs": {},
+ "risk_costs": {},
+ "summary": {}
+ }
+
+ # بدء التسعير في خيط منفصل
+ thread = threading.Thread(
+ target=self._calculate_pricing_thread,
+ args=(project_id, strategy, callback)
+ )
+ thread.daemon = True
+ thread.start()
+
+ return True
+
+ def _calculate_pricing_thread(self, project_id, strategy, callback):
+ """خيط حساب التسعير"""
+ try:
+ # محاكاة جلب بيانات المشروع من قاعدة البيانات
+ project_data = self._get_project_data(project_id)
+
+ if not project_data:
+ logger.error(f"لم يتم العثور على بيانات المشروع: {project_id}")
+ self.pricing_results["status"] = "فشل التسعير"
+ self.pricing_results["error"] = "لم يتم العثور على بيانات المشروع"
+ return
+
+ # حساب التكاليف المباشرة
+ self._calculate_direct_costs(project_data)
+
+ # حساب التكاليف غير المباشرة
+ self._calculate_indirect_costs(project_data, strategy)
+
+ # حساب تكاليف المخاطر
+ self._calculate_risk_costs(project_data, strategy)
+
+ # حساب ملخص التسعير
+ self._calculate_pricing_summary(strategy)
+
+ # تحديث حالة التسعير
+ self.pricing_results["status"] = "اكتمل التسعير"
+ self.pricing_results["pricing_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+
+ logger.info(f"اكتمل تسعير المشروع: {project_id}")
+
+ except Exception as e:
+ logger.error(f"خطأ في تسعير المشروع: {str(e)}")
+ self.pricing_results["status"] = "فشل التسعير"
+ self.pricing_results["error"] = str(e)
+
+ finally:
+ self.pricing_in_progress = False
+
+ # استدعاء دالة الاستجابة إذا تم توفيرها
+ if callback and callable(callback):
+ callback(self.pricing_results)
+
+ def _get_project_data(self, project_id):
+ """الحصول على بيانات المشروع"""
+ # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
+ # هنا نقوم بمحاكاة البيانات للتوضيح
+
+ return {
+ "id": project_id,
+ "name": "مشروع الطرق السريعة",
+ "client": "وزارة النقل",
+ "items": [
+ {"id": 1, "name": "أعمال الحفر", "unit": "م³", "quantity": 1500, "unit_cost": 45},
+ {"id": 2, "name": "أعمال الخرسانة", "unit": "م³", "quantity": 750, "unit_cost": 1200},
+ {"id": 3, "name": "أعمال الأسفلت", "unit": "م²", "quantity": 5000, "unit_cost": 120},
+ {"id": 4, "name": "أعمال الإنارة", "unit": "عدد", "quantity": 50, "unit_cost": 3500}
+ ],
+ "resources": {
+ "materials": [
+ {"id": 1, "name": "أسمنت", "unit": "طن", "quantity": 300, "unit_cost": 950},
+ {"id": 2, "name": "حديد تسليح", "unit": "طن", "quantity": 120, "unit_cost": 3200},
+ {"id": 3, "name": "رمل", "unit": "م³", "quantity": 450, "unit_cost": 75},
+ {"id": 4, "name": "أسفلت", "unit": "طن", "quantity": 200, "unit_cost": 1800}
+ ],
+ "equipment": [
+ {"id": 1, "name": "حفارة", "unit": "يوم", "quantity": 45, "unit_cost": 1500},
+ {"id": 2, "name": "لودر", "unit": "يوم", "quantity": 30, "unit_cost": 1200},
+ {"id": 3, "name": "شاحنة نقل", "unit": "يوم", "quantity": 60, "unit_cost": 800},
+ {"id": 4, "name": "خلاطة خرسانة", "unit": "يوم", "quantity": 40, "unit_cost": 600}
+ ],
+ "labor": [
+ {"id": 1, "name": "عمال", "unit": "يوم", "quantity": 1200, "unit_cost": 150},
+ {"id": 2, "name": "فنيون", "unit": "يوم", "quantity": 600, "unit_cost": 300},
+ {"id": 3, "name": "مهندسون", "unit": "يوم", "quantity": 180, "unit_cost": 800}
+ ]
+ },
+ "risks": [
+ {"id": 1, "name": "تأخر توريد المواد", "probability": "متوسط", "impact": "عالي", "cost_impact": 0.05},
+ {"id": 2, "name": "تغير أسعار المواد", "probability": "عالي", "impact": "عالي", "cost_impact": 0.08},
+ {"id": 3, "name": "ظروف جوية غير مواتية", "probability": "منخفض", "impact": "متوسط", "cost_impact": 0.03},
+ {"id": 4, "name": "نقص العمالة", "probability": "متوسط", "impact": "متوسط", "cost_impact": 0.04}
+ ],
+ "project_duration": 180, # بالأيام
+ "location": "المنطقة الشرقية"
+ }
+
+ def _calculate_direct_costs(self, project_data):
+ """حساب التكاليف المباشرة"""
+ # حساب تكاليف البنود
+ items_cost = 0
+ items_details = []
+
+ for item in project_data["items"]:
+ total_cost = item["quantity"] * item["unit_cost"]
+ items_cost += total_cost
+
+ items_details.append({
+ "id": item["id"],
+ "name": item["name"],
+ "unit": item["unit"],
+ "quantity": item["quantity"],
+ "unit_cost": item["unit_cost"],
+ "total_cost": total_cost
+ })
+
+ # حساب تكاليف الموارد
+ materials_cost = 0
+ equipment_cost = 0
+ labor_cost = 0
+
+ for material in project_data["resources"]["materials"]:
+ materials_cost += material["quantity"] * material["unit_cost"]
+
+ for equipment in project_data["resources"]["equipment"]:
+ equipment_cost += equipment["quantity"] * equipment["unit_cost"]
+
+ for labor in project_data["resources"]["labor"]:
+ labor_cost += labor["quantity"] * labor["unit_cost"]
+
+ resources_cost = materials_cost + equipment_cost + labor_cost
+
+ # تخزين نتائج التكاليف المباشرة
+ self.pricing_results["direct_costs"] = {
+ "items": {
+ "total": items_cost,
+ "details": items_details
+ },
+ "resources": {
+ "total": resources_cost,
+ "materials": materials_cost,
+ "equipment": equipment_cost,
+ "labor": labor_cost
+ },
+ "total_direct_costs": items_cost
+ }
+
+ def _calculate_indirect_costs(self, project_data, strategy):
+ """حساب التكاليف غير المباشرة"""
+ direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
+
+ # تحديد نسب التكاليف غير المباشرة بناءً على استراتيجية التسعير
+ if strategy == "comprehensive":
+ overhead_rate = 0.15 # 15% نفقات عامة
+ profit_rate = 0.10 # 10% ربح
+ admin_rate = 0.05 # 5% تكاليف إدارية
+ elif strategy == "competitive":
+ overhead_rate = 0.12 # 12% نفقات عامة
+ profit_rate = 0.07 # 7% ربح
+ admin_rate = 0.04 # 4% تكاليف إدارية
+ else: # balanced
+ overhead_rate = 0.13 # 13% نفقات عامة
+ profit_rate = 0.08 # 8% ربح
+ admin_rate = 0.045 # 4.5% تكاليف إدارية
+
+ # حساب التكاليف غير المباشرة
+ overhead_cost = direct_costs * overhead_rate
+ profit_cost = direct_costs * profit_rate
+ admin_cost = direct_costs * admin_rate
+
+ # تكاليف إضافية
+ mobilization_cost = direct_costs * 0.03 # 3% تكاليف التجهيز
+ bonds_insurance_cost = direct_costs * 0.02 # 2% تكاليف الضمانات والتأمين
+
+ # إجمالي التكاليف غير المباشرة
+ total_indirect_costs = overhead_cost + profit_cost + admin_cost + mobilization_cost + bonds_insurance_cost
+
+ # تخزين نتائج التكاليف غير المباشرة
+ self.pricing_results["indirect_costs"] = {
+ "overhead": {
+ "rate": overhead_rate,
+ "cost": overhead_cost
+ },
+ "profit": {
+ "rate": profit_rate,
+ "cost": profit_cost
+ },
+ "administrative": {
+ "rate": admin_rate,
+ "cost": admin_cost
+ },
+ "mobilization": {
+ "rate": 0.03,
+ "cost": mobilization_cost
+ },
+ "bonds_insurance": {
+ "rate": 0.02,
+ "cost": bonds_insurance_cost
+ },
+ "total_indirect_costs": total_indirect_costs
+ }
+
+ def _calculate_risk_costs(self, project_data, strategy):
+ """حساب تكاليف المخاطر"""
+ direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
+
+ # تحويل احتمالية وتأثير المخاطر إلى قيم رقمية
+ probability_map = {
+ "منخفض": 0.3,
+ "متوسط": 0.5,
+ "عالي": 0.7
+ }
+
+ impact_map = {
+ "منخفض": 0.3,
+ "متوسط": 0.5,
+ "عالي": 0.7
+ }
+
+ # حساب تكاليف المخاطر
+ risk_costs = []
+ total_risk_cost = 0
+
+ for risk in project_data["risks"]:
+ probability = probability_map.get(risk["probability"], 0.5)
+ impact = impact_map.get(risk["impact"], 0.5)
+
+ # حساب درجة المخاطرة
+ risk_score = probability * impact
+
+ # حساب تكلفة المخاطرة
+ risk_cost = direct_costs * risk["cost_impact"] * risk_score
+
+ # تعديل تكلفة المخاطرة بناءً على استراتيجية التسعير
+ if strategy == "comprehensive":
+ risk_cost_factor = 1.0 # تغطية كاملة للمخاطر
+ elif strategy == "competitive":
+ risk_cost_factor = 0.7 # تغطية جزئية للمخاطر
+ else: # balanced
+ risk_cost_factor = 0.85 # تغطية متوازنة للمخاطر
+
+ adjusted_risk_cost = risk_cost * risk_cost_factor
+ total_risk_cost += adjusted_risk_cost
+
+ risk_costs.append({
+ "id": risk["id"],
+ "name": risk["name"],
+ "probability": risk["probability"],
+ "impact": risk["impact"],
+ "risk_score": risk_score,
+ "cost_impact": risk["cost_impact"],
+ "risk_cost": risk_cost,
+ "adjusted_risk_cost": adjusted_risk_cost
+ })
+
+ # تخزين نتائج تكاليف المخاطر
+ self.pricing_results["risk_costs"] = {
+ "risks": risk_costs,
+ "total_risk_cost": total_risk_cost,
+ "strategy_factor": 1.0 if strategy == "comprehensive" else (0.7 if strategy == "competitive" else 0.85)
+ }
+
+ def _calculate_pricing_summary(self, strategy):
+ """حساب ملخص التسعير"""
+ direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
+ indirect_costs = self.pricing_results["indirect_costs"]["total_indirect_costs"]
+ risk_costs = self.pricing_results["risk_costs"]["total_risk_cost"]
+
+ # حساب إجمالي التكاليف
+ total_costs = direct_costs + indirect_costs + risk_costs
+
+ # حساب ضريبة القيمة المضافة (15%)
+ vat = total_costs * 0.15
+
+ # حساب السعر النهائي
+ final_price = total_costs + vat
+
+ # تخزين ملخص التسعير
+ self.pricing_results["summary"] = {
+ "direct_costs": direct_costs,
+ "indirect_costs": indirect_costs,
+ "risk_costs": risk_costs,
+ "total_costs": total_costs,
+ "vat": {
+ "rate": 0.15,
+ "amount": vat
+ },
+ "final_price": final_price,
+ "strategy": strategy,
+ "pricing_notes": self._generate_pricing_notes(strategy)
+ }
+
+ def _generate_pricing_notes(self, strategy):
+ """توليد ملاحظات التسعير"""
+ if strategy == "comprehensive":
+ return [
+ "تم تطبيق استراتيجية التسعير الشاملة التي تغطي جميع التكاليف والمخاطر",
+ "تم تضمين هامش ربح مناسب (10%) لضمان ربحية المشروع",
+ "تم تغطية جميع المخاطر المحتملة بشكل كامل",
+ "يوصى بمراجعة أسعار المواد قبل تقديم العرض النهائي"
+ ]
+ elif strategy == "competitive":
+ return [
+ "تم تطبيق استراتيجية التسعير التنافسية لزيادة فرص الفوز بالمناقصة",
+ "تم تخفيض هامش الربح (7%) لتقديم سعر تنافسي",
+ "تم تغطية المخاطر بشكل جزئي، مما يتطلب إدارة مخاطر فعالة أثناء التنفيذ",
+ "يجب مراقبة التكاليف بدقة أثناء تنفيذ المشروع لضمان الربحية"
+ ]
+ else: # balanced
+ return [
+ "تم تطبيق استراتيجية التسعير المتوازنة التي توازن بين الربحية والتنافسية",
+ "تم تضمين هامش ربح معقول (8%) يوازن بين الربحية والتنافسية",
+ "تم تغطية المخاطر الرئيسية بشكل مناسب",
+ "يوصى بمراجعة بنود التكلفة العالية قبل تقديم العرض النهائي"
+ ]
+
+ def get_pricing_status(self):
+ """الحصول على حالة التسعير الحالي"""
+ if not self.pricing_in_progress:
+ if not self.pricing_results:
+ return {"status": "لا يوجد تسعير جارٍ"}
+ else:
+ return {"status": self.pricing_results.get("status", "غير معروف")}
+
+ return {
+ "status": "جاري التسعير",
+ "project_id": self.current_project,
+ "start_time": self.pricing_results.get("pricing_start_time")
+ }
+
+ def get_pricing_results(self):
+ """الحصول على نتائج التسعير"""
+ return self.pricing_results
+
+ def export_pricing_results(self, output_path=None):
+ """تصدير نتائج التسعير إلى ملف JSON"""
+ if not self.pricing_results:
+ logger.warning("لا توجد نتائج تسعير للتصدير")
+ return None
+
+ if not output_path:
+ # إنشاء اسم ملف افتراضي
+ timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
+ filename = f"pricing_results_{timestamp}.json"
+ output_path = os.path.join(self.exports_path, filename)
+
+ try:
+ with open(output_path, 'w', encoding='utf-8') as f:
+ json.dump(self.pricing_results, f, ensure_ascii=False, indent=4)
+
+ logger.info(f"تم تصدير نتائج التسعير إلى: {output_path}")
+ return output_path
+
+ except Exception as e:
+ logger.error(f"خطأ في تصدير نتائج التسعير: {str(e)}")
+ return None
+
+ def import_pricing_results(self, input_path):
+ """استيراد نتائج التسعير من ملف JSON"""
+ if not os.path.exists(input_path):
+ logger.error(f"ملف نتائج التسعير غير موجود: {input_path}")
+ return False
+
+ try:
+ with open(input_path, 'r', encoding='utf-8') as f:
+ self.pricing_results = json.load(f)
+
+ logger.info(f"تم استيراد نتائج التسعير من: {input_path}")
+ return True
+
+ except Exception as e:
+ logger.error(f"خطأ في استيراد نتائج التسعير: {str(e)}")
+ return False
diff --git a/modules/project_management/project_management_app.py b/modules/project_management/project_management_app.py
index d731b68e21c3ae64fb6e8b5b2357e7dc46e8b598..cc07df77e18197b039df6e94a9d2ddad7c578efc 100644
--- a/modules/project_management/project_management_app.py
+++ b/modules/project_management/project_management_app.py
@@ -1,666 +1,666 @@
-"""
-وحدة إدارة المشاريع - نظام تحليل المناقصات
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-from datetime import datetime, timedelta
-import os
-import time
-import io
-import sys
-from pathlib import Path
-
-# إضافة مسار المشروع للنظام
-sys.path.append(str(Path(__file__).parent.parent))
-
-# استيراد محسن واجهة المستخدم
-from styling.enhanced_ui import UIEnhancer
-
-class ProjectsApp:
- """وحدة إدارة المشاريع"""
-
- def __init__(self):
- """تهيئة وحدة إدارة المشاريع"""
- self.ui = UIEnhancer(page_title="إدارة المشاريع - نظام تحليل المناقصات", page_icon="📋")
- self.ui.apply_theme_colors()
-
- # تهيئة البيانات المبدئية
- if 'projects' not in st.session_state:
- st.session_state.projects = self._generate_sample_projects()
-
- def run(self):
- """تشغيل وحدة إدارة المشاريع"""
- # إنشاء قائمة العناصر
- menu_items = [
- {"name": "لوحة المعلومات", "icon": "house"},
- {"name": "المناقصات والعقود", "icon": "file-text"},
- {"name": "تحليل المستندات", "icon": "file-earmark-text"},
- {"name": "نظام التسعير", "icon": "calculator"},
- {"name": "حاسبة تكاليف البناء", "icon": "building"},
- {"name": "الموارد والتكاليف", "icon": "people"},
- {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
- {"name": "إدارة المشاريع", "icon": "kanban"},
- {"name": "الخرائط والمواقع", "icon": "geo-alt"},
- {"name": "الجدول الزمني", "icon": "calendar3"},
- {"name": "الإشعارات", "icon": "bell"},
- {"name": "مقارنة المستندات", "icon": "files"},
- {"name": "الترجمة", "icon": "translate"},
- {"name": "المساعد الذكي", "icon": "robot"},
- {"name": "التقارير", "icon": "bar-chart"},
- {"name": "الإعدادات", "icon": "gear"}
- ]
-
- # إنشاء الشريط الجانبي
- selected = self.ui.create_sidebar(menu_items)
-
- # إنشاء ترويسة الصفحة
- self.ui.create_header("إدارة المشاريع", "إدارة ومتابعة المشاريع والمناقصات")
-
- # عرض واجهة وحدة إدارة المشاريع
- tabs = st.tabs([
- "قائمة المشاريع",
- "إضافة مشروع جديد",
- "تفاصيل المشروع",
- "متابعة المشاريع"
- ])
-
- with tabs[0]:
- self._render_projects_list_tab()
-
- with tabs[1]:
- self._render_add_project_tab()
-
- with tabs[2]:
- self._render_project_details_tab()
-
- with tabs[3]:
- self._render_projects_tracking_tab()
-
- def _render_projects_list_tab(self):
- """عرض تبويب قائمة المشاريع"""
-
- st.markdown("### قائمة المشاريع")
-
- # فلترة المشاريع
- col1, col2, col3 = st.columns(3)
-
- with col1:
- search_term = st.text_input("البحث في المشاريع", key="project_search")
-
- with col2:
- status_filter = st.multiselect(
- "حالة المشروع",
- ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
- default=["جديد", "قيد التسعير", "تم التقديم"],
- key="project_status_filter"
- )
-
- with col3:
- client_filter = st.multiselect(
- "الجهة المالكة",
- list(set([p['client'] for p in st.session_state.projects])),
- key="project_client_filter"
- )
-
- # تطبيق الفلترة
- filtered_projects = st.session_state.projects
-
- if search_term:
- filtered_projects = [p for p in filtered_projects if search_term.lower() in p['name'].lower() or search_term in p['number']]
-
- if status_filter:
- filtered_projects = [p for p in filtered_projects if p['status'] in status_filter]
-
- if client_filter:
- filtered_projects = [p for p in filtered_projects if p['client'] in client_filter]
-
- # تحويل المشاريع المفلترة إلى DataFrame للعرض
- if filtered_projects:
- projects_df = pd.DataFrame(filtered_projects)
-
- # اختيار وترتيب الأعمدة
- display_columns = [
- 'name', 'number', 'client', 'location', 'status',
- 'submission_date', 'tender_type', 'created_at'
- ]
-
- # تغيير أسماء الأعمدة للعرض
- column_names = {
- 'name': 'اسم المشروع',
- 'number': 'رقم المناقصة',
- 'client': 'الجهة المالكة',
- 'location': 'الموقع',
- 'status': 'الحالة',
- 'submission_date': 'تاريخ التقديم',
- 'tender_type': 'نوع المناقصة',
- 'created_at': 'تاريخ الإنشاء'
- }
-
- display_df = projects_df[display_columns].rename(columns=column_names)
-
- # تنسيق التواريخ
- date_columns = ['تاريخ التقديم', 'تاريخ الإنشاء']
- for col in date_columns:
- if col in display_df.columns:
- display_df[col] = pd.to_datetime(display_df[col]).dt.strftime('%Y-%m-%d')
-
- # عرض الجدول
- st.dataframe(display_df, use_container_width=True, hide_index=True)
-
- # زر تصدير المشاريع
- if st.button("تصدير المشاريع إلى Excel"):
- # محاكاة التصدير
- st.success("تم تصدير المشاريع بنجاح!")
- else:
- st.info("لا توجد مشاريع تطابق معايير البحث.")
-
- def _render_add_project_tab(self):
- """عرض تبويب إضافة مشروع جديد"""
-
- st.markdown("### إضافة مشروع جديد")
-
- # نموذج إدخال بيانات المشروع
- with st.form("new_project_form"):
- col1, col2 = st.columns(2)
-
- with col1:
- project_name = st.text_input("اسم المشروع", key="new_project_name")
- client = st.text_input("الجهة المالكة", key="new_project_client")
- location = st.text_input("الموقع", key="new_project_location")
- tender_type = st.selectbox(
- "نوع المناقصة",
- ["عامة", "خاصة", "أمر مباشر"],
- key="new_project_tender_type"
- )
-
- with col2:
- tender_number = st.text_input("رقم المناقصة", key="new_project_number")
- submission_date = st.date_input("تاريخ التقديم", key="new_project_submission_date")
- pricing_method = st.selectbox(
- "طريقة التسعير",
- ["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
- key="new_project_pricing_method"
- )
- status = st.selectbox(
- "حالة المشروع",
- ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
- index=0,
- key="new_project_status"
- )
-
- description = st.text_area("وصف المشروع", key="new_project_description")
-
- submitted = st.form_submit_button("إضافة المشروع")
-
- if submitted:
- # التحقق من تعبئة الحقول الإلزامية
- if not project_name or not tender_number or not client:
- st.error("يرجى تعبئة جميع الحقول الإلزامية (اسم المشروع، رقم المناقصة، الجهة المالكة).")
- else:
- # إنشاء مشروع جديد
- new_project = {
- 'id': len(st.session_state.projects) + 1,
- 'name': project_name,
- 'number': tender_number,
- 'client': client,
- 'location': location,
- 'description': description,
- 'status': status,
- 'tender_type': tender_type,
- 'pricing_method': pricing_method,
- 'submission_date': submission_date,
- 'created_at': datetime.now(),
- 'created_by_id': 1 # معرف المستخدم الحالي
- }
-
- # إضافة المشروع إلى قائمة المشاريع
- st.session_state.projects.append(new_project)
-
- # رسالة نجاح
- st.success(f"تم إضافة المشروع [{project_name}] بنجاح!")
-
- # تعيين المشروع الحالي
- st.session_state.current_project = new_project
-
- def _render_project_details_tab(self):
- """عرض تبويب تفاصيل المشروع"""
-
- st.markdown("### تفاصيل المشروع")
-
- # التحقق من وجود مشروع حالي
- if 'current_project' not in st.session_state or st.session_state.current_project is None:
- # إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع
- project_names = [p['name'] for p in st.session_state.projects]
- selected_project_name = st.selectbox("اختر المشروع", project_names)
-
- if selected_project_name:
- selected_project = next((p for p in st.session_state.projects if p['name'] == selected_project_name), None)
- if selected_project:
- st.session_state.current_project = selected_project
- else:
- st.warning("لم يتم العثور على المشروع المحدد.")
- return
- else:
- st.info("يرجى اختيار مشروع لعرض تفاصيله.")
- return
-
- # عرض تفاصيل المشروع
- project = st.session_state.current_project
-
- # عرض معلومات المشروع الأساسية
- col1, col2, col3 = st.columns(3)
-
- with col1:
- st.markdown(f"**اسم المشروع**: {project['name']}")
- st.markdown(f"**رقم المناقصة**: {project['number']}")
- st.markdown(f"**الجهة المالكة**: {project['client']}")
-
- with col2:
- st.markdown(f"**الموقع**: {project['location']}")
- st.markdown(f"**نوع المناقصة**: {project['tender_type']}")
- st.markdown(f"**حالة المشروع**: {project['status']}")
-
- with col3:
- st.markdown(f"**طريقة التسعير**: {project['pricing_method']}")
- st.markdown(f"**تاريخ التقديم**: {project['submission_date'].strftime('%Y-%m-%d') if isinstance(project['submission_date'], datetime) else project['submission_date']}")
- st.markdown(f"**تاريخ الإنشاء**: {project['created_at'].strftime('%Y-%m-%d') if isinstance(project['created_at'], datetime) else project['created_at']}")
-
- # عرض وصف المشروع
- st.markdown("#### وصف المشروع")
- st.text_area("", value=project.get('description', ''), disabled=True, height=100)
-
- # عرض المستندات المرتبطة بالمشروع
- st.markdown("#### مستندات المشروع")
-
- if 'documents' in project and project['documents']:
- docs_df = pd.DataFrame(project['documents'])
- st.dataframe(docs_df, use_container_width=True, hide_index=True)
- else:
- st.info("لا توجد مستندات مرتبطة بهذا المشروع حاليًا.")
-
- # زر إضافة مستندات
- if st.button("إضافة مستندات"):
- st.session_state.upload_documents = True
-
- # واجهة تحميل المستندات
- if 'upload_documents' in st.session_state and st.session_state.upload_documents:
- st.markdown("#### تحميل مستندات جديدة")
-
- uploaded_file = st.file_uploader("اختر ملفًا", type=['pdf', 'docx', 'xlsx', 'png', 'jpg', 'dwg'])
- doc_type = st.selectbox("نوع المستند", ["كراسة شروط", "عقد", "مخططات", "جدول كميات", "مواصفات فنية", "تعديلات وملاحق"])
-
- if uploaded_file and st.button("تحميل المستند"):
- # محاكاة تحميل المستند
- with st.spinner("جاري تحميل المستند..."):
- time.sleep(2)
-
- # إنشاء مستند جديد
- new_document = {
- 'filename': uploaded_file.name,
- 'type': doc_type,
- 'upload_date': datetime.now().strftime('%Y-%m-%d'),
- 'size': f"{uploaded_file.size / 1024:.1f} KB"
- }
-
- # إضافة المستند إلى المشروع
- if 'documents' not in project:
- project['documents'] = []
-
- project['documents'].append(new_document)
-
- st.success(f"تم تحميل المستند [{uploaded_file.name}] بنجاح!")
- st.session_state.upload_documents = False
- st.experimental_rerun()
-
- # عرض البنود والكميات
- st.markdown("#### بنود وكميات المشروع")
-
- if 'items' in project and project['items']:
- items_df = pd.DataFrame(project['items'])
- st.dataframe(items_df, use_container_width=True, hide_index=True)
-
- # زر لتحويل البنود إلى وحدة التسعير
- if st.button("تحويل البنود إلى وحدة التسعير"):
- if 'manual_items' not in st.session_state:
- st.session_state.manual_items = pd.DataFrame()
-
- st.session_state.manual_items = items_df.copy()
- st.success("تم تحويل البنود إلى وحدة التسعير بنجاح!")
- else:
- st.info("لا توجد بنود وكميات لهذا المشروع حاليًا.")
-
- # زر استيراد البنود من وحدة تحليل المستندات
- if st.button("استيراد البنود من تحليل المستندات"):
- st.warning("ميزة استيراد البنود من تحليل المستندات قيد التطوير.")
-
- # أزرار الإجراءات
- col1, col2, col3 = st.columns(3)
-
- with col1:
- if st.button("تعديل المشروع"):
- st.session_state.edit_project = True
- st.experimental_rerun()
-
- with col2:
- if st.button("تصدير بيانات المشروع"):
- st.success("تم تصدير بيانات المشروع بنجاح!")
-
- with col3:
- if st.button("إرسال للاعتماد"):
- st.success("تم إرسال المشروع للاعتماد بنجاح!")
-
- # نموذج تعديل المشروع
- if 'edit_project' in st.session_state and st.session_state.edit_project:
- st.markdown("#### تعديل المشروع")
-
- with st.form("edit_project_form"):
- col1, col2 = st.columns(2)
-
- with col1:
- project_name = st.text_input("اسم المشروع", value=project['name'])
- client = st.text_input("الجهة المالكة", value=project['client'])
- location = st.text_input("الموقع", value=project['location'])
- tender_type = st.selectbox(
- "نوع المناقصة",
- ["عامة", "خاصة", "أمر مباشر"],
- index=["عامة", "خاصة", "أمر مباشر"].index(project['tender_type'])
- )
-
- with col2:
- tender_number = st.text_input("رقم المناقصة", value=project['number'])
- submission_date = st.date_input(
- "تاريخ التقديم",
- value=datetime.strptime(project['submission_date'], "%Y-%m-%d") if isinstance(project['submission_date'], str) else project['submission_date']
- )
- pricing_method = st.selectbox(
- "طريقة التسعير",
- ["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
- index=["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"].index(project['pricing_method'])
- )
- status = st.selectbox(
- "حالة المشروع",
- ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
- index=["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"].index(project['status'])
- )
-
- description = st.text_area("وصف المشروع", value=project.get('description', ''))
-
- col1, col2 = st.columns(2)
-
- with col1:
- submit = st.form_submit_button("حفظ التعديلات")
-
- with col2:
- cancel = st.form_submit_button("إلغاء")
-
- if submit:
- # تحديث بيانات المشروع
- project['name'] = project_name
- project['number'] = tender_number
- project['client'] = client
- project['location'] = location
- project['description'] = description
- project['status'] = status
- project['tender_type'] = tender_type
- project['pricing_method'] = pricing_method
- project['submission_date'] = submission_date
-
- st.success("تم تحديث بيانات المشروع بنجاح!")
- st.session_state.edit_project = False
- st.experimental_rerun()
-
- elif cancel:
- st.session_state.edit_project = False
- st.experimental_rerun()
-
- def _render_projects_tracking_tab(self):
- """عرض تبويب متابعة المشاريع"""
-
- st.markdown("### متابعة المشاريع")
-
- # عرض إحصائيات المشاريع
- col1, col2, col3, col4 = st.columns(4)
-
- projects = st.session_state.projects
-
- with col1:
- total_projects = len(projects)
- self.ui.create_metric_card("إجمالي المشاريع", str(total_projects), None, self.ui.COLORS['primary'])
-
- with col2:
- active_projects = len([p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]])
- self.ui.create_metric_card("المشاريع النشطة", str(active_projects), None, self.ui.COLORS['success'])
-
- with col3:
- pending_submission = len([p for p in projects if p['status'] in ["جديد", "قيد التسعير"]])
- self.ui.create_metric_card("مشاريع قيد التسعير", str(pending_submission), None, self.ui.COLORS['warning'])
-
- with col4:
- completed_projects = len([p for p in projects if p['status'] in ["منتهي"]])
- self.ui.create_metric_card("المشاريع المنتهية", str(completed_projects), None, self.ui.COLORS['info'])
-
- # عرض رسم بياني لحالة المشاريع
- st.markdown("#### توزيع المشاريع حسب الحالة")
-
- status_counts = {}
- for p in projects:
- status = p['status']
- status_counts[status] = status_counts.get(status, 0) + 1
-
- status_df = pd.DataFrame({
- 'الحالة': list(status_counts.keys()),
- 'عدد المشاريع': list(status_counts.values())
- })
-
- st.bar_chart(status_df.set_index('الحالة'))
-
- # عرض المشاريع قيد المتابعة
- st.markdown("#### المشاريع قيد المتابعة")
-
- # عرض المشاريع النشطة المرتبة حسب تاريخ التقديم
- active_projects_list = [p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]]
-
- if active_projects_list:
- # تحويل التواريخ إلى كائنات تاريخ إذا كانت نصوصًا
- for p in active_projects_list:
- if isinstance(p['submission_date'], str):
- p['submission_date'] = datetime.strptime(p['submission_date'], "%Y-%m-%d")
-
- # ترتيب المشاريع حسب تاريخ التقديم
- active_projects_list.sort(key=lambda x: x['submission_date'])
-
- # تحويل إلى DataFrame
- active_df = pd.DataFrame(active_projects_list)
-
- # اختيار وترتيب الأعمدة
- display_columns = [
- 'name', 'number', 'client', 'status',
- 'submission_date', 'tender_type'
- ]
-
- # تغيير أسماء الأعمدة
- column_names = {
- 'name': 'اسم المشروع',
- 'number': 'رقم المناقصة',
- 'client': 'الجهة المالكة',
- 'status': 'الحالة',
- 'submission_date': 'تاريخ التقديم',
- 'tender_type': 'نوع المناقصة'
- }
-
- # تنسيق البيانات
- display_df = active_df[display_columns].rename(columns=column_names)
- display_df['تاريخ التقديم'] = pd.to_datetime(display_df['تاريخ التقديم']).dt.strftime('%Y-%m-%d')
-
- # عرض الجدول
- st.dataframe(display_df, use_container_width=True, hide_index=True)
- else:
- st.info("لا توجد مشاريع نشطة حاليًا.")
-
- # عرض المشاريع المقبلة
- st.markdown("#### المواعيد المقبلة")
-
- upcoming_events = []
- today = datetime.now().date()
-
- for p in projects:
- submission_date = p['submission_date']
- if isinstance(submission_date, str):
- submission_date = datetime.strptime(submission_date, "%Y-%m-%d").date()
- elif isinstance(submission_date, datetime):
- submission_date = submission_date.date()
-
- # المشاريع التي موعد تقديمها خلال الأسبوعين القادمين
- if today <= submission_date <= today + timedelta(days=14) and p['status'] in ["قيد التسعير"]:
- days_left = (submission_date - today).days
- upcoming_events.append({
- 'المشروع': p['name'],
- 'الحدث': 'موعد تقديم المناقصة',
- 'التاريخ': submission_date.strftime('%Y-%m-%d'),
- 'الأيام المتبقية': days_left
- })
-
- if upcoming_events:
- events_df = pd.DataFrame(upcoming_events)
- st.dataframe(events_df, use_container_width=True, hide_index=True)
- else:
- st.info("لا توجد مواعيد قريبة.")
-
- def _generate_sample_projects(self):
- """توليد بيانات افتراضية للمشاريع"""
-
- projects = [
- {
- 'id': 1,
- 'name': "إنشاء مبنى مستشفى الولادة والأطفال بمنطقة الشرقية",
- 'number': "SHPD-2025-001",
- 'client': "وزارة الصحة",
- 'location': "الدمام، المنطقة الشرقية",
- 'description': "يشمل المشروع إنشاء وتجهيز مبنى مستشفى الولادة والأطفال بسعة 300 سرير، ويتكون المبنى من 4 طوابق بمساحة إجمالية 15,000 متر مربع.",
- 'status': "قيد التسعير",
- 'tender_type': "عامة",
- 'pricing_method': "قياسي",
- 'submission_date': (datetime.now() + timedelta(days=5)),
- 'created_at': datetime.now() - timedelta(days=10),
- 'created_by_id': 1,
- 'documents': [
- {
- 'filename': "كراسة الشروط والمواصفات.pdf",
- 'type': "كراسة شروط",
- 'upload_date': (datetime.now() - timedelta(days=9)).strftime('%Y-%m-%d'),
- 'size': "5.2 MB"
- },
- {
- 'filename': "المخططات الهندسية.dwg",
- 'type': "مخططات",
- 'upload_date': (datetime.now() - timedelta(days=8)).strftime('%Y-%m-%d'),
- 'size': "25.7 MB"
- },
- {
- 'filename': "جدول الكميات.xlsx",
- 'type': "جدول كميات",
- 'upload_date': (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'),
- 'size': "1.8 MB"
- }
- ],
- 'items': [
- {
- 'رقم البند': "A1",
- 'وصف البند': "أعمال الحفر والردم",
- 'الوحدة': "م3",
- 'الكمية': 12500
- },
- {
- 'رقم البند': "A2",
- 'وصف البند': "أعمال الخرسانة المسلحة للأساسات",
- 'الوحدة': "م3",
- 'الكمية': 3500
- },
- {
- 'رقم البند': "A3",
- 'وصف البند': "أعمال حديد التسليح",
- 'الوحدة': "طن",
- 'الكمية': 450
- }
- ]
- },
- {
- 'id': 2,
- 'name': "صيانة وتطوير طريق الملك عبدالله",
- 'number': "MOT-2025-042",
- 'client': "وزارة النقل",
- 'location': "الرياض، المنطقة الوسطى",
- 'description': "صيانة وتطوير طريق الملك عبدالله بطول 25 كم، ويشمل المشروع إعادة الرصف وتحسين الإنارة وتركيب اللوحات الإرشادية.",
- 'status': "تم التقديم",
- 'tender_type': "عامة",
- 'pricing_method': "غير متزن",
- 'submission_date': (datetime.now() - timedelta(days=15)),
- 'created_at': datetime.now() - timedelta(days=45),
- 'created_by_id': 1
- },
- {
- 'id': 3,
- 'name': "إنشاء محطة معالجة مياه الصرف الصحي",
- 'number': "SWPC-2025-007",
- 'client': "شركة المياه الوطنية",
- 'location': "جدة، المنطقة الغربية",
- 'description': "إنشاء محطة معالجة مياه الصرف الصحي بطاقة استيعابية 50,000 م3/يوم، مع جميع الأعمال المدنية والكهروميكانيكية.",
- 'status': "تمت الترسية",
- 'tender_type': "عامة",
- 'pricing_method': "قياسي",
- 'submission_date': (datetime.now() - timedelta(days=90)),
- 'created_at': datetime.now() - timedelta(days=120),
- 'created_by_id': 1
- },
- {
- 'id': 4,
- 'name': "إنشاء منتزه الملك سلمان",
- 'number': "RAM-2025-015",
- 'client': "أمانة منطقة الرياض",
- 'location': "الرياض، المنطقة الوسطى",
- 'description': "إنشاء منتزه الملك سلمان على مساحة 500,000 متر مربع، ويشمل المشروع أعمال التشجير والتنسيق والمسطحات المائية والمباني الخدمية.",
- 'status': "قيد التنفيذ",
- 'tender_type': "عامة",
- 'pricing_method': "قياسي",
- 'submission_date': (datetime.now() - timedelta(days=180)),
- 'created_at': datetime.now() - timedelta(days=210),
- 'created_by_id': 1
- },
- {
- 'id': 5,
- 'name': "إنشاء مبنى مختبرات كلية العلوم",
- 'number': "KSU-2025-032",
- 'client': "جامعة الملك سعود",
- 'location': "الرياض، المنطقة الوسطى",
- 'description': "إنشاء مبنى المختبرات الجديد لكلية العلوم بمساحة 8,000 متر مربع، ويتكون من 3 طوابق ويشمل تجهيز المعامل والمختبرات العلمية.",
- 'status': "جديد",
- 'tender_type': "خاصة",
- 'pricing_method': "تنافسي",
- 'submission_date': (datetime.now() + timedelta(days=10)),
- 'created_at': datetime.now() - timedelta(days=5),
- 'created_by_id': 1
- },
- {
- 'id': 6,
- 'name': "توريد وتركيب أنظمة الطاقة الشمسية",
- 'number': "SEC-2025-098",
- 'client': "الشركة السعودية للكهرباء",
- 'location': "تبوك، المنطقة الشمالية",
- 'description': "توريد وتركيب أنظمة الطاقة الشمسية بقدرة 5 ميجاوات، مع جميع الأعمال المدنية والكهربائية.",
- 'status': "جديد",
- 'tender_type': "عامة",
- 'pricing_method': "قياسي",
- 'submission_date': (datetime.now() + timedelta(days=20)),
- 'created_at': datetime.now() - timedelta(days=2),
- 'created_by_id': 1
- }
- ]
-
- return projects
-
-# تشغيل التطبيق
-if __name__ == "__main__":
- projects_app = ProjectsApp()
- projects_app.run()
+"""
+وحدة إدارة المشاريع - نظام تحليل المناقصات
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+from datetime import datetime, timedelta
+import os
+import time
+import io
+import sys
+from pathlib import Path
+
+# إضافة مسار المشروع للنظام
+sys.path.append(str(Path(__file__).parent.parent))
+
+# استيراد محسن واجهة المستخدم
+from styling.enhanced_ui import UIEnhancer
+
+class ProjectsApp:
+ """وحدة إدارة المشاريع"""
+
+ def __init__(self):
+ """تهيئة وحدة إدارة المشاريع"""
+ self.ui = UIEnhancer(page_title="إدارة المشاريع - نظام تحليل المناقصات", page_icon="📋")
+ self.ui.apply_theme_colors()
+
+ # تهيئة البيانات المبدئية
+ if 'projects' not in st.session_state:
+ st.session_state.projects = self._generate_sample_projects()
+
+ def run(self):
+ """تشغيل وحدة إدارة المشاريع"""
+ # إنشاء قائمة العناصر
+ menu_items = [
+ {"name": "لوحة المعلومات", "icon": "house"},
+ {"name": "المناقصات والعقود", "icon": "file-text"},
+ {"name": "تحليل المستندات", "icon": "file-earmark-text"},
+ {"name": "نظام التسعير", "icon": "calculator"},
+ {"name": "حاسبة تكاليف البناء", "icon": "building"},
+ {"name": "الموارد والتكاليف", "icon": "people"},
+ {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
+ {"name": "إدارة المشاريع", "icon": "kanban"},
+ {"name": "الخرائط والمواقع", "icon": "geo-alt"},
+ {"name": "الجدول الزمني", "icon": "calendar3"},
+ {"name": "الإشعارات", "icon": "bell"},
+ {"name": "مقارنة المستندات", "icon": "files"},
+ {"name": "الترجمة", "icon": "translate"},
+ {"name": "المساعد الذكي", "icon": "robot"},
+ {"name": "التقارير", "icon": "bar-chart"},
+ {"name": "الإعدادات", "icon": "gear"}
+ ]
+
+ # إنشاء الشريط الجانبي
+ selected = self.ui.create_sidebar(menu_items)
+
+ # إنشاء ترويسة الصفحة
+ self.ui.create_header("إدارة المشاريع", "إدارة ومتابعة المشاريع والمناقصات")
+
+ # عرض واجهة وحدة إدارة المشاريع
+ tabs = st.tabs([
+ "قائمة المشاريع",
+ "إضافة مشروع جديد",
+ "تفاصيل المشروع",
+ "متابعة المشاريع"
+ ])
+
+ with tabs[0]:
+ self._render_projects_list_tab()
+
+ with tabs[1]:
+ self._render_add_project_tab()
+
+ with tabs[2]:
+ self._render_project_details_tab()
+
+ with tabs[3]:
+ self._render_projects_tracking_tab()
+
+ def _render_projects_list_tab(self):
+ """عرض تبويب قائمة المشاريع"""
+
+ st.markdown("### قائمة المشاريع")
+
+ # فلترة المشاريع
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ search_term = st.text_input("البحث في المشاريع", key="project_search")
+
+ with col2:
+ status_filter = st.multiselect(
+ "حالة المشروع",
+ ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
+ default=["جديد", "قيد التسعير", "تم التقديم"],
+ key="project_status_filter"
+ )
+
+ with col3:
+ client_filter = st.multiselect(
+ "الجهة المالكة",
+ list(set([p['client'] for p in st.session_state.projects])),
+ key="project_client_filter"
+ )
+
+ # تطبيق الفلترة
+ filtered_projects = st.session_state.projects
+
+ if search_term:
+ filtered_projects = [p for p in filtered_projects if search_term.lower() in p['name'].lower() or search_term in p['number']]
+
+ if status_filter:
+ filtered_projects = [p for p in filtered_projects if p['status'] in status_filter]
+
+ if client_filter:
+ filtered_projects = [p for p in filtered_projects if p['client'] in client_filter]
+
+ # تحويل المشاريع المفلترة إلى DataFrame للعرض
+ if filtered_projects:
+ projects_df = pd.DataFrame(filtered_projects)
+
+ # اختيار وترتيب الأعمدة
+ display_columns = [
+ 'name', 'number', 'client', 'location', 'status',
+ 'submission_date', 'tender_type', 'created_at'
+ ]
+
+ # تغيير أسماء الأعمدة للعرض
+ column_names = {
+ 'name': 'اسم المشروع',
+ 'number': 'رقم المناقصة',
+ 'client': 'الجهة المالكة',
+ 'location': 'الموقع',
+ 'status': 'الحالة',
+ 'submission_date': 'تاريخ التقديم',
+ 'tender_type': 'نوع المناقصة',
+ 'created_at': 'تاريخ الإنشاء'
+ }
+
+ display_df = projects_df[display_columns].rename(columns=column_names)
+
+ # تنسيق التواريخ
+ date_columns = ['تاريخ التقديم', 'تاريخ الإنشاء']
+ for col in date_columns:
+ if col in display_df.columns:
+ display_df[col] = pd.to_datetime(display_df[col]).dt.strftime('%Y-%m-%d')
+
+ # عرض الجدول
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
+
+ # زر تصدير المشاريع
+ if st.button("تصدير المشاريع إلى Excel"):
+ # محاكاة التصدير
+ st.success("تم تصدير المشاريع بنجاح!")
+ else:
+ st.info("لا توجد مشاريع تطابق معايير البحث.")
+
+ def _render_add_project_tab(self):
+ """عرض تبويب إضافة مشروع جديد"""
+
+ st.markdown("### إضافة مشروع جديد")
+
+ # نموذج إدخال بيانات المشروع
+ with st.form("new_project_form"):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_name = st.text_input("اسم المشروع", key="new_project_name")
+ client = st.text_input("الجهة المالكة", key="new_project_client")
+ location = st.text_input("الموقع", key="new_project_location")
+ tender_type = st.selectbox(
+ "نوع المناقصة",
+ ["عامة", "خاصة", "أمر مباشر"],
+ key="new_project_tender_type"
+ )
+
+ with col2:
+ tender_number = st.text_input("رقم المناقصة", key="new_project_number")
+ submission_date = st.date_input("تاريخ التقديم", key="new_project_submission_date")
+ pricing_method = st.selectbox(
+ "طريقة التسعير",
+ ["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
+ key="new_project_pricing_method"
+ )
+ status = st.selectbox(
+ "حالة المشروع",
+ ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
+ index=0,
+ key="new_project_status"
+ )
+
+ description = st.text_area("وصف المشروع", key="new_project_description")
+
+ submitted = st.form_submit_button("إضافة المشروع")
+
+ if submitted:
+ # التحقق من تعبئة الحقول الإلزامية
+ if not project_name or not tender_number or not client:
+ st.error("يرجى تعبئة جميع الحقول الإلزامية (اسم المشروع، رقم المناقصة، الجهة المالكة).")
+ else:
+ # إنشاء مشروع جديد
+ new_project = {
+ 'id': len(st.session_state.projects) + 1,
+ 'name': project_name,
+ 'number': tender_number,
+ 'client': client,
+ 'location': location,
+ 'description': description,
+ 'status': status,
+ 'tender_type': tender_type,
+ 'pricing_method': pricing_method,
+ 'submission_date': submission_date,
+ 'created_at': datetime.now(),
+ 'created_by_id': 1 # معرف المستخدم الحالي
+ }
+
+ # إضافة المشروع إلى قائمة المشاريع
+ st.session_state.projects.append(new_project)
+
+ # رسالة نجاح
+ st.success(f"تم إضافة المشروع [{project_name}] بنجاح!")
+
+ # تعيين المشروع الحالي
+ st.session_state.current_project = new_project
+
+ def _render_project_details_tab(self):
+ """عرض تبويب تفاصيل المشروع"""
+
+ st.markdown("### تفاصيل المشروع")
+
+ # التحقق من وجود مشروع حالي
+ if 'current_project' not in st.session_state or st.session_state.current_project is None:
+ # إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع
+ project_names = [p['name'] for p in st.session_state.projects]
+ selected_project_name = st.selectbox("اختر المشروع", project_names)
+
+ if selected_project_name:
+ selected_project = next((p for p in st.session_state.projects if p['name'] == selected_project_name), None)
+ if selected_project:
+ st.session_state.current_project = selected_project
+ else:
+ st.warning("لم يتم العثور على المشروع المحدد.")
+ return
+ else:
+ st.info("يرجى اختيار مشروع لعرض تفاصيله.")
+ return
+
+ # عرض تفاصيل المشروع
+ project = st.session_state.current_project
+
+ # عرض معلومات المشروع الأساسية
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.markdown(f"**اسم المشروع**: {project['name']}")
+ st.markdown(f"**رقم المناقصة**: {project['number']}")
+ st.markdown(f"**الجهة المالكة**: {project['client']}")
+
+ with col2:
+ st.markdown(f"**الموقع**: {project['location']}")
+ st.markdown(f"**نوع المناقصة**: {project['tender_type']}")
+ st.markdown(f"**حالة المشروع**: {project['status']}")
+
+ with col3:
+ st.markdown(f"**طريقة التسعير**: {project['pricing_method']}")
+ st.markdown(f"**تاريخ التقديم**: {project['submission_date'].strftime('%Y-%m-%d') if isinstance(project['submission_date'], datetime) else project['submission_date']}")
+ st.markdown(f"**تاريخ الإنشاء**: {project['created_at'].strftime('%Y-%m-%d') if isinstance(project['created_at'], datetime) else project['created_at']}")
+
+ # عرض وصف المشروع
+ st.markdown("#### وصف المشروع")
+ st.text_area("", value=project.get('description', ''), disabled=True, height=100)
+
+ # عرض المستندات المرتبطة بالمشروع
+ st.markdown("#### مستندات المشروع")
+
+ if 'documents' in project and project['documents']:
+ docs_df = pd.DataFrame(project['documents'])
+ st.dataframe(docs_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد مستندات مرتبطة بهذا المشروع حاليًا.")
+
+ # زر إضافة مستندات
+ if st.button("إضافة مستندات"):
+ st.session_state.upload_documents = True
+
+ # واجهة تحميل المستندات
+ if 'upload_documents' in st.session_state and st.session_state.upload_documents:
+ st.markdown("#### تحميل مستندات جديدة")
+
+ uploaded_file = st.file_uploader("اختر ملفًا", type=['pdf', 'docx', 'xlsx', 'png', 'jpg', 'dwg'])
+ doc_type = st.selectbox("نوع المستند", ["كراسة شروط", "عقد", "مخططات", "جدول كميات", "مواصفات فنية", "تعديلات وملاحق"])
+
+ if uploaded_file and st.button("تحميل المستند"):
+ # محاكاة تحميل المستند
+ with st.spinner("جاري تحميل المستند..."):
+ time.sleep(2)
+
+ # إنشاء مستند جديد
+ new_document = {
+ 'filename': uploaded_file.name,
+ 'type': doc_type,
+ 'upload_date': datetime.now().strftime('%Y-%m-%d'),
+ 'size': f"{uploaded_file.size / 1024:.1f} KB"
+ }
+
+ # إضافة المستند إلى المشروع
+ if 'documents' not in project:
+ project['documents'] = []
+
+ project['documents'].append(new_document)
+
+ st.success(f"تم تحميل المستند [{uploaded_file.name}] بنجاح!")
+ st.session_state.upload_documents = False
+ st.experimental_rerun()
+
+ # عرض البنود والكميات
+ st.markdown("#### بنود وكميات المشروع")
+
+ if 'items' in project and project['items']:
+ items_df = pd.DataFrame(project['items'])
+ st.dataframe(items_df, use_container_width=True, hide_index=True)
+
+ # زر لتحويل البنود إلى وحدة التسعير
+ if st.button("تحويل البنود إلى وحدة التسعير"):
+ if 'manual_items' not in st.session_state:
+ st.session_state.manual_items = pd.DataFrame()
+
+ st.session_state.manual_items = items_df.copy()
+ st.success("تم تحويل البنود إلى وحدة التسعير بنجاح!")
+ else:
+ st.info("لا توجد بنود وكميات لهذا المشروع حاليًا.")
+
+ # زر استيراد البنود من وحدة تحليل المستندات
+ if st.button("استيراد البنود من تحليل المستندات"):
+ st.warning("ميزة استيراد البنود من تحليل المستندات قيد التطوير.")
+
+ # أزرار الإجراءات
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ if st.button("تعديل المشروع"):
+ st.session_state.edit_project = True
+ st.experimental_rerun()
+
+ with col2:
+ if st.button("تصدير بيانات المشروع"):
+ st.success("تم تصدير بيانات المشروع بنجاح!")
+
+ with col3:
+ if st.button("إرسال للاعتماد"):
+ st.success("تم إرسال المشروع للاعتماد بنجاح!")
+
+ # نموذج تعديل المشروع
+ if 'edit_project' in st.session_state and st.session_state.edit_project:
+ st.markdown("#### تعديل المشروع")
+
+ with st.form("edit_project_form"):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_name = st.text_input("اسم المشروع", value=project['name'])
+ client = st.text_input("الجهة المالكة", value=project['client'])
+ location = st.text_input("الموقع", value=project['location'])
+ tender_type = st.selectbox(
+ "نوع المناقصة",
+ ["عامة", "خاصة", "أمر مباشر"],
+ index=["عامة", "خاصة", "أمر مباشر"].index(project['tender_type'])
+ )
+
+ with col2:
+ tender_number = st.text_input("رقم المناقصة", value=project['number'])
+ submission_date = st.date_input(
+ "تاريخ التقديم",
+ value=datetime.strptime(project['submission_date'], "%Y-%m-%d") if isinstance(project['submission_date'], str) else project['submission_date']
+ )
+ pricing_method = st.selectbox(
+ "طريقة التسعير",
+ ["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
+ index=["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"].index(project['pricing_method'])
+ )
+ status = st.selectbox(
+ "حالة المشروع",
+ ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
+ index=["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"].index(project['status'])
+ )
+
+ description = st.text_area("وصف المشروع", value=project.get('description', ''))
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ submit = st.form_submit_button("حفظ التعديلات")
+
+ with col2:
+ cancel = st.form_submit_button("إلغاء")
+
+ if submit:
+ # تحديث بيانات المشروع
+ project['name'] = project_name
+ project['number'] = tender_number
+ project['client'] = client
+ project['location'] = location
+ project['description'] = description
+ project['status'] = status
+ project['tender_type'] = tender_type
+ project['pricing_method'] = pricing_method
+ project['submission_date'] = submission_date
+
+ st.success("تم تحديث بيانات المشروع بنجاح!")
+ st.session_state.edit_project = False
+ st.experimental_rerun()
+
+ elif cancel:
+ st.session_state.edit_project = False
+ st.experimental_rerun()
+
+ def _render_projects_tracking_tab(self):
+ """عرض تبويب متابعة المشاريع"""
+
+ st.markdown("### متابعة المشاريع")
+
+ # عرض إحصائيات المشاريع
+ col1, col2, col3, col4 = st.columns(4)
+
+ projects = st.session_state.projects
+
+ with col1:
+ total_projects = len(projects)
+ self.ui.create_metric_card("إجمالي المشاريع", str(total_projects), None, self.ui.COLORS['primary'])
+
+ with col2:
+ active_projects = len([p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]])
+ self.ui.create_metric_card("المشاريع النشطة", str(active_projects), None, self.ui.COLORS['success'])
+
+ with col3:
+ pending_submission = len([p for p in projects if p['status'] in ["جديد", "قيد التسعير"]])
+ self.ui.create_metric_card("مشاريع قيد التسعير", str(pending_submission), None, self.ui.COLORS['warning'])
+
+ with col4:
+ completed_projects = len([p for p in projects if p['status'] in ["منتهي"]])
+ self.ui.create_metric_card("المشاريع المنتهية", str(completed_projects), None, self.ui.COLORS['info'])
+
+ # عرض رسم بياني لحالة المشاريع
+ st.markdown("#### توزيع المشاريع حسب الحالة")
+
+ status_counts = {}
+ for p in projects:
+ status = p['status']
+ status_counts[status] = status_counts.get(status, 0) + 1
+
+ status_df = pd.DataFrame({
+ 'الحالة': list(status_counts.keys()),
+ 'عدد المشاريع': list(status_counts.values())
+ })
+
+ st.bar_chart(status_df.set_index('الحالة'))
+
+ # عرض المشاريع قيد المتابعة
+ st.markdown("#### المشاريع قيد المتابعة")
+
+ # عرض المشاريع النشطة المرتبة حسب تاريخ التقديم
+ active_projects_list = [p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]]
+
+ if active_projects_list:
+ # تحويل التواريخ إلى كائنات تاريخ إذا كانت نصوصًا
+ for p in active_projects_list:
+ if isinstance(p['submission_date'], str):
+ p['submission_date'] = datetime.strptime(p['submission_date'], "%Y-%m-%d")
+
+ # ترتيب المشاريع حسب تاريخ التقديم
+ active_projects_list.sort(key=lambda x: x['submission_date'])
+
+ # تحويل إلى DataFrame
+ active_df = pd.DataFrame(active_projects_list)
+
+ # اختيار وترتيب الأعمدة
+ display_columns = [
+ 'name', 'number', 'client', 'status',
+ 'submission_date', 'tender_type'
+ ]
+
+ # تغيير أسماء الأعمدة
+ column_names = {
+ 'name': 'اسم المشروع',
+ 'number': 'رقم المناقصة',
+ 'client': 'الجهة المالكة',
+ 'status': 'الحالة',
+ 'submission_date': 'تاريخ التقديم',
+ 'tender_type': 'نوع المناقصة'
+ }
+
+ # تنسيق البيانات
+ display_df = active_df[display_columns].rename(columns=column_names)
+ display_df['تاريخ التقديم'] = pd.to_datetime(display_df['تاريخ التقديم']).dt.strftime('%Y-%m-%d')
+
+ # عرض الجدول
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد مشاريع نشطة حاليًا.")
+
+ # عرض المشاريع المقبلة
+ st.markdown("#### المواعيد المقبلة")
+
+ upcoming_events = []
+ today = datetime.now().date()
+
+ for p in projects:
+ submission_date = p['submission_date']
+ if isinstance(submission_date, str):
+ submission_date = datetime.strptime(submission_date, "%Y-%m-%d").date()
+ elif isinstance(submission_date, datetime):
+ submission_date = submission_date.date()
+
+ # المشاريع التي موعد تقديمها خلال الأسبوعين القادمين
+ if today <= submission_date <= today + timedelta(days=14) and p['status'] in ["قيد التسعير"]:
+ days_left = (submission_date - today).days
+ upcoming_events.append({
+ 'المشروع': p['name'],
+ 'الحدث': 'موعد تقديم المناقصة',
+ 'التاريخ': submission_date.strftime('%Y-%m-%d'),
+ 'الأيام المتبقية': days_left
+ })
+
+ if upcoming_events:
+ events_df = pd.DataFrame(upcoming_events)
+ st.dataframe(events_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد مواعيد قريبة.")
+
+ def _generate_sample_projects(self):
+ """توليد بيانات افتراضية للمشاريع"""
+
+ projects = [
+ {
+ 'id': 1,
+ 'name': "إنشاء مبنى مستشفى الولادة والأطفال بمنطقة الشرقية",
+ 'number': "SHPD-2025-001",
+ 'client': "وزارة الصحة",
+ 'location': "الدمام، المنطقة الشرقية",
+ 'description': "يشمل المشروع إنشاء وتجهيز مبنى مستشفى الولادة والأطفال بسعة 300 سرير، ويتكون المبنى من 4 طوابق بمساحة إجمالية 15,000 متر مربع.",
+ 'status': "قيد التسعير",
+ 'tender_type': "عامة",
+ 'pricing_method': "قياسي",
+ 'submission_date': (datetime.now() + timedelta(days=5)),
+ 'created_at': datetime.now() - timedelta(days=10),
+ 'created_by_id': 1,
+ 'documents': [
+ {
+ 'filename': "كراسة الشروط والمواصفات.pdf",
+ 'type': "كراسة شروط",
+ 'upload_date': (datetime.now() - timedelta(days=9)).strftime('%Y-%m-%d'),
+ 'size': "5.2 MB"
+ },
+ {
+ 'filename': "المخططات الهندسية.dwg",
+ 'type': "مخططات",
+ 'upload_date': (datetime.now() - timedelta(days=8)).strftime('%Y-%m-%d'),
+ 'size': "25.7 MB"
+ },
+ {
+ 'filename': "جدول الكميات.xlsx",
+ 'type': "جدول كميات",
+ 'upload_date': (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'),
+ 'size': "1.8 MB"
+ }
+ ],
+ 'items': [
+ {
+ 'رقم البند': "A1",
+ 'وصف البند': "أعمال الحفر والردم",
+ 'الوحدة': "م3",
+ 'الكمية': 12500
+ },
+ {
+ 'رقم البند': "A2",
+ 'وصف البند': "أعمال الخرسانة المسلحة للأساسات",
+ 'الوحدة': "م3",
+ 'الكمية': 3500
+ },
+ {
+ 'رقم البند': "A3",
+ 'وصف البند': "أعمال حديد التسليح",
+ 'الوحدة': "طن",
+ 'الكمية': 450
+ }
+ ]
+ },
+ {
+ 'id': 2,
+ 'name': "صيانة وتطوير طريق الملك عبدالله",
+ 'number': "MOT-2025-042",
+ 'client': "وزارة النقل",
+ 'location': "الرياض، المنطقة الوسطى",
+ 'description': "صيانة وتطوير طريق الملك عبدالله بطول 25 كم، ويشمل المشروع إعادة الرصف وتحسين الإنارة وتركيب اللوحات الإرشادية.",
+ 'status': "تم التقديم",
+ 'tender_type': "عامة",
+ 'pricing_method': "غير متزن",
+ 'submission_date': (datetime.now() - timedelta(days=15)),
+ 'created_at': datetime.now() - timedelta(days=45),
+ 'created_by_id': 1
+ },
+ {
+ 'id': 3,
+ 'name': "إنشاء محطة معالجة مياه الصرف الصحي",
+ 'number': "SWPC-2025-007",
+ 'client': "شركة المياه الوطنية",
+ 'location': "جدة، المنطقة الغربية",
+ 'description': "إنشاء محطة معالجة مياه الصرف الصحي بطاقة استيعابية 50,000 م3/يوم، مع جميع الأعمال المدنية والكهروميكانيكية.",
+ 'status': "تمت الترسية",
+ 'tender_type': "عامة",
+ 'pricing_method': "قياسي",
+ 'submission_date': (datetime.now() - timedelta(days=90)),
+ 'created_at': datetime.now() - timedelta(days=120),
+ 'created_by_id': 1
+ },
+ {
+ 'id': 4,
+ 'name': "إنشاء منتزه الملك سلمان",
+ 'number': "RAM-2025-015",
+ 'client': "أمانة منطقة الرياض",
+ 'location': "الرياض، المنطقة الوسطى",
+ 'description': "إنشاء منتزه الملك سلمان على مساحة 500,000 متر مربع، ويشمل المشروع أعمال التشجير والتنسيق والمسطحات المائية والمباني الخدمية.",
+ 'status': "قيد التنفيذ",
+ 'tender_type': "عامة",
+ 'pricing_method': "قياسي",
+ 'submission_date': (datetime.now() - timedelta(days=180)),
+ 'created_at': datetime.now() - timedelta(days=210),
+ 'created_by_id': 1
+ },
+ {
+ 'id': 5,
+ 'name': "إنشاء مبنى مختبرات كلية العلوم",
+ 'number': "KSU-2025-032",
+ 'client': "جامعة الملك سعود",
+ 'location': "الرياض، المنطقة الوسطى",
+ 'description': "إنشاء مبنى المختبرات الجديد لكلية العلوم بمساحة 8,000 متر مربع، ويتكون من 3 طوابق ويشمل تجهيز المعامل والمختبرات العلمية.",
+ 'status': "جديد",
+ 'tender_type': "خاصة",
+ 'pricing_method': "تنافسي",
+ 'submission_date': (datetime.now() + timedelta(days=10)),
+ 'created_at': datetime.now() - timedelta(days=5),
+ 'created_by_id': 1
+ },
+ {
+ 'id': 6,
+ 'name': "توريد وتركيب أنظمة الطاقة الشمسية",
+ 'number': "SEC-2025-098",
+ 'client': "الشركة السعودية للكهرباء",
+ 'location': "تبوك، المنطقة الشمالية",
+ 'description': "توريد وتركيب أنظمة الطاقة الشمسية بقدرة 5 ميجاوات، مع جميع الأعمال المدنية والكهربائية.",
+ 'status': "جديد",
+ 'tender_type': "عامة",
+ 'pricing_method': "قياسي",
+ 'submission_date': (datetime.now() + timedelta(days=20)),
+ 'created_at': datetime.now() - timedelta(days=2),
+ 'created_by_id': 1
+ }
+ ]
+
+ return projects
+
+# تشغيل التطبيق
+if __name__ == "__main__":
+ projects_app = ProjectsApp()
+ projects_app.run()
diff --git a/modules/reports/reports_app.py b/modules/reports/reports_app.py
index c42337b686be965d6c9edf376b298596b453a203..9242e4120de280ab8091e6b8ffb4e7d72bc460a5 100644
--- a/modules/reports/reports_app.py
+++ b/modules/reports/reports_app.py
@@ -1,405 +1,405 @@
-import streamlit as st
-import pandas as pd
-import plotly.express as px
-from datetime import datetime, timedelta
-import time
-
-class ReportsApp:
- """وحدة التقارير والتحليلات"""
-
- def __init__(self):
- """تهيئة وحدة التقارير والتحليلات"""
- # تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
- if 'theme' not in st.session_state:
- st.session_state.theme = 'light'
-
- def run(self):
- """
- تشغيل وحدة التقارير والتحليلات
-
- هذه الدالة هي نقطة الدخول الرئيسية لوحدة التقارير والتحليلات.
- تقوم بتهيئة واجهة المستخدم وعرض الوظائف المختلفة للتقارير والتحليلات.
- """
- try:
- # تعيين عنوان الصفحة
- st.set_page_config(
- page_title="وحدة التقارير والتحليلات - نظام المناقصات",
- page_icon="📊",
- layout="wide",
- initial_sidebar_state="expanded"
- )
-
- # تطبيق التنسيق المخصص
- st.markdown("""
-
- """, unsafe_allow_html=True)
-
- # إضافة زر تبديل السمة في أعلى الصفحة
- col1, col2, col3 = st.columns([1, 8, 1])
- with col3:
- if st.button("🌓 تبديل السمة"):
- # تبديل السمة
- if st.session_state.theme == "light":
- st.session_state.theme = "dark"
- else:
- st.session_state.theme = "light"
-
- # تطبيق السمة الجديدة وإعادة تشغيل التطبيق
- st.rerun()
-
- # عرض الشريط الجانبي
- with st.sidebar:
- st.image("/home/ubuntu/tender_system/tender_system/assets/images/logo.png", width=200)
- st.markdown("## نظام تحليل المناقصات")
- st.markdown("### وحدة التقارير والتحليلات")
-
- st.markdown("---")
-
- # إضافة خيارات تصفية التقارير
- st.markdown("### خيارات التصفية")
-
- # تصفية حسب الفترة الزمنية
- date_range = st.selectbox(
- "الفترة الزمنية",
- ["آخر 7 أيام", "آخر 30 يوم", "آخر 90 يوم", "آخر 365 يوم", "كل الفترات"]
- )
-
- # تصفية حسب نوع المشروع
- project_type = st.multiselect(
- "نوع المشروع",
- ["مباني", "طرق", "جسور", "أنفاق", "بنية تحتية", "أخرى"],
- default=["مباني", "طرق", "جسور", "أنفاق", "بنية تحتية", "أخرى"]
- )
-
- # تصفية حسب حالة المشروع
- project_status = st.multiselect(
- "حالة المشروع",
- ["جديد", "قيد التقديم", "تم التقديم", "فائز", "خاسر", "ملغي"],
- default=["جديد", "قيد التقديم", "تم التقديم", "فائز", "خاسر"]
- )
-
- # زر تطبيق التصفية
- if st.button("تطبيق التصفية"):
- st.success("تم تطبيق التصفية بنجاح!")
-
- st.markdown("---")
-
- # إضافة معلومات المستخدم
- st.markdown("### معلومات المستخدم")
- st.markdown("**المستخدم:** مهندس تامر الجوهري")
- st.markdown("**الدور:** محلل مناقصات")
- st.markdown("**تاريخ آخر دخول:** " + datetime.now().strftime("%Y-%m-%d %H:%M"))
-
- # عرض واجهة وحدة التقارير والتحليلات
- self.render()
-
- # إضافة معلومات في أسفل الصفحة
- st.markdown("---")
- st.markdown("### نظام تحليل المناقصات - وحدة التقارير والتحليلات")
- st.markdown("**الإصدار:** 2.0.0")
- st.markdown("**تاريخ التحديث:** 2025-03-31")
- st.markdown("**جميع الحقوق محفوظة © 2025**")
-
- return True
-
- except Exception as e:
- st.error(f"حدث خطأ أثناء تشغيل وحدة التقارير والتحليلات: {str(e)}")
- return False
-
- def render(self):
- """عرض واجهة وحدة التقارير والتحليلات"""
-
- st.markdown("وحدة التقارير والتحليلات ", unsafe_allow_html=True)
-
- tabs = st.tabs(["لوحة المعلومات", "تقارير المشاريع", "تقارير التسعير", "تقارير المخاطر", "التقارير المخصصة"])
-
- with tabs[0]:
- self._render_dashboard_tab()
-
- with tabs[1]:
- self._render_projects_reports_tab()
-
- with tabs[2]:
- self._render_pricing_reports_tab()
-
- with tabs[3]:
- self._render_risk_reports_tab()
-
- with tabs[4]:
- self._render_custom_reports_tab()
-
- def _render_dashboard_tab(self):
- """عرض تبويب لوحة المعلومات"""
-
- st.markdown("### لوحة معلومات النظام")
-
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- total_projects = self._get_total_projects()
- st.metric("إجمالي المشاريع", total_projects)
-
- with col2:
- active_projects = self._get_active_projects()
- st.metric("المشاريع النشطة", active_projects, delta=f"{active_projects/total_projects*100:.1f}%" if total_projects > 0 else "0%")
-
- with col3:
- won_projects = self._get_won_projects()
- st.metric("المشاريع المرساة", won_projects, delta=f"{won_projects/total_projects*100:.1f}%" if total_projects > 0 else "0%")
-
- with col4:
- avg_local_content = self._get_avg_local_content()
- st.metric("متوسط المحتوى المحلي", f"{avg_local_content:.1f}%", delta=f"{avg_local_content-70:.1f}%" if avg_local_content > 0 else "0%")
-
- st.markdown("#### توزيع المشاريع حسب الحالة")
- project_status_data = self._get_project_status_data()
- fig = px.pie(project_status_data, values='count', names='status', title='توزيع المشاريع حسب الحالة', hole=0.4)
- st.plotly_chart(fig, use_container_width=True)
-
- st.markdown("#### اتجاه المشاريع الشهري")
- monthly_data = self._get_monthly_project_data()
- fig = px.line(monthly_data, x='month', y=['new', 'submitted', 'won'], title='اتجاه المشاريع الشهري')
- st.plotly_chart(fig, use_container_width=True)
-
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown("#### توزيع المشاريع حسب النوع")
- project_type_data = self._get_project_type_data()
- fig = px.bar(project_type_data, x='type', y='count', title='توزيع المشاريع حسب النوع')
- st.plotly_chart(fig, use_container_width=True)
-
- with col2:
- st.markdown("#### توزيع المشاريع حسب الموقع")
- project_location_data = self._get_project_location_data()
- fig = px.bar(project_location_data, x='location', y='count', title='توزيع المشاريع حسب الموقع')
- st.plotly_chart(fig, use_container_width=True)
-
- st.markdown("#### أحدث المشاريع")
- latest_projects = self._get_latest_projects()
- st.dataframe(latest_projects)
-
- def _render_projects_reports_tab(self):
- """عرض تبويب تقارير المشاريع"""
-
- st.markdown("### تقارير المشاريع")
-
- report_type = st.selectbox(
- "نوع التقرير",
- ["تقرير حالة المشاريع", "تقرير أداء المشاريع", "تقرير المشاريع المتأخرة", "تقرير المشاريع المكتملة"]
- )
-
- if report_type == "تقرير حالة المشاريع":
- self._render_project_status_report()
- elif report_type == "تقرير أداء المشاريع":
- self._render_project_performance_report()
- elif report_type == "تقرير المشاريع المتأخرة":
- self._render_delayed_projects_report()
- elif report_type == "تقرير المشاريع المكتملة":
- self._render_completed_projects_report()
-
- def _render_pricing_reports_tab(self):
- """عرض تبويب تقارير التسعير"""
-
- st.markdown("### تقارير التسعير")
-
- report_type = st.selectbox(
- "نوع التقرير",
- ["تقرير تحليل الأسعار", "تقرير مقارنة الأسعار", "تقرير اتجاهات الأسعار", "تقرير تحليل المنافسين"]
- )
-
- if report_type == "تقرير تحليل الأسعار":
- self._render_price_analysis_report()
- elif report_type == "تقرير مقارنة الأسعار":
- self._render_price_comparison_report()
- elif report_type == "تقرير اتجاهات الأسعار":
- self._render_price_trends_report()
- elif report_type == "تقرير تحليل المنافسين":
- self._render_competitors_analysis_report()
-
- def _render_risk_reports_tab(self):
- """عرض تبويب تقارير المخاطر"""
-
- st.markdown("### تقارير المخاطر")
-
- report_type = st.selectbox(
- "نوع التقرير",
- ["تقرير تحليل المخاطر", "تقرير مصفوفة المخاطر", "تقرير متابعة المخاطر", "تقرير استراتيجيات التخفيف"]
- )
-
- if report_type == "تقرير تحليل المخاطر":
- self._render_risk_analysis_report()
- elif report_type == "تقرير مصفوفة المخاطر":
- self._render_risk_matrix_report()
- elif report_type == "تقرير متابعة المخاطر":
- self._render_risk_monitoring_report()
- elif report_type == "تقرير استراتيجيات التخفيف":
- self._render_risk_mitigation_report()
-
- def _render_custom_reports_tab(self):
- """عرض تبويب التقارير المخصصة"""
-
- st.markdown("### التقارير المخصصة")
-
- st.markdown("#### إنشاء تقرير مخصص")
-
- col1, col2 = st.columns(2)
-
- with col1:
- report_name = st.text_input("اسم التقرير")
- report_description = st.text_area("وصف التقرير")
-
- with col2:
- report_fields = st.multiselect(
- "حقول التقرير",
- ["رقم المشروع", "اسم المشروع", "نوع المشروع", "حالة المشروع", "تاريخ البدء", "تاريخ الانتهاء", "الميزانية", "التكلفة الفعلية", "نسبة الإنجاز", "المخاطر", "الموقع", "المالك", "المقاول"]
- )
-
- report_filters = st.multiselect(
- "تصفية التقرير",
- ["نوع المشروع", "حالة المشروع", "الفترة الزمنية", "الميزانية", "الموقع", "المالك", "المقاول"]
- )
-
- if st.button("إنشاء التقرير"):
- if report_name and report_description and report_fields:
- with st.spinner("جاري إنشاء التقرير..."):
- time.sleep(2) # محاكاة وقت المعالجة
- st.success("تم إنشاء التقرير بنجاح!")
-
- # عرض التقرير المخصص (محاكاة)
- custom_report_data = self._generate_custom_report(report_fields)
- st.dataframe(custom_report_data)
-
- # تصدير التقرير
- st.download_button(
- label="تصدير التقرير (Excel)",
- data=self._export_to_excel(custom_report_data),
- file_name=f"{report_name}.xlsx",
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
- )
- else:
- st.warning("يرجى ملء جميع الحقول المطلوبة")
-
- st.markdown("#### التقارير المخصصة المحفوظة")
-
- saved_reports = [
- {"id": 1, "name": "تقرير المشاريع المتأخرة في الرياض", "created_at": "2025-03-15", "last_run": "2025-03-30"},
- {"id": 2, "name": "تقرير مشاريع الطرق ذات المخاطر العالية", "created_at": "2025-03-10", "last_run": "2025-03-28"},
- {"id": 3, "name": "تقرير المشاريع المكتملة في الربع الأول", "created_at": "2025-03-05", "last_run": "2025-03-25"}
- ]
-
- saved_reports_df = pd.DataFrame(saved_reports)
- st.dataframe(saved_reports_df)
-
- # تنفيذ دوال الحصول على البيانات
-
- def _get_total_projects(self):
- """الحصول على إجمالي عدد المشاريع"""
- # محاكاة البيانات
- return 120
-
- def _get_active_projects(self):
- """الحصول على عدد المشاريع النشطة"""
- # محاكاة البيانات
- return 45
-
- def _get_won_projects(self):
- """الحصول على عدد المشاريع المرساة"""
- # محاكاة البيانات
- return 30
-
- def _get_avg_local_content(self):
- """الحصول على متوسط المحتوى المحلي"""
- # محاكاة البيانات
- return 75.5
-
- def _get_project_status_data(self):
- """الحصول على بيانات توزيع المشاريع حسب الحالة"""
- # محاكاة البيانات
- data = {
- 'status': ['جديد', 'قيد التقديم', 'تم التقديم', 'فائز', 'خاسر', 'ملغي'],
- 'count': [25, 20, 15, 30, 25, 5]
- }
- return pd.DataFrame(data)
-
- def _get_monthly_project_data(self):
- """الحصول على بيانات اتجاه المشاريع الشهري"""
- # محاكاة البيانات
- data = {
- 'month': ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو'],
- 'new': [10, 15, 12, 8, 20, 18],
- 'submitted': [8, 12, 10, 6, 15, 14],
- 'won': [5, 8, 6, 4, 10, 9]
- }
- return pd.DataFrame(data)
-
- def _get_project_type_data(self):
- """الحصول على بيانات توزيع المشاريع حسب النوع"""
- # محاكاة البيانات
- data = {
- 'type': ['مباني', 'طرق', 'جسور', 'أنفاق', 'بنية تحتية', 'أخرى'],
- 'count': [40, 30, 15, 10, 20, 5]
- }
- return pd.DataFrame(data)
-
- def _get_project_location_data(self):
- """الحصول على بيانات توزيع المشاريع حسب الموقع"""
- # محاكاة البيانات
- data = {
- 'location': ['الرياض', 'جدة', 'الدمام', 'مكة', 'المدينة', 'أخرى'],
- 'count': [35, 25, 20, 15, 10, 15]
- }
- return pd.DataFrame(data)
-
- def _get_latest_projects(self):
- """الحصول على بيانات أحدث المشاريع"""
- # محاكاة البيانات
- data = {
- 'رقم المشروع': ['P-2025-001', 'P-2025-002', 'P-2025-003', 'P-2025-004', 'P-2025-005'],
- 'اسم المشروع': ['إنشاء مبنى إداري', 'تطوير شبكة طرق', 'إنشاء جسر', 'بناء مدرسة', 'تطوير شبكة مياه'],
- 'نوع المشروع': ['مباني', 'طرق', 'جسور', 'مباني', 'بنية تحتية'],
- 'حالة المشروع': ['جديد', 'قيد التقديم', 'تم التقديم', 'فائز', 'جديد'],
- 'تاريخ الإضافة': ['2025-03-30', '2025-03-28', '2025-03-25', '2025-03-20', '2025-03-18']
- }
- return pd.DataFrame(data)
-
- # تنفيذ دوال عرض التقارير
-
- def _render_project_status_report(self):
- """عرض تقرير حالة المشاريع"""
-
- st.markdown("#### تقرير حالة المشاريع")
-
- # محاكاة بيانات التقرير
- data = {
- 'رقم المشروع': ['P-2025-001', 'P-2025-002', 'P-2025-003', 'P-2025-004', 'P-2025-005', 'P-2025-006', 'P-2025-007', 'P-2025-008', 'P-2025-009', 'P-2025-010'],
- 'اسم المشروع': ['إنشاء مبنى إداري', 'تطوير شبكة طرق', 'إنشاء جسر', 'بناء مدرسة', 'تطوير شبكة مياه', 'إنشاء مستشفى', 'بناء مركز تجاري', 'تطوير حديقة عامة', 'إنشاء مصنع', 'تطوير مطار'],
- 'نوع المشروع': ['مباني', 'طرق', 'جسور', 'مباني', 'بنية تحتية', 'مباني', 'مباني', 'أخرى', 'مباني', 'بنية تحتية'],
- '
-(Content truncated due to size limit. Use line ranges to read in chunks)
+import streamlit as st
+import pandas as pd
+import plotly.express as px
+from datetime import datetime, timedelta
+import time
+
+class ReportsApp:
+ """وحدة التقارير والتحليلات"""
+
+ def __init__(self):
+ """تهيئة وحدة التقارير والتحليلات"""
+ # تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
+ if 'theme' not in st.session_state:
+ st.session_state.theme = 'light'
+
+ def run(self):
+ """
+ تشغيل وحدة التقارير والتحليلات
+
+ هذه الدالة هي نقطة الدخول الرئيسية لوحدة التقارير والتحليلات.
+ تقوم بتهيئة واجهة المستخدم وعرض الوظائف المختلفة للتقارير والتحليلات.
+ """
+ try:
+ # تعيين عنوان الصفحة
+ st.set_page_config(
+ page_title="وحدة التقارير والتحليلات - نظام المناقصات",
+ page_icon="📊",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ # تطبيق التنسيق المخصص
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ # إضافة زر تبديل السمة في أعلى الصفحة
+ col1, col2, col3 = st.columns([1, 8, 1])
+ with col3:
+ if st.button("🌓 تبديل السمة"):
+ # تبديل السمة
+ if st.session_state.theme == "light":
+ st.session_state.theme = "dark"
+ else:
+ st.session_state.theme = "light"
+
+ # تطبيق السمة الجديدة وإعادة تشغيل التطبيق
+ st.rerun()
+
+ # عرض الشريط الجانبي
+ with st.sidebar:
+ st.image("/home/ubuntu/tender_system/tender_system/assets/images/logo.png", width=200)
+ st.markdown("## نظام تحليل المناقصات")
+ st.markdown("### وحدة التقارير والتحليلات")
+
+ st.markdown("---")
+
+ # إضافة خيارات تصفية التقارير
+ st.markdown("### خيارات التصفية")
+
+ # تصفية حسب الفترة الزمنية
+ date_range = st.selectbox(
+ "الفترة الزمنية",
+ ["آخر 7 أيام", "آخر 30 يوم", "آخر 90 يوم", "آخر 365 يوم", "كل الفترات"]
+ )
+
+ # تصفية حسب نوع المشروع
+ project_type = st.multiselect(
+ "نوع المشروع",
+ ["مباني", "طرق", "جسور", "أنفاق", "بنية تحتية", "أخرى"],
+ default=["مباني", "طرق", "جسور", "أنفاق", "بنية تحتية", "أخرى"]
+ )
+
+ # تصفية حسب حالة المشروع
+ project_status = st.multiselect(
+ "حالة المشروع",
+ ["جديد", "قيد التقديم", "تم التقديم", "فائز", "خاسر", "ملغي"],
+ default=["جديد", "قيد التقديم", "تم التقديم", "فائز", "خاسر"]
+ )
+
+ # زر تطبيق التصفية
+ if st.button("تطبيق التصفية"):
+ st.success("تم تطبيق التصفية بنجاح!")
+
+ st.markdown("---")
+
+ # إضافة معلومات المستخدم
+ st.markdown("### معلومات المستخدم")
+ st.markdown("**المستخدم:** مهندس تامر الجوهري")
+ st.markdown("**الدور:** محلل مناقصات")
+ st.markdown("**تاريخ آخر دخول:** " + datetime.now().strftime("%Y-%m-%d %H:%M"))
+
+ # عرض واجهة وحدة التقارير والتحليلات
+ self.render()
+
+ # إضافة معلومات في أسفل الصفحة
+ st.markdown("---")
+ st.markdown("### نظام تحليل المناقصات - وحدة التقارير والتحليلات")
+ st.markdown("**الإصدار:** 2.0.0")
+ st.markdown("**تاريخ التحديث:** 2025-03-31")
+ st.markdown("**جميع الحقوق محفوظة © 2025**")
+
+ return True
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء تشغيل وحدة التقارير والتحليلات: {str(e)}")
+ return False
+
+ def render(self):
+ """عرض واجهة وحدة التقارير والتحليلات"""
+
+ st.markdown("وحدة التقارير والتحليلات ", unsafe_allow_html=True)
+
+ tabs = st.tabs(["لوحة المعلومات", "تقارير المشاريع", "تقارير التسعير", "تقارير المخاطر", "التقارير المخصصة"])
+
+ with tabs[0]:
+ self._render_dashboard_tab()
+
+ with tabs[1]:
+ self._render_projects_reports_tab()
+
+ with tabs[2]:
+ self._render_pricing_reports_tab()
+
+ with tabs[3]:
+ self._render_risk_reports_tab()
+
+ with tabs[4]:
+ self._render_custom_reports_tab()
+
+ def _render_dashboard_tab(self):
+ """عرض تبويب لوحة المعلومات"""
+
+ st.markdown("### لوحة معلومات النظام")
+
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ total_projects = self._get_total_projects()
+ st.metric("إجمالي المشاريع", total_projects)
+
+ with col2:
+ active_projects = self._get_active_projects()
+ st.metric("المشاريع النشطة", active_projects, delta=f"{active_projects/total_projects*100:.1f}%" if total_projects > 0 else "0%")
+
+ with col3:
+ won_projects = self._get_won_projects()
+ st.metric("المشاريع المرساة", won_projects, delta=f"{won_projects/total_projects*100:.1f}%" if total_projects > 0 else "0%")
+
+ with col4:
+ avg_local_content = self._get_avg_local_content()
+ st.metric("متوسط المحتوى المحلي", f"{avg_local_content:.1f}%", delta=f"{avg_local_content-70:.1f}%" if avg_local_content > 0 else "0%")
+
+ st.markdown("#### توزيع المشاريع حسب الحالة")
+ project_status_data = self._get_project_status_data()
+ fig = px.pie(project_status_data, values='count', names='status', title='توزيع المشاريع حسب الحالة', hole=0.4)
+ st.plotly_chart(fig, use_container_width=True)
+
+ st.markdown("#### اتجاه المشاريع الشهري")
+ monthly_data = self._get_monthly_project_data()
+ fig = px.line(monthly_data, x='month', y=['new', 'submitted', 'won'], title='اتجاه المشاريع الشهري')
+ st.plotly_chart(fig, use_container_width=True)
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown("#### توزيع المشاريع حسب النوع")
+ project_type_data = self._get_project_type_data()
+ fig = px.bar(project_type_data, x='type', y='count', title='توزيع المشاريع حسب النوع')
+ st.plotly_chart(fig, use_container_width=True)
+
+ with col2:
+ st.markdown("#### توزيع المشاريع حسب الموقع")
+ project_location_data = self._get_project_location_data()
+ fig = px.bar(project_location_data, x='location', y='count', title='توزيع المشاريع حسب الموقع')
+ st.plotly_chart(fig, use_container_width=True)
+
+ st.markdown("#### أحدث المشاريع")
+ latest_projects = self._get_latest_projects()
+ st.dataframe(latest_projects)
+
+ def _render_projects_reports_tab(self):
+ """عرض تبويب تقارير المشاريع"""
+
+ st.markdown("### تقارير المشاريع")
+
+ report_type = st.selectbox(
+ "نوع التقرير",
+ ["تقرير حالة المشاريع", "تقرير أداء المشاريع", "تقرير المشاريع المتأخرة", "تقرير المشاريع المكتملة"]
+ )
+
+ if report_type == "تقرير حالة المشاريع":
+ self._render_project_status_report()
+ elif report_type == "تقرير أداء المشاريع":
+ self._render_project_performance_report()
+ elif report_type == "تقرير المشاريع المتأخرة":
+ self._render_delayed_projects_report()
+ elif report_type == "تقرير المشاريع المكتملة":
+ self._render_completed_projects_report()
+
+ def _render_pricing_reports_tab(self):
+ """عرض تبويب تقارير التسعير"""
+
+ st.markdown("### تقارير التسعير")
+
+ report_type = st.selectbox(
+ "نوع التقرير",
+ ["تقرير تحليل الأسعار", "تقرير مقارنة الأسعار", "تقرير اتجاهات الأسعار", "تقرير تحليل المنافسين"]
+ )
+
+ if report_type == "تقرير تحليل الأسعار":
+ self._render_price_analysis_report()
+ elif report_type == "تقرير مقارنة الأسعار":
+ self._render_price_comparison_report()
+ elif report_type == "تقرير اتجاهات الأسعار":
+ self._render_price_trends_report()
+ elif report_type == "تقرير تحليل المنافسين":
+ self._render_competitors_analysis_report()
+
+ def _render_risk_reports_tab(self):
+ """عرض تبويب تقارير المخاطر"""
+
+ st.markdown("### تقارير المخاطر")
+
+ report_type = st.selectbox(
+ "نوع التقرير",
+ ["تقرير تحليل المخاطر", "تقرير مصفوفة المخاطر", "تقرير متابعة المخاطر", "تقرير استراتيجيات التخفيف"]
+ )
+
+ if report_type == "تقرير تحليل المخاطر":
+ self._render_risk_analysis_report()
+ elif report_type == "تقرير مصفوفة المخاطر":
+ self._render_risk_matrix_report()
+ elif report_type == "تقرير متابعة المخاطر":
+ self._render_risk_monitoring_report()
+ elif report_type == "تقرير استراتيجيات التخفيف":
+ self._render_risk_mitigation_report()
+
+ def _render_custom_reports_tab(self):
+ """عرض تبويب التقارير المخصصة"""
+
+ st.markdown("### التقارير المخصصة")
+
+ st.markdown("#### إنشاء تقرير مخصص")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ report_name = st.text_input("اسم التقرير")
+ report_description = st.text_area("وصف التقرير")
+
+ with col2:
+ report_fields = st.multiselect(
+ "حقول التقرير",
+ ["رقم المشروع", "اسم المشروع", "نوع المشروع", "حالة المشروع", "تاريخ البدء", "تاريخ الانتهاء", "الميزانية", "التكلفة الفعلية", "نسبة الإنجاز", "المخاطر", "الموقع", "المالك", "المقاول"]
+ )
+
+ report_filters = st.multiselect(
+ "تصفية التقرير",
+ ["نوع المشروع", "حالة المشروع", "الفترة الزمنية", "الميزانية", "الموقع", "المالك", "المقاول"]
+ )
+
+ if st.button("إنشاء التقرير"):
+ if report_name and report_description and report_fields:
+ with st.spinner("جاري إنشاء التقرير..."):
+ time.sleep(2) # محاكاة وقت المعالجة
+ st.success("تم إنشاء التقرير بنجاح!")
+
+ # عرض التقرير المخصص (محاكاة)
+ custom_report_data = self._generate_custom_report(report_fields)
+ st.dataframe(custom_report_data)
+
+ # تصدير التقرير
+ st.download_button(
+ label="تصدير التقرير (Excel)",
+ data=self._export_to_excel(custom_report_data),
+ file_name=f"{report_name}.xlsx",
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ )
+ else:
+ st.warning("يرجى ملء جميع الحقول المطلوبة")
+
+ st.markdown("#### التقارير المخصصة المحفوظة")
+
+ saved_reports = [
+ {"id": 1, "name": "تقرير المشاريع المتأخرة في الرياض", "created_at": "2025-03-15", "last_run": "2025-03-30"},
+ {"id": 2, "name": "تقرير مشاريع الطرق ذات المخاطر العالية", "created_at": "2025-03-10", "last_run": "2025-03-28"},
+ {"id": 3, "name": "تقرير المشاريع المكتملة في الربع الأول", "created_at": "2025-03-05", "last_run": "2025-03-25"}
+ ]
+
+ saved_reports_df = pd.DataFrame(saved_reports)
+ st.dataframe(saved_reports_df)
+
+ # تنفيذ دوال الحصول على البيانات
+
+ def _get_total_projects(self):
+ """الحصول على إجمالي عدد المشاريع"""
+ # محاكاة البيانات
+ return 120
+
+ def _get_active_projects(self):
+ """الحصول على عدد المشاريع النشطة"""
+ # محاكاة البيانات
+ return 45
+
+ def _get_won_projects(self):
+ """الحصول على عدد المشاريع المرساة"""
+ # محاكاة البيانات
+ return 30
+
+ def _get_avg_local_content(self):
+ """الحصول على متوسط المحتوى المحلي"""
+ # محاكاة البيانات
+ return 75.5
+
+ def _get_project_status_data(self):
+ """الحصول على بيانات توزيع المشاريع حسب الحالة"""
+ # محاكاة البيانات
+ data = {
+ 'status': ['جديد', 'قيد التقديم', 'تم التقديم', 'فائز', 'خاسر', 'ملغي'],
+ 'count': [25, 20, 15, 30, 25, 5]
+ }
+ return pd.DataFrame(data)
+
+ def _get_monthly_project_data(self):
+ """الحصول على بيانات اتجاه المشاريع الشهري"""
+ # محاكاة البيانات
+ data = {
+ 'month': ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو'],
+ 'new': [10, 15, 12, 8, 20, 18],
+ 'submitted': [8, 12, 10, 6, 15, 14],
+ 'won': [5, 8, 6, 4, 10, 9]
+ }
+ return pd.DataFrame(data)
+
+ def _get_project_type_data(self):
+ """الحصول على بيانات توزيع المشاريع حسب النوع"""
+ # محاكاة البيانات
+ data = {
+ 'type': ['مباني', 'طرق', 'جسور', 'أنفاق', 'بنية تحتية', 'أخرى'],
+ 'count': [40, 30, 15, 10, 20, 5]
+ }
+ return pd.DataFrame(data)
+
+ def _get_project_location_data(self):
+ """الحصول على بيانات توزيع المشاريع حسب الموقع"""
+ # محاكاة البيانات
+ data = {
+ 'location': ['الرياض', 'جدة', 'الدمام', 'مكة', 'المدينة', 'أخرى'],
+ 'count': [35, 25, 20, 15, 10, 15]
+ }
+ return pd.DataFrame(data)
+
+ def _get_latest_projects(self):
+ """الحصول على بيانات أحدث المشاريع"""
+ # محاكاة البيانات
+ data = {
+ 'رقم المشروع': ['P-2025-001', 'P-2025-002', 'P-2025-003', 'P-2025-004', 'P-2025-005'],
+ 'اسم المشروع': ['إنشاء مبنى إداري', 'تطوير شبكة طرق', 'إنشاء جسر', 'بناء مدرسة', 'تطوير شبكة مياه'],
+ 'نوع المشروع': ['مباني', 'طرق', 'جسور', 'مباني', 'بنية تحتية'],
+ 'حالة المشروع': ['جديد', 'قيد التقديم', 'تم التقديم', 'فائز', 'جديد'],
+ 'تاريخ الإضافة': ['2025-03-30', '2025-03-28', '2025-03-25', '2025-03-20', '2025-03-18']
+ }
+ return pd.DataFrame(data)
+
+ # تنفيذ دوال عرض التقارير
+
+ def _render_project_status_report(self):
+ """عرض تقرير حالة المشاريع"""
+
+ st.markdown("#### تقرير حالة المشاريع")
+
+ # محاكاة بيانات التقرير
+ data = {
+ 'رقم المشروع': ['P-2025-001', 'P-2025-002', 'P-2025-003', 'P-2025-004', 'P-2025-005', 'P-2025-006', 'P-2025-007', 'P-2025-008', 'P-2025-009', 'P-2025-010'],
+ 'اسم المشروع': ['إنشاء مبنى إداري', 'تطوير شبكة طرق', 'إنشاء جسر', 'بناء مدرسة', 'تطوير شبكة مياه', 'إنشاء مستشفى', 'بناء مركز تجاري', 'تطوير حديقة عامة', 'إنشاء مصنع', 'تطوير مطار'],
+ 'نوع المشروع': ['مباني', 'طرق', 'جسور', 'مباني', 'بنية تحتية', 'مباني', 'مباني', 'أخرى', 'مباني', 'بنية تحتية'],
+ '
+(Content truncated due to size limit. Use line ranges to read in chunks)
\ No newline at end of file
diff --git a/modules/resources/resources_app.py b/modules/resources/resources_app.py
index 632b389d3ead22b82a6784f7642aa4f2b6f35071..3ca86ea1337333b24c775ec52cc4d4a2571b1fb7 100644
--- a/modules/resources/resources_app.py
+++ b/modules/resources/resources_app.py
@@ -18,15 +18,15 @@ from pathlib import Path
class ResourcesApp:
"""وحدة الموارد"""
-
+
def __init__(self):
"""تهيئة وحدة الموارد"""
-
+
# تهيئة حالة الجلسة
if 'resources_data' not in st.session_state:
# إنشاء بيانات افتراضية للموارد البشرية
np.random.seed(42)
-
+
# إنشاء بيانات الموظفين
n_employees = 50
employee_ids = [f"EMP-{i+1:03d}" for i in range(n_employees)]
@@ -47,7 +47,7 @@ class ResourcesApp:
employee_salaries = np.random.randint(5000, 25000, n_employees)
employee_experiences = np.random.randint(1, 20, n_employees)
employee_availabilities = np.random.choice([True, False], n_employees, p=[0.7, 0.3])
-
+
# إنشاء DataFrame للموظفين
employees_data = {
"رقم الموظف": employee_ids,
@@ -58,7 +58,7 @@ class ResourcesApp:
"سنوات الخبرة": employee_experiences,
"متاح": employee_availabilities
}
-
+
# إنشاء بيانات المعدات
n_equipment = 30
equipment_ids = [f"EQP-{i+1:03d}" for i in range(n_equipment)]
@@ -74,7 +74,7 @@ class ResourcesApp:
equipment_availabilities = np.random.choice([True, False], n_equipment, p=[0.8, 0.2])
equipment_conditions = np.random.choice(["ممتاز", "جيد", "متوسط", "سيء"], n_equipment, p=[0.4, 0.3, 0.2, 0.1])
equipment_locations = np.random.choice(["المستودع", "موقع العمل 1", "موقع العمل 2", "موقع العمل 3", "في الصيانة"], n_equipment)
-
+
# إنشاء DataFrame للمعدات
equipment_data = {
"رقم المعدة": equipment_ids,
@@ -85,7 +85,7 @@ class ResourcesApp:
"الحالة": equipment_conditions,
"الموقع": equipment_locations
}
-
+
# إنشاء بيانات المواد
n_materials = 40
material_ids = [f"MAT-{i+1:03d}" for i in range(n_materials)]
@@ -102,7 +102,7 @@ class ResourcesApp:
material_costs = np.random.randint(50, 5000, n_materials)
material_suppliers = np.random.choice(["المورد 1", "المورد 2", "المورد 3", "المورد 4", "المورد 5"], n_materials)
material_lead_times = np.random.randint(1, 30, n_materials)
-
+
# إنشاء DataFrame للمواد
materials_data = {
"رقم المادة": material_ids,
@@ -113,7 +113,7 @@ class ResourcesApp:
"المورد": material_suppliers,
"مدة التوريد (يوم)": material_lead_times
}
-
+
# إنشاء بيانات المشاريع
n_projects = 10
project_ids = [f"PRJ-{i+1:03d}" for i in range(n_projects)]
@@ -134,7 +134,7 @@ class ResourcesApp:
]
project_budgets = np.random.randint(1000000, 50000000, n_projects)
project_statuses = np.random.choice(["قيد التنفيذ", "مكتمل", "متوقف", "مخطط"], n_projects)
-
+
# إنشاء DataFrame للمشاريع
projects_data = {
"رقم المشروع": project_ids,
@@ -145,7 +145,7 @@ class ResourcesApp:
"الميزانية": project_budgets,
"الحالة": project_statuses
}
-
+
# إنشاء بيانات تخصيص الموارد للمشاريع
n_allocations = 100
allocation_ids = [f"ALLOC-{i+1:03d}" for i in range(n_allocations)]
@@ -159,7 +159,7 @@ class ResourcesApp:
allocation_resource_ids.append(np.random.choice(equipment_ids))
else:
allocation_resource_ids.append(np.random.choice(material_ids))
-
+
allocation_start_dates = [
(datetime.now() - timedelta(days=np.random.randint(0, 90))).strftime("%Y-%m-%d")
for _ in range(n_allocations)
@@ -170,7 +170,7 @@ class ResourcesApp:
]
allocation_quantities = np.random.randint(1, 10, n_allocations)
allocation_costs = np.random.randint(5000, 50000, n_allocations)
-
+
# إنشاء DataFrame لتخصيص الموارد
allocations_data = {
"رقم التخصيص": allocation_ids,
@@ -182,7 +182,7 @@ class ResourcesApp:
"الكمية": allocation_quantities,
"التكلفة": allocation_costs
}
-
+
# تخزين البيانات في حالة الجلسة
st.session_state.resources_data = {
"employees": pd.DataFrame(employees_data),
@@ -191,22 +191,22 @@ class ResourcesApp:
"projects": pd.DataFrame(projects_data),
"allocations": pd.DataFrame(allocations_data)
}
-
+
def run(self):
"""
تشغيل وحدة الموارد
-
+
هذه الدالة هي نقطة الدخول الرئيسية لوحدة الموارد وتقوم بتهيئة واجهة المستخدم
وعرض جميع العناصر المطلوبة.
"""
# استدعاء دالة العرض الرئيسية
self.render()
-
+
def render(self):
"""عرض واجهة وحدة الموارد"""
-
+
st.markdown("وحدة الموارد ", unsafe_allow_html=True)
-
+
tabs = st.tabs([
"لوحة المعلومات",
"الموارد البشرية",
@@ -215,68 +215,68 @@ class ResourcesApp:
"تخصيص الموارد",
"تخطيط الموارد"
])
-
+
with tabs[0]:
self._render_dashboard_tab()
-
+
with tabs[1]:
self._render_human_resources_tab()
-
+
with tabs[2]:
self._render_equipment_tab()
-
+
with tabs[3]:
self._render_materials_tab()
-
+
with tabs[4]:
self._render_resource_allocation_tab()
-
+
with tabs[5]:
self._render_resource_planning_tab()
-
+
def _render_dashboard_tab(self):
"""عرض تبويب لوحة المعلومات"""
-
+
st.markdown("### لوحة معلومات الموارد")
-
+
# استخراج البيانات
employees_df = st.session_state.resources_data["employees"]
equipment_df = st.session_state.resources_data["equipment"]
materials_df = st.session_state.resources_data["materials"]
projects_df = st.session_state.resources_data["projects"]
allocations_df = st.session_state.resources_data["allocations"]
-
+
# عرض مؤشرات الأداء الرئيسية
st.markdown("#### مؤشرات الأداء الرئيسية")
-
+
col1, col2, col3, col4 = st.columns(4)
-
+
with col1:
total_employees = len(employees_df)
available_employees = len(employees_df[employees_df["متاح"] == True])
st.metric("الموظفون", f"{available_employees}/{total_employees}")
-
+
with col2:
total_equipment = len(equipment_df)
available_equipment = len(equipment_df[equipment_df["متاحة"] == True])
st.metric("المعدات", f"{available_equipment}/{total_equipment}")
-
+
with col3:
total_materials = len(materials_df)
low_stock_materials = len(materials_df[materials_df["الكمية المتاحة"] < 50])
st.metric("المواد", f"{total_materials}", f"-{low_stock_materials} منخفضة المخزون")
-
+
with col4:
total_projects = len(projects_df)
active_projects = len(projects_df[projects_df["الحالة"] == "قيد التنفيذ"])
st.metric("المشاريع النشطة", f"{active_projects}/{total_projects}")
-
+
# عرض توزيع الموارد البشرية حسب القسم
st.markdown("#### توزيع الموارد البشرية حسب القسم")
-
+
dept_counts = employees_df["القسم"].value_counts().reset_index()
dept_counts.columns = ["القسم", "العدد"]
-
+
fig = px.pie(
dept_counts,
values="العدد",
@@ -284,15 +284,15 @@ class ResourcesApp:
title="توزيع الموظفين حسب القسم",
color="القسم"
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
# عرض توزيع المعدات حسب النوع
st.markdown("#### توزيع المعدات حسب النوع")
-
+
type_counts = equipment_df["النوع"].value_counts().reset_index()
type_counts.columns = ["النوع", "العدد"]
-
+
fig = px.bar(
type_counts,
x="النوع",
@@ -301,15 +301,15 @@ class ResourcesApp:
color="النوع",
text_auto=True
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
# عرض توزيع المواد حسب المورد
st.markdown("#### توزيع المواد حسب المورد")
-
+
supplier_counts = materials_df["المورد"].value_counts().reset_index()
supplier_counts.columns = ["المورد", "العدد"]
-
+
fig = px.pie(
supplier_counts,
values="العدد",
@@ -317,27 +317,27 @@ class ResourcesApp:
title="توزيع المواد حسب المورد",
color="المورد"
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
# عرض توزيع تكاليف الموارد
st.markdown("#### توزيع تكاليف الموارد")
-
+
# حساب إجمالي تكاليف الموظفين
total_employee_cost = employees_df["التكلفة الشهرية"].sum()
-
+
# حساب إجمالي تكاليف المعدات (افتراضياً لشهر واحد)
total_equipment_cost = equipment_df["التكلفة اليومية"].sum() * 30
-
+
# حساب إجمالي تكاليف المواد
total_material_cost = (materials_df["الكمية المتاحة"] * materials_df["تكلفة الوحدة"]).sum()
-
+
# إنشاء DataFrame لتوزيع التكاليف
cost_distribution = pd.DataFrame({
"نوع المورد": ["الموظفون", "المعدات", "المواد"],
"التكلفة": [total_employee_cost, total_equipment_cost, total_material_cost]
})
-
+
fig = px.pie(
cost_distribution,
values="التكلفة",
@@ -350,30 +350,59 @@ class ResourcesApp:
"المواد": "#f39c12"
}
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
# عرض تخصيص الموارد للمشاريع
st.markdown("#### تخصيص الموارد للمشاريع")
-
+
# حساب عدد الموارد المخصصة لكل مشروع
project_allocations = allocations_df["رقم المشروع"].value_counts().reset_index()
project_allocations.columns = ["رقم المشروع", "عدد الموارد المخصصة"]
-
+
# دمج بيانات المشاريع مع بيانات التخصيص
project_allocations = project_allocations.merge(
projects_df[["رقم المشروع", "اسم المشروع", "الحالة"]],
on="رقم المشروع",
how="left"
)
-
+
fig = px.bar(
project_allocations,
x="اسم المشروع",
- y="الكمية المخصصة",
+ y="عدد الموارد المخصصة",
title="توزيع الموارد على المشاريع",
- color="اسم المورد",
text_auto=True
- ) # قوس الإغلاق المفقود
+ )
st.plotly_chart(fig, use_container_width=True)
+
+ def _render_human_resources_tab(self):
+ """عرض تبويب الموارد البشرية"""
+ st.markdown("### الموارد البشرية")
+ # إضافة محتوى تبويب الموارد البشرية هنا ...
+ pass
+
+ def _render_equipment_tab(self):
+ """عرض تبويب المعدات"""
+ st.markdown("### المعدات")
+ # إضافة محتوى تبويب المعدات هنا ...
+ pass
+
+ def _render_materials_tab(self):
+ """عرض تبويب المواد"""
+ st.markdown("### المواد")
+ # إضافة محتوى تبويب المواد هنا ...
+ pass
+
+ def _render_resource_allocation_tab(self):
+ """عرض تبويب تخصيص الموارد"""
+ st.markdown("### تخصيص الموارد")
+ # إضافة محتوى تبويب تخصيص الموارد هنا ...
+ pass
+
+ def _render_resource_planning_tab(self):
+ """عرض تبويب تخطيط الموارد"""
+ st.markdown("### تخطيط الموارد")
+ # إضافة محتوى تبويب تخطيط الموارد هنا ...
+ pass
\ No newline at end of file
diff --git a/modules/risk_analysis/risk_analyzer.py b/modules/risk_analysis/risk_analyzer.py
index e373f833c742ca681e81f787725669297b34e05a..bd184758a957d78f63a32436968d7abdd9beacc5 100644
--- a/modules/risk_analysis/risk_analyzer.py
+++ b/modules/risk_analysis/risk_analyzer.py
@@ -14,6 +14,7 @@ import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import sys
+import plotly.express as px
# إضافة مسار المشروع للنظام
sys.path.append(str(Path(__file__).parent.parent))
@@ -28,526 +29,168 @@ logging.basicConfig(
)
logger = logging.getLogger('risk_analysis')
+"""
+محلل المخاطر المتقدم للمشاريع
+"""
+
class RiskAnalyzer:
- """محلل المخاطر"""
-
- def __init__(self, config=None, db=None):
- """تهيئة محلل المخاطر"""
- self.config = config
- self.db = db
- self.analysis_in_progress = False
- self.current_project = None
- self.analysis_results = {}
-
- # إنشاء مجلد التحليل إذا لم يكن موجوداً
- if config and hasattr(config, 'EXPORTS_PATH'):
- self.exports_path = Path(config.EXPORTS_PATH)
- else:
- self.exports_path = Path('data/exports')
-
- if not self.exports_path.exists():
- self.exports_path.mkdir(parents=True, exist_ok=True)
-
- def analyze_risks(self, project_id, method="comprehensive", callback=None):
- """تحليل مخاطر المشروع"""
- if self.analysis_in_progress:
- logger.warning("هناك عملية تحليل مخاطر جارية بالفعل")
- return False
-
- self.analysis_in_progress = True
- self.current_project = project_id
- self.analysis_results = {
- "project_id": project_id,
- "method": method,
- "analysis_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- "status": "جاري التحليل",
- "identified_risks": [],
- "risk_categories": {},
- "risk_matrix": {},
- "mitigation_strategies": [],
- "summary": {}
- }
-
- # بدء التحليل في خيط منفصل
- thread = threading.Thread(
- target=self._analyze_risks_thread,
- args=(project_id, method, callback)
- )
- thread.daemon = True
- thread.start()
-
- return True
-
- def _analyze_risks_thread(self, project_id, method, callback):
- """خيط تحليل المخاطر"""
- try:
- # محاكاة جلب بيانات المشروع من قاعدة البيانات
- project_data = self._get_project_data(project_id)
-
- if not project_data:
- logger.error(f"لم يتم العثور على بيانات المشروع: {project_id}")
- self.analysis_results["status"] = "فشل التحليل"
- self.analysis_results["error"] = "لم يتم العثور على بيانات المشروع"
- return
-
- # تحديد المخاطر
- self._identify_risks(project_data, method)
-
- # تصنيف المخاطر
- self._categorize_risks()
-
- # إنشاء مصفوفة المخاطر
- self._create_risk_matrix()
-
- # تطوير استراتيجيات التخفيف
- self._develop_mitigation_strategies(method)
-
- # إنشاء ملخص التحليل
- self._create_analysis_summary(method)
-
- # تحديث حالة التحليل
- self.analysis_results["status"] = "اكتمل التحليل"
- self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-
- logger.info(f"اكتمل تحليل مخاطر المشروع: {project_id}")
-
- except Exception as e:
- logger.error(f"خطأ في تحليل مخاطر المشروع: {str(e)}")
- self.analysis_results["status"] = "فشل التحليل"
- self.analysis_results["error"] = str(e)
-
- finally:
- self.analysis_in_progress = False
-
- # استدعاء دالة الاستجابة إذا تم توفيرها
- if callback and callable(callback):
- callback(self.analysis_results)
-
- def _get_project_data(self, project_id):
- """الحصول على بيانات المشروع"""
- # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
- # هنا نقوم بمحاكاة البيانات للتوضيح
-
- return {
- "id": project_id,
- "name": "مشروع الطرق السريعة",
- "client": "وزارة النقل",
- "description": "إنشاء طرق سريعة بطول 50 كم في المنطقة الشرقية",
- "start_date": "2025-05-01",
- "end_date": "2025-11-30",
- "status": "تخطيط",
- "budget": 50000000,
- "location": "المنطقة الشرقية",
- "project_type": "بنية تحتية",
- "complexity": "متوسط",
- "existing_risks": [
- {"id": 1, "name": "تأخر توريد المواد", "probability": "متوسط", "impact": "عالي", "category": "توريد"},
- {"id": 2, "name": "تغير أسعار المواد", "probability": "عالي", "impact": "عالي", "category": "مالي"},
- {"id": 3, "name": "ظروف جوية غير مواتية", "probability": "منخفض", "impact": "متوسط", "category": "بيئي"},
- {"id": 4, "name": "نقص العمالة", "probability": "متوسط", "impact": "متوسط", "category": "موارد بشرية"}
- ]
- }
-
- def _identify_risks(self, project_data, method):
- """تحديد المخاطر"""
- # دمج المخاطر الموجودة
- identified_risks = []
- for risk in project_data["existing_risks"]:
- identified_risks.append({
- "id": risk["id"],
- "name": risk["name"],
- "description": f"مخاطر {risk['name']} في المشروع",
- "category": risk["category"],
- "probability": risk["probability"],
- "impact": risk["impact"],
- "risk_score": self._calculate_risk_score(risk["probability"], risk["impact"]),
- "source": "existing"
- })
-
- # إضافة مخاطر إضافية بناءً على نوع المشروع وموقعه وتعقيده
- additional_risks = self._generate_additional_risks(project_data, method)
- identified_risks.extend(additional_risks)
-
- # تخزين المخاطر المحددة
- self.analysis_results["identified_risks"] = identified_risks
-
- def _generate_additional_risks(self, project_data, method):
- """توليد مخاطر إضافية بناءً على بيانات المشروع"""
- additional_risks = []
-
- # مخاطر مرتبطة بنوع المشروع
- if project_data["project_type"] == "بنية تحتية":
- additional_risks.extend([
- {
- "id": 101,
- "name": "مشاكل جيوتقنية",
- "description": "مشاكل غير متوقعة في التربة أو الظروف الجيولوجية",
- "category": "فني",
- "probability": "متوسط",
- "impact": "عالي",
- "risk_score": self._calculate_risk_score("متوسط", "عالي"),
- "source": "generated"
- },
- {
- "id": 102,
- "name": "تعارض مع مرافق قائمة",
- "description": "تعارض أعمال الحفر مع خطوط المرافق القائمة (كهرباء، مياه، اتصالات)",
- "category": "فني",
- "probability": "متوسط",
- "impact": "متوسط",
- "risk_score": self._calculate_risk_score("متوسط", "متوسط"),
- "source": "generated"
- }
- ])
-
- # مخاطر مرتبطة بالموقع
- if project_data["location"] == "المنطقة الشرقية":
- additional_risks.extend([
- {
- "id": 201,
- "name": "ارتفاع درجات الحرارة",
- "description": "تأثير ارتفاع درجات الحرارة على إنتاجية العمل وجودة المواد",
- "category": "بيئي",
- "probability": "عالي",
- "impact": "متوسط",
- "risk_score": self._calculate_risk_score("عالي", "متوسط"),
- "source": "generated"
- },
- {
- "id": 202,
- "name": "رطوبة عالية",
- "description": "تأثير الرطوبة العالية على جودة المواد وتقنيات البناء",
- "category": "بيئي",
- "probability": "عالي",
- "impact": "منخفض",
- "risk_score": self._calculate_risk_score("عالي", "منخفض"),
- "source": "generated"
- }
- ])
-
- # مخاطر مرتبطة بتعقيد المشروع
- if project_data["complexity"] in ["متوسط", "عالي"]:
- additional_risks.extend([
- {
- "id": 301,
- "name": "تغييرات في نطاق العمل",
- "description": "طلبات تغيير من العميل أو تعديلات في متطلبات المشروع",
- "category": "إداري",
- "probability": "عالي",
- "impact": "عالي",
- "risk_score": self._calculate_risk_score("عالي", "عالي"),
- "source": "generated"
- },
- {
- "id": 302,
- "name": "تأخر الموافقات",
- "description": "تأخر الحصول على الموافقات والتصاريح اللازمة",
- "category": "تنظيمي",
- "probability": "متوسط",
- "impact": "عالي",
- "risk_score": self._calculate_risk_score("متوسط", "عالي"),
- "source": "generated"
- }
- ])
-
- # إضافة مخاطر إضافية إذا كانت طريقة التحليل شاملة
- if method == "comprehensive":
- additional_risks.extend([
- {
- "id": 401,
- "name": "مخاطر سياسية",
- "description": "تغييرات في السياسات الحكومية أو اللوائح التنظيمية",
- "category": "خارجي",
- "probability": "منخفض",
- "impact": "عالي",
- "risk_score": self._calculate_risk_score("منخفض", "عالي"),
- "source": "generated"
- },
- {
- "id": 402,
- "name": "مخاطر اقتصادية",
- "description": "تقلبات في أسعار العملات أو التضخم",
- "category": "مالي",
- "probability": "متوسط",
- "impact": "متوسط",
- "risk_score": self._calculate_risk_score("متوسط", "متوسط"),
- "source": "generated"
- },
- {
- "id": 403,
- "name": "مخاطر تقنية",
- "description": "مشاكل في التقنيات الجديدة أو المعدات",
- "category": "فني",
- "probability": "متوسط",
- "impact": "متوسط",
- "risk_score": self._calculate_risk_score("متوسط", "متوسط"),
- "source": "generated"
- }
- ])
-
- return additional_risks
-
- def _calculate_risk_score(self, probability, impact):
- """حساب درجة المخاطرة"""
- # تحويل القيم النصية إلى قيم رقمية
- probability_values = {"منخفض": 1, "متوسط": 2, "عالي": 3}
- impact_values = {"منخفض": 1, "متوسط": 2, "عالي": 3}
-
- # حساب درجة المخاطرة
- p_value = probability_values.get(probability, 1)
- i_value = impact_values.get(impact, 1)
-
- return p_value * i_value
-
- def _categorize_risks(self):
- """تصنيف المخاطر"""
- categories = {}
-
- for risk in self.analysis_results["identified_risks"]:
- category = risk["category"]
- if category not in categories:
- categories[category] = []
-
- categories[category].append(risk)
-
- # حساب إحصائيات لكل فئة
- for category, risks in categories.items():
- total_score = sum(risk["risk_score"] for risk in risks)
- avg_score = total_score / len(risks) if risks else 0
- max_score = max(risk["risk_score"] for risk in risks) if risks else 0
-
- categories[category] = {
- "risks": risks,
- "count": len(risks),
- "total_score": total_score,
- "avg_score": avg_score,
- "max_score": max_score
- }
-
- self.analysis_results["risk_categories"] = categories
-
- def _create_risk_matrix(self):
- """إنشاء مصفوفة المخاطر"""
- matrix = {
- "high_impact": {"high_prob": [], "medium_prob": [], "low_prob": []},
- "medium_impact": {"high_prob": [], "medium_prob": [], "low_prob": []},
- "low_impact": {"high_prob": [], "medium_prob": [], "low_prob": []}
- }
-
- for risk in self.analysis_results["identified_risks"]:
- impact = risk["impact"].lower()
- probability = risk["probability"].lower()
-
- impact_key = f"{impact}_impact"
- if impact == "عالي":
- impact_key = "high_impact"
- elif impact == "متوسط":
- impact_key = "medium_impact"
- else:
- impact_key = "low_impact"
-
- prob_key = f"{probability}_prob"
- if probability == "عالي":
- prob_key = "high_prob"
- elif probability == "متوسط":
- prob_key = "medium_prob"
- else:
- prob_key = "low_prob"
-
- if impact_key in matrix and prob_key in matrix[impact_key]:
- matrix[impact_key][prob_key].append(risk)
-
- self.analysis_results["risk_matrix"] = matrix
-
- def _develop_mitigation_strategies(self, method):
- """تطوير استراتيجيات التخفيف"""
- strategies = []
-
- # استراتيجيات للمخاطر ذات الأولوية العالية
- high_priority_risks = []
-
- # المخاطر ذات التأثير العالي واحتمالية عالية
- high_priority_risks.extend(self.analysis_results["risk_matrix"]["high_impact"]["high_prob"])
-
- # المخاطر ذات التأثير العالي واحتمالية متوسطة
- high_priority_risks.extend(self.analysis_results["risk_matrix"]["high_impact"]["medium_prob"])
-
- # المخاطر ذات التأثير المتوسط واحتمالية عالية
- high_priority_risks.extend(self.analysis_results["risk_matrix"]["medium_impact"]["high_prob"])
-
- for risk in high_priority_risks:
- strategy = self._generate_mitigation_strategy(risk)
- strategies.append(strategy)
-
- # إذا كانت طريقة التحليل شاملة، أضف استراتيجيات للمخاطر ذات الأولوية المتوسطة
- if method == "comprehensive":
- medium_priority_risks = []
-
- # المخاطر ذات التأثير العالي واحتمالية منخفضة
- medium_priority_risks.extend(self.analysis_results["risk_matrix"]["high_impact"]["low_prob"])
-
- # المخاطر ذات التأثير المتوسط واحتمالية متوسطة
- medium_priority_risks.extend(self.analysis_results["risk_matrix"]["medium_impact"]["medium_prob"])
-
- # المخاطر ذات التأثير المنخفض واحتمالية عالية
- medium_priority_risks.extend(self.analysis_results["risk_matrix"]["low_impact"]["high_prob"])
-
- for risk in medium_priority_risks:
- strategy = self._generate_mitigation_strategy(risk)
- strategies.append(strategy)
-
- self.analysis_results["mitigation_strategies"] = strategies
-
- def _generate_mitigation_strategy(self, risk):
- """توليد استراتيجية تخفيف للمخاطر"""
- strategy_templates = {
- "توريد": [
- "إنشاء قائمة بموردين بديلين",
- "التعاقد المسبق مع الموردين",
- "تخزين المواد الحرجة مسبقًا",
- "وضع خطة للتوريد المرحلي"
- ],
- "مالي": [
- "تضمين بند تعديل الأسعار في العقود",
- "تخصيص ميزانية احتياطية",
- "التأمين ضد المخاطر المالية",
- "تحديث دراسة الجدوى بانتظام"
- ],
- "بيئي": [
- "وضع خطة للطوارئ البيئية",
- "جدولة الأنشطة الحرجة في المواسم المناسبة",
- "توفير معدات حماية إضافية",
- "تطبيق تقنيات مقاومة للظروف البيئية"
- ],
- "موارد بشرية": [
- "التعاقد مع شركات توظيف إضافية",
- "تدريب فرق العمل على مهام متعددة",
- "وضع خطة للحوافز والمكافآت",
- "تطوير برنامج للاحتفاظ بالموظفين"
- ],
- "فني": [
- "إجراء اختبارات إضافية قبل التنفيذ",
- "الاستعانة بخبراء متخصصين",
- "تطبيق منهجية مراجعة التصميم",
- "إعداد خطط بديلة للحلول التقنية"
- ],
- "إداري": [
- "تطبيق إجراءات إدارة التغيير",
- "عقد اجتماعات دورية مع أصحاب المصلحة",
- "توثيق متطلبات المشروع بشكل تفصيلي",
- "تحديد نطاق العمل بوضوح في العقود"
- ],
- "تنظيمي": [
- "التواصل المبكر مع الجهات التنظيمية",
- "تعيين مستشار قانوني متخصص",
- "متابعة التحديثات التنظيمية بانتظام",
- "تخصيص وقت إضافي للحصول على الموافقات"
- ],
- "خارجي": [
- "متابعة التطورات السياسية والاقتصادية",
- "وضع خطط بديلة للسيناريوهات المختلفة",
- "التأمين ضد المخاطر الخارجية",
- "تنويع مصادر التوريد والتمويل"
- ]
- }
-
- # اختيار استراتيجيات مناسبة بناءً على فئة المخاطر
- category = risk["category"]
- templates = strategy_templates.get(category, strategy_templates["إداري"])
-
- # اختيار استراتيجية عشوائية من القائمة
- import random
- strategy_text = random.choice(templates)
-
- return {
- "risk_id": risk["id"],
- "risk_name": risk["name"],
- "strategy": strategy_text,
- "priority": "عالية" if risk["risk_score"] >= 6 else "متوسطة" if risk["risk_score"] >= 3 else "منخفضة",
- "responsible": "مدير المشروع",
- "timeline": "قبل بدء المشروع"
+ def __init__(self):
+ self.risk_categories = [
+ "مخاطر السوق",
+ "مخاطر التنفيذ",
+ "مخاطر العقود",
+ "مخاطر التمويل",
+ "مخاطر الموارد"
+ ]
+
+ self.impact_levels = {
+ "منخفض": 1,
+ "متوسط": 2,
+ "عالي": 3,
+ "حرج": 4
}
-
- def _create_analysis_summary(self, method):
- """إنشاء ملخص التحليل"""
- total_risks = len(self.analysis_results["identified_risks"])
- high_risks = len([r for r in self.analysis_results["identified_risks"] if r["risk_score"] >= 6])
- medium_risks = len([r for r in self.analysis_results["identified_risks"] if 3 <= r["risk_score"] < 6])
- low_risks = len([r for r in self.analysis_results["identified_risks"] if r["risk_score"] < 3])
-
- # حساب توزيع المخاطر حسب الفئة
- category_distribution = {}
- for risk in self.analysis_results["identified_risks"]:
- category = risk["category"]
- if category not in category_distribution:
- category_distribution[category] = 0
- category_distribution[category] += 1
-
- # حساب متوسط درجة المخاطرة
- avg_risk_score = sum(risk["risk_score"] for risk in self.analysis_results["identified_risks"]) / total_risks if total_risks > 0 else 0
-
- # إنشاء الملخص
- summary = {
- "total_risks": total_risks,
- "high_risks": high_risks,
- "medium_risks": medium_risks,
- "low_risks": low_risks,
- "category_distribution": category_distribution,
- "avg_risk_score": avg_risk_score,
- "analysis_method": method,
- "recommendations": self._generate_recommendations(high_risks, medium_risks, low_risks, category_distribution)
+
+ self.probability_levels = {
+ "نادر": 1,
+ "محتمل": 2,
+ "مرجح": 3,
+ "شبه مؤكد": 4
}
-
- self.analysis_results["summary"] = summary
-
- def _generate_recommendations(self, high_risks, medium_risks, low_risks, category_distribution):
- """توليد توصيات بناءً على نتائج التحليل"""
- recommendations = []
-
- # توصيات بناءً على عدد المخاطر العالية
- if high_risks > 3:
- recommendations.append("يجب إجراء مراجعة شاملة لخطة المشروع نظرًا لوجود عدد كبير من المخاطر عالية الخطورة.")
-
- if high_risks > 0:
- recommendations.append("تطوير خطط استجابة تفصيلية لجميع المخاطر عالية الخطورة.")
-
- # توصيات بناءً على توزيع المخاطر حسب الفئة
- max_category = max(category_distribution.items(), key=lambda x: x[1], default=(None, 0))
- if max_category[0]:
- recommendations.append(f"التركيز على إدارة مخاطر فئة '{max_category[0]}' حيث تمثل النسبة الأكبر من المخاطر المحددة.")
-
- # توصيات عامة
- recommendations.append("إجراء مراجعات دورية لسجل المخاطر وتحديثه بانتظام.")
- recommendations.append("تعيين مسؤولين محددين لمتابعة استراتيجيات التخفيف من المخاطر.")
-
- return recommendations
-
- def get_analysis_results(self):
- """الحصول على نتائج التحليل"""
- return self.analysis_results
-
- def export_analysis_results(self, format="json"):
- """تصدير نتائج التحليل"""
- if not self.analysis_results:
- logger.warning("لا توجد نتائج تحليل للتصدير")
- return None
-
- timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
- project_id = self.analysis_results.get("project_id", "unknown")
-
- if format == "json":
- filename = f"risk_analysis_{project_id}_{timestamp}.json"
- filepath = self.exports_path / filename
-
- with open(filepath, 'w', encoding='utf-8') as f:
- json.dump(self.analysis_results, f, ensure_ascii=False, indent=4)
-
- logger.info(f"تم تصدير نتائج التحليل إلى: {filepath}")
- return filepath
-
- else:
- logger.error(f"تنسيق التصدير غير مدعوم: {format}")
- return None
+
+ def analyze_risks(self, project_data):
+ """تحليل المخاطر للمشروع"""
+ risks = []
+
+ # تحليل مخاطر السوق
+ market_risks = self._analyze_market_risks(project_data)
+ risks.extend(market_risks)
+
+ # تحليل مخاطر التنفيذ
+ execution_risks = self._analyze_execution_risks(project_data)
+ risks.extend(execution_risks)
+
+ # تحليل المخاطر المالية
+ financial_risks = self._analyze_financial_risks(project_data)
+ risks.extend(financial_risks)
+
+ return pd.DataFrame(risks)
+
+ def calculate_risk_score(self, probability, impact):
+ """حساب درجة الخطر"""
+ return self.probability_levels[probability] * self.impact_levels[impact]
+
+ def render_risk_analysis(self, project_data):
+ """عرض تحليل المخاطر"""
+ st.header("تحليل المخاطر")
+
+ # تحليل المخاطر
+ risks_df = self.analyze_risks(project_data)
+
+ # عرض مصفوفة المخاطر
+ self._render_risk_matrix(risks_df)
+
+ # عرض تفاصيل المخاطر
+ self._render_risk_details(risks_df)
+
+ # عرض خطة الاستجابة للمخاطر
+ self._render_risk_response_plan(risks_df)
+
+ def _render_risk_matrix(self, risks_df):
+ """عرض مصفوفة المخاطر"""
+ st.subheader("مصفوفة المخاطر")
+
+ # إنشاء مصفوفة المخاطر
+ matrix_data = np.zeros((4, 4))
+ for _, risk in risks_df.iterrows():
+ prob_idx = self.probability_levels[risk['probability']] - 1
+ impact_idx = self.impact_levels[risk['impact']] - 1
+ matrix_data[prob_idx, impact_idx] += 1
+
+ fig = px.imshow(
+ matrix_data,
+ labels=dict(x="التأثير", y="الاحتمالية"),
+ x=list(self.impact_levels.keys()),
+ y=list(self.probability_levels.keys())
+ )
+ st.plotly_chart(fig)
+
+ def _render_risk_details(self, risks_df):
+ """عرض تفاصيل المخاطر"""
+ st.subheader("تفاصيل المخاطر")
+
+ # تصنيف المخاطر حسب درجة الخطورة
+ risks_df['risk_score'] = risks_df.apply(
+ lambda x: self.calculate_risk_score(x['probability'], x['impact']),
+ axis=1
+ )
+
+ # عرض المخاطر مرتبة حسب درجة الخطورة
+ st.dataframe(
+ risks_df.sort_values('risk_score', ascending=False),
+ use_container_width=True
+ )
+
+ def _render_risk_response_plan(self, risks_df):
+ """عرض خطة الاستجابة للمخاطر"""
+ st.subheader("خطة الاستجابة للمخاطر")
+
+ # عرض استراتيجيات الاستجابة للمخاطر العالية
+ high_risks = risks_df[risks_df['risk_score'] >= 9]
+
+ for _, risk in high_risks.iterrows():
+ with st.expander(f"{risk['category']} - {risk['description']}"):
+ st.write("**استراتيجية الاستجابة:**", risk['response_strategy'])
+ st.write("**خطة العمل:**", risk['action_plan'])
+ st.write("**المسؤول:**", risk['responsible'])
+ st.write("**الموعد النهائي:**", risk['deadline'])
+
+ def _analyze_market_risks(self, project_data):
+ """تحليل مخاطر السوق"""
+ return [
+ {
+ 'category': 'مخاطر السوق',
+ 'description': 'تقلبات أسعار المواد الخام',
+ 'probability': 'مرجح',
+ 'impact': 'عالي',
+ 'response_strategy': 'تحوط',
+ 'action_plan': 'التعاقد المسبق مع الموردين وتثبيت الأسعار',
+ 'responsible': 'مدير المشتريات',
+ 'deadline': '2024-03-01'
+ },
+ # إضافة المزيد من المخاطر
+ ]
+
+ def _analyze_execution_risks(self, project_data):
+ """تحليل مخاطر التنفيذ"""
+ return [
+ {
+ 'category': 'مخاطر التنفيذ',
+ 'description': 'تأخر في جدول التنفيذ',
+ 'probability': 'محتمل',
+ 'impact': 'عالي',
+ 'response_strategy': 'تخفيف',
+ 'action_plan': 'إعداد خطة تسريع وتحديد المسار الحرج',
+ 'responsible': 'مدير المشروع',
+ 'deadline': '2024-02-15'
+ },
+ # إضافة المزيد من المخاطر
+ ]
+
+ def _analyze_financial_risks(self, project_data):
+ """تحليل المخاطر المالية"""
+ return [
+ {
+ 'category': 'مخاطر التمويل',
+ 'description': 'تأخر الدفعات',
+ 'probability': 'محتمل',
+ 'impact': 'عالي',
+ 'response_strategy': 'نقل',
+ 'action_plan': 'التأمين على مخاطر عدم السداد',
+ 'responsible': 'المدير المالي',
+ 'deadline': '2024-02-01'
+ },
+ # إضافة المزيد من المخاطر
+ ]
class RiskAnalysisApp:
@@ -732,13 +375,6 @@ class RiskAnalysisApp:
st.markdown(f"**نوع المشروع**: {project['project_type']}")
st.markdown(f"**مستوى التعقيد**: {project['complexity']}")
- # اختيار طريقة التحليل
- analysis_method = st.radio(
- "طريقة التحليل",
- ["أساسي", "شامل"],
- format_func=lambda x: "تحليل أساسي" if x == "أساسي" else "تحليل شامل"
- )
-
# زر بدء التحليل
if st.button("بدء تحليل المخاطر"):
with st.spinner("جاري تحليل مخاطر المشروع..."):
@@ -746,69 +382,15 @@ class RiskAnalysisApp:
import time
time.sleep(2)
- # إجراء تحليل المخاطر
- self.risk_analyzer.analyze_risks(project_id, method="comprehensive" if analysis_method == "شامل" else "basic")
-
- # الحصول على نتائج التحليل
- results = self.risk_analyzer.get_analysis_results()
+ # إجراء تحليل المخاطر (Using the new RiskAnalyzer)
+ results = self.risk_analyzer.render_risk_analysis(project)
# تخزين النتائج في حالة الجلسة
- st.session_state.risk_analysis_results[str(project_id)] = results
+ st.session_state.risk_analysis_results[str(project_id)] = {"analysis_results": results}
st.success("تم الانتهاء من تحليل المخاطر بنجاح!")
st.experimental_rerun()
- # عرض نتائج التحليل إذا كانت متوفرة
- if str(project_id) in st.session_state.risk_analysis_results:
- results = st.session_state.risk_analysis_results[str(project_id)]
-
- st.markdown("#### ملخص نتائج التحليل")
-
- if "summary" in results:
- summary = results["summary"]
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- self.ui.create_metric_card("إجمالي المخاطر", str(summary["total_risks"]), None, self.ui.COLORS['primary'])
-
- with col2:
- self.ui.create_metric_card("مخاطر عالية", str(summary["high_risks"]), None, self.ui.COLORS['danger'])
-
- with col3:
- self.ui.create_metric_card("مخاطر متوسطة", str(summary["medium_risks"]), None, self.ui.COLORS['warning'])
-
- # عرض توزيع المخاطر حسب الفئة
- st.markdown("#### توزيع المخاطر حسب الفئة")
-
- if "category_distribution" in summary:
- category_df = pd.DataFrame({
- 'الفئة': list(summary["category_distribution"].keys()),
- 'عدد المخاطر': list(summary["category_distribution"].values())
- })
-
- st.bar_chart(category_df.set_index('الفئة'))
-
- # عرض التوصيات
- st.markdown("#### التوصيات")
-
- if "recommendations" in summary:
- for i, recommendation in enumerate(summary["recommendations"]):
- st.markdown(f"{i+1}. {recommendation}")
-
- # زر تصدير النتائج
- if st.button("تصدير نتائج التحليل"):
- with st.spinner("جاري تصدير النتائج..."):
- # محاكاة وقت المعالجة
- time.sleep(1)
-
- # تصدير النتائج
- export_path = self.risk_analyzer.export_analysis_results()
-
- if export_path:
- st.success(f"تم تصدير نتائج التحليل بنجاح!")
- else:
- st.error("حدث خطأ أثناء تصدير النتائج.")
def _render_risk_register_tab(self):
"""عرض تبويب سجل المخاطر"""
@@ -826,12 +408,12 @@ class RiskAnalysisApp:
if selected_project_option == "جميع المشاريع":
# جمع المخاطر من جميع المشاريع
for project_id, results in st.session_state.risk_analysis_results.items():
- if "identified_risks" in results:
+ if "analysis_results" in results:
project = next((p for p in st.session_state.projects if p["id"] == int(project_id)), None)
project_name = project["name"] if project else f"مشروع {project_id}"
- for risk in results["identified_risks"]:
- risk_copy = risk.copy()
+ for index, row in results["analysis_results"].iterrows():
+ risk_copy = row.to_dict()
risk_copy["project_name"] = project_name
all_risks.append(risk_copy)
else:
@@ -843,9 +425,9 @@ class RiskAnalysisApp:
# جمع المخاطر من المشروع المحدد
if str(project_id) in st.session_state.risk_analysis_results:
results = st.session_state.risk_analysis_results[str(project_id)]
- if "identified_risks" in results:
- for risk in results["identified_risks"]:
- risk_copy = risk.copy()
+ if "analysis_results" in results:
+ for index, row in results["analysis_results"].iterrows():
+ risk_copy = row.to_dict()
risk_copy["project_name"] = project["name"]
all_risks.append(risk_copy)
@@ -862,14 +444,14 @@ class RiskAnalysisApp:
with col2:
probability_filter = st.multiselect(
"الاحتمالية",
- ["عالي", "متوسط", "منخفض"],
+ list(set(risk["probability"] for risk in all_risks)) if all_risks else [],
key="risk_register_probability"
)
with col3:
impact_filter = st.multiselect(
"التأثير",
- ["عالي", "متوسط", "منخفض"],
+ list(set(risk["impact"] for risk in all_risks)) if all_risks else [],
key="risk_register_impact"
)
@@ -888,22 +470,10 @@ class RiskAnalysisApp:
# عرض سجل المخاطر
if filtered_risks:
# تحويل المخاطر إلى DataFrame
- risk_data = []
- for risk in filtered_risks:
- risk_data.append({
- 'المشروع': risk.get("project_name", ""),
- 'اسم المخاطرة': risk["name"],
- 'الوصف': risk.get("description", ""),
- 'الفئة': risk["category"],
- 'الاحتمالية': risk["probability"],
- 'التأثير': risk["impact"],
- 'درجة المخاطرة': risk["risk_score"]
- })
-
- risk_df = pd.DataFrame(risk_data)
+ risk_df = pd.DataFrame(filtered_risks)
# ترتيب المخاطر حسب درجة المخاطرة (تنازليًا)
- risk_df = risk_df.sort_values(by='درجة المخاطرة', ascending=False)
+ risk_df = risk_df.sort_values(by='risk_score', ascending=False)
# عرض الجدول
st.dataframe(risk_df, use_container_width=True, hide_index=True)
@@ -936,92 +506,14 @@ class RiskAnalysisApp:
if str(project_id) in st.session_state.risk_analysis_results:
results = st.session_state.risk_analysis_results[str(project_id)]
- if "risk_matrix" in results:
- matrix = results["risk_matrix"]
-
- # إنشاء مصفوفة المخاطر
- st.markdown("#### مصفوفة احتمالية وتأثير المخاطر")
-
- # إنشاء بيانات المصفوفة
- matrix_data = [
- [len(matrix["high_impact"]["high_prob"]), len(matrix["high_impact"]["medium_prob"]), len(matrix["high_impact"]["low_prob"])],
- [len(matrix["medium_impact"]["high_prob"]), len(matrix["medium_impact"]["medium_prob"]), len(matrix["medium_impact"]["low_prob"])],
- [len(matrix["low_impact"]["high_prob"]), len(matrix["low_impact"]["medium_prob"]), len(matrix["low_impact"]["low_prob"])]
- ]
-
- # تحويل البيانات إلى DataFrame
- matrix_df = pd.DataFrame(
- matrix_data,
- columns=["احتمالية عالية", "احتمالية متوسطة", "احتمالية منخفضة"],
- index=["تأثير عالي", "تأثير متوسط", "تأثير منخفض"]
- )
-
- # عرض المصفوفة كجدول
- st.dataframe(matrix_df)
-
- # عرض تفاصيل المخاطر في كل خلية
- st.markdown("#### تفاصيل المخاطر في المصفوفة")
-
- # إنشاء تبويبات للخلايا المختلفة
- matrix_tabs = st.tabs([
- "تأثير عالي / احتمالية عالية",
- "تأثير عالي / احتمالية متوسطة",
- "تأثير متوسط / احتمالية عالية",
- "تأثير متوسط / احتمالية متوسطة",
- "أخرى"
- ])
-
- # عرض المخاطر في كل تبويب
- with matrix_tabs[0]:
- self._display_cell_risks(matrix["high_impact"]["high_prob"], "تأثير عالي / احتمالية عالية")
-
- with matrix_tabs[1]:
- self._display_cell_risks(matrix["high_impact"]["medium_prob"], "تأثير عالي / احتمالية متوسطة")
-
- with matrix_tabs[2]:
- self._display_cell_risks(matrix["medium_impact"]["high_prob"], "تأثير متوسط / احتمالية عالية")
-
- with matrix_tabs[3]:
- self._display_cell_risks(matrix["medium_impact"]["medium_prob"], "تأثير متوسط / احتمالية متوسطة")
-
- with matrix_tabs[4]:
- # جمع المخاطر الأخرى
- other_risks = []
- other_risks.extend(matrix["high_impact"]["low_prob"])
- other_risks.extend(matrix["medium_impact"]["low_prob"])
- other_risks.extend(matrix["low_impact"]["high_prob"])
- other_risks.extend(matrix["low_impact"]["medium_prob"])
- other_risks.extend(matrix["low_impact"]["low_prob"])
-
- self._display_cell_risks(other_risks, "مخاطر أخرى")
+ if "analysis_results" in results:
+ risks_df = results["analysis_results"]
+ self.risk_analyzer._render_risk_matrix(risks_df)
else:
st.warning("لم يتم العثور على مصفوفة المخاطر للمشروع المحدد.")
else:
st.warning("لم يتم إجراء تحليل للمخاطر لهذا المشروع بعد.")
- def _display_cell_risks(self, risks, cell_title):
- """عرض المخاطر في خلية من مصفوفة المخاطر"""
-
- if risks:
- st.markdown(f"##### {cell_title} ({len(risks)} مخاطر)")
-
- # تحويل المخاطر إلى DataFrame
- risk_data = []
- for risk in risks:
- risk_data.append({
- 'اسم المخاطرة': risk["name"],
- 'الوصف': risk.get("description", ""),
- 'الفئة': risk["category"],
- 'درجة المخاطرة': risk["risk_score"]
- })
-
- risk_df = pd.DataFrame(risk_data)
-
- # عرض الجدول
- st.dataframe(risk_df, use_container_width=True, hide_index=True)
- else:
- st.info(f"لا توجد مخاطر في خلية {cell_title}.")
-
def _render_mitigation_strategies_tab(self):
"""عرض تبويب استراتيجيات التخفيف"""
@@ -1041,54 +533,9 @@ class RiskAnalysisApp:
if str(project_id) in st.session_state.risk_analysis_results:
results = st.session_state.risk_analysis_results[str(project_id)]
- if "mitigation_strategies" in results and results["mitigation_strategies"]:
- strategies = results["mitigation_strategies"]
-
- # فلترة الاستراتيجيات
- priority_filter = st.multiselect(
- "الأولوية",
- ["عالية", "متوسطة", "منخفضة"],
- default=["عالية"],
- key="mitigation_priority"
- )
-
- # تطبيق الفلترة
- filtered_strategies = strategies
- if priority_filter:
- filtered_strategies = [s for s in filtered_strategies if s["priority"] in priority_filter]
-
- # عرض استراتيجيات التخفيف
- if filtered_strategies:
- # تحويل الاستراتيجيات إلى DataFrame
- strategy_data = []
- for strategy in filtered_strategies:
- strategy_data.append({
- 'المخاطرة': strategy["risk_name"],
- 'استراتيجية التخفيف': strategy["strategy"],
- 'الأولوية': strategy["priority"],
- 'المسؤول': strategy["responsible"],
- 'الإطار الزمني': strategy["timeline"]
- })
-
- strategy_df = pd.DataFrame(strategy_data)
-
- # ترتيب الاستراتيجيات حسب الأولوية
- priority_order = {"عالية": 0, "متوسطة": 1, "منخفضة": 2}
- strategy_df["priority_order"] = strategy_df["الأولوية"].map(priority_order)
- strategy_df = strategy_df.sort_values(by="priority_order")
- strategy_df = strategy_df.drop(columns=["priority_order"])
-
- # عرض الجدول
- st.dataframe(strategy_df, use_container_width=True, hide_index=True)
-
- # زر تصدير استراتيجيات التخفيف
- if st.button("تصدير استراتيجيات التخفيف"):
- with st.spinner("جاري تصدير استراتيجيات التخفيف..."):
- # محاكاة وقت المعالجة
- time.sleep(1)
- st.success("تم تصدير استراتيجيات التخفيف بنجاح!")
- else:
- st.info("لا توجد استراتيجيات تخفيف تطابق معايير الفلترة.")
+ if "analysis_results" in results and not results["analysis_results"].empty:
+ risks_df = results["analysis_results"]
+ self.risk_analyzer._render_risk_response_plan(risks_df)
else:
st.warning("لم يتم العثور على استراتيجيات تخفيف للمشروع المحدد.")
else:
@@ -1151,4 +598,4 @@ class RiskAnalysisApp:
# تشغيل التطبيق
if __name__ == "__main__":
risk_app = RiskAnalysisApp()
- risk_app.run()
+ risk_app.run()
\ No newline at end of file
diff --git a/modules/scheduling/schedule_app.py b/modules/scheduling/schedule_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a40cc0706d7131596ee93e23e1b1e0fdbf98594
--- /dev/null
+++ b/modules/scheduling/schedule_app.py
@@ -0,0 +1,249 @@
+
+import streamlit as st
+import pandas as pd
+import plotly.figure_factory as ff
+from datetime import datetime, timedelta
+import plotly.express as px
+import numpy as np
+import io
+import openpyxl
+
+class ScheduleApp:
+ def __init__(self):
+ if 'saved_pricing' not in st.session_state:
+ st.session_state.saved_pricing = []
+ if 'uploaded_files' not in st.session_state:
+ st.session_state.uploaded_files = {}
+
+ def run(self):
+ st.title("الجدول الزمني للمشروع")
+
+ # إضافة تبويب للملفات
+ tabs = st.tabs(["جدول الكميات", "ملفات المشروع"])
+
+ with tabs[0]:
+ self._handle_boq_tab()
+
+ with tabs[1]:
+ self._handle_project_files()
+
+ def _handle_project_files(self):
+ st.subheader("إدارة ملفات المشروع")
+
+ # رفع الملفات
+ uploaded_file = st.file_uploader(
+ "قم برفع ملفات المشروع",
+ type=['xls', 'xlsx', 'xml', 'xer', 'pmxml', 'mpp', 'vsdx'],
+ help="يمكنك رفع ملفات من برامج مثل Primavera P6, Microsoft Project, Power BI, Visio"
+ )
+
+ if uploaded_file:
+ try:
+ # قراءة وتحليل الملف
+ if uploaded_file.name.endswith(('.xls', '.xlsx')):
+ df = pd.read_excel(uploaded_file)
+ st.session_state.uploaded_files[uploaded_file.name] = {
+ 'data': df,
+ 'type': 'excel',
+ 'upload_time': datetime.now()
+ }
+
+ # عرض معلومات الملف
+ st.success(f"تم استيراد الملف: {uploaded_file.name}")
+ st.write("معلومات الملف:")
+ st.write(f"- عدد الأنشطة: {len(df)}")
+ st.write(f"- الأعمدة: {', '.join(df.columns)}")
+
+ # عرض البيانات في جدول
+ st.dataframe(df, use_container_width=True)
+
+ # إنشاء مخطط جانت تفاعلي
+ if 'Start' in df.columns and 'Finish' in df.columns:
+ self._create_interactive_gantt(df)
+
+ else:
+ st.info(f"تم استلام الملف {uploaded_file.name}. سيتم إضافة دعم لهذا النوع من الملفات قريباً.")
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء معالجة الملف: {str(e)}")
+
+ # عرض الملفات المحفوظة
+ if st.session_state.uploaded_files:
+ st.subheader("الملفات المحفوظة")
+ for filename, file_info in st.session_state.uploaded_files.items():
+ with st.expander(filename):
+ st.write(f"نوع الملف: {file_info['type']}")
+ st.write(f"تاريخ الرفع: {file_info['upload_time']}")
+ if file_info['type'] == 'excel':
+ st.dataframe(file_info['data'], use_container_width=True)
+
+ def _create_interactive_gantt(self, df):
+ st.subheader("مخطط جانت التفاعلي")
+
+ # تحضير البيانات للمخطط
+ df['Start'] = pd.to_datetime(df['Start'])
+ df['Finish'] = pd.to_datetime(df['Finish'])
+
+ fig = ff.create_gantt(
+ df,
+ colors={
+ 'Task': '#2196F3',
+ 'Complete': '#4CAF50'
+ },
+ index_col='Resource',
+ show_colorbar=True,
+ group_tasks=True,
+ showgrid_x=True,
+ showgrid_y=True
+ )
+
+ fig.update_layout(
+ title="مخطط جانت للمشروع",
+ xaxis_title="التاريخ",
+ yaxis_title="الأنشطة",
+ height=600
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _handle_boq_tab(self):
+ # نفس الكود السابق لمعالجة جدول الكميات
+ source_type = st.radio("اختر مصدر جدول الكميات:",
+ ["جدول كميات محفوظ", "رفع جدول كميات جديد"],
+ key="boq_source")
+
+ if source_type == "جدول كميات محفوظ":
+ self._handle_saved_boq()
+ else:
+ self._handle_new_boq()
+
+ def _handle_saved_boq(self):
+ if not st.session_state.saved_pricing:
+ st.warning("لا توجد جداول كميات محفوظة.")
+ return
+
+ projects = [(p['project_name'], i) for i, p in enumerate(st.session_state.saved_pricing)]
+ selected_project_name = st.selectbox("اختر المشروع", [p[0] for p in projects])
+ project_index = next(p[1] for p in projects if p[0] == selected_project_name)
+ project = st.session_state.saved_pricing[project_index]
+
+ self._display_project_schedule(project)
+
+ def _handle_new_boq(self):
+ uploaded_file = st.file_uploader("قم برفع ملف Excel لجدول الكميات", type=['xlsx', 'xls'])
+ if uploaded_file:
+ try:
+ df = pd.read_excel(uploaded_file)
+ project = self._create_project_from_boq(df)
+ self._display_project_schedule(project)
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء قراءة الملف: {str(e)}")
+
+ def _create_project_from_boq(self, df):
+ return {
+ 'project_name': 'مشروع جديد',
+ 'items': [row.to_dict() for _, row in df.iterrows()],
+ 'total_price': df.get('الإجمالي', df.get('total_price', 0)).sum(),
+ 'project_duration': 180
+ }
+
+ def _display_project_schedule(self, project):
+ if not project:
+ return
+
+ st.subheader("تفاصيل المشروع")
+ col1, col2 = st.columns(2)
+ with col1:
+ st.write(f"اسم المشروع: {project['project_name']}")
+ st.write(f"إجمالي القيمة: {project['total_price']:,.2f} ريال")
+ with col2:
+ project['project_duration'] = st.number_input(
+ "مدة المشروع (بالأيام)",
+ min_value=30,
+ max_value=1800,
+ value=project.get('project_duration', 180)
+ )
+
+ self._generate_and_display_schedule(project)
+
+ def _generate_and_display_schedule(self, project):
+ if 'schedule_items' not in project:
+ self._initialize_schedule_items(project)
+
+ st.subheader("تحرير الجدول الزمني")
+ edited_df = self._edit_schedule(project['schedule_items'])
+ project['schedule_items'] = edited_df.to_dict('records')
+
+ self._display_gantt_chart(project['schedule_items'])
+ self._display_progress_report(project['schedule_items'])
+
+ def _initialize_schedule_items(self, project):
+ project['schedule_items'] = []
+ for item in project['items']:
+ relative_duration = int((item['total_price'] / project['total_price']) * project['project_duration'])
+ schedule_item = {
+ 'Task': item.get('description', ''),
+ 'Start': datetime.now().strftime('%Y-%m-%d'),
+ 'Finish': (datetime.now() + timedelta(days=relative_duration)).strftime('%Y-%m-%d'),
+ 'Duration': relative_duration,
+ 'Dependencies': '',
+ 'Progress': 0,
+ 'Resource': ''
+ }
+ project['schedule_items'].append(schedule_item)
+
+ def _edit_schedule(self, schedule_items):
+ return st.data_editor(
+ pd.DataFrame(schedule_items),
+ column_config={
+ "Task": "البند",
+ "Start": st.column_config.DateColumn("تاريخ البداية"),
+ "Finish": st.column_config.DateColumn("تاريخ النهاية"),
+ "Duration": "المدة (أيام)",
+ "Dependencies": "الاعتماديات",
+ "Progress": st.column_config.NumberColumn("نسبة الإنجاز %", min_value=0, max_value=100),
+ "Resource": "الموارد"
+ },
+ use_container_width=True,
+ hide_index=True
+ )
+
+ def _display_gantt_chart(self, schedule_items):
+ st.subheader("مخطط جانت")
+ df = pd.DataFrame(schedule_items)
+ fig = ff.create_gantt(
+ df,
+ colors={
+ 'Complete': 'rgb(0, 255, 100)',
+ 'Incomplete': 'rgb(160, 160, 160)'
+ },
+ index_col='Resource',
+ show_colorbar=True,
+ group_tasks=True,
+ showgrid_x=True,
+ showgrid_y=True
+ )
+
+ fig.update_layout(
+ title="مخطط جانت للمشروع",
+ xaxis_title="التاريخ",
+ yaxis_title="البنود",
+ height=600
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _display_progress_report(self, schedule_items):
+ st.subheader("تقرير تقدم المشروع")
+ df = pd.DataFrame(schedule_items)
+ avg_progress = df['Progress'].mean()
+
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ st.metric("متوسط نسبة الإنجاز", f"{avg_progress:.1f}%")
+ with col2:
+ completed_tasks = len(df[df['Progress'] == 100])
+ st.metric("البنود المكتملة", f"{completed_tasks} من {len(df)}")
+ with col3:
+ not_started = len(df[df['Progress'] == 0])
+ st.metric("البنود غير المبدوءة", not_started)
diff --git a/modules/translation/translation_app.py b/modules/translation/translation_app.py
index 7eac1aa02f855b7a6a59041834ce72a461ad6314..b7130e3dd09f117f61b4fe00dfea42214e997802 100644
--- a/modules/translation/translation_app.py
+++ b/modules/translation/translation_app.py
@@ -1,936 +1,936 @@
-"""
-وحدة الترجمة - نظام تحليل المناقصات
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import os
-import sys
-from pathlib import Path
-import re
-import datetime
-
-# إضافة مسار المشروع للنظام
-sys.path.append(str(Path(__file__).parent.parent))
-
-# استيراد محسن واجهة المستخدم
-from styling.enhanced_ui import UIEnhancer
-
-class TranslationApp:
- """تطبيق الترجمة"""
-
- def __init__(self):
- """تهيئة تطبيق الترجمة"""
- self.ui = UIEnhancer(page_title="الترجمة - نظام تحليل المناقصات", page_icon="🌐")
- self.ui.apply_theme_colors()
-
- # قائمة اللغات المدعومة
- self.supported_languages = {
- "ar": "العربية",
- "en": "الإنجليزية",
- "fr": "الفرنسية",
- "de": "الألمانية",
- "es": "الإسبانية",
- "it": "الإيطالية",
- "zh": "الصينية",
- "ja": "اليابانية",
- "ru": "الروسية",
- "tr": "التركية"
- }
-
- # بيانات نموذجية للمصطلحات الفنية
- self.technical_terms = [
- {"ar": "كراسة الشروط", "en": "Terms and Conditions Document", "category": "مستندات"},
- {"ar": "جدول الكميات", "en": "Bill of Quantities (BOQ)", "category": "مستندات"},
- {"ar": "المواصفات الفنية", "en": "Technical Specifications", "category": "مستندات"},
- {"ar": "ضمان ابتدائي", "en": "Bid Bond", "category": "ضمانات"},
- {"ar": "ضمان حسن التنفيذ", "en": "Performance Bond", "category": "ضمانات"},
- {"ar": "ضمان دفعة مقدمة", "en": "Advance Payment Guarantee", "category": "ضمانات"},
- {"ar": "ضمان صيانة", "en": "Maintenance Bond", "category": "ضمانات"},
- {"ar": "مناقصة عامة", "en": "Public Tender", "category": "أنواع المناقصات"},
- {"ar": "مناقصة محدودة", "en": "Limited Tender", "category": "أنواع المناقصات"},
- {"ar": "منافسة", "en": "Competition", "category": "أنواع المناقصات"},
- {"ar": "أمر شراء", "en": "Purchase Order", "category": "عقود"},
- {"ar": "عقد إطاري", "en": "Framework Agreement", "category": "عقود"},
- {"ar": "عقد زمني", "en": "Time-based Contract", "category": "عقود"},
- {"ar": "عقد تسليم مفتاح", "en": "Turnkey Contract", "category": "عقود"},
- {"ar": "مقاول من الباطن", "en": "Subcontractor", "category": "أطراف"},
- {"ar": "استشاري", "en": "Consultant", "category": "أطراف"},
- {"ar": "مالك المشروع", "en": "Project Owner", "category": "أطراف"},
- {"ar": "مدير المشروع", "en": "Project Manager", "category": "أطراف"},
- {"ar": "مهندس الموقع", "en": "Site Engineer", "category": "أطراف"},
- {"ar": "مراقب الجودة", "en": "Quality Control", "category": "أطراف"},
- {"ar": "أعمال مدنية", "en": "Civil Works", "category": "أعمال"},
- {"ar": "أعمال كهربائية", "en": "Electrical Works", "category": "أعمال"},
- {"ar": "أعمال ميكانيكية", "en": "Mechanical Works", "category": "أعمال"},
- {"ar": "أعمال معمارية", "en": "Architectural Works", "category": "أعمال"},
- {"ar": "أعمال تشطيبات", "en": "Finishing Works", "category": "أعمال"},
- {"ar": "غرامة تأخير", "en": "Delay Penalty", "category": "شروط"},
- {"ar": "مدة التنفيذ", "en": "Execution Period", "category": "شروط"},
- {"ar": "فترة الضمان", "en": "Warranty Period", "category": "شروط"},
- {"ar": "شروط الدفع", "en": "Payment Terms", "category": "شروط"},
- {"ar": "تسوية النزاعات", "en": "Dispute Resolution", "category": "شروط"}
- ]
-
- # بيانات نموذجية للمستندات المترجمة
- self.translated_documents = [
- {
- "id": "TD001",
- "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "specs_v2.0_ar.pdf",
- "translated_file": "specs_v2.0_en.pdf",
- "translation_date": "2025-03-15",
- "translated_by": "أحمد محمد",
- "status": "مكتمل",
- "pages": 52,
- "related_entity": "T-2025-001"
- },
- {
- "id": "TD002",
- "name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "boq_v1.1_ar.xlsx",
- "translated_file": "boq_v1.1_en.xlsx",
- "translation_date": "2025-02-25",
- "translated_by": "سارة عبدالله",
- "status": "مكتمل",
- "pages": 22,
- "related_entity": "T-2025-001"
- },
- {
- "id": "TD003",
- "name": "المخططات - مناقصة إنشاء مبنى إداري",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "drawings_v2.0_ar.pdf",
- "translated_file": "drawings_v2.0_en.pdf",
- "translation_date": "2025-03-20",
- "translated_by": "محمد علي",
- "status": "مكتمل",
- "pages": 35,
- "related_entity": "T-2025-001"
- },
- {
- "id": "TD004",
- "name": "كراسة الشروط - مناقصة صيانة طرق",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "specs_v1.1_ar.pdf",
- "translated_file": "specs_v1.1_en.pdf",
- "translation_date": "2025-03-25",
- "translated_by": "فاطمة أحمد",
- "status": "مكتمل",
- "pages": 34,
- "related_entity": "T-2025-002"
- },
- {
- "id": "TD005",
- "name": "جدول الكميات - مناقصة صيانة طرق",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "boq_v1.0_ar.xlsx",
- "translated_file": "boq_v1.0_en.xlsx",
- "translation_date": "2025-03-10",
- "translated_by": "خالد عمر",
- "status": "مكتمل",
- "pages": 15,
- "related_entity": "T-2025-002"
- },
- {
- "id": "TD006",
- "name": "كراسة الشروط - مناقصة توريد معدات",
- "source_language": "en",
- "target_language": "ar",
- "original_file": "specs_v1.0_en.pdf",
- "translated_file": "specs_v1.0_ar.pdf",
- "translation_date": "2025-02-15",
- "translated_by": "أحمد محمد",
- "status": "مكتمل",
- "pages": 28,
- "related_entity": "T-2025-003"
- },
- {
- "id": "TD007",
- "name": "عقد توريد - مناقصة توريد معدات",
- "source_language": "en",
- "target_language": "ar",
- "original_file": "contract_v1.0_en.pdf",
- "translated_file": "contract_v1.0_ar.pdf",
- "translation_date": "2025-03-05",
- "translated_by": "سارة عبدالله",
- "status": "مكتمل",
- "pages": 20,
- "related_entity": "T-2025-003"
- },
- {
- "id": "TD008",
- "name": "كراسة الشروط - مناقصة تجهيز مختبرات",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "specs_v1.0_ar.pdf",
- "translated_file": "specs_v1.0_en.pdf",
- "translation_date": "2025-03-28",
- "translated_by": "محمد علي",
- "status": "قيد التنفيذ",
- "pages": 30,
- "related_entity": "T-2025-004"
- }
- ]
-
- # بيانات نموذجية للنصوص المترجمة
- self.sample_translations = {
- "text1": {
- "ar": """
- # كراسة الشروط والمواصفات
- ## مناقصة إنشاء مبنى إداري
-
- ### 1. مقدمة
- تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
-
- ### 2. نطاق العمل
- يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
- - أعمال الهيكل الإنشائي
- - أعمال التشطيبات الداخلية والخارجية
- - أعمال الكهرباء والميكانيكا
- - أعمال تنسيق الموقع
- - أعمال أنظمة الأمن والسلامة
- - أعمال أنظمة المباني الذكية
- """,
-
- "en": """
- # Terms and Conditions Document
- ## Administrative Building Construction Tender
-
- ### 1. Introduction
- Peninsula Contracting Company invites specialized companies to submit their offers for the implementation of an administrative building construction project in Riyadh.
-
- ### 2. Scope of Work
- The scope of work includes the design and implementation of a 6-floor administrative building with a total area of 6000 square meters, including:
- - Structural works
- - Interior and exterior finishing works
- - Electrical and mechanical works
- - Site coordination works
- - Security and safety systems works
- - Smart building systems works
- """
- },
-
- "text2": {
- "ar": """
- ### 3. المواصفات الفنية
- #### 3.1 أعمال الخرسانة
- - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
- - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
- - يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
-
- #### 3.2 أعمال التشطيبات
- - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
- - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
- - يجب استخدام زجاج عاكس للحرارة للواجهات
- - يجب استخدام مواد صديقة للبيئة
- """,
-
- "en": """
- ### 3. Technical Specifications
- #### 3.1 Concrete Works
- - Reinforced concrete must have a strength of not less than 40 Newton/mm²
- - Reinforcement steel must comply with Saudi specifications
- - Concrete additives must be used to increase its resistance to weather conditions
-
- #### 3.2 Finishing Works
- - High-quality materials must be used for interior finishes
- - Exterior facades must be weather-resistant
- - Heat-reflective glass must be used for facades
- - Environmentally friendly materials must be used
- """
- }
- }
-
- def run(self):
- """تشغيل تطبيق الترجمة"""
- # إنشاء قائمة العناصر
- menu_items = [
- {"name": "لوحة المعلومات", "icon": "house"},
- {"name": "المناقصات والعقود", "icon": "file-text"},
- {"name": "تحليل المستندات", "icon": "file-earmark-text"},
- {"name": "نظام التسعير", "icon": "calculator"},
- {"name": "حاسبة تكاليف البناء", "icon": "building"},
- {"name": "الموارد والتكاليف", "icon": "people"},
- {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
- {"name": "إدارة المشاريع", "icon": "kanban"},
- {"name": "الخرائط والمواقع", "icon": "geo-alt"},
- {"name": "الجدول الزمني", "icon": "calendar3"},
- {"name": "الإشعارات", "icon": "bell"},
- {"name": "مقارنة المستندات", "icon": "files"},
- {"name": "الترجمة", "icon": "translate"},
- {"name": "المساعد الذكي", "icon": "robot"},
- {"name": "التقارير", "icon": "bar-chart"},
- {"name": "الإعدادات", "icon": "gear"}
- ]
-
- # إنشاء الشريط الجانبي
- selected = self.ui.create_sidebar(menu_items)
-
- # إنشاء ترويسة الصفحة
- self.ui.create_header("الترجمة", "أدوات ترجمة المستندات والنصوص")
-
- # إنشاء علامات تبويب للوظائف المختلفة
- tabs = st.tabs(["ترجمة النصوص", "ترجمة المستندات", "قاموس المصطلحات", "المستندات المترجمة"])
-
- # علامة تبويب ترجمة النصوص
- with tabs[0]:
- self.translate_text()
-
- # علامة تبويب ترجمة المستندات
- with tabs[1]:
- self.translate_documents()
-
- # علامة تبويب قاموس المصطلحات
- with tabs[2]:
- self.technical_terms_dictionary()
-
- # علامة تبويب المستندات المترجمة
- with tabs[3]:
- self.show_translated_documents()
-
- def translate_text(self):
- """ترجمة النصوص"""
- st.markdown("### ترجمة النصوص")
-
- # اختيار لغات الترجمة
- col1, col2 = st.columns(2)
-
- with col1:
- source_language = st.selectbox(
- "لغة المصدر",
- options=list(self.supported_languages.keys()),
- format_func=lambda x: self.supported_languages[x],
- index=0 # العربية كلغة افتراضية
- )
-
- with col2:
- # استبعاد لغة المصدر من خيارات لغة الهدف
- target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
- target_language = st.selectbox(
- "لغة الهدف",
- options=list(target_languages.keys()),
- format_func=lambda x: self.supported_languages[x],
- index=0 # أول لغة متاحة
- )
-
- # خيارات الترجمة
- st.markdown("#### خيارات الترجمة")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- translation_engine = st.radio(
- "محرك الترجمة",
- options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"]
- )
-
- with col2:
- use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True)
-
- with col3:
- preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True)
-
- # إدخال النص المراد ترجمته
- st.markdown("#### النص المراد ترجمته")
-
- # إضافة أمثلة نصية
- examples = st.expander("أمثلة نصية")
- with examples:
- if st.button("مثال 1: مقدمة كراسة الشروط"):
- source_text = self.sample_translations["text1"][source_language] if source_language in self.sample_translations["text1"] else self.sample_translations["text1"]["ar"]
- elif st.button("مثال 2: المواصفات الفنية"):
- source_text = self.sample_translations["text2"][source_language] if source_language in self.sample_translations["text2"] else self.sample_translations["text2"]["ar"]
- else:
- source_text = ""
-
- if "source_text" not in locals():
- source_text = ""
-
- source_text = st.text_area(
- "أدخل النص المراد ترجمته",
- value=source_text,
- height=200
- )
-
- # زر الترجمة
- if st.button("ترجمة النص", use_container_width=True):
- if not source_text:
- st.error("يرجى إدخال النص المراد ترجمته")
- else:
- # في تطبيق حقيقي، سيتم استدعاء واجهة برمجة التطبيقات للترجمة
- # هنا نستخدم النصوص النموذجية المحددة مسبقاً للعرض
-
- with st.spinner("جاري الترجمة..."):
- # محاكاة تأخير الترجمة
- import time
- time.sleep(1)
-
- # التحقق من وجود ترجمة نموذجية
- if source_language == "ar" and target_language == "en" and source_text.strip() in [self.sample_translations["text1"]["ar"].strip(), self.sample_translations["text2"]["ar"].strip()]:
- if source_text.strip() == self.sample_translations["text1"]["ar"].strip():
- translated_text = self.sample_translations["text1"]["en"]
- else:
- translated_text = self.sample_translations["text2"]["en"]
- elif source_language == "en" and target_language == "ar" and source_text.strip() in [self.sample_translations["text1"]["en"].strip(), self.sample_translations["text2"]["en"].strip()]:
- if source_text.strip() == self.sample_translations["text1"]["en"].strip():
- translated_text = self.sample_translations["text1"]["ar"]
- else:
- translated_text = self.sample_translations["text2"]["ar"]
- else:
- # ترجمة نموذجية للعرض فقط
- translated_text = f"[هذا نص مترجم نموذجي من {self.supported_languages[source_language]} إلى {self.supported_languages[target_language]}]\n\n{source_text}"
-
- # عرض النص المترجم
- st.markdown("#### النص المترجم")
- st.text_area(
- "النص المترجم",
- value=translated_text,
- height=200
- )
-
- # أزرار إضافية
- col1, col2, col3 = st.columns(3)
-
- with col1:
- if st.button("نسخ النص المترجم", use_container_width=True):
- st.success("تم نسخ النص المترجم إلى الحافظة")
-
- with col2:
- if st.button("حفظ الترجمة", use_container_width=True):
- st.success("تم حفظ الترجمة بنجاح")
-
- with col3:
- if st.button("تصدير كملف", use_container_width=True):
- st.success("تم تصدير الترجمة كملف بنجاح")
-
- # عرض إحصائيات الترجمة
- st.markdown("#### إحصائيات الترجمة")
-
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- self.ui.create_metric_card(
- "عدد الكلمات",
- str(len(source_text.split())),
- None,
- self.ui.COLORS['primary']
- )
-
- with col2:
- self.ui.create_metric_card(
- "عدد الأحرف",
- str(len(source_text)),
- None,
- self.ui.COLORS['secondary']
- )
-
- with col3:
- self.ui.create_metric_card(
- "وقت الترجمة",
- "1.2 ثانية",
- None,
- self.ui.COLORS['success']
- )
-
- with col4:
- self.ui.create_metric_card(
- "المصطلحات الفنية",
- "5",
- None,
- self.ui.COLORS['accent']
- )
-
- def translate_documents(self):
- """ترجمة المستندات"""
- st.markdown("### ترجمة المستندات")
-
- # اختيار لغات الترجمة
- col1, col2 = st.columns(2)
-
- with col1:
- source_language = st.selectbox(
- "لغة المصدر",
- options=list(self.supported_languages.keys()),
- format_func=lambda x: self.supported_languages[x],
- index=0, # العربية كلغة افتراضية
- key="doc_source_lang"
- )
-
- with col2:
- # استبعاد لغة المصدر من خيارات لغة الهدف
- target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
- target_language = st.selectbox(
- "لغة الهدف",
- options=list(target_languages.keys()),
- format_func=lambda x: self.supported_languages[x],
- index=0, # أول لغة متاحة
- key="doc_target_lang"
- )
-
- # تحميل المستند
- st.markdown("#### تحميل المستند")
-
- uploaded_file = st.file_uploader("اختر المستند المراد ترجمته", type=["pdf", "docx", "xlsx", "txt"])
-
- if uploaded_file is not None:
- st.success(f"تم تحميل الملف: {uploaded_file.name}")
-
- # عرض معلومات الملف
- file_details = {
- "اسم الملف": uploaded_file.name,
- "نوع الملف": uploaded_file.type,
- "حجم الملف": f"{uploaded_file.size / 1024:.1f} كيلوبايت"
- }
-
- st.json(file_details)
-
- # خيارات الترجمة
- st.markdown("#### خيارات الترجمة")
-
- col1, col2 = st.columns(2)
-
- with col1:
- translation_engine = st.radio(
- "محرك الترجمة",
- options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"],
- key="doc_engine"
- )
-
- use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True, key="doc_terms")
-
- with col2:
- preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True, key="doc_format")
-
- translate_images = st.checkbox("ترجمة النصوص في الصور", value=False)
-
- maintain_layout = st.checkbox("الحفاظ على تخطيط المستند", value=True)
-
- # معلومات إضافية
- st.markdown("#### معلومات إضافية")
-
- col1, col2 = st.columns(2)
-
- with col1:
- document_name = st.text_input("اسم المستند")
-
- with col2:
- related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة أو المشروع)")
-
- # زر بدء الترجمة
- if st.button("بدء ترجمة المستند", use_container_width=True):
- if uploaded_file is None:
- st.error("يرجى تحميل المستند المراد ترجمته")
- else:
- # في تطبيق حقيقي، سيتم إرسال المستند إلى خدمة الترجمة
- # هنا نعرض محاكاة لعملية الترجمة
-
- progress_bar = st.progress(0)
- status_text = st.empty()
-
- # محاكاة تقدم الترجمة
- import time
- for i in range(101):
- progress_bar.progress(i)
-
- if i < 10:
- status_text.text("جاري تحليل المستند...")
- elif i < 30:
- status_text.text("جاري استخراج النصوص...")
- elif i < 70:
- status_text.text("جاري ترجمة المحتوى...")
- elif i < 90:
- status_text.text("جاري إعادة بناء المستند...")
- else:
- status_text.text("جاري إنهاء الترجمة...")
-
- time.sleep(0.05)
-
- # عرض نتيجة الترجمة
- st.success("تمت ترجمة المستند بنجاح!")
-
- # إنشاء اسم الملف المترجم
- file_name_parts = uploaded_file.name.split('.')
- translated_file_name = f"{'.'.join(file_name_parts[:-1])}_{target_language}.{file_name_parts[-1]}"
-
- # عرض معلومات الملف المترجم
- st.markdown("#### معلومات الملف المترجم")
-
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown(f"**اسم الملف:** {translated_file_name}")
- st.markdown(f"**لغة المصدر:** {self.supported_languages[source_language]}")
- st.markdown(f"**لغة الهدف:** {self.supported_languages[target_language]}")
-
- with col2:
- st.markdown(f"**محرك الترجمة:** {translation_engine}")
- st.markdown(f"**تاريخ الترجمة:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
- st.markdown(f"**حالة الترجمة:** مكتمل")
-
- # أزرار إضافية
- col1, col2, col3 = st.columns(3)
-
- with col1:
- if st.button("تنزيل الملف المترجم", use_container_width=True):
- st.success("تم بدء تنزيل الملف المترجم")
-
- with col2:
- if st.button("حفظ في المستندات المترجمة", use_container_width=True):
- st.success("تم حفظ الملف في المستندات المترجمة")
-
- with col3:
- if st.button("مشاركة الملف", use_container_width=True):
- st.success("تم نسخ رابط مشاركة الملف")
-
- # عرض إحصائيات الترجمة
- st.markdown("#### إحصائيات الترجمة")
-
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- self.ui.create_metric_card(
- "عدد الصفحات",
- "12",
- None,
- self.ui.COLORS['primary']
- )
-
- with col2:
- self.ui.create_metric_card(
- "عدد الكلمات",
- "2,450",
- None,
- self.ui.COLORS['secondary']
- )
-
- with col3:
- self.ui.create_metric_card(
- "وقت الترجمة",
- "45 ثانية",
- None,
- self.ui.COLORS['success']
- )
-
- with col4:
- self.ui.create_metric_card(
- "المصطلحات الفنية",
- "28",
- None,
- self.ui.COLORS['accent']
- )
-
- def technical_terms_dictionary(self):
- """قاموس المصطلحات الفنية"""
- st.markdown("### قاموس المصطلحات الفنية")
-
- # إضافة مصطلح جديد
- with st.expander("إضافة مصطلح جديد"):
- with st.form("add_term_form"):
- col1, col2, col3 = st.columns(3)
-
- with col1:
- term_ar = st.text_input("المصطلح بالعربية")
-
- with col2:
- term_en = st.text_input("المصطلح بالإنجليزية")
-
- with col3:
- term_category = st.selectbox(
- "الفئة",
- options=["مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
- )
-
- # زر إضافة المصطلح
- submit_button = st.form_submit_button("إضافة المصطلح")
-
- if submit_button:
- if not term_ar or not term_en:
- st.error("يرجى ملء جميع الحقول المطلوبة")
- else:
- # في تطبيق حقيقي، سيتم إضافة المصطلح إلى قاعدة البيانات
- st.success("تمت إضافة المصطلح بنجاح")
-
- # البحث في المصطلحات
- st.markdown("#### البحث في المصطلحات")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- search_term = st.text_input("البحث عن مصطلح")
-
- with col2:
- search_language = st.radio(
- "لغة البحث",
- options=["الكل", "العربية", "الإنجليزية"],
- horizontal=True
- )
-
- with col3:
- category_filter = st.selectbox(
- "تصفية حسب الفئة",
- options=["الكل", "مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
- )
-
- # تطبيق الفلاتر
- filtered_terms = self.technical_terms
-
- if search_term:
- if search_language == "العربية":
- filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower()]
- elif search_language == "الإنجليزية":
- filtered_terms = [term for term in filtered_terms if search_term.lower() in term["en"].lower()]
- else:
- filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower() or search_term.lower() in term["en"].lower()]
-
- if category_filter != "الكل":
- filtered_terms = [term for term in filtered_terms if term["category"] == category_filter]
-
- # عرض المصطلحات
- st.markdown("#### المصطلحات الفنية")
-
- if not filtered_terms:
- st.info("لا توجد مصطلحات تطابق معايير البحث")
- else:
- # تحويل البيانات إلى DataFrame
- terms_df = pd.DataFrame(filtered_terms)
-
- # إعادة تسمية الأعمدة
- terms_df = terms_df.rename(columns={
- "ar": "المصطلح بالعربية",
- "en": "المصطلح بالإنجليزية",
- "category": "الفئة"
- })
-
- # عرض الجدول
- st.dataframe(
- terms_df,
- use_container_width=True,
- hide_index=True
- )
-
- # أزرار إضافية
- col1, col2 = st.columns([1, 5])
-
- with col1:
- if st.button("تصدير القاموس", use_container_width=True):
- st.success("تم تصدير القاموس بنجاح")
-
- # عرض إحصائيات القاموس
- st.markdown("#### إحصائيات القاموس")
-
- # حساب عدد المصطلحات في كل فئة
- category_counts = {}
- for term in self.technical_terms:
- if term["category"] not in category_counts:
- category_counts[term["category"]] = 0
- category_counts[term["category"]] += 1
-
- # عرض الإحصائيات
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown("##### عدد المصطلحات حسب الفئة")
-
- # تحويل البيانات إلى DataFrame
- category_df = pd.DataFrame({
- "الفئة": list(category_counts.keys()),
- "العدد": list(category_counts.values())
- })
-
- # عرض الرسم البياني
- st.bar_chart(category_df.set_index("الفئة"))
-
- with col2:
- st.markdown("##### إحصائيات عامة")
-
- total_terms = len(self.technical_terms)
- categories_count = len(category_counts)
-
- st.markdown(f"**إجمالي المصطلحات:** {total_terms}")
- st.markdown(f"**عدد الفئات:** {categories_count}")
- st.markdown(f"**متوسط المصطلحات لكل فئة:** {total_terms / categories_count:.1f}")
- st.markdown(f"**آخر تحديث للقاموس:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
-
- def show_translated_documents(self):
- """عرض المستندات المترجمة"""
- st.markdown("### المستندات المترجمة")
-
- # إنشاء فلاتر للمستندات
- col1, col2, col3 = st.columns(3)
-
- with col1:
- entity_filter = st.selectbox(
- "تصفية حسب الكيان",
- options=["الكل"] + list(set([doc["related_entity"] for doc in self.translated_documents]))
- )
-
- with col2:
- language_pair_filter = st.selectbox(
- "تصفية حسب زوج اللغات",
- options=["الكل"] + list(set([f"{doc['source_language']} -> {doc['target_language']}" for doc in self.translated_documents]))
- )
-
- with col3:
- status_filter = st.selectbox(
- "تصفية حسب الحالة",
- options=["الكل", "مكتمل", "قيد التنفيذ"]
- )
-
- # تطبيق الفلاتر
- filtered_docs = self.translated_documents
-
- if entity_filter != "الكل":
- filtered_docs = [doc for doc in filtered_docs if doc["related_entity"] == entity_filter]
-
- if language_pair_filter != "الكل":
- source_lang, target_lang = language_pair_filter.split(" -> ")
- filtered_docs = [doc for doc in filtered_docs if doc["source_language"] == source_lang and doc["target_language"] == target_lang]
-
- if status_filter != "الكل":
- filtered_docs = [doc for doc in filtered_docs if doc["status"] == status_filter]
-
- # عرض المستندات المترجمة
- if not filtered_docs:
- st.info("لا توجد مستندات مترجمة تطابق معايير التصفية")
- else:
- # تحويل البيانات إلى DataFrame
- docs_df = pd.DataFrame(filtered_docs)
-
- # تحويل رموز اللغات إلى أسماء اللغات
- docs_df["source_language"] = docs_df["source_language"].map(self.supported_languages)
- docs_df["target_language"] = docs_df["target_language"].map(self.supported_languages)
-
- # إعادة ترتيب الأعمدة وتغيير أسمائها
- display_df = docs_df[[
- "id", "name", "source_language", "target_language", "translation_date", "status", "pages", "related_entity"
- ]].rename(columns={
- "id": "الرقم",
- "name": "اسم المستند",
- "source_language": "لغة المصدر",
- "target_language": "لغة الهدف",
- "translation_date": "تاريخ الترجمة",
- "status": "الحالة",
- "pages": "عدد الصفحات",
- "related_entity": "الكيان المرتبط"
- })
-
- # عرض الجدول
- st.dataframe(
- display_df,
- use_container_width=True,
- hide_index=True
- )
-
- # عرض تفاصيل المستند المحدد
- st.markdown("#### تفاصيل المستند المترجم")
-
- selected_doc_id = st.selectbox(
- "اختر مستنداً لعرض التفاصيل",
- options=[doc["id"] for doc in filtered_docs],
- format_func=lambda x: next((f"{doc['id']} - {doc['name']}" for doc in filtered_docs if doc["id"] == x), "")
- )
-
- # العثور على المستند المحدد
- selected_doc = next((doc for doc in filtered_docs if doc["id"] == selected_doc_id), None)
-
- if selected_doc:
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown(f"**اسم المستند:** {selected_doc['name']}")
- st.markdown(f"**لغة المصدر:** {self.supported_languages[selected_doc['source_language']]}")
- st.markdown(f"**لغة الهدف:** {self.supported_languages[selected_doc['target_language']]}")
- st.markdown(f"**تاريخ الترجمة:** {selected_doc['translation_date']}")
-
- with col2:
- st.markdown(f"**الملف الأصلي:** {selected_doc['original_file']}")
- st.markdown(f"**الملف المترجم:** {selected_doc['translated_file']}")
- st.markdown(f"**المترجم:** {selected_doc['translated_by']}")
- st.markdown(f"**الحالة:** {selected_doc['status']}")
-
- # أزرار الإجراءات
- col1, col2, col3 = st.columns(3)
-
- with col1:
- if st.button("تنزيل الملف الأصلي", use_container_width=True):
- st.success("تم بدء تنزيل الملف الأصلي")
-
- with col2:
- if st.button("تنزيل الملف المترجم", use_container_width=True):
- st.success("تم بدء تنزيل الملف المترجم")
-
- with col3:
- if st.button("مشاركة الملف المترجم", use_container_width=True):
- st.success("تم نسخ رابط مشاركة الملف المترجم")
-
- # عرض إحصائيات الترجمة
- st.markdown("#### إحصائيات الترجمة")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- # إحصائيات حسب زوج اللغات
- language_pairs = {}
- for doc in self.translated_documents:
- pair = f"{self.supported_languages[doc['source_language']]} -> {self.supported_languages[doc['target_language']]}"
- if pair not in language_pairs:
- language_pairs[pair] = 0
- language_pairs[pair] += 1
-
- st.markdown("##### المستندات حسب زوج اللغات")
-
- # تحويل البيانات إلى DataFrame
- language_df = pd.DataFrame({
- "زوج اللغات": list(language_pairs.keys()),
- "العدد": list(language_pairs.values())
- })
-
- # عرض الرسم البياني
- st.bar_chart(language_df.set_index("زوج اللغات"))
-
- with col2:
- # إحصائيات حسب الكيان المرتبط
- entity_counts = {}
- for doc in self.translated_documents:
- if doc["related_entity"] not in entity_counts:
- entity_counts[doc["related_entity"]] = 0
- entity_counts[doc["related_entity"]] += 1
-
- st.markdown("##### المستندات حسب الكيان المرتبط")
-
- # تحويل البيانات إلى DataFrame
- entity_df = pd.DataFrame({
- "الكيان المرتبط": list(entity_counts.keys()),
- "العدد": list(entity_counts.values())
- })
-
- # عرض الرسم البياني
- st.bar_chart(entity_df.set_index("الكيان المرتبط"))
-
- with col3:
- # إحصائيات عامة
- total_docs = len(self.translated_documents)
- completed_docs = len([doc for doc in self.translated_documents if doc["status"] == "مكتمل"])
- in_progress_docs = len([doc for doc in self.translated_documents if doc["status"] == "قيد التنفيذ"])
- total_pages = sum([doc["pages"] for doc in self.translated_documents])
-
- st.markdown("##### إحصائيات عامة")
- st.markdown(f"**إجمالي المستندات المترجمة:** {total_docs}")
- st.markdown(f"**المستندات المكتملة:** {completed_docs}")
- st.markdown(f"**المستندات قيد التنفيذ:** {in_progress_docs}")
- st.markdown(f"**إجمالي الصفحات المترجمة:** {total_pages}")
- st.markdown(f"**متوسط الصفحات لكل مستند:** {total_pages / total_docs:.1f}")
-
-# تشغيل التطبيق
-if __name__ == "__main__":
- translation_app = TranslationApp()
- translation_app.run()
+"""
+وحدة الترجمة - نظام تحليل المناقصات
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import os
+import sys
+from pathlib import Path
+import re
+import datetime
+
+# إضافة مسار المشروع للنظام
+sys.path.append(str(Path(__file__).parent.parent))
+
+# استيراد محسن واجهة المستخدم
+from styling.enhanced_ui import UIEnhancer
+
+class TranslationApp:
+ """تطبيق الترجمة"""
+
+ def __init__(self):
+ """تهيئة تطبيق الترجمة"""
+ self.ui = UIEnhancer(page_title="الترجمة - نظام تحليل المناقصات", page_icon="🌐")
+ self.ui.apply_theme_colors()
+
+ # قائمة اللغات المدعومة
+ self.supported_languages = {
+ "ar": "العربية",
+ "en": "الإنجليزية",
+ "fr": "الفرنسية",
+ "de": "الألمانية",
+ "es": "الإسبانية",
+ "it": "الإيطالية",
+ "zh": "الصينية",
+ "ja": "اليابانية",
+ "ru": "الروسية",
+ "tr": "التركية"
+ }
+
+ # بيانات نموذجية للمصطلحات الفنية
+ self.technical_terms = [
+ {"ar": "كراسة الشروط", "en": "Terms and Conditions Document", "category": "مستندات"},
+ {"ar": "جدول الكميات", "en": "Bill of Quantities (BOQ)", "category": "مستندات"},
+ {"ar": "المواصفات الفنية", "en": "Technical Specifications", "category": "مستندات"},
+ {"ar": "ضمان ابتدائي", "en": "Bid Bond", "category": "ضمانات"},
+ {"ar": "ضمان حسن التنفيذ", "en": "Performance Bond", "category": "ضمانات"},
+ {"ar": "ضمان دفعة مقدمة", "en": "Advance Payment Guarantee", "category": "ضمانات"},
+ {"ar": "ضمان صيانة", "en": "Maintenance Bond", "category": "ضمانات"},
+ {"ar": "مناقصة عامة", "en": "Public Tender", "category": "أنواع المناقصات"},
+ {"ar": "مناقصة محدودة", "en": "Limited Tender", "category": "أنواع المناقصات"},
+ {"ar": "منافسة", "en": "Competition", "category": "أنواع المناقصات"},
+ {"ar": "أمر شراء", "en": "Purchase Order", "category": "عقود"},
+ {"ar": "عقد إطاري", "en": "Framework Agreement", "category": "عقود"},
+ {"ar": "عقد زمني", "en": "Time-based Contract", "category": "عقود"},
+ {"ar": "عقد تسليم مفتاح", "en": "Turnkey Contract", "category": "عقود"},
+ {"ar": "مقاول من الباطن", "en": "Subcontractor", "category": "أطراف"},
+ {"ar": "استشاري", "en": "Consultant", "category": "أطراف"},
+ {"ar": "مالك المشروع", "en": "Project Owner", "category": "أطراف"},
+ {"ar": "مدير المشروع", "en": "Project Manager", "category": "أطراف"},
+ {"ar": "مهندس الموقع", "en": "Site Engineer", "category": "أطراف"},
+ {"ar": "مراقب الجودة", "en": "Quality Control", "category": "أطراف"},
+ {"ar": "أعمال مدنية", "en": "Civil Works", "category": "أعمال"},
+ {"ar": "أعمال كهربائية", "en": "Electrical Works", "category": "أعمال"},
+ {"ar": "أعمال ميكانيكية", "en": "Mechanical Works", "category": "أعمال"},
+ {"ar": "أعمال معمارية", "en": "Architectural Works", "category": "أعمال"},
+ {"ar": "أعمال تشطيبات", "en": "Finishing Works", "category": "أعمال"},
+ {"ar": "غرامة تأخير", "en": "Delay Penalty", "category": "شروط"},
+ {"ar": "مدة التنفيذ", "en": "Execution Period", "category": "شروط"},
+ {"ar": "فترة الضمان", "en": "Warranty Period", "category": "شروط"},
+ {"ar": "شروط الدفع", "en": "Payment Terms", "category": "شروط"},
+ {"ar": "تسوية النزاعات", "en": "Dispute Resolution", "category": "شروط"}
+ ]
+
+ # بيانات نموذجية للمستندات المترجمة
+ self.translated_documents = [
+ {
+ "id": "TD001",
+ "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
+ "source_language": "ar",
+ "target_language": "en",
+ "original_file": "specs_v2.0_ar.pdf",
+ "translated_file": "specs_v2.0_en.pdf",
+ "translation_date": "2025-03-15",
+ "translated_by": "أحمد محمد",
+ "status": "مكتمل",
+ "pages": 52,
+ "related_entity": "T-2025-001"
+ },
+ {
+ "id": "TD002",
+ "name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
+ "source_language": "ar",
+ "target_language": "en",
+ "original_file": "boq_v1.1_ar.xlsx",
+ "translated_file": "boq_v1.1_en.xlsx",
+ "translation_date": "2025-02-25",
+ "translated_by": "سارة عبدالله",
+ "status": "مكتمل",
+ "pages": 22,
+ "related_entity": "T-2025-001"
+ },
+ {
+ "id": "TD003",
+ "name": "المخططات - مناقصة إنشاء مبنى إداري",
+ "source_language": "ar",
+ "target_language": "en",
+ "original_file": "drawings_v2.0_ar.pdf",
+ "translated_file": "drawings_v2.0_en.pdf",
+ "translation_date": "2025-03-20",
+ "translated_by": "محمد علي",
+ "status": "مكتمل",
+ "pages": 35,
+ "related_entity": "T-2025-001"
+ },
+ {
+ "id": "TD004",
+ "name": "كراسة الشروط - مناقصة صيانة طرق",
+ "source_language": "ar",
+ "target_language": "en",
+ "original_file": "specs_v1.1_ar.pdf",
+ "translated_file": "specs_v1.1_en.pdf",
+ "translation_date": "2025-03-25",
+ "translated_by": "فاطمة أحمد",
+ "status": "مكتمل",
+ "pages": 34,
+ "related_entity": "T-2025-002"
+ },
+ {
+ "id": "TD005",
+ "name": "جدول الكميات - مناقصة صيانة طرق",
+ "source_language": "ar",
+ "target_language": "en",
+ "original_file": "boq_v1.0_ar.xlsx",
+ "translated_file": "boq_v1.0_en.xlsx",
+ "translation_date": "2025-03-10",
+ "translated_by": "خالد عمر",
+ "status": "مكتمل",
+ "pages": 15,
+ "related_entity": "T-2025-002"
+ },
+ {
+ "id": "TD006",
+ "name": "كراسة الشروط - مناقصة توريد معدات",
+ "source_language": "en",
+ "target_language": "ar",
+ "original_file": "specs_v1.0_en.pdf",
+ "translated_file": "specs_v1.0_ar.pdf",
+ "translation_date": "2025-02-15",
+ "translated_by": "أحمد محمد",
+ "status": "مكتمل",
+ "pages": 28,
+ "related_entity": "T-2025-003"
+ },
+ {
+ "id": "TD007",
+ "name": "عقد توريد - مناقصة توريد معدات",
+ "source_language": "en",
+ "target_language": "ar",
+ "original_file": "contract_v1.0_en.pdf",
+ "translated_file": "contract_v1.0_ar.pdf",
+ "translation_date": "2025-03-05",
+ "translated_by": "سارة عبدالله",
+ "status": "مكتمل",
+ "pages": 20,
+ "related_entity": "T-2025-003"
+ },
+ {
+ "id": "TD008",
+ "name": "كراسة الشروط - مناقصة تجهيز مختبرات",
+ "source_language": "ar",
+ "target_language": "en",
+ "original_file": "specs_v1.0_ar.pdf",
+ "translated_file": "specs_v1.0_en.pdf",
+ "translation_date": "2025-03-28",
+ "translated_by": "محمد علي",
+ "status": "قيد التنفيذ",
+ "pages": 30,
+ "related_entity": "T-2025-004"
+ }
+ ]
+
+ # بيانات نموذجية للنصوص المترجمة
+ self.sample_translations = {
+ "text1": {
+ "ar": """
+ # كراسة الشروط والمواصفات
+ ## مناقصة إنشاء مبنى إداري
+
+ ### 1. مقدمة
+ تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
+
+ ### 2. نطاق العمل
+ يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
+ - أعمال الهيكل الإنشائي
+ - أعمال التشطيبات الداخلية والخارجية
+ - أعمال الكهرباء والميكانيكا
+ - أعمال تنسيق الموقع
+ - أعمال أنظمة الأمن والسلامة
+ - أعمال أنظمة المباني الذكية
+ """,
+
+ "en": """
+ # Terms and Conditions Document
+ ## Administrative Building Construction Tender
+
+ ### 1. Introduction
+ Peninsula Contracting Company invites specialized companies to submit their offers for the implementation of an administrative building construction project in Riyadh.
+
+ ### 2. Scope of Work
+ The scope of work includes the design and implementation of a 6-floor administrative building with a total area of 6000 square meters, including:
+ - Structural works
+ - Interior and exterior finishing works
+ - Electrical and mechanical works
+ - Site coordination works
+ - Security and safety systems works
+ - Smart building systems works
+ """
+ },
+
+ "text2": {
+ "ar": """
+ ### 3. المواصفات الفنية
+ #### 3.1 أعمال الخرسانة
+ - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
+ - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
+ - يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
+
+ #### 3.2 أعمال التشطيبات
+ - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
+ - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
+ - يجب استخدام زجاج عاكس للحرارة للواجهات
+ - يجب استخدام مواد صديقة للبيئة
+ """,
+
+ "en": """
+ ### 3. Technical Specifications
+ #### 3.1 Concrete Works
+ - Reinforced concrete must have a strength of not less than 40 Newton/mm²
+ - Reinforcement steel must comply with Saudi specifications
+ - Concrete additives must be used to increase its resistance to weather conditions
+
+ #### 3.2 Finishing Works
+ - High-quality materials must be used for interior finishes
+ - Exterior facades must be weather-resistant
+ - Heat-reflective glass must be used for facades
+ - Environmentally friendly materials must be used
+ """
+ }
+ }
+
+ def run(self):
+ """تشغيل تطبيق الترجمة"""
+ # إنشاء قائمة العناصر
+ menu_items = [
+ {"name": "لوحة المعلومات", "icon": "house"},
+ {"name": "المناقصات والعقود", "icon": "file-text"},
+ {"name": "تحليل المستندات", "icon": "file-earmark-text"},
+ {"name": "نظام التسعير", "icon": "calculator"},
+ {"name": "حاسبة تكاليف البناء", "icon": "building"},
+ {"name": "الموارد والتكاليف", "icon": "people"},
+ {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
+ {"name": "إدارة المشاريع", "icon": "kanban"},
+ {"name": "الخرائط والمواقع", "icon": "geo-alt"},
+ {"name": "الجدول الزمني", "icon": "calendar3"},
+ {"name": "الإشعارات", "icon": "bell"},
+ {"name": "مقارنة المستندات", "icon": "files"},
+ {"name": "الترجمة", "icon": "translate"},
+ {"name": "المساعد الذكي", "icon": "robot"},
+ {"name": "التقارير", "icon": "bar-chart"},
+ {"name": "الإعدادات", "icon": "gear"}
+ ]
+
+ # إنشاء الشريط الجانبي
+ selected = self.ui.create_sidebar(menu_items)
+
+ # إنشاء ترويسة الصفحة
+ self.ui.create_header("الترجمة", "أدوات ترجمة المستندات والنصوص")
+
+ # إنشاء علامات تبويب للوظائف المختلفة
+ tabs = st.tabs(["ترجمة النصوص", "ترجمة المستندات", "قاموس المصطلحات", "المستندات المترجمة"])
+
+ # علامة تبويب ترجمة النصوص
+ with tabs[0]:
+ self.translate_text()
+
+ # علامة تبويب ترجمة المستندات
+ with tabs[1]:
+ self.translate_documents()
+
+ # علامة تبويب قاموس المصطلحات
+ with tabs[2]:
+ self.technical_terms_dictionary()
+
+ # علامة تبويب المستندات المترجمة
+ with tabs[3]:
+ self.show_translated_documents()
+
+ def translate_text(self):
+ """ترجمة النصوص"""
+ st.markdown("### ترجمة النصوص")
+
+ # اختيار لغات الترجمة
+ col1, col2 = st.columns(2)
+
+ with col1:
+ source_language = st.selectbox(
+ "لغة المصدر",
+ options=list(self.supported_languages.keys()),
+ format_func=lambda x: self.supported_languages[x],
+ index=0 # العربية كلغة افتراضية
+ )
+
+ with col2:
+ # استبعاد لغة المصدر من خيارات لغة الهدف
+ target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
+ target_language = st.selectbox(
+ "لغة الهدف",
+ options=list(target_languages.keys()),
+ format_func=lambda x: self.supported_languages[x],
+ index=0 # أول لغة متاحة
+ )
+
+ # خيارات الترجمة
+ st.markdown("#### خيارات الترجمة")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ translation_engine = st.radio(
+ "محرك الترجمة",
+ options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"]
+ )
+
+ with col2:
+ use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True)
+
+ with col3:
+ preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True)
+
+ # إدخال النص المراد ترجمته
+ st.markdown("#### النص المراد ترجمته")
+
+ # إضافة أمثلة نصية
+ examples = st.expander("أمثلة نصية")
+ with examples:
+ if st.button("مثال 1: مقدمة كراسة الشروط"):
+ source_text = self.sample_translations["text1"][source_language] if source_language in self.sample_translations["text1"] else self.sample_translations["text1"]["ar"]
+ elif st.button("مثال 2: المواصفات الفنية"):
+ source_text = self.sample_translations["text2"][source_language] if source_language in self.sample_translations["text2"] else self.sample_translations["text2"]["ar"]
+ else:
+ source_text = ""
+
+ if "source_text" not in locals():
+ source_text = ""
+
+ source_text = st.text_area(
+ "أدخل النص المراد ترجمته",
+ value=source_text,
+ height=200
+ )
+
+ # زر الترجمة
+ if st.button("ترجمة النص", use_container_width=True):
+ if not source_text:
+ st.error("يرجى إدخال النص المراد ترجمته")
+ else:
+ # في تطبيق حقيقي، سيتم استدعاء واجهة برمجة التطبيقات للترجمة
+ # هنا نستخدم النصوص النموذجية المحددة مسبقاً للعرض
+
+ with st.spinner("جاري الترجمة..."):
+ # محاكاة تأخير الترجمة
+ import time
+ time.sleep(1)
+
+ # التحقق من وجود ترجمة نموذجية
+ if source_language == "ar" and target_language == "en" and source_text.strip() in [self.sample_translations["text1"]["ar"].strip(), self.sample_translations["text2"]["ar"].strip()]:
+ if source_text.strip() == self.sample_translations["text1"]["ar"].strip():
+ translated_text = self.sample_translations["text1"]["en"]
+ else:
+ translated_text = self.sample_translations["text2"]["en"]
+ elif source_language == "en" and target_language == "ar" and source_text.strip() in [self.sample_translations["text1"]["en"].strip(), self.sample_translations["text2"]["en"].strip()]:
+ if source_text.strip() == self.sample_translations["text1"]["en"].strip():
+ translated_text = self.sample_translations["text1"]["ar"]
+ else:
+ translated_text = self.sample_translations["text2"]["ar"]
+ else:
+ # ترجمة نموذجية للعرض فقط
+ translated_text = f"[هذا نص مترجم نموذجي من {self.supported_languages[source_language]} إلى {self.supported_languages[target_language]}]\n\n{source_text}"
+
+ # عرض النص المترجم
+ st.markdown("#### النص المترجم")
+ st.text_area(
+ "النص المترجم",
+ value=translated_text,
+ height=200
+ )
+
+ # أزرار إضافية
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ if st.button("نسخ النص المترجم", use_container_width=True):
+ st.success("تم نسخ النص المترجم إلى الحافظة")
+
+ with col2:
+ if st.button("حفظ الترجمة", use_container_width=True):
+ st.success("تم حفظ الترجمة بنجاح")
+
+ with col3:
+ if st.button("تصدير كملف", use_container_width=True):
+ st.success("تم تصدير الترجمة كملف بنجاح")
+
+ # عرض إحصائيات الترجمة
+ st.markdown("#### إحصائيات الترجمة")
+
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ self.ui.create_metric_card(
+ "عدد الكلمات",
+ str(len(source_text.split())),
+ None,
+ self.ui.COLORS['primary']
+ )
+
+ with col2:
+ self.ui.create_metric_card(
+ "عدد الأحرف",
+ str(len(source_text)),
+ None,
+ self.ui.COLORS['secondary']
+ )
+
+ with col3:
+ self.ui.create_metric_card(
+ "وقت الترجمة",
+ "1.2 ثانية",
+ None,
+ self.ui.COLORS['success']
+ )
+
+ with col4:
+ self.ui.create_metric_card(
+ "المصطلحات الفنية",
+ "5",
+ None,
+ self.ui.COLORS['accent']
+ )
+
+ def translate_documents(self):
+ """ترجمة المستندات"""
+ st.markdown("### ترجمة المستندات")
+
+ # اختيار لغات الترجمة
+ col1, col2 = st.columns(2)
+
+ with col1:
+ source_language = st.selectbox(
+ "لغة المصدر",
+ options=list(self.supported_languages.keys()),
+ format_func=lambda x: self.supported_languages[x],
+ index=0, # العربية كلغة افتراضية
+ key="doc_source_lang"
+ )
+
+ with col2:
+ # استبعاد لغة المصدر من خيارات لغة الهدف
+ target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
+ target_language = st.selectbox(
+ "لغة الهدف",
+ options=list(target_languages.keys()),
+ format_func=lambda x: self.supported_languages[x],
+ index=0, # أول لغة متاحة
+ key="doc_target_lang"
+ )
+
+ # تحميل المستند
+ st.markdown("#### تحميل المستند")
+
+ uploaded_file = st.file_uploader("اختر المستند المراد ترجمته", type=["pdf", "docx", "xlsx", "txt"])
+
+ if uploaded_file is not None:
+ st.success(f"تم تحميل الملف: {uploaded_file.name}")
+
+ # عرض معلومات الملف
+ file_details = {
+ "اسم الملف": uploaded_file.name,
+ "نوع الملف": uploaded_file.type,
+ "حجم الملف": f"{uploaded_file.size / 1024:.1f} كيلوبايت"
+ }
+
+ st.json(file_details)
+
+ # خيارات الترجمة
+ st.markdown("#### خيارات الترجمة")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ translation_engine = st.radio(
+ "محرك الترجمة",
+ options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"],
+ key="doc_engine"
+ )
+
+ use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True, key="doc_terms")
+
+ with col2:
+ preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True, key="doc_format")
+
+ translate_images = st.checkbox("ترجمة النصوص في الصور", value=False)
+
+ maintain_layout = st.checkbox("الحفاظ على تخطيط المستند", value=True)
+
+ # معلومات إضافية
+ st.markdown("#### معلومات إضافية")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ document_name = st.text_input("اسم المستند")
+
+ with col2:
+ related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة أو المشروع)")
+
+ # زر بدء الترجمة
+ if st.button("بدء ترجمة المستند", use_container_width=True):
+ if uploaded_file is None:
+ st.error("يرجى تحميل المستند المراد ترجمته")
+ else:
+ # في تطبيق حقيقي، سيتم إرسال المستند إلى خدمة الترجمة
+ # هنا نعرض محاكاة لعملية الترجمة
+
+ progress_bar = st.progress(0)
+ status_text = st.empty()
+
+ # محاكاة تقدم الترجمة
+ import time
+ for i in range(101):
+ progress_bar.progress(i)
+
+ if i < 10:
+ status_text.text("جاري تحليل المستند...")
+ elif i < 30:
+ status_text.text("جاري استخراج النصوص...")
+ elif i < 70:
+ status_text.text("جاري ترجمة المحتوى...")
+ elif i < 90:
+ status_text.text("جاري إعادة بناء المستند...")
+ else:
+ status_text.text("جاري إنهاء الترجمة...")
+
+ time.sleep(0.05)
+
+ # عرض نتيجة الترجمة
+ st.success("تمت ترجمة المستند بنجاح!")
+
+ # إنشاء اسم الملف المترجم
+ file_name_parts = uploaded_file.name.split('.')
+ translated_file_name = f"{'.'.join(file_name_parts[:-1])}_{target_language}.{file_name_parts[-1]}"
+
+ # عرض معلومات الملف المترجم
+ st.markdown("#### معلومات الملف المترجم")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown(f"**اسم الملف:** {translated_file_name}")
+ st.markdown(f"**لغة المصدر:** {self.supported_languages[source_language]}")
+ st.markdown(f"**لغة الهدف:** {self.supported_languages[target_language]}")
+
+ with col2:
+ st.markdown(f"**محرك الترجمة:** {translation_engine}")
+ st.markdown(f"**تاريخ الترجمة:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
+ st.markdown(f"**حالة الترجمة:** مكتمل")
+
+ # أزرار إضافية
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ if st.button("تنزيل الملف المترجم", use_container_width=True):
+ st.success("تم بدء تنزيل الملف المترجم")
+
+ with col2:
+ if st.button("حفظ في المستندات المترجمة", use_container_width=True):
+ st.success("تم حفظ الملف في المستندات المترجمة")
+
+ with col3:
+ if st.button("مشاركة الملف", use_container_width=True):
+ st.success("تم نسخ رابط مشاركة الملف")
+
+ # عرض إحصائيات الترجمة
+ st.markdown("#### إحصائيات الترجمة")
+
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ self.ui.create_metric_card(
+ "عدد الصفحات",
+ "12",
+ None,
+ self.ui.COLORS['primary']
+ )
+
+ with col2:
+ self.ui.create_metric_card(
+ "عدد الكلمات",
+ "2,450",
+ None,
+ self.ui.COLORS['secondary']
+ )
+
+ with col3:
+ self.ui.create_metric_card(
+ "وقت الترجمة",
+ "45 ثانية",
+ None,
+ self.ui.COLORS['success']
+ )
+
+ with col4:
+ self.ui.create_metric_card(
+ "المصطلحات الفنية",
+ "28",
+ None,
+ self.ui.COLORS['accent']
+ )
+
+ def technical_terms_dictionary(self):
+ """قاموس المصطلحات الفنية"""
+ st.markdown("### قاموس المصطلحات الفنية")
+
+ # إضافة مصطلح جديد
+ with st.expander("إضافة مصطلح جديد"):
+ with st.form("add_term_form"):
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ term_ar = st.text_input("المصطلح بالعربية")
+
+ with col2:
+ term_en = st.text_input("المصطلح بالإنجليزية")
+
+ with col3:
+ term_category = st.selectbox(
+ "الفئة",
+ options=["مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
+ )
+
+ # زر إضافة المصطلح
+ submit_button = st.form_submit_button("إضافة المصطلح")
+
+ if submit_button:
+ if not term_ar or not term_en:
+ st.error("يرجى ملء جميع الحقول المطلوبة")
+ else:
+ # في تطبيق حقيقي، سيتم إضافة المصطلح إلى قاعدة البيانات
+ st.success("تمت إضافة المصطلح بنجاح")
+
+ # البحث في المصطلحات
+ st.markdown("#### البحث في المصطلحات")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ search_term = st.text_input("البحث عن مصطلح")
+
+ with col2:
+ search_language = st.radio(
+ "لغة البحث",
+ options=["الكل", "العربية", "الإنجليزية"],
+ horizontal=True
+ )
+
+ with col3:
+ category_filter = st.selectbox(
+ "تصفية حسب الفئة",
+ options=["الكل", "مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
+ )
+
+ # تطبيق الفلاتر
+ filtered_terms = self.technical_terms
+
+ if search_term:
+ if search_language == "العربية":
+ filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower()]
+ elif search_language == "الإنجليزية":
+ filtered_terms = [term for term in filtered_terms if search_term.lower() in term["en"].lower()]
+ else:
+ filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower() or search_term.lower() in term["en"].lower()]
+
+ if category_filter != "الكل":
+ filtered_terms = [term for term in filtered_terms if term["category"] == category_filter]
+
+ # عرض المصطلحات
+ st.markdown("#### المصطلحات الفنية")
+
+ if not filtered_terms:
+ st.info("لا توجد مصطلحات تطابق معايير البحث")
+ else:
+ # تحويل البيانات إلى DataFrame
+ terms_df = pd.DataFrame(filtered_terms)
+
+ # إعادة تسمية الأعمدة
+ terms_df = terms_df.rename(columns={
+ "ar": "المصطلح بالعربية",
+ "en": "المصطلح بالإنجليزية",
+ "category": "الفئة"
+ })
+
+ # عرض الجدول
+ st.dataframe(
+ terms_df,
+ use_container_width=True,
+ hide_index=True
+ )
+
+ # أزرار إضافية
+ col1, col2 = st.columns([1, 5])
+
+ with col1:
+ if st.button("تصدير القاموس", use_container_width=True):
+ st.success("تم تصدير القاموس بنجاح")
+
+ # عرض إحصائيات القاموس
+ st.markdown("#### إحصائيات القاموس")
+
+ # حساب عدد المصطلحات في كل فئة
+ category_counts = {}
+ for term in self.technical_terms:
+ if term["category"] not in category_counts:
+ category_counts[term["category"]] = 0
+ category_counts[term["category"]] += 1
+
+ # عرض الإحصائيات
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown("##### عدد المصطلحات حسب الفئة")
+
+ # تحويل البيانات إلى DataFrame
+ category_df = pd.DataFrame({
+ "الفئة": list(category_counts.keys()),
+ "العدد": list(category_counts.values())
+ })
+
+ # عرض الرسم البياني
+ st.bar_chart(category_df.set_index("الفئة"))
+
+ with col2:
+ st.markdown("##### إحصائيات عامة")
+
+ total_terms = len(self.technical_terms)
+ categories_count = len(category_counts)
+
+ st.markdown(f"**إجمالي المصطلحات:** {total_terms}")
+ st.markdown(f"**عدد الفئات:** {categories_count}")
+ st.markdown(f"**متوسط المصطلحات لكل فئة:** {total_terms / categories_count:.1f}")
+ st.markdown(f"**آخر تحديث للقاموس:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
+
+ def show_translated_documents(self):
+ """عرض المستندات المترجمة"""
+ st.markdown("### المستندات المترجمة")
+
+ # إنشاء فلاتر للمستندات
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ entity_filter = st.selectbox(
+ "تصفية حسب الكيان",
+ options=["الكل"] + list(set([doc["related_entity"] for doc in self.translated_documents]))
+ )
+
+ with col2:
+ language_pair_filter = st.selectbox(
+ "تصفية حسب زوج اللغات",
+ options=["الكل"] + list(set([f"{doc['source_language']} -> {doc['target_language']}" for doc in self.translated_documents]))
+ )
+
+ with col3:
+ status_filter = st.selectbox(
+ "تصفية حسب الحالة",
+ options=["الكل", "مكتمل", "قيد التنفيذ"]
+ )
+
+ # تطبيق الفلاتر
+ filtered_docs = self.translated_documents
+
+ if entity_filter != "الكل":
+ filtered_docs = [doc for doc in filtered_docs if doc["related_entity"] == entity_filter]
+
+ if language_pair_filter != "الكل":
+ source_lang, target_lang = language_pair_filter.split(" -> ")
+ filtered_docs = [doc for doc in filtered_docs if doc["source_language"] == source_lang and doc["target_language"] == target_lang]
+
+ if status_filter != "الكل":
+ filtered_docs = [doc for doc in filtered_docs if doc["status"] == status_filter]
+
+ # عرض المستندات المترجمة
+ if not filtered_docs:
+ st.info("لا توجد مستندات مترجمة تطابق معايير التصفية")
+ else:
+ # تحويل البيانات إلى DataFrame
+ docs_df = pd.DataFrame(filtered_docs)
+
+ # تحويل رموز اللغات إلى أسماء اللغات
+ docs_df["source_language"] = docs_df["source_language"].map(self.supported_languages)
+ docs_df["target_language"] = docs_df["target_language"].map(self.supported_languages)
+
+ # إعادة ترتيب الأعمدة وتغيير أسمائها
+ display_df = docs_df[[
+ "id", "name", "source_language", "target_language", "translation_date", "status", "pages", "related_entity"
+ ]].rename(columns={
+ "id": "الرقم",
+ "name": "اسم المستند",
+ "source_language": "لغة المصدر",
+ "target_language": "لغة الهدف",
+ "translation_date": "تاريخ الترجمة",
+ "status": "الحالة",
+ "pages": "عدد الصفحات",
+ "related_entity": "الكيان المرتبط"
+ })
+
+ # عرض الجدول
+ st.dataframe(
+ display_df,
+ use_container_width=True,
+ hide_index=True
+ )
+
+ # عرض تفاصيل المستند المحدد
+ st.markdown("#### تفاصيل المستند المترجم")
+
+ selected_doc_id = st.selectbox(
+ "اختر مستنداً لعرض التفاصيل",
+ options=[doc["id"] for doc in filtered_docs],
+ format_func=lambda x: next((f"{doc['id']} - {doc['name']}" for doc in filtered_docs if doc["id"] == x), "")
+ )
+
+ # العثور على المستند المحدد
+ selected_doc = next((doc for doc in filtered_docs if doc["id"] == selected_doc_id), None)
+
+ if selected_doc:
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown(f"**اسم المستند:** {selected_doc['name']}")
+ st.markdown(f"**لغة المصدر:** {self.supported_languages[selected_doc['source_language']]}")
+ st.markdown(f"**لغة الهدف:** {self.supported_languages[selected_doc['target_language']]}")
+ st.markdown(f"**تاريخ الترجمة:** {selected_doc['translation_date']}")
+
+ with col2:
+ st.markdown(f"**الملف الأصلي:** {selected_doc['original_file']}")
+ st.markdown(f"**الملف المترجم:** {selected_doc['translated_file']}")
+ st.markdown(f"**المترجم:** {selected_doc['translated_by']}")
+ st.markdown(f"**الحالة:** {selected_doc['status']}")
+
+ # أزرار الإجراءات
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ if st.button("تنزيل الملف الأصلي", use_container_width=True):
+ st.success("تم بدء تنزيل الملف الأصلي")
+
+ with col2:
+ if st.button("تنزيل الملف المترجم", use_container_width=True):
+ st.success("تم بدء تنزيل الملف المترجم")
+
+ with col3:
+ if st.button("مشاركة الملف المترجم", use_container_width=True):
+ st.success("تم نسخ رابط مشاركة الملف المترجم")
+
+ # عرض إحصائيات الترجمة
+ st.markdown("#### إحصائيات الترجمة")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ # إحصائيات حسب زوج اللغات
+ language_pairs = {}
+ for doc in self.translated_documents:
+ pair = f"{self.supported_languages[doc['source_language']]} -> {self.supported_languages[doc['target_language']]}"
+ if pair not in language_pairs:
+ language_pairs[pair] = 0
+ language_pairs[pair] += 1
+
+ st.markdown("##### المستندات حسب زوج اللغات")
+
+ # تحويل البيانات إلى DataFrame
+ language_df = pd.DataFrame({
+ "زوج اللغات": list(language_pairs.keys()),
+ "العدد": list(language_pairs.values())
+ })
+
+ # عرض الرسم البياني
+ st.bar_chart(language_df.set_index("زوج اللغات"))
+
+ with col2:
+ # إحصائيات حسب الكيان المرتبط
+ entity_counts = {}
+ for doc in self.translated_documents:
+ if doc["related_entity"] not in entity_counts:
+ entity_counts[doc["related_entity"]] = 0
+ entity_counts[doc["related_entity"]] += 1
+
+ st.markdown("##### المستندات حسب الكيان المرتبط")
+
+ # تحويل البيانات إلى DataFrame
+ entity_df = pd.DataFrame({
+ "الكيان المرتبط": list(entity_counts.keys()),
+ "العدد": list(entity_counts.values())
+ })
+
+ # عرض الرسم البياني
+ st.bar_chart(entity_df.set_index("الكيان المرتبط"))
+
+ with col3:
+ # إحصائيات عامة
+ total_docs = len(self.translated_documents)
+ completed_docs = len([doc for doc in self.translated_documents if doc["status"] == "مكتمل"])
+ in_progress_docs = len([doc for doc in self.translated_documents if doc["status"] == "قيد التنفيذ"])
+ total_pages = sum([doc["pages"] for doc in self.translated_documents])
+
+ st.markdown("##### إحصائيات عامة")
+ st.markdown(f"**إجمالي المستندات المترجمة:** {total_docs}")
+ st.markdown(f"**المستندات المكتملة:** {completed_docs}")
+ st.markdown(f"**المستندات قيد التنفيذ:** {in_progress_docs}")
+ st.markdown(f"**إجمالي الصفحات المترجمة:** {total_pages}")
+ st.markdown(f"**متوسط الصفحات لكل مستند:** {total_pages / total_docs:.1f}")
+
+# تشغيل التطبيق
+if __name__ == "__main__":
+ translation_app = TranslationApp()
+ translation_app.run()
diff --git a/pricing_system/docs/user_guide.md b/pricing_system/docs/user_guide.md
index 1b92829da96bb618221c26965c47ed5a76f0d4df..f25790336ec4991f075eef62648145bb499ec2e1 100644
--- a/pricing_system/docs/user_guide.md
+++ b/pricing_system/docs/user_guide.md
@@ -1,235 +1,235 @@
-# نظام تحليل المناقصات وتسعير المشاريع - دليل التثبيت والاستخدام
-
-## مقدمة
-
-نظام تحليل المناقصات وتسعير المشاريع هو نظام متكامل يساعد المهندسين في تسعير المناقصات والمشاريع بطريقة احترافية ودقيقة. يوفر النظام مجموعة شاملة من الأدوات والوظائف التي تغطي جميع جوانب عملية التسعير، بدءًا من إدارة جداول الكميات وحتى تحليل المحتوى المحلي وإصدار التقارير.
-
-## متطلبات النظام
-
-- Python 3.8 أو أحدث
-- Streamlit 1.10.0 أو أحدث
-- Pandas 1.3.0 أو أحدث
-- NumPy 1.20.0 أو أحدث
-- Matplotlib 3.4.0 أو أحدث
-- Seaborn 0.11.0 أو أحدث
-- Plotly 5.3.0 أو أحدث
-- OpenPyXL 3.0.0 أو أحدث
-- XlsxWriter 3.0.0 أو أحدث
-
-## التثبيت
-
-1. قم بتثبيت Python من الموقع الرسمي: https://www.python.org/downloads/
-2. قم بتثبيت المكتبات المطلوبة باستخدام الأمر التالي:
-
-```bash
-pip install streamlit pandas numpy matplotlib seaborn plotly openpyxl xlsxwriter
-```
-
-3. قم بتنزيل ملفات النظام وفك ضغطها في المجلد المطلوب.
-4. انتقل إلى مجلد النظام واستخدم الأمر التالي لتشغيل النظام:
-
-```bash
-streamlit run app.py
-```
-
-## هيكل النظام
-
-يتكون النظام من الوحدات الرئيسية التالية:
-
-1. **وحدة التسعير (PricingApp)**:
- - إدارة جداول الكميات (BOQ)
- - تحليل التكاليف
- - سيناريوهات التسعير
- - التحليل التنافسي
- - التقارير
-
-2. **وحدة الموارد (ResourcesApp)**:
- - إدارة المعدات
- - إدارة المواد
- - إدارة العمالة
- - إدارة مقاولي الباطن
-
-3. **وحدة الكتالوجات**:
- - كتالوج المعدات
- - كتالوج المواد
- - كتالوج العمالة
- - كتالوج مقاولي الباطن
-
-4. **وحدة التحليل الذكي للأسعار**:
- - تحليل تكاليف البنود
- - تحليل المواد والمعدات والعمالة
- - تحليل التكاليف غير المباشرة
- - تحليل هامش الربح
-
-5. **وحدة الإدارات المساندة**:
- - إدارة تكاليف الإدارات المساندة
- - توزيع التكاليف غير المباشرة
-
-6. **وحدة استراتيجيات التسعير**:
- - التسعير القياسي
- - التسعير المتزن
- - التسعير غير المتزن
- - التسعير الموجه للربحية
- - التسعير بالتجميع
- - التسعير بالمحتوى المحلي
-
-7. **وحدة تحليل المحتوى المحلي**:
- - حساب نسبة المحتوى المحلي
- - تحليل المحتوى المحلي حسب نوع الموارد
- - توصيات لتحسين نسبة المحتوى المحلي
-
-## دليل الاستخدام
-
-### الشاشة الرئيسية
-
-عند تشغيل النظام، ستظهر الشاشة الرئيسية التي تحتوي على قائمة بالوحدات الرئيسية:
-
-- التسعير
-- الموارد
-- المشاريع
-- التقارير
-- الإعدادات
-
-اختر الوحدة المطلوبة للانتقال إليها.
-
-### وحدة التسعير
-
-تتضمن وحدة التسعير العديد من علامات التبويب:
-
-#### جدول الكميات (BOQ)
-
-- **استيراد جدول الكميات**: يمكنك استيراد جدول الكميات من ملف Excel.
-- **إضافة بنود يدويًا**: يمكنك إضافة بنود جديدة يدويًا.
-- **تحرير البنود**: يمكنك تحرير البنود الموجودة.
-- **التحليل الذكي للبنود**: يمكنك تحليل كل بند إلى مكوناته (مواد، معدات، عمالة، تكاليف غير مباشرة).
-
-#### تحليل التكاليف
-
-- **تحليل التكاليف الإجمالية**: عرض تحليل التكاليف الإجمالية للمشروع.
-- **تحليل التكاليف حسب البنود**: عرض تحليل التكاليف لكل بند.
-- **تحليل المواد الأكثر تكلفة**: عرض المواد الأكثر تكلفة في المشروع.
-- **تحليل المعدات الأكثر تكلفة**: عرض المعدات الأكثر تكلفة في المشروع.
-
-#### سيناريوهات التسعير
-
-- **استراتيجيات التسعير**: يمكنك اختيار استراتيجية التسعير المناسبة وتطبيقها.
-- **مقارنة استراتيجيات التسعير**: يمكنك مقارنة نتائج استراتيجيات التسعير المختلفة.
-
-#### كتالوجات الموارد
-
-- **كتالوج المعدات**: إدارة كتالوج المعدات واستيراده من Excel.
-- **كتالوج المواد**: إدارة كتالوج المواد واستيراده من Excel.
-- **كتالوج العمالة**: إدارة كتالوج العمالة واستيراده من Excel.
-- **كتالوج مقاولي الباطن**: إدارة كتالوج مقاولي الباطن واستيراده من Excel.
-
-#### الإدارات المساندة
-
-- **إدارة الإدارات المساندة**: إضافة وتحرير الإدارات المساندة.
-- **تحليل تكاليف الإدارات المساندة**: عرض تحليل تكاليف الإدارات المساندة.
-- **توزيع التكاليف**: توزيع تكاليف الإدارات المساندة على بنود المشروع.
-
-#### المحتوى المحلي
-
-- **تحليل المحتوى المحلي**: عرض تحليل المحتوى المحلي للمشروع.
-- **تحليل المحتوى المحلي حسب نوع الموارد**: عرض تحليل المحتوى المحلي حسب نوع الموارد.
-- **توصيات لتحسين نسبة المحتوى المحلي**: عرض توصيات لتحسين نسبة المحتوى المحلي.
-
-### وحدة الموارد
-
-تتضمن وحدة الموارد إدارة الموارد المختلفة:
-
-- **إدارة المعدات**: إضافة وتحرير المعدات.
-- **إدارة المواد**: إضافة وتحرير المواد.
-- **إدارة العمالة**: إضافة وتحرير العمالة.
-- **إدارة مقاولي الباطن**: إضافة وتحرير مقاولي الباطن.
-
-### وحدة المشاريع
-
-تتضمن وحدة المشاريع إدارة المشاريع:
-
-- **قائمة المشاريع**: عرض قائمة المشاريع.
-- **إضافة مشروع جديد**: إضافة مشروع جديد.
-- **تفاصيل المشروع**: عرض تفاصيل المشروع.
-
-### وحدة التقارير
-
-تتضمن وحدة التقارير إنشاء وعرض التقارير المختلفة:
-
-- **تقرير ملخص المشروع**: إنشاء تقرير ملخص للمشروع.
-- **تقرير تحليل التكاليف**: إنشاء تقرير تحليل التكاليف.
-- **تقرير سيناريوهات التسعير**: إنشاء تقرير سيناريوهات التسعير.
-- **تقرير المحتوى المحلي**: إنشاء تقرير المحتوى المحلي.
-- **تقرير الإدارات المساندة**: إنشاء تقرير الإدارات المساندة.
-- **تقرير المقارنة التنافسية**: إنشاء تقرير المقارنة التنافسية.
-- **تقرير الموارد المستخدمة**: إنشاء تقرير الموارد المستخدمة.
-
-### وحدة الإعدادات
-
-تتضمن وحدة الإعدادات تخصيص إعدادات النظام:
-
-- **إعدادات عامة**: تخصيص الإعدادات العامة مثل اللغة والسمة.
-- **إعدادات المستخدم**: تخصيص إعدادات المستخدم.
-- **إعدادات النظام**: تخصيص إعدادات النظام.
-- **النسخ الاحتياطي**: إنشاء واستعادة النسخ الاحتياطية.
-
-## الميزات الرئيسية
-
-### 1. إدارة البنود (BOQ)
-
-- استيراد جداول الكميات من Excel
-- إدخال البنود يدويًا وتحريرها
-- تحليل كل بند إلى مكوناته
-
-### 2. التحليل الذكي للأسعار
-
-- تحليل تفصيلي لتكاليف المواد والمعدات والعمالة
-- حساب المصاريف غير المباشرة وهامش الربح
-- تحليل التكاليف الإجمالية للمشروع
-
-### 3. كتالوجات الموارد
-
-- كتالوج شامل للمعدات المستخدمة في مشاريع البنية التحتية والصرف الصحي والطرق والسيول والكباري
-- كتالوج شامل للمواد المستخدمة في هذه المشاريع مع الأسعار التقريبية للسوق السعودي
-- كتالوج شامل للعمالة والمهندسين مع الأسعار بالساعة واليوم والأسبوع والشهر
-- كتالوج شامل لمقاولي الباطن المتخصصين في أعمال اليوتيلتيز والكهرباء وأنظمة ITC وCCTV وأنظمة التحكم وشبكات الري
-
-### 4. إدارة الإدارات المساندة
-
-- إدارة تكاليف الإدارات المساندة المختلفة
-- توزيع التكاليف غير المباشرة على بنود المشروع
-
-### 5. استراتيجيات التسعير المتقدمة
-
-- التسعير القياسي: تحديد سعر كل بند بناءً على تكلفته الفعلية مضافاً إليها نسبة ربح ثابتة
-- التسعير المتزن: توزيع هامش الربح بشكل متوازن على جميع بنود المشروع مع مراعاة المخاطر
-- التسعير غير المتزن: زيادة أسعار البنود المبكرة في المشروع وتخفيض أسعار البنود المتأخرة
-- التسعير الموجه للربحية: زيادة أسعار البنود ذات التكلفة المنخفضة والكميات الكبيرة
-- التسعير بالتجميع: تجميع البنود المتشابهة وتسعيرها كمجموعة واحدة
-- التسعير بالمحتوى المحلي: زيادة نسبة المحتوى المحلي في المشروع لتحقيق متطلبات الجهات المالكة
-
-### 6. تحليل المحتوى المحلي
-
-- حساب نسبة المحتوى المحلي في المشروع
-- تحليل المحتوى المحلي حسب نوع الموارد
-- توصيات لتحسين نسبة المحتوى المحلي
-
-### 7. التقارير المتقدمة
-
-- تقارير تفصيلية وإجمالية للمشروع
-- تقارير تحليل التكاليف
-- تقارير سيناريوهات التسعير
-- تقارير المحتوى المحلي
-- تقارير الإدارات المساندة
-- تقارير المقارنة التنافسية
-- تقارير الموارد المستخدمة
-
-## الدعم الفني
-
-للحصول على الدعم الفني، يرجى التواصل معنا عبر:
-
-- البريد الإلكتروني: support@tender-analysis-system.com
-- الهاتف: +966 12 345 6789
-
-## حقوق الملكية
-
-جميع حقوق الملكية محفوظة © 2025 نظام تحليل المناقصات وتسعير المشاريع.
+# نظام تحليل المناقصات وتسعير المشاريع - دليل التثبيت والاستخدام
+
+## مقدمة
+
+نظام تحليل المناقصات وتسعير المشاريع هو نظام متكامل يساعد المهندسين في تسعير المناقصات والمشاريع بطريقة احترافية ودقيقة. يوفر النظام مجموعة شاملة من الأدوات والوظائف التي تغطي جميع جوانب عملية التسعير، بدءًا من إدارة جداول الكميات وحتى تحليل المحتوى المحلي وإصدار التقارير.
+
+## متطلبات النظام
+
+- Python 3.8 أو أحدث
+- Streamlit 1.10.0 أو أحدث
+- Pandas 1.3.0 أو أحدث
+- NumPy 1.20.0 أو أحدث
+- Matplotlib 3.4.0 أو أحدث
+- Seaborn 0.11.0 أو أحدث
+- Plotly 5.3.0 أو أحدث
+- OpenPyXL 3.0.0 أو أحدث
+- XlsxWriter 3.0.0 أو أحدث
+
+## التثبيت
+
+1. قم بتثبيت Python من الموقع الرسمي: https://www.python.org/downloads/
+2. قم بتثبيت المكتبات المطلوبة باستخدام الأمر التالي:
+
+```bash
+pip install streamlit pandas numpy matplotlib seaborn plotly openpyxl xlsxwriter
+```
+
+3. قم بتنزيل ملفات النظام وفك ضغطها في المجلد المطلوب.
+4. انتقل إلى مجلد النظام واستخدم الأمر التالي لتشغيل النظام:
+
+```bash
+streamlit run app.py
+```
+
+## هيكل النظام
+
+يتكون النظام من الوحدات الرئيسية التالية:
+
+1. **وحدة التسعير (PricingApp)**:
+ - إدارة جداول الكميات (BOQ)
+ - تحليل التكاليف
+ - سيناريوهات التسعير
+ - التحليل التنافسي
+ - التقارير
+
+2. **وحدة الموارد (ResourcesApp)**:
+ - إدارة المعدات
+ - إدارة المواد
+ - إدارة العمالة
+ - إدارة مقاولي الباطن
+
+3. **وحدة الكتالوجات**:
+ - كتالوج المعدات
+ - كتالوج المواد
+ - كتالوج العمالة
+ - كتالوج مقاولي الباطن
+
+4. **وحدة التحليل الذكي للأسعار**:
+ - تحليل تكاليف البنود
+ - تحليل المواد والمعدات والعمالة
+ - تحليل التكاليف غير المباشرة
+ - تحليل هامش الربح
+
+5. **وحدة الإدارات المساندة**:
+ - إدارة تكاليف الإدارات المساندة
+ - توزيع التكاليف غير المباشرة
+
+6. **وحدة استراتيجيات التسعير**:
+ - التسعير القياسي
+ - التسعير المتزن
+ - التسعير غير المتزن
+ - التسعير الموجه للربحية
+ - التسعير بالتجميع
+ - التسعير بالمحتوى المحلي
+
+7. **وحدة تحليل المحتوى المحلي**:
+ - حساب نسبة المحتوى المحلي
+ - تحليل المحتوى المحلي حسب نوع الموارد
+ - توصيات لتحسين نسبة المحتوى المحلي
+
+## دليل الاستخدام
+
+### الشاشة الرئيسية
+
+عند تشغيل النظام، ستظهر الشاشة الرئيسية التي تحتوي على قائمة بالوحدات الرئيسية:
+
+- التسعير
+- الموارد
+- المشاريع
+- التقارير
+- الإعدادات
+
+اختر الوحدة المطلوبة للانتقال إليها.
+
+### وحدة التسعير
+
+تتضمن وحدة التسعير العديد من علامات التبويب:
+
+#### جدول الكميات (BOQ)
+
+- **استيراد جدول الكميات**: يمكنك استيراد جدول الكميات من ملف Excel.
+- **إضافة بنود يدويًا**: يمكنك إضافة بنود جديدة يدويًا.
+- **تحرير البنود**: يمكنك تحرير البنود الموجودة.
+- **التحليل الذكي للبنود**: يمكنك تحليل كل بند إلى مكوناته (مواد، معدات، عمالة، تكاليف غير مباشرة).
+
+#### تحليل التكاليف
+
+- **تحليل التكاليف الإجمالية**: عرض تحليل التكاليف الإجمالية للمشروع.
+- **تحليل التكاليف حسب البنود**: عرض تحليل التكاليف لكل بند.
+- **تحليل المواد الأكثر تكلفة**: عرض المواد الأكثر تكلفة في المشروع.
+- **تحليل المعدات الأكثر تكلفة**: عرض المعدات الأكثر تكلفة في المشروع.
+
+#### سيناريوهات التسعير
+
+- **استراتيجيات التسعير**: يمكنك اختيار استراتيجية التسعير المناسبة وتطبيقها.
+- **مقارنة استراتيجيات التسعير**: يمكنك مقارنة نتائج استراتيجيات التسعير المختلفة.
+
+#### كتالوجات الموارد
+
+- **كتالوج المعدات**: إدارة كتالوج المعدات واستيراده من Excel.
+- **كتالوج المواد**: إدارة كتالوج المواد واستيراده من Excel.
+- **كتالوج العمالة**: إدارة كتالوج العمالة واستيراده من Excel.
+- **كتالوج مقاولي الباطن**: إدارة كتالوج مقاولي الباطن واستيراده من Excel.
+
+#### الإدارات المساندة
+
+- **إدارة الإدارات المساندة**: إضافة وتحرير الإدارات المساندة.
+- **تحليل تكاليف الإدارات المساندة**: عرض تحليل تكاليف الإدارات المساندة.
+- **توزيع التكاليف**: توزيع تكاليف الإدارات المساندة على بنود المشروع.
+
+#### المحتوى المحلي
+
+- **تحليل المحتوى المحلي**: عرض تحليل المحتوى المحلي للمشروع.
+- **تحليل المحتوى المحلي حسب نوع الموارد**: عرض تحليل المحتوى المحلي حسب نوع الموارد.
+- **توصيات لتحسين نسبة المحتوى المحلي**: عرض توصيات لتحسين نسبة المحتوى المحلي.
+
+### وحدة الموارد
+
+تتضمن وحدة الموارد إدارة الموارد المختلفة:
+
+- **إدارة المعدات**: إضافة وتحرير المعدات.
+- **إدارة المواد**: إضافة وتحرير المواد.
+- **إدارة العمالة**: إضافة وتحرير العمالة.
+- **إدارة مقاولي الباطن**: إضافة وتحرير مقاولي الباطن.
+
+### وحدة المشاريع
+
+تتضمن وحدة المشاريع إدارة المشاريع:
+
+- **قائمة المشاريع**: عرض قائمة المشاريع.
+- **إضافة مشروع جديد**: إضافة مشروع جديد.
+- **تفاصيل المشروع**: عرض تفاصيل المشروع.
+
+### وحدة التقارير
+
+تتضمن وحدة التقارير إنشاء وعرض التقارير المختلفة:
+
+- **تقرير ملخص المشروع**: إنشاء تقرير ملخص للمشروع.
+- **تقرير تحليل التكاليف**: إنشاء تقرير تحليل التكاليف.
+- **تقرير سيناريوهات التسعير**: إنشاء تقرير سيناريوهات التسعير.
+- **تقرير المحتوى المحلي**: إنشاء تقرير المحتوى المحلي.
+- **تقرير الإدارات المساندة**: إنشاء تقرير الإدارات المساندة.
+- **تقرير المقارنة التنافسية**: إنشاء تقرير المقارنة التنافسية.
+- **تقرير الموارد المستخدمة**: إنشاء تقرير الموارد المستخدمة.
+
+### وحدة الإعدادات
+
+تتضمن وحدة الإعدادات تخصيص إعدادات النظام:
+
+- **إعدادات عامة**: تخصيص الإعدادات العامة مثل اللغة والسمة.
+- **إعدادات المستخدم**: تخصيص إعدادات المستخدم.
+- **إعدادات النظام**: تخصيص إعدادات النظام.
+- **النسخ الاحتياطي**: إنشاء واستعادة النسخ الاحتياطية.
+
+## الميزات الرئيسية
+
+### 1. إدارة البنود (BOQ)
+
+- استيراد جداول الكميات من Excel
+- إدخال البنود يدويًا وتحريرها
+- تحليل كل بند إلى مكوناته
+
+### 2. التحليل الذكي للأسعار
+
+- تحليل تفصيلي لتكاليف المواد والمعدات والعمالة
+- حساب المصاريف غير المباشرة وهامش الربح
+- تحليل التكاليف الإجمالية للمشروع
+
+### 3. كتالوجات الموارد
+
+- كتالوج شامل للمعدات المستخدمة في مشاريع البنية التحتية والصرف الصحي والطرق والسيول والكباري
+- كتالوج شامل للمواد المستخدمة في هذه المشاريع مع الأسعار التقريبية للسوق السعودي
+- كتالوج شامل للعمالة والمهندسين مع الأسعار بالساعة واليوم والأسبوع والشهر
+- كتالوج شامل لمقاولي الباطن المتخصصين في أعمال اليوتيلتيز والكهرباء وأنظمة ITC وCCTV وأنظمة التحكم وشبكات الري
+
+### 4. إدارة الإدارات المساندة
+
+- إدارة تكاليف الإدارات المساندة المختلفة
+- توزيع التكاليف غير المباشرة على بنود المشروع
+
+### 5. استراتيجيات التسعير المتقدمة
+
+- التسعير القياسي: تحديد سعر كل بند بناءً على تكلفته الفعلية مضافاً إليها نسبة ربح ثابتة
+- التسعير المتزن: توزيع هامش الربح بشكل متوازن على جميع بنود المشروع مع مراعاة المخاطر
+- التسعير غير المتزن: زيادة أسعار البنود المبكرة في المشروع وتخفيض أسعار البنود المتأخرة
+- التسعير الموجه للربحية: زيادة أسعار البنود ذات التكلفة المنخفضة والكميات الكبيرة
+- التسعير بالتجميع: تجميع البنود المتشابهة وتسعيرها كمجموعة واحدة
+- التسعير بالمحتوى المحلي: زيادة نسبة المحتوى المحلي في المشروع لتحقيق متطلبات الجهات المالكة
+
+### 6. تحليل المحتوى المحلي
+
+- حساب نسبة المحتوى المحلي في المشروع
+- تحليل المحتوى المحلي حسب نوع الموارد
+- توصيات لتحسين نسبة المحتوى المحلي
+
+### 7. التقارير المتقدمة
+
+- تقارير تفصيلية وإجمالية للمشروع
+- تقارير تحليل التكاليف
+- تقارير سيناريوهات التسعير
+- تقارير المحتوى المحلي
+- تقارير الإدارات المساندة
+- تقارير المقارنة التنافسية
+- تقارير الموارد المستخدمة
+
+## الدعم الفني
+
+للحصول على الدعم الفني، يرجى التواصل معنا عبر:
+
+- البريد الإلكتروني: support@tender-analysis-system.com
+- الهاتف: +966 12 345 6789
+
+## حقوق الملكية
+
+جميع حقوق الملكية محفوظة © 2025 نظام تحليل المناقصات وتسعير المشاريع.
diff --git a/pricing_system/integrated_app.py b/pricing_system/integrated_app.py
index 13eb682d97ce365c0c3c6a8123e2c459db8b241c..a4912653601bfa5c3335137ca7fb927c56d7381e 100644
--- a/pricing_system/integrated_app.py
+++ b/pricing_system/integrated_app.py
@@ -1,334 +1,1015 @@
import streamlit as st
-import sys
-import os
+import pandas as pd
+from pricing_system.modules.stages.project_entry import render_project_entry
+from pricing_system.modules.pricing_strategies import balanced_pricing
+from pricing_system.modules.indirect_support import overheads
+from pricing_system.modules.analysis.smart_price_analysis import SmartPriceAnalysis
+from pricing_system.modules.analysis.market_analysis import MarketAnalysis
+
+import openpyxl
-# إضافة مسار الوحدات الجديدة
-sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+from pricing_system.modules.risk_analysis.risk_analyzer import RiskAnalyzer
+from pricing_system.modules.reference_guides.pricing_guidelines import PricingGuidelines
+import os
+from datetime import datetime
+import pdfkit
-# استيراد إطار التكامل
-from pricing_system.integration_framework import IntegrationFramework
+class ReferenceGuides:
+ def render(self):
+ st.title("المراجع والأدلة")
+ st.markdown("## دليل تحليل أسعار بنود الإنشاءات")
+ st.markdown("**المرجع الأول:** [رابط للمرجع الأول](link_to_reference_1)")
+ st.markdown("**المرجع الثاني:** [رابط للمرجع الثاني](link_to_reference_2)")
-# استيراد الوحدات الأصلية
-from modules.pricing.pricing_app import PricingApp
-from modules.resources.resources_app import ResourcesApp
class IntegratedApp:
- """
- التطبيق المتكامل الذي يجمع بين النظام القديم والنظام الجديد
- """
-
def __init__(self):
- """
- تهيئة التطبيق المتكامل
- """
- # تهيئة حالة الجلسة إذا لم تكن موجودة
- if 'integrated_app_initialized' not in st.session_state:
- st.session_state.integrated_app_initialized = True
- st.session_state.current_module = "pricing" # القيمة الافتراضية
-
- # إنشاء مثيلات من الوحدات الأصلية
- self.pricing_app = PricingApp()
- self.resources_app = ResourcesApp()
-
- # إنشاء مثيل من إطار التكامل
- self.integration_framework = IntegrationFramework()
-
- # ربط الوحدات مع إطار التكامل
- self.integration_framework.connect_pricing_app(self.pricing_app)
- self.integration_framework.connect_resources_app(self.resources_app)
-
+ from config_manager import ConfigManager
+ config_manager = ConfigManager()
+ config_manager.set_page_config_if_needed(
+ page_title="نظام التسعير المتكامل",
+ page_icon="💰",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ if 'pricing_stage' not in st.session_state:
+ st.session_state.pricing_stage = 1
+
+ if 'current_project' not in st.session_state:
+ st.session_state.current_project = {
+ 'name': '',
+ 'code': '',
+ 'boq_items': [],
+ 'indirect_costs': {
+ 'overhead': 0.15,
+ 'profit': 0.10,
+ 'risk': 0.05
+ }
+ }
+ elif 'boq_items' not in st.session_state.current_project:
+ st.session_state.current_project['boq_items'] = []
+
+ self.smart_analysis = SmartPriceAnalysis()
+ self.market_analysis = MarketAnalysis()
+ self.risk_analyzer = RiskAnalyzer()
+ self.reference_guides = ReferenceGuides()
+
def run(self):
- """
- تشغيل التطبيق المتكامل
- """
- # عرض شعار التطبيق والعنوان
- st.sidebar.image("logo.png", width=200)
- st.sidebar.title("نظام تحليل المناقصات")
-
- # عرض قائمة الوحدات الرئيسية
- main_modules = ["التسعير", "الموارد", "المشاريع", "التقارير", "الإعدادات"]
- selected_module = st.sidebar.radio("اختر الوحدة:", main_modules)
-
- # تحديد الوحدة الحالية
- if selected_module == "التسعير":
- st.session_state.current_module = "pricing"
- elif selected_module == "الموارد":
- st.session_state.current_module = "resources"
- elif selected_module == "المشاريع":
- st.session_state.current_module = "projects"
- elif selected_module == "التقارير":
- st.session_state.current_module = "reports"
- elif selected_module == "الإعدادات":
- st.session_state.current_module = "settings"
-
- # عرض الوحدة المختارة
- if st.session_state.current_module == "pricing":
- self.pricing_app.render()
- elif st.session_state.current_module == "resources":
- self.resources_app.render()
- elif st.session_state.current_module == "projects":
- self._render_projects_module()
- elif st.session_state.current_module == "reports":
- self._render_reports_module()
- elif st.session_state.current_module == "settings":
- self._render_settings_module()
-
- def _render_projects_module(self):
- """
- عرض وحدة المشاريع
- """
- st.title("إدارة المشاريع")
-
- # عرض قائمة المشاريع
- st.subheader("قائمة المشاريع")
-
- # بيانات المشاريع النموذجية
- projects = [
- {"name": "مشروع البنية التحتية للحي السكني", "client": "وزارة الإسكان", "status": "قيد التنفيذ"},
- {"name": "مشروع تطوير شبكة الصرف الصحي", "client": "وزارة البيئة والمياه والزراعة", "status": "مكتمل"},
- {"name": "مشروع إنشاء طريق سريع", "client": "وزارة النقل", "status": "قيد التخطيط"},
- {"name": "مشروع بناء جسر", "client": "أمانة المنطقة الشرقية", "status": "قيد التنفيذ"},
- {"name": "مشروع تصريف مياه السيول", "client": "وزارة البيئة والمياه والزراعة", "status": "قيد التخطيط"}
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ st.markdown('نظام التسعير المتكامل ', unsafe_allow_html=True)
+ st.sidebar.markdown('', unsafe_allow_html=True)
+
+ self._render_sidebar_stages()
+
+ if st.session_state.pricing_stage == 1:
+ self._render_project_entry()
+ elif st.session_state.pricing_stage == 2:
+ self._render_boq_items()
+ elif st.session_state.pricing_stage == 3:
+ self._render_price_analysis()
+ elif st.session_state.pricing_stage == 4:
+ self._render_risk_analysis()
+ elif st.session_state.pricing_stage == 5:
+ self._render_pricing_strategies()
+ elif st.session_state.pricing_stage == 6:
+ self._render_local_content()
+ elif st.session_state.pricing_stage == 7:
+ self._render_final_boq()
+ elif st.session_state.pricing_stage == 8:
+ self._render_reference_guides()
+
+ self._render_navigation()
+
+ def _render_sidebar_stages(self):
+ st.sidebar.markdown("### مراحل التسعير")
+ stages = [
+ "بيانات المشروع",
+ "جدول الكميات",
+ "تحليل الأسعار",
+ "تحليل المخاطر",
+ "استراتيجيات التسعير",
+ "المحتوى المحلي",
+ "الجدول النهائي",
+ "المراجع والأدلة"
]
-
- # عرض المشاريع في جدول
- for i, project in enumerate(projects):
- col1, col2, col3, col4 = st.columns([3, 2, 2, 1])
+ for i, stage in enumerate(stages, 1):
+ if st.session_state.pricing_stage > i:
+ status = "✓"
+ elif st.session_state.pricing_stage == i:
+ status = "🔄"
+ else:
+ status = ""
+ st.sidebar.markdown(f"{i}. {stage} {status}")
+
+ def _render_project_entry(self):
+ st.title("بيانات المشروع")
+
+ if 'current_project' not in st.session_state:
+ st.session_state.current_project = {}
+ st.session_state.show_entry_form = True
+
+ if st.session_state.get('show_entry_form', True):
+ st.subheader("إدخال بيانات المشروع")
+ with st.form("project_entry_form"):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ name = st.text_input("اسم المشروع")
+ code = st.text_input("رقم المشروع")
+ location = st.text_input("الموقع")
+
+ with col2:
+ start_date = st.date_input("تاريخ البدء")
+ duration = st.number_input("مدة المشروع (يوم)", min_value=1, value=180)
+ budget = st.number_input("الميزانية (ريال)", min_value=0.0, step=1000.0)
+
+ description = st.text_area("وصف المشروع")
+
+ if st.form_submit_button("حفظ البيانات"):
+ st.session_state.current_project.update({
+ 'name': name,
+ 'code': code,
+ 'location': location,
+ 'start_date': start_date,
+ 'duration': duration,
+ 'budget': budget,
+ 'description': description
+ })
+ st.session_state.show_entry_form = False
+ st.success("تم حفظ بيانات المشروع بنجاح!")
+ st.rerun()
+
+ else:
+ st.subheader("بيانات المشروع المحفوظة")
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown(f"**اسم المشروع:** {st.session_state.current_project.get('name', '')}")
+ st.markdown(f"**رقم المشروع:** {st.session_state.current_project.get('code', '')}")
+ st.markdown(f"**الموقع:** {st.session_state.current_project.get('location', '')}")
+
+ with col2:
+ st.markdown(f"**تاريخ البدء:** {st.session_state.current_project.get('start_date', '')}")
+ st.markdown(f"**مدة المشروع:** {st.session_state.current_project.get('duration', '')} يوم")
+ st.markdown(f"**الميزانية:** {st.session_state.current_project.get('budget', '')} ريال")
+
+ st.markdown(f"**الوصف:** {st.session_state.current_project.get('description', '')}")
+
+ if st.button("تعديل البيانات"):
+ st.session_state.show_entry_form = True
+ st.rerun()
+
+ def _render_boq_items(self):
+ st.title("جدول الكميات")
+ tabs = st.tabs(["إدخال البنود", "استيراد/تصدير جدول الكميات", "تحليل البنود"])
+
+ with tabs[0]:
+ st.subheader("إضافة بند جديد")
+ st.markdown("### إضافة بند جديد")
+ new_item_type = st.selectbox("نوع البند", ["عمالة", "معدات", "مواد"])
+
+ col1, col2, col3 = st.columns(3)
with col1:
- st.write(project["name"])
+ item_name = st.text_input("اسم البند")
with col2:
- st.write(project["client"])
+ item_quantity = st.number_input("الكمية", min_value=0.0, step=0.1)
with col3:
- status_color = "green" if project["status"] == "مكتمل" else "blue" if project["status"] == "قيد التنفيذ" else "orange"
- st.markdown(f"{project['status']} ", unsafe_allow_html=True)
- with col4:
- if st.button("عرض", key=f"view_project_{i}"):
- st.session_state.selected_project = project
-
- # إضافة مشروع جديد
- st.subheader("إضافة مشروع جديد")
-
- with st.form("add_project_form"):
- project_name = st.text_input("اسم المشروع")
- project_client = st.text_input("الجهة المالكة")
- project_location = st.text_input("الموقع")
- project_type = st.selectbox(
- "نوع المشروع",
- ["بنية تحتية", "صرف صحي", "طرق", "سيول", "كباري", "مباني", "أخرى"]
- )
- project_budget = st.number_input("الميزانية التقديرية (ريال)", min_value=0.0, step=100000.0)
-
+ item_price = st.number_input("السعر", min_value=0.0, step=0.1)
+
+ if st.button("إضافة البند"):
+ new_item = {
+ "type": new_item_type,
+ "name": item_name,
+ "quantity": item_quantity,
+ "price": item_price,
+ "total": item_quantity * item_price
+ }
+
+ if new_item_type == "عمالة":
+ if 'labor' not in st.session_state.current_project:
+ st.session_state.current_project['labor'] = []
+ st.session_state.current_project['labor'].append(new_item)
+ elif new_item_type == "معدات":
+ if 'equipment' not in st.session_state.current_project:
+ st.session_state.current_project['equipment'] = []
+ st.session_state.current_project['equipment'].append(new_item)
+ else:
+ if 'materials' not in st.session_state.current_project:
+ st.session_state.current_project['materials'] = []
+ st.session_state.current_project['materials'].append(new_item)
+
+ st.success(f"تم إضافة {item_name} بنجاح")
+ st.rerun()
+
+
+ with st.form("boq_item_form"):
col1, col2 = st.columns(2)
+
with col1:
- project_start_date = st.date_input("تاريخ البدء")
+ item_code = st.text_input("كود البند")
+ item_desc = st.text_area("وصف البند")
+ quantity = st.number_input("الكمية", min_value=0.0)
+
with col2:
- project_end_date = st.date_input("تاريخ الانتهاء المتوقع")
-
- project_description = st.text_area("وصف المشروع")
-
- submit_button = st.form_submit_button("إضافة المشروع")
-
- if submit_button:
- if project_name and project_client:
- st.success(f"تمت إضافة مشروع {project_name} بنجاح")
+ unit = st.selectbox("الوحدة", ["متر مربع", "متر مكعب", "متر طولي", "عدد", "طن", "كجم"])
+ unit_price = st.number_input("سعر الوحدة", min_value=0.0)
+
+ if st.form_submit_button("إضافة البند"):
+ if not item_code or not item_desc or quantity <= 0 or unit_price <= 0:
+ st.error("يرجى إدخال جميع البيانات المطلوبة")
else:
- st.error("يرجى إدخال اسم المشروع والجهة المالكة")
-
- def _render_reports_module(self):
- """
- عرض وحدة التقارير
- """
- st.title("التقارير")
-
- # أنواع التقارير
- report_types = [
- "تقرير ملخص المشروع",
- "تقرير تحليل التكاليف",
- "تقرير سيناريوهات التسعير",
- "تقرير المحتوى المحلي",
- "تقرير الإدارات المساندة",
- "تقرير المقارنة التنافسية",
- "تقرير الموارد المستخدمة"
- ]
-
- # اختيار نوع التقرير
- selected_report = st.selectbox("اختر نوع التقرير", report_types)
-
- # اختيار المشروع
- projects = [
- "مشروع البنية التحتية للحي السكني",
- "مشروع تطوير شبكة الصرف الصحي",
- "مشروع إنشاء طريق سريع",
- "مشروع بناء جسر",
- "مشروع تصريف مياه السيول"
- ]
-
- selected_project = st.selectbox("اختر المشروع", projects)
-
- # خيارات التقرير
- st.subheader("خيارات التقرير")
-
+ new_item = {
+ 'code': item_code,
+ 'description': item_desc,
+ 'unit': unit,
+ 'quantity': quantity,
+ 'unit_price': unit_price,
+ 'total_price': quantity * unit_price
+ }
+ st.session_state.current_project['boq_items'].append(new_item)
+ st.success("تم إضافة البند بنجاح")
+ st.rerun()
+
+ with tabs[1]:
+ st.subheader("استيراد/تصدير جدول الكميات")
+
+ uploaded_file = st.file_uploader("رفع ملف جدول كميات", type=['xlsx', 'xls'], key="boq_upload")
+ if uploaded_file:
+ try:
+ df = pd.read_excel(uploaded_file)
+ st.write("معاينة البيانات:")
+ st.dataframe(df)
+
+ if st.button("تأكيد استيراد البيانات", key="confirm_import"):
+ for _, row in df.iterrows():
+ new_item = {
+ 'code': str(row.get('كود البند', '')),
+ 'description': str(row.get('وصف البند', '')),
+ 'unit': str(row.get('الوحدة', '')),
+ 'quantity': float(row.get('الكمية', 0)),
+ 'unit_price': float(row.get('سعر الوحدة', 0)),
+ 'total_price': float(row.get('السعر الإجمالي', 0))
+ }
+ st.session_state.current_project['boq_items'].append(new_item)
+ st.success("تم استيراد البيانات بنجاح")
+ st.rerun()
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}")
+
+ st.divider()
+
+ if st.session_state.current_project.get('boq_items'):
+ if st.button("تصدير جدول الكميات الحالي", key="export_current_boq"):
+ try:
+ df = pd.DataFrame(st.session_state.current_project['boq_items'])
+ # Rename columns to Arabic
+ df = df.rename(columns={
+ 'code': 'كود البند',
+ 'description': 'وصف البند',
+ 'unit': 'الوحدة',
+ 'quantity': 'الكمية',
+ 'unit_price': 'سعر الوحدة',
+ 'total_price': 'السعر الإجمالي'
+ })
+
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ excel_file = f"data/exports/boq_{timestamp}.xlsx"
+ os.makedirs("data/exports", exist_ok=True)
+ df.to_excel(excel_file, index=False)
+
+ with open(excel_file, 'rb') as f:
+ st.download_button(
+ label="تحميل الملف",
+ data=f,
+ file_name=f"boq_{timestamp}.xlsx",
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ key="download_boq"
+ )
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء تصدير الملف: {str(e)}")
+
+ with tabs[2]:
+ st.subheader("تحليل البنود")
+ if st.session_state.current_project.get('boq_items'):
+ df = pd.DataFrame(st.session_state.current_project['boq_items'])
+
+ categories = ["أعمال ترابية", "أعمال خرسانية", "أعمال حديد", "أعمال بناء", "أعمال تشطيبات"]
+ selected_category = st.selectbox("اختر فئة البند", categories)
+
+ if 'category' in df.columns:
+ category_items = df[df['category'] == selected_category]
+ if not category_items.empty:
+ st.write("### البنود المتاحة في هذه الفئة:")
+ for _, item in category_items.iterrows():
+ st.write(f"- {item['description']}")
+ else:
+ st.info("لا توجد بنود في هذه الفئة")
+
+ total_cost = df['total_price'].sum()
+ st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال")
+
+ st.subheader("توزيع التكاليف حسب الوحدات")
+ unit_costs = df.groupby('unit')['total_price'].sum()
+ st.bar_chart(unit_costs)
+
+ st.subheader("البنود الأعلى تكلفة")
+ top_items = df.nlargest(5, 'total_price')[['code', 'description', 'total_price']]
+ st.dataframe(top_items)
+
+ st.subheader("تحليل تفصيلي للبند")
+ selected_item = st.selectbox("اختر بند للتحليل", df['code'].tolist())
+ if selected_item:
+ item = df[df['code'] == selected_item].iloc[0]
+ col1, col2 = st.columns(2)
+ with col1:
+ st.write("**الوصف:**", item['description'])
+ st.write("**الكمية:**", item['quantity'])
+ with col2:
+ st.write("**سعر الوحدة:**", f"{item['unit_price']:,.2f} ريال")
+ st.write("**الإجمالي:**", f"{item['total_price']:,.2f} ريال")
+ else:
+ st.warning("لا توجد بنود للتحليل. الرجاء إضافة بنود أولاً.")
+
+ if st.session_state.current_project['boq_items']:
+ st.markdown("### البنود المضافة")
+
+ for idx, item in enumerate(st.session_state.current_project['boq_items']):
+ st.markdown(f"### البند {idx+1}: {item['description'][:50]}...")
+ col1, col2, col3 = st.columns([2,2,1])
+
+ with col1:
+ st.text_input("كود البند", value=item['code'], key=f"code_{idx}")
+ st.text_area("وصف البند", value=item['description'], key=f"desc_{idx}")
+
+ if st.checkbox(f"عرض تحليل مكونات البند {idx+1}", key=f"show_analysis_{idx}"):
+ with st.expander("تحليل مكونات البند"):
+ # المواد
+ st.subheader("المواد")
+ materials_container = st.container()
+ with materials_container:
+ if 'materials' not in st.session_state:
+ st.session_state.materials = []
+ materials = st.session_state.materials
+
+ for i in range(len(materials)):
+ cols = st.columns([3, 2, 2, 1])
+ with cols[0]:
+ materials[i]['name'] = st.selectbox(f"المادة {i+1}", ["اسمنت", "رمل", "حصى", "حديد"], key=f"mat_{idx}_{i}", index = 0 if materials[i]['name'] == "" else ["اسمنت", "رمل", "حصى", "حديد"].index(materials[i]['name']))
+ with cols[1]:
+ unit_col1, unit_col2 = st.columns(2)
+ with unit_col1:
+ materials[i]['unit'] = st.selectbox(
+ "وحدة القياس",
+ ["متر مربع", "متر مكعب", "متر طولي", "رول", "بوكس", "كجم", "طن", "عدد", "لتر"],
+ key=f"mat_unit_{idx}_{i}"
+ )
+ with unit_col2:
+ try:
+ current_qty = float(materials[i]['quantity'])
+ except (ValueError, TypeError):
+ current_qty = 0.0
+ materials[i]['quantity'] = st.number_input(
+ "الكمية",
+ min_value=0.0,
+ value=current_qty,
+ key=f"mat_qty_{idx}_{i}"
+ )
+ with cols[2]:
+ try:
+ current_price = float(materials[i]['price'])
+ except (ValueError, TypeError):
+ current_price = 0.0
+ materials[i]['price'] = st.number_input("السعر", min_value=0.0, value=current_price, key=f"mat_price_{idx}_{i}")
+ with cols[3]:
+ materials[i]['total'] = materials[i]['quantity'] * materials[i]['price']
+ st.text(f"{materials[i]['total']:.2f}")
+ if st.button("🗑️ حذف", key=f"delete_mat_{idx}_{i}"):
+ if len(materials) > i:
+ materials.pop(i)
+ st.rerun()
+
+ if st.button("➕ إضافة مادة جديدة", key=f"add_mat_{idx}"):
+ materials.append({
+ "name": "",
+ "quantity": 0,
+ "price": 0,
+ "total": 0
+ })
+ st.rerun()
+
+ # العمالة
+ st.subheader("العمالة")
+ with st.container():
+ labor_container = st.container()
+ with labor_container:
+ if 'labor' not in st.session_state:
+ st.session_state.labor = []
+ labor = st.session_state.labor
+
+ for i in range(len(labor)):
+ cols = st.columns([3, 2, 2, 1])
+ with cols[0]:
+ labor[i]['name'] = st.selectbox(f"العامل {i+1}", ["نجار", "حداد", "عامل", "فني"], key=f"labor_{idx}_{i}")
+ with cols[1]:
+ labor[i]['quantity'] = st.number_input("العدد", min_value=0, value=labor[i].get('quantity', 0), key=f"labor_qty_{idx}_{i}")
+ with cols[2]:
+ current_price = float(labor[i].get('price', 0.0))
+ labor[i]['price'] = st.number_input("الأجر اليومي", min_value=0.0, value=current_price, key=f"labor_price_{idx}_{i}")
+ with cols[3]:
+ labor[i]['total'] = labor[i]['quantity'] * labor[i]['price']
+ st.text(f"{labor[i]['total']:.2f}")
+ if st.button("🗑️ حذف", key=f"delete_labor_{idx}_{i}"):
+ if len(labor) > i:
+ labor.pop(i)
+ st.rerun()
+
+ if st.button("➕ إضافة عامل جديد", key=f"add_labor_{idx}"):
+ labor.append({
+ "name": "",
+ "quantity": 0,
+ "price": 0,
+ "total": 0
+ })
+ st.rerun()
+
+ # المعدات
+ st.subheader("المعدات")
+ equipment_container = st.container()
+ with equipment_container:
+ if 'equipment' not in st.session_state:
+ st.session_state.equipment = []
+ equipment = st.session_state.equipment
+
+ for i in range(len(equipment)):
+ cols = st.columns([3, 2, 2, 1])
+ with cols[0]:
+ equipment[i]['name'] = st.selectbox(f"المعدة {i+1}", ["خلاطة", "هزاز", "ونش", "مضخة"], key=f"equip_{idx}_{i}")
+ with cols[1]:
+ equipment[i]['quantity'] = st.number_input("العدد", min_value=0, value=int(equipment[i].get('quantity', 0)), key=f"equip_qty_{idx}_{i}")
+ with cols[2]:
+ current_price = float(equipment[i].get('price', 0.0))
+ equipment[i]['price'] = st.number_input("السعر اليومي", min_value=0.0, value=current_price, key=f"equip_price_{idx}_{i}")
+ with cols[3]:
+ equipment[i]['total'] = equipment[i]['quantity'] * equipment[i]['price']
+ st.text(f"{equipment[i]['total']:.2f}")
+ if st.button("🗑️ حذف", key=f"delete_equip_{idx}_{i}"):
+ if len(equipment) > i:
+ equipment.pop(i)
+ st.rerun()
+
+ if st.button("➕ إضافة معدة جديدة", key=f"add_equip_{idx}"):
+ equipment.append({
+ "name": "",
+ "quantity": 0,
+ "price": 0,
+ "total": 0
+ })
+ st.rerun()
+ col1, col2 = st.columns(2)
+ with col1:
+ if st.button("➕ إضافة بند جديد", key=f"add_item_{idx}"):
+ if 'materials' not in st.session_state:
+ st.session_state.materials = []
+ st.session_state.materials.append({
+ "name": "",
+ "quantity": 0,
+ "price": 0,
+ "total": 0
+ })
+ st.rerun()
+ with col2:
+ if st.button("❌ حذف البند", key=f"delete_item_{idx}"):
+ if len(st.session_state.materials) > 0:
+ st.session_state.materials.pop()
+ st.rerun()
+
+ total_materials = sum(m["total"] for m in materials)
+ total_labor = sum(l["total"] for l in labor)
+ total_equipment = sum(e["total"] for e in equipment)
+ total_cost = total_materials + total_labor + total_equipment
+
+ st.markdown("---")
+ st.subheader("ملخص التكاليف والحاسبة")
+
+ col1, col2, col3, col4 = st.columns(4)
+ with col1:
+ st.metric("تكلفة المواد", f"{total_materials:.2f}")
+ with col2:
+ st.metric("تكلفة العمالة", f"{total_labor:.2f}")
+ with col3:
+ st.metric("تكلفة المعدات", f"{total_equipment:.2f}")
+ with col4:
+ st.metric("التكلفة الإجمالية", f"{total_cost:.2f}")
+
+ st.markdown("### الحاسبة")
+ calc_col1, calc_col2, calc_col3 = st.columns([2,1,2])
+
+ with calc_col1:
+ num1 = st.number_input("الرقم الأول", value=0.0, format="%.2f")
+ num2 = st.number_input("الرقم الثاني", value=0.0, format="%.2f")
+
+ with calc_col2:
+ operation = st.selectbox("العملية", ['+', '-', '×', '÷'])
+
+ with calc_col3:
+ if operation == '+':
+ result = num1 + num2
+ elif operation == '-':
+ result = num1 - num2
+ elif operation == '×':
+ result = num1 * num2
+ elif operation == '÷':
+ result = num1 / num2 if num2 != 0 else 0
+
+ st.metric("النتيجة", f"{result:.2f}")
+
+ unit_cost = total_cost / quantity if quantity > 0 else 0
+ st.success(f"تكلفة الوحدة: {unit_cost:.2f}")
+
+ if st.button("تطبيق السعر", key=f"apply_price_{idx}"):
+ item['unit_price'] = unit_cost
+ item['total_price'] = unit_cost * quantity
+ st.success("تم تحديث سعر البند")
+ st.rerun()
+
+ with col2:
+ units_list = ["م3", "م2", "متر طولي", "عدد", "متر مربع", "طن", "كجم"]
+ try:
+ default_index = units_list.index(item['unit'])
+ except ValueError:
+ default_index = 0
+ unit = st.selectbox("الوحدة", units_list, key=f"unit_{idx}", index=default_index)
+ quantity = st.number_input("الكمية", value=float(item['quantity']), key=f"quantity_{idx}")
+
+
+ with col3:
+ unit_price = st.number_input("سعر الوحدة", value=float(item['unit_price']), key=f"price_{idx}")
+ if st.button("تحديث البند", key=f"update_{idx}"):
+ st.session_state.current_project['boq_items'][idx].update({
+ 'code': st.session_state[f"code_{idx}"],
+ 'description': st.session_state[f"desc_{idx}"],
+ 'unit': st.session_state[f"unit_{idx}"],
+ 'quantity': st.session_state[f"quantity_{idx}"],
+ 'unit_price': st.session_state[f"price_{idx}"],
+ 'total_price': st.session_state[f"quantity_{idx}"] * st.session_state[f"price_{idx}"]
+ })
+ st.success("تم تحديث البند بنجاح")
+ st.rerun()
+
+ if st.button("حذف البند", key=f"delete_{idx}"):
+ st.session_state.current_project['boq_items'].pop(idx)
+ st.success("تم حذف البند بنجاح")
+ st.rerun()
+
+ df = pd.DataFrame(st.session_state.current_project['boq_items'])
+ column_names = {
+ 'code': 'كود البند',
+ 'description': 'وصف البند',
+ 'unit': 'الوحدة',
+ 'quantity': 'الكمية',
+ 'unit_price': 'سعر الوحدة',
+ 'total_price': 'السعر الإجمالي'
+ }
+ df = df.rename(columns=column_names)
+ st.markdown("### ملخص البنود")
+ st.dataframe(df, use_container_width=True)
+
+ def _render_price_analysis(self):
+ st.title("تحليل الأسعار")
+ tabs = st.tabs(["تحليل البنود", "تحليل التكاليف", "تحليل السوق", "التحليل الذكي"])
+
+ with tabs[0]:
+ if 'current_project' in st.session_state and 'boq_items' in st.session_state.current_project:
+ df = pd.DataFrame(st.session_state.current_project['boq_items'])
+ if not df.empty:
+ # تغيير أسماء الأعمدة إلى العربية
+ df = df.rename(columns={
+ 'code': 'كود البند',
+ 'description': 'وصف البند',
+ 'unit': 'الوحدة',
+ 'quantity': 'الكمية',
+ 'unit_price': 'سعر الوحدة',
+ 'total_price': 'السعر الإجمالي'
+ })
+ st.dataframe(df)
+
+ selected_item = st.selectbox("اختر بند للتحليل", df['كود البند'].tolist())
+ if selected_item:
+ item = df[df['كود البند'] == selected_item].iloc[0]
+ st.write("### تفاصيل البند")
+ st.write(f"الوصف: {item['وصف البند']}")
+ st.write(f"الكمية: {item['الكمية']}")
+ st.write(f"سعر الوحدة: {item['سعر الوحدة']}")
+ st.write(f"الإجمالي: {item['السعر الإجمالي']}")
+ else:
+ st.warning("لا توجد بنود مضافة بعد")
+ else:
+ st.warning("الرجاء إضافة بنود للمشروع أولاً")
+
+ with tabs[1]:
+ self._render_cost_analysis()
+ with tabs[2]:
+ self.market_analysis.render()
+ with tabs[3]:
+ self.smart_analysis.render()
+
+ def _render_cost_analysis(self):
+ if not st.session_state.current_project['boq_items']:
+ st.warning("لا توجد بنود لتحليلها")
+ return
+
+ total_direct_cost = sum(item['total_price'] for item in st.session_state.current_project['boq_items'])
+ indirect_costs = st.session_state.current_project['indirect_costs']
+
+ st.markdown("### ملخص التكاليف")
col1, col2 = st.columns(2)
-
+
with col1:
- include_charts = st.checkbox("تضمين الرسوم البيانية", value=True)
- include_details = st.checkbox("تضمين التفاصيل", value=True)
-
+ st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.2f} ريال")
+ st.metric("المصاريف العامة", f"{total_direct_cost * indirect_costs['overhead']:,.2f} ريال")
+ st.metric("هامش الربح", f"{total_direct_cost * indirect_costs['profit']:,.2f} ريال")
+
with col2:
- report_format = st.radio("تنسيق التقرير", ["PDF", "Excel", "Word"])
- include_logo = st.checkbox("تضمين الشعار", value=True)
-
- # إنشاء التقرير
- if st.button("إنشاء التقرير"):
- st.success(f"تم إنشاء {selected_report} لـ {selected_project} بنجاح")
-
- # عرض نموذج للتقرير
- st.subheader("معاينة التقرير")
-
- if selected_report == "تقرير ملخص المشروع":
- st.write("**معلومات المشروع:**")
- st.write(f"- اسم المشروع: {selected_project}")
- st.write("- الجهة المالكة: وزارة الإسكان")
- st.write("- الموقع: الرياض")
- st.write("- تاريخ البدء: 2023-01-15")
- st.write("- تاريخ الانتهاء المتوقع: 2024-06-30")
- st.write("- الميزانية التقديرية: 25,000,000 ريال")
-
- st.write("**ملخص التكاليف:**")
- st.write("- إجمالي التكاليف المباشرة: 18,500,000 ريال")
- st.write("- إجمالي التكاليف غير المباشرة: 3,700,000 ريال")
- st.write("- هامش الربح: 2,800,000 ريال")
- st.write("- إجمالي سعر المشروع: 25,000,000 ريال")
-
- if include_charts:
- st.subheader("الرسوم البيانية")
- st.bar_chart({"التكاليف المباشرة": 18.5, "التكاليف غير المباشرة": 3.7, "هامش الربح": 2.8})
-
- elif selected_report == "تقرير المحتوى المحلي":
- st.write("**تحليل المحتوى المحلي:**")
- st.write("- نسبة المحتوى المحلي: 42.5%")
- st.write("- النسبة المستهدفة: 40.0%")
- st.write("- الفائض: 2.5%")
-
- st.write("**تفاصيل المحتوى المحلي حسب نوع الموارد:**")
- st.write("- المواد: 35.2%")
- st.write("- المعدات: 48.7%")
- st.write("- العمالة: 52.3%")
- st.write("- مقاولي الباطن: 38.1%")
-
- if include_charts:
- st.subheader("الرسوم البيانية")
- st.bar_chart({"المواد": 35.2, "المعدات": 48.7, "العمالة": 52.3, "مقاولي الباطن": 38.1})
-
- # خيارات تصدير التقرير
- st.download_button(
- label=f"تحميل التقرير بتنسيق {report_format}",
- data="تقرير نموذجي".encode("utf-8"), # ✅ الحل الصحيح
- file_name=f"{selected_report}_{selected_project}.{report_format.lower()}",
- mime="application/octet-stream"
+ st.metric("احتياطي المخاطر", f"{total_direct_cost * indirect_costs['risk']:,.2f} ريال")
+ total_cost = total_direct_cost * (1 + sum(indirect_costs.values()))
+ st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال")
+
+ def _render_risk_analysis(self):
+ st.title("تحليل المخاطر")
+ self.risk_analyzer.render()
+
+ def _render_pricing_strategies(self):
+ st.title("استراتيجيات التسعير")
+ balanced_pricing.render_balanced_strategy()
+
+ def _render_local_content(self):
+ st.title("المحتوى المحلي")
+ st.subheader("نسب المكونات المحلية")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ materials_percentage = st.number_input("نسبة المواد المحلية (%)", 0, 100, 40)
+ equipment_percentage = st.number_input("نسبة المعدات المحلية (%)", 0, 100, 30)
+
+ with col2:
+ labor_percentage = st.number_input("نسبة العمالة المحلية (%)", 0, 100, 80)
+ subcontractors_percentage = st.number_input("نسبة المقاولين المحليين (%)", 0, 100, 50)
+
+ total_local_content = (
+ materials_percentage * 0.4 +
+ equipment_percentage * 0.2 +
+ labor_percentage * 0.3 +
+ subcontractors_percentage * 0.1
+ )
+
+ st.markdown("### نتيجة تحليل المحتوى المحلي")
+ st.metric("نسبة المحتوى المحلي الإجمالية", f"{total_local_content:.1f}%")
+
+ if st.checkbox("عرض التحليل التفصيلي"):
+ data = {
+ 'المكون': ['المواد', 'المعدات', 'العمالة', 'المقاولين'],
+ 'النسبة المحلية': [materials_percentage, equipment_percentage,
+ labor_percentage, subcontractors_percentage]
+ }
+ df = pd.DataFrame(data)
+ st.bar_chart(df.set_index('المكون'))
+
+ def _render_final_boq(self):
+ st.title("جدول الكميات النهائي")
+
+ if not st.session_state.current_project['boq_items']:
+ st.warning("لا توجد بنود في جدول الكميات")
+ return
+
+ # Create fresh DataFrame with numeric values
+ df = pd.DataFrame(st.session_state.current_project['boq_items'])
+
+ # Convert quantity and prices to float
+ df['quantity'] = pd.to_numeric(df['quantity'], errors='coerce')
+ df['unit_price'] = pd.to_numeric(df['unit_price'], errors='coerce')
+
+ # Calculate total price
+ df['total_price'] = df['quantity'] * df['unit_price']
+
+ # Calculate grand total
+ total = df['total_price'].sum()
+
+ # Format numbers for display
+ df['quantity'] = df['quantity'].apply(lambda x: '{:.2f}'.format(x))
+ df['unit_price'] = df['unit_price'].apply(lambda x: '{:.2f}'.format(x))
+ df['total_price'] = df['total_price'].apply(lambda x: '{:.2f}'.format(x))
+
+ # Rename columns to Arabic
+ # Get local content percentage if available
+ local_content = 0
+ if hasattr(st.session_state, 'local_content'):
+ materials_percentage = st.session_state.local_content.get('materials_local', 0.4) * 100
+ equipment_percentage = st.session_state.local_content.get('equipment_local', 0.3) * 100
+ labor_percentage = st.session_state.local_content.get('labor_local', 0.8) * 100
+ subcontractors_percentage = st.session_state.local_content.get('subcontractors_local', 0.5) * 100
+
+ local_content = (
+ materials_percentage * 0.4 +
+ equipment_percentage * 0.2 +
+ labor_percentage * 0.3 +
+ subcontractors_percentage * 0.1
)
-
- def _render_settings_module(self):
- """
- عرض وحدة الإعدادات
- """
- st.title("الإعدادات")
-
- tab1, tab2, tab3, tab4 = st.tabs(["إعدادات عامة", "إعدادات المستخدم", "إعدادات النظام", "النسخ الاحتياطي"])
-
- with tab1:
- st.subheader("الإعدادات العامة")
-
- # إعدادات اللغة
- st.write("**إعدادات اللغة:**")
- language = st.selectbox("اللغة", ["العربية", "English"])
-
- # إعدادات العرض
- st.write("**إعدادات العرض:**")
- theme = st.selectbox("السمة", ["الافتراضية", "داكنة", "فاتحة"])
- sidebar_position = st.radio("موضع الشريط الجانبي", ["يمين", "يسار"])
-
- # إعدادات التنبيهات
- st.write("**إعدادات التنبيهات:**")
- enable_notifications = st.checkbox("تفعيل التنبيهات", value=True)
- notification_sound = st.checkbox("تفعيل صوت التنبيهات", value=True)
-
- if st.button("حفظ الإعدادات العامة"):
- st.success("تم حفظ الإعدادات العامة بنجاح")
-
- with tab2:
- st.subheader("إعدادات المستخدم")
-
- # معلومات المستخدم
- st.write("**معلومات المستخدم:**")
- user_name = st.text_input("اسم المستخدم", value="محمد أحمد")
- user_email = st.text_input("البريد الإلكتروني", value="mohammed@example.com")
- user_phone = st.text_input("رقم الهاتف", value="0555555555")
-
- # تغيير كلمة المرور
- st.write("**تغيير كلمة المرور:**")
- current_password = st.text_input("كلمة المرور الحالية", type="password")
- new_password = st.text_input("كلمة المرور الجديدة", type="password")
- confirm_password = st.text_input("تأكيد كلمة المرور الجديدة", type="password")
-
- if st.button("حفظ إعدادات المستخدم"):
- if new_password and new_password == confirm_password:
- st.success("تم حفظ إعدادات المستخدم بنجاح")
- elif new_password and new_password != confirm_password:
- st.error("كلمة المرور الجديدة وتأكيدها غير متطابقين")
- else:
- st.success("تم حفظ إعدادات المستخدم بنجاح")
-
- with tab3:
- st.subheader("إعدادات النظام")
-
- # إعدادات قاعدة البيانات
- st.write("**إعدادات قاعدة البيانات:**")
- db_type = st.selectbox("نوع قاعدة البيانات", ["SQLite", "MySQL", "PostgreSQL"])
- db_host = st.text_input("عنوان الخادم", value="localhost")
- db_port = st.text_input("المنفذ", value="3306")
- db_name = st.text_input("اسم قاعدة البيانات", value="tender_analysis_db")
-
- # إعدادات النسخ الاحتياطي التلقائي
- st.write("**إعدادات النسخ الاحتياطي التلقائي:**")
- auto_backup = st.checkbox("تفعيل النسخ الاحتياطي التلقائي", value=True)
- backup_frequency = st.selectbox("تكرار النسخ الاحتياطي", ["يومي", "أسبوعي", "شهري"])
- backup_time = st.time_input("وقت النسخ الاحتياطي")
-
- if st.button("حفظ إعدادات النظام"):
- st.success("تم حفظ إعدادات النظام بنجاح")
-
- with tab4:
- st.subheader("النسخ الاحتياطي")
-
- # إنشاء نسخة احتياطية
- st.write("**إنشاء نسخة احتياطية:**")
- backup_type = st.radio("نوع النسخ الاحتياطي", ["كامل", "جزئي"])
-
- if backup_type == "جزئي":
- st.multiselect(
- "اختر البيانات المراد نسخها",
- ["المشاريع", "المناقصات", "الموارد", "التسعير", "المستخدمين", "الإعدادات"]
- )
-
- if st.button("إنشاء نسخة احتياطية"):
- st.success("تم إنشاء النسخة الاحتياطية بنجاح")
- st.download_button(
- label="تحميل النسخة الاحتياطية",
- data="تحميل النسخة الاحتياطية".encode("utf-8"), # ✅ الحل الصحيح
- file_name="backup_2025-04-01.zip",
- mime="application/zip"
- )
-
- # استعادة نسخة احتياطية
- st.write("**استعادة نسخة احتياطية:**")
- uploaded_file = st.file_uploader("اختر ملف النسخة الاحتياطية", type=["zip"])
-
- if uploaded_file is not None:
- if st.button("استعادة النسخة الاحتياطية"):
- st.success("تم استعادة النسخة الاحتياطية بنجاح")
-
-# تشغيل التطبيق المتكامل
-if __name__ == "__main__":
- app = IntegratedApp()
- app.run()
+
+ # Add local content column
+ df['نسبة المحتوى المحلي'] = local_content
+
+ df = df.rename(columns={
+ 'code': 'الكود',
+ 'description': 'الوصف',
+ 'unit': 'الوحدة',
+ 'quantity': 'الكمية',
+ 'unit_price': 'سعر الوحدة',
+ 'total_price': 'السعر الإجمالي'
+ })
+
+ # Add total row to display dataframe
+ total_row = pd.DataFrame([{
+ 'الكود': 'الإجمالي',
+ 'الوصف': '',
+ 'الوحدة': '',
+ 'الكمية': '',
+ 'سعر الوحدة': '',
+ 'السعر الإجمالي': f"{total:,.2f}"
+ }])
+
+ # Combine original dataframe with total row
+ df_with_total = pd.concat([df, total_row], ignore_index=True)
+
+ st.dataframe(
+ df_with_total,
+ use_container_width=True,
+ hide_index=True,
+ column_config={
+ "الكود": st.column_config.TextColumn("الكود", width="small", help="كود البند"),
+ "الوصف": st.column_config.TextColumn("الوصف", width="medium", help="وصف البند"),
+ "الوحدة": st.column_config.TextColumn("الوحدة", width="small", help="وحدة القياس"),
+ "الكمية": st.column_config.NumberColumn("الكمية", width="small", help="كمية البند", format="%.2f"),
+ "سعر الوحدة": st.column_config.NumberColumn("سعر الوحدة", width="small", help="سعر الوحدة", format="%.2f"),
+ "السعر الإجمالي": st.column_config.NumberColumn("السعر الإجمالي", width="small", help="السعر الإجمالي", format="%.2f"),
+ "نسبة المحتوى المحلي": st.column_config.NumberColumn("نسبة المحتوى المحلي", width="small", help="نسبة المحتوى المحلي", format="%.1f%%")
+ }
+ )
+ st.metric("إجمالي جدول الكميات", f"{total:,.2f} ريال")
+
+ # Show saved pricing history with export option
+ if st.button("عرض التسعيرات المحفوظة", key="show_saved_pricing"):
+ if 'saved_pricing' in st.session_state and st.session_state.saved_pricing:
+ st.subheader("التسعيرات المحفوظة")
+
+ # Create selection for saved pricing
+ pricing_options = [f"{p['project_name']} - {p['timestamp']}" for p in st.session_state.saved_pricing]
+ selected_pricing = st.selectbox("اختر التسعير", pricing_options, key="pricing_select")
+
+ if selected_pricing:
+ selected_idx = pricing_options.index(selected_pricing)
+ pricing = st.session_state.saved_pricing[selected_idx]
+
+ st.write(f"إجمالي السعر: {pricing['total_price']:,.2f} ريال")
+ st.write(f"نسبة المحتوى المحلي: {pricing['local_content']:.1f}%")
+ df = pd.DataFrame(pricing['items'])
+ st.dataframe(df)
+
+ # Export selected pricing to Excel
+ if st.button("تصدير التسعير المحدد إلى Excel", key="export_selected_pricing"):
+ try:
+ export_path = "data/exports"
+ os.makedirs(export_path, exist_ok=True)
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ excel_file = f"{export_path}/saved_pricing_{timestamp}.xlsx"
+
+ # Create Excel writer
+ with pd.ExcelWriter(excel_file, engine='openpyxl') as writer:
+ df.to_excel(writer, index=False, sheet_name='التسعير المحفوظ')
+ worksheet = writer.sheets['التسعير المحفوظ']
+
+ # Add summary information
+ worksheet['A1'] = f"اسم المشروع: {pricing['project_name']}"
+ worksheet['A2'] = f"التاريخ: {pricing['timestamp']}"
+ worksheet['A3'] = f"إجمالي السعر: {pricing['total_price']:,.2f} ريال"
+ worksheet['A4'] = f"نسبة المحتوى المحلي: {pricing['local_content']:.1f}%"
+
+ # Provide download button
+ with open(excel_file, 'rb') as f:
+ excel_data = f.read()
+ st.download_button(
+ label="تحميل ملف Excel",
+ data=excel_data,
+ file_name=f"saved_pricing_{timestamp}.xlsx",
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ )
+ st.success("تم تصدير التسعير المحدد بنجاح!")
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء التصدير: {str(e)}")
+ else:
+ st.info("لا توجد تسعيرات محفوظة")
+
+ col1, col2, col3, col4 = st.columns(4)
+
+ # Add save button
+ with col1:
+ if st.button("💾 حفظ التسعير", key="save_pricing_btn"):
+ try:
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ pricing_data = {
+ 'timestamp': timestamp,
+ 'project_name': st.session_state.current_project.get('name', 'مشروع جديد'),
+ 'total_price': total,
+ 'items': df.to_dict('records'),
+ 'local_content': local_content
+ }
+
+ # Save to session state database
+ if 'saved_pricing' not in st.session_state:
+ st.session_state.saved_pricing = []
+
+ # Check for duplicates before saving
+ is_duplicate = False
+ for saved_pricing in st.session_state.saved_pricing:
+ if (saved_pricing['project_name'] == pricing_data['project_name'] and
+ saved_pricing['total_price'] == pricing_data['total_price'] and
+ saved_pricing['timestamp'] == pricing_data['timestamp']):
+ is_duplicate = True
+ break
+
+ if not is_duplicate:
+ st.session_state.saved_pricing.append(pricing_data)
+ st.success("تم حفظ التسعير بنجاح!")
+ else:
+ st.warning("هذا التسعير موجود بالفعل!")
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء الحفظ: {str(e)}")
+
+ # Export button
+ with col2:
+ st.empty() # Placeholder for future functionality
+
+ with col3:
+ if st.button("تصدير إلى Excel"):
+ try:
+ export_path = "data/exports"
+ os.makedirs(export_path, exist_ok=True)
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ excel_file = f"{export_path}/boq_{timestamp}.xlsx"
+
+ # Add a total row
+ total_row = pd.DataFrame([{
+ 'الكود': 'الإجمالي',
+ 'الوصف': '',
+ 'الوحدة': '',
+ 'الكمية': '',
+ 'سعر الوحدة': '',
+ 'السعر الإجمالي': f"{total:,.2f}"
+ }])
+
+ # Combine original dataframe with total row
+ df_with_total = pd.concat([df, total_row], ignore_index=True)
+
+ # Write to Excel with styling
+ with pd.ExcelWriter(excel_file, engine='openpyxl') as writer:
+ df_with_total.to_excel(writer, index=False, sheet_name='جدول الكميات')
+ worksheet = writer.sheets['جدول الكميات']
+ # Style the total row
+ for col in range(1, worksheet.max_column + 1):
+ cell = worksheet.cell(row=len(df_with_total), column=col)
+ cell.font = openpyxl.styles.Font(bold=True)
+
+ with open(excel_file, 'rb') as f:
+ excel_data = f.read()
+ st.download_button(
+ label="تحميل ملف Excel",
+ data=excel_data,
+ file_name=f"boq_{timestamp}.xlsx",
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ )
+ st.success("تم تصدير الملف بنجاح!")
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء التصدير: {str(e)}")
+
+ with col2:
+ if st.button("تصدير إلى PDF"):
+ try:
+ export_path = "data/exports"
+ os.makedirs(export_path, exist_ok=True)
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ pdf_file = f"{export_path}/boq_{timestamp}.pdf"
+
+ # Create HTML with Arabic support and styling
+ html = f"""
+
+
+
+
+
+ جدول الكميات
+ {df.to_html(index=False)}
+ إجمالي جدول الكميات: {total:,.2f} ريال
+
+
+ """
+
+ # Configure PDF options
+ options = {
+ 'page-size': 'A4',
+ 'margin-top': '1.0in',
+ 'margin-right': '0.75in',
+ 'margin-bottom': '1.0in',
+ 'margin-left': '0.75in',
+ 'encoding': 'UTF-8',
+ 'enable-local-file-access': None
+ }
+
+ # Generate PDF
+ pdfkit.from_string(html, pdf_file, options=options)
+
+ # Provide download button
+ with open(pdf_file, 'rb') as f:
+ pdf_data = f.read()
+ st.download_button(
+ label="تحميل ملف PDF",
+ data=pdf_data,
+ file_name=f"boq_{timestamp}.pdf",
+ mime="application/pdf"
+ )
+ st.success("تم تصدير الملف بنجاح!")
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء التصدير: {str(e)}")
+
+
+ def _render_navigation(self):
+ can_proceed = self._validate_current_stage()
+ st.markdown("---")
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ nav_col1, nav_col2, nav_col3 = st.columns([2, 4, 2])
+
+ with nav_col1:
+ if st.session_state.pricing_stage > 1:
+ st.button("⮕ السابق", key="prev_button", help="العودة للمرحلة السابقة", on_click=lambda: setattr(st.session_state, 'pricing_stage', st.session_state.pricing_stage - 1))
+
+ with nav_col2:
+ st.markdown(f"المرحلة {st.session_state.pricing_stage} من 8 ", unsafe_allow_html=True)
+
+ with nav_col3:
+ if st.session_state.pricing_stage < 8:
+ st.button("التالي ⬅️", key="next_button", help="الانتقال للمرحلة التالية", disabled=not can_proceed, on_click=lambda: setattr(st.session_state, 'pricing_stage', st.session_state.pricing_stage + 1))
+
+ progress = (st.session_state.pricing_stage - 1) / 7
+ st.progress(progress, text=f"اكتمال {int(progress * 100)}% من مراحل التسعير")
+
+ progress = (st.session_state.pricing_stage - 1) / 7
+ st.progress(progress, text=f"اكتمال {int(progress * 100)}% من مراحل التسعير")
+
+ def _validate_current_stage(self):
+ if st.session_state.pricing_stage == 1:
+ return True
+ elif st.session_state.pricing_stage == 2:
+ return len(st.session_state.current_project.get('boq_items', [])) > 0
+ elif st.session_state.pricing_stage == 3:
+ return True
+ elif st.session_state.pricing_stage == 4:
+ return True
+ return True
+
+ def _render_reference_guides(self):
+ self.reference_guides.render()
\ No newline at end of file
diff --git a/pricing_system/integration_framework.py b/pricing_system/integration_framework.py
index 3083de0fd78f30529fe287af50344247656abaf4..a6659e1d572a8777d4b2975999d5294c85c9eb4e 100644
--- a/pricing_system/integration_framework.py
+++ b/pricing_system/integration_framework.py
@@ -17,7 +17,7 @@ from modules.catalogs.materials_catalog import MaterialsCatalog
from modules.catalogs.labor_catalog import LaborCatalog
from modules.catalogs.subcontractors_catalog import SubcontractorsCatalog
from modules.analysis.smart_price_analysis import SmartPriceAnalysis
-from modules.indirect_support.indirect_support_management import IndirectSupportManagement
+from pricing_system.modules.indirect_support.overheads import IndirectSupportManagement
from modules.pricing_strategies.pricing_strategies import PricingStrategies
class IntegrationFramework:
@@ -43,7 +43,8 @@ class IntegrationFramework:
# تهيئة وحدات التحليل والتسعير
st.session_state.smart_price_analysis = SmartPriceAnalysis()
st.session_state.indirect_support = IndirectSupportManagement()
- st.session_state.pricing_strategies = PricingStrategies()
+ if 'pricing_strategies' not in st.session_state or not isinstance(st.session_state.pricing_strategies, PricingStrategies):
+ st.session_state.pricing_strategies = PricingStrategies()
# تهيئة بيانات المشروع
st.session_state.project_data = {
@@ -1381,3 +1382,49 @@ class IntegrationFramework:
st.error(f"حدث خطأ أثناء تحليل المحتوى المحلي: {local_content_analysis['message']}")
else:
st.warning("لا توجد بنود في المشروع")
+from pricing_system.modules.analysis.market_analysis import MarketAnalysis
+from pricing_system.modules.analysis.smart_price_analysis import SmartPriceAnalysis
+from modules.risk_analysis.risk_analyzer import RiskAnalyzer
+
+class IntegratedPricingSystem:
+ def __init__(self):
+ self.market_analysis = MarketAnalysis()
+ self.smart_analysis = SmartPriceAnalysis()
+ self.risk_analyzer = RiskAnalyzer()
+
+ if 'integrated_data' not in st.session_state:
+ self._initialize_integrated_data()
+
+ def _initialize_integrated_data(self):
+ st.session_state.integrated_data = {
+ 'market_trends': {},
+ 'risk_analysis': {},
+ 'price_analysis': {},
+ 'local_content': {}
+ }
+
+ def render(self):
+ st.title("نظام التسعير المتكامل")
+
+ tabs = st.tabs([
+ "تحليل السوق",
+ "تحليل المخاطر",
+ "التحليل الذكي للأسعار",
+ "المحتوى المحلي"
+ ])
+
+ with tabs[0]:
+ self.market_analysis.render()
+
+ with tabs[1]:
+ self.risk_analyzer.render_risk_analysis(st.session_state.project_data)
+
+ with tabs[2]:
+ self.smart_analysis.render()
+
+ with tabs[3]:
+ self._render_local_content()
+
+ def _render_local_content(self):
+ st.header("تحليل المحتوى المحلي")
+ # إضافة تحليل المحتوى المحلي هنا
diff --git a/pricing_system/modules/analysis/market_analysis.py b/pricing_system/modules/analysis/market_analysis.py
new file mode 100644
index 0000000000000000000000000000000000000000..163bf50f00e908f2a7a63c5d3436770c8a621fa4
--- /dev/null
+++ b/pricing_system/modules/analysis/market_analysis.py
@@ -0,0 +1,114 @@
+"""
+وحدة تحليل السوق والأسعار التاريخية
+"""
+import streamlit as st
+import pandas as pd
+import numpy as np
+import plotly.express as px
+from datetime import datetime, timedelta
+
+class MarketAnalysis:
+ def __init__(self):
+ if 'market_data' not in st.session_state:
+ self._initialize_market_data()
+
+ def _initialize_market_data(self):
+ st.session_state.market_data = {
+ 'price_indices': {},
+ 'historical_prices': {},
+ 'market_trends': {}
+ }
+
+ def render(self):
+ st.header("تحليل السوق والأسعار")
+
+ tabs = st.tabs([
+ "مؤشرات الأسعار",
+ "التحليل التاريخي",
+ "اتجاهات السوق"
+ ])
+
+ with tabs[0]:
+ self._render_price_indices()
+
+ with tabs[1]:
+ self._render_historical_analysis()
+
+ with tabs[2]:
+ self._render_market_trends()
+
+ def _render_price_indices(self):
+ st.subheader("مؤشرات الأسعار الرئيسية")
+
+ # عرض مؤشرات المواد الرئيسية
+ materials = {
+ 'الحديد': {'current': 3200, 'change': 5.2},
+ 'الأسمنت': {'current': 400, 'change': -2.1},
+ 'الخرسانة': {'current': 250, 'change': 1.5},
+ 'الأسفلت': {'current': 2800, 'change': 3.8}
+ }
+
+ cols = st.columns(4)
+ for i, (material, data) in enumerate(materials.items()):
+ with cols[i]:
+ st.metric(
+ material,
+ f"{data['current']} ريال",
+ f"{data['change']}%"
+ )
+
+ def _render_historical_analysis(self):
+ st.subheader("تحليل الأسعار التاريخي")
+
+ # إنشاء بيانات تاريخية افتراضية
+ dates = pd.date_range(start='2023-01-01', end='2023-12-31', freq='M')
+ materials = ['الحديد', 'الأسمنت', 'الخرسانة', 'الأسفلت']
+
+ data = []
+ for material in materials:
+ base_price = 1000 if material == 'الحديد' else 500
+ for date in dates:
+ data.append({
+ 'التاريخ': date,
+ 'المادة': material,
+ 'السعر': base_price * (1 + 0.1 * np.random.randn())
+ })
+
+ df = pd.DataFrame(data)
+
+ # رسم بياني للأسعار التاريخية
+ fig = px.line(
+ df,
+ x='التاريخ',
+ y='السعر',
+ color='المادة',
+ title='تطور الأسعار خلال العام'
+ )
+ st.plotly_chart(fig)
+
+ def _render_market_trends(self):
+ st.subheader("اتجاهات السوق والتوقعات")
+
+ # تحليل الاتجاهات
+ trends = {
+ 'قصير المدى': {
+ 'الحديد': 'صعود',
+ 'الأسمنت': 'هبوط',
+ 'الخرسانة': 'ثبات',
+ 'الأسفلت': 'صعود'
+ },
+ 'متوسط المدى': {
+ 'الحديد': 'ثبات',
+ 'الأسمنت': 'صعود',
+ 'الخرسانة': 'صعود',
+ 'الأسفلت': 'ثبات'
+ },
+ 'طويل المدى': {
+ 'الحديد': 'صعود',
+ 'الأسمنت': 'صعود',
+ 'الخرسانة': 'صعود',
+ 'الأسفلت': 'صعود'
+ }
+ }
+
+ st.dataframe(pd.DataFrame(trends))
\ No newline at end of file
diff --git a/pricing_system/modules/analysis/smart_price_analysis.py b/pricing_system/modules/analysis/smart_price_analysis.py
index 1741c4f8b510ee2d4de1f1f33de22a27ae381001..9240c84ebfe536a1e628eacd2ea3debb60176dd5 100644
--- a/pricing_system/modules/analysis/smart_price_analysis.py
+++ b/pricing_system/modules/analysis/smart_price_analysis.py
@@ -15,23 +15,24 @@ import math
class SmartPriceAnalysis:
"""فئة التحليل الذكي للأسعار"""
-
+
def __init__(self):
"""تهيئة وحدة التحليل الذكي للأسعار"""
-
+
# تهيئة حالة الجلسة للتحليل الذكي للأسعار
if 'smart_price_analysis' not in st.session_state:
self._initialize_smart_price_analysis()
-
+
# الوصول إلى كتالوجات الموارد
self.equipment_catalog = self._get_equipment_catalog()
self.materials_catalog = self._get_materials_catalog()
self.labor_catalog = self._get_labor_catalog()
self.subcontractors_catalog = self._get_subcontractors_catalog()
-
+ self.cost_breakdown = {} #Added this line
+
def _initialize_smart_price_analysis(self):
"""تهيئة بيانات التحليل الذكي للأسعار"""
-
+
# إنشاء بيانات افتراضية للتحليل الذكي للأسعار
st.session_state.smart_price_analysis = {
"price_components": {
@@ -64,14 +65,14 @@ class SmartPriceAnalysis:
"analysis_history": [], # سجل تحليلات الأسعار
"current_item": None # البند الحالي قيد التحليل
}
-
+
# إنشاء بيانات افتراضية لبنود جدول الكميات
if 'boq_items' not in st.session_state:
self._initialize_boq_items()
-
+
def _initialize_boq_items(self):
"""تهيئة بيانات بنود جدول الكميات"""
-
+
# إنشاء بيانات افتراضية لبنود جدول الكميات
boq_items = [
{
@@ -195,75 +196,75 @@ class SmartPriceAnalysis:
"components": {}
}
]
-
+
# تخزين البيانات في حالة الجلسة
st.session_state.boq_items = pd.DataFrame(boq_items)
-
+
def _get_equipment_catalog(self):
"""الحصول على كتالوج المعدات"""
-
+
# التحقق من وجود كتالوج المعدات في حالة الجلسة
if 'equipment_catalog' in st.session_state:
return st.session_state.equipment_catalog
-
+
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
equipment_data = []
-
+
# تخزين البيانات في حالة الجلسة
st.session_state.equipment_catalog = pd.DataFrame(equipment_data)
-
+
return st.session_state.equipment_catalog
-
+
def _get_materials_catalog(self):
"""الحصول على كتالوج المواد"""
-
+
# التحقق من وجود كتالوج المواد في حالة الجلسة
if 'materials_catalog' in st.session_state:
return st.session_state.materials_catalog
-
+
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
materials_data = []
-
+
# تخزين البيانات في حالة الجلسة
st.session_state.materials_catalog = pd.DataFrame(materials_data)
-
+
return st.session_state.materials_catalog
-
+
def _get_labor_catalog(self):
"""الحصول على كتالوج العمالة"""
-
+
# التحقق من وجود كتالوج العمالة في حالة الجلسة
if 'labor_catalog' in st.session_state:
return st.session_state.labor_catalog
-
+
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
labor_data = []
-
+
# تخزين البيانات في حالة الجلسة
st.session_state.labor_catalog = pd.DataFrame(labor_data)
-
+
return st.session_state.labor_catalog
-
+
def _get_subcontractors_catalog(self):
"""الحصول على كتالوج مقاولي الباطن"""
-
+
# التحقق من وجود كتالوج مقاولي الباطن في حالة الجلسة
if 'subcontractors_catalog' in st.session_state:
return st.session_state.subcontractors_catalog
-
+
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
subcontractors_data = []
-
+
# تخزين البيانات في حالة الجلسة
st.session_state.subcontractors_catalog = pd.DataFrame(subcontractors_data)
-
+
return st.session_state.subcontractors_catalog
-
+
def render(self):
"""عرض واجهة التحليل الذكي للأسعار"""
-
+
st.markdown("## التحليل الذكي للأسعار")
-
+
# إنشاء تبويبات لعرض التحليل الذكي للأسعار
tabs = st.tabs([
"تحليل البنود",
@@ -271,152 +272,152 @@ class SmartPriceAnalysis:
"تقارير التحليل",
"المحتوى المحلي"
])
-
+
with tabs[0]:
self._render_item_analysis_tab()
-
+
with tabs[1]:
self._render_analysis_settings_tab()
-
+
with tabs[2]:
self._render_analysis_reports_tab()
-
+
with tabs[3]:
self._render_local_content_tab()
-
+
def _render_item_analysis_tab(self):
"""عرض تبويب تحليل البنود"""
-
+
st.markdown("### تحليل بنود جدول الكميات")
-
+
# استخراج البيانات
boq_items = st.session_state.boq_items
-
+
# إنشاء فلاتر للعرض
col1, col2, col3 = st.columns(3)
-
+
with col1:
# فلتر حسب الفئة
categories = ["الكل"] + sorted(boq_items["category"].unique().tolist())
selected_category = st.selectbox("اختر فئة البند", categories, key="item_analysis_category")
-
+
with col2:
# فلتر حسب الفئة الفرعية
if selected_category != "الكل":
subcategories = ["الكل"] + sorted(boq_items[boq_items["category"] == selected_category]["subcategory"].unique().tolist())
else:
subcategories = ["الكل"] + sorted(boq_items["subcategory"].unique().tolist())
-
+
selected_subcategory = st.selectbox("اختر التخصص", subcategories, key="item_analysis_subcategory")
-
+
with col3:
# فلتر حسب حالة التحليل
analysis_status = ["الكل", "تم التحليل", "لم يتم التحليل"]
selected_status = st.selectbox("اختر حالة التحليل", analysis_status, key="item_analysis_status")
-
+
# تطبيق الفلاتر
filtered_df = boq_items.copy()
-
+
if selected_category != "الكل":
filtered_df = filtered_df[filtered_df["category"] == selected_category]
-
+
if selected_subcategory != "الكل":
filtered_df = filtered_df[filtered_df["subcategory"] == selected_subcategory]
-
+
if selected_status != "الكل":
if selected_status == "تم التحليل":
filtered_df = filtered_df[filtered_df["analyzed"] == True]
else:
filtered_df = filtered_df[filtered_df["analyzed"] == False]
-
+
# عرض البيانات
if not filtered_df.empty:
# عرض عدد النتائج
st.info(f"تم العثور على {len(filtered_df)} بند")
-
+
# إنشاء جدول للعرض
display_df = filtered_df[["id", "description", "unit", "quantity", "unit_price", "total_price", "analyzed"]].copy()
display_df.columns = ["الكود", "الوصف", "الوحدة", "الكمية", "سعر الوحدة", "الإجمالي", "تم التحليل"]
display_df["تم التحليل"] = display_df["تم التحليل"].map({True: "✅", False: "❌"})
-
+
# عرض الجدول
st.dataframe(display_df, use_container_width=True)
-
+
# اختيار بند للتحليل
st.markdown("#### اختر بند للتحليل")
-
+
selected_item_id = st.selectbox("اختر كود البند", filtered_df["id"].tolist(), key="item_analysis_selected_id")
-
+
# استخراج البند المختار
selected_item = filtered_df[filtered_df["id"] == selected_item_id].iloc[0]
-
+
# عرض تفاصيل البند
st.markdown(f"**البند:** {selected_item['description']}")
st.markdown(f"**الوحدة:** {selected_item['unit']} | **الكمية:** {selected_item['quantity']} | **سعر الوحدة:** {selected_item['unit_price']} ريال | **الإجمالي:** {selected_item['total_price']} ريال")
-
+
# تحليل البند
st.markdown("#### تحليل البند")
-
+
# التحقق من حالة التحليل
if selected_item["analyzed"]:
# عرض نتائج التحليل السابق
st.success("تم تحليل هذا البند مسبقاً")
-
+
# استخراج مكونات البند
components = selected_item["components"]
-
+
# عرض مكونات البند
self._display_item_components(selected_item)
-
+
# زر إعادة التحليل
if st.button("إعادة تحليل البند", key="reanalyze_button"):
# تعيين البند الحالي
st.session_state.smart_price_analysis["current_item"] = selected_item.to_dict()
-
+
# إعادة توجيه إلى صفحة التحليل
- st.experimental_rerun()
+ st.rerun()
else:
# تحليل البند لأول مرة
if st.button("تحليل البند", key="analyze_button"):
# تعيين البند الحالي
st.session_state.smart_price_analysis["current_item"] = selected_item.to_dict()
-
+
# إعادة توجيه إلى صفحة التحليل
- st.experimental_rerun()
-
+ st.rerun()
+
# التحقق من وجود بند حالي قيد التحليل
current_item = st.session_state.smart_price_analysis["current_item"]
-
+
if current_item and current_item["id"] == selected_item_id:
# عرض نموذج التحليل
self._render_analysis_form(current_item)
else:
st.warning("لا يوجد بنود تطابق معايير البحث")
-
+
def _render_analysis_form(self, item):
"""عرض نموذج تحليل البند"""
-
+
st.markdown("### تحليل البند")
st.markdown(f"**البند:** {item['description']}")
st.markdown(f"**الوحدة:** {item['unit']} | **الكمية:** {item['quantity']} | **سعر الوحدة:** {item['unit_price']} ريال | **الإجمالي:** {item['total_price']} ريال")
-
+
# استخراج نسب المكونات
price_components = st.session_state.smart_price_analysis["price_components"]
-
+
# حساب قيم المكونات
materials_value = item["unit_price"] * price_components["materials"]
equipment_value = item["unit_price"] * price_components["equipment"]
labor_value = item["unit_price"] * price_components["labor"]
subcontractors_value = item["unit_price"] * price_components["subcontractors"]
-
+
# إنشاء نموذج التحليل
with st.form("analysis_form"):
st.markdown("#### تحليل سعر الوحدة")
-
+
# المواد
st.markdown("##### المواد")
materials_col1, materials_col2 = st.columns(2)
-
+
with materials_col1:
materials_percentage = st.slider(
"نسبة المواد من سعر الوحدة",
@@ -427,7 +428,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="materials_percentage"
) * 100
-
+
with materials_col2:
materials_amount = st.number_input(
"قيمة المواد (ريال)",
@@ -436,27 +437,27 @@ class SmartPriceAnalysis:
step=10.0,
key="materials_amount"
)
-
+
# إضافة المواد
materials_items = []
-
+
st.markdown("إضافة المواد")
-
+
for i in range(3): # السماح بإضافة 3 مواد كحد أقصى
material_col1, material_col2, material_col3, material_col4 = st.columns([3, 1, 1, 1])
-
+
with material_col1:
material_name = st.text_input(
"اسم المادة",
key=f"material_name_{i}"
)
-
+
with material_col2:
material_unit = st.text_input(
"الوحدة",
key=f"material_unit_{i}"
)
-
+
with material_col3:
material_quantity = st.number_input(
"الكمية",
@@ -464,7 +465,7 @@ class SmartPriceAnalysis:
step=0.1,
key=f"material_quantity_{i}"
)
-
+
with material_col4:
material_price = st.number_input(
"السعر",
@@ -472,7 +473,7 @@ class SmartPriceAnalysis:
step=10.0,
key=f"material_price_{i}"
)
-
+
if material_name and material_unit and material_quantity > 0 and material_price > 0:
materials_items.append({
"name": material_name,
@@ -481,11 +482,11 @@ class SmartPriceAnalysis:
"price": material_price,
"total": material_quantity * material_price
})
-
+
# المعدات
st.markdown("##### المعدات")
equipment_col1, equipment_col2 = st.columns(2)
-
+
with equipment_col1:
equipment_percentage = st.slider(
"نسبة المعدات من سعر الوحدة",
@@ -496,7 +497,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="equipment_percentage"
) * 100
-
+
with equipment_col2:
equipment_amount = st.number_input(
"قيمة المعدات (ريال)",
@@ -505,27 +506,27 @@ class SmartPriceAnalysis:
step=10.0,
key="equipment_amount"
)
-
+
# إضافة المعدات
equipment_items = []
-
+
st.markdown("إضافة المعدات")
-
+
for i in range(3): # السماح بإضافة 3 معدات كحد أقصى
equipment_col1, equipment_col2, equipment_col3, equipment_col4 = st.columns([3, 1, 1, 1])
-
+
with equipment_col1:
equipment_name = st.text_input(
"اسم المعدة",
key=f"equipment_name_{i}"
)
-
+
with equipment_col2:
equipment_unit = st.text_input(
"الوحدة",
key=f"equipment_unit_{i}"
)
-
+
with equipment_col3:
equipment_quantity = st.number_input(
"الكمية",
@@ -533,7 +534,7 @@ class SmartPriceAnalysis:
step=0.1,
key=f"equipment_quantity_{i}"
)
-
+
with equipment_col4:
equipment_price = st.number_input(
"السعر",
@@ -541,7 +542,7 @@ class SmartPriceAnalysis:
step=10.0,
key=f"equipment_price_{i}"
)
-
+
if equipment_name and equipment_unit and equipment_quantity > 0 and equipment_price > 0:
equipment_items.append({
"name": equipment_name,
@@ -550,11 +551,11 @@ class SmartPriceAnalysis:
"price": equipment_price,
"total": equipment_quantity * equipment_price
})
-
+
# العمالة
st.markdown("##### العمالة")
labor_col1, labor_col2 = st.columns(2)
-
+
with labor_col1:
labor_percentage = st.slider(
"نسبة العمالة من سعر الوحدة",
@@ -565,7 +566,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="labor_percentage"
) * 100
-
+
with labor_col2:
labor_amount = st.number_input(
"قيمة العمالة (ريال)",
@@ -574,27 +575,27 @@ class SmartPriceAnalysis:
step=10.0,
key="labor_amount"
)
-
+
# إضافة العمالة
labor_items = []
-
+
st.markdown("إضافة العمالة")
-
+
for i in range(3): # السماح بإضافة 3 عمال كحد أقصى
labor_col1, labor_col2, labor_col3, labor_col4 = st.columns([3, 1, 1, 1])
-
+
with labor_col1:
labor_name = st.text_input(
"المسمى الوظيفي",
key=f"labor_name_{i}"
)
-
+
with labor_col2:
labor_unit = st.text_input(
"الوحدة",
key=f"labor_unit_{i}"
)
-
+
with labor_col3:
labor_quantity = st.number_input(
"الكمية",
@@ -602,7 +603,7 @@ class SmartPriceAnalysis:
step=0.1,
key=f"labor_quantity_{i}"
)
-
+
with labor_col4:
labor_price = st.number_input(
"السعر",
@@ -610,7 +611,7 @@ class SmartPriceAnalysis:
step=10.0,
key=f"labor_price_{i}"
)
-
+
if labor_name and labor_unit and labor_quantity > 0 and labor_price > 0:
labor_items.append({
"name": labor_name,
@@ -619,11 +620,11 @@ class SmartPriceAnalysis:
"price": labor_price,
"total": labor_quantity * labor_price
})
-
+
# مقاولي الباطن
st.markdown("##### مقاولي الباطن")
subcontractors_col1, subcontractors_col2 = st.columns(2)
-
+
with subcontractors_col1:
subcontractors_percentage = st.slider(
"نسبة مقاولي الباطن من سعر الوحدة",
@@ -634,7 +635,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="subcontractors_percentage"
) * 100
-
+
with subcontractors_col2:
subcontractors_amount = st.number_input(
"قيمة مقاولي الباطن (ريال)",
@@ -643,27 +644,27 @@ class SmartPriceAnalysis:
step=10.0,
key="subcontractors_amount"
)
-
+
# إضافة مقاولي الباطن
subcontractors_items = []
-
+
st.markdown("إضافة مقاولي الباطن")
-
+
for i in range(2): # السماح بإضافة 2 مقاول باطن كحد أقصى
subcontractor_col1, subcontractor_col2, subcontractor_col3 = st.columns([4, 1, 1])
-
+
with subcontractor_col1:
subcontractor_name = st.text_input(
"اسم مقاول الباطن",
key=f"subcontractor_name_{i}"
)
-
+
with subcontractor_col2:
subcontractor_work = st.text_input(
"نوع العمل",
key=f"subcontractor_work_{i}"
)
-
+
with subcontractor_col3:
subcontractor_price = st.number_input(
"السعر",
@@ -671,22 +672,22 @@ class SmartPriceAnalysis:
step=10.0,
key=f"subcontractor_price_{i}"
)
-
+
if subcontractor_name and subcontractor_work and subcontractor_price > 0:
subcontractors_items.append({
"name": subcontractor_name,
"work": subcontractor_work,
"price": subcontractor_price
})
-
+
# التكاليف غير المباشرة
st.markdown("##### التكاليف غير المباشرة")
-
+
# استخراج نسب التكاليف غير المباشرة
indirect_costs = st.session_state.smart_price_analysis["indirect_costs"]
-
+
indirect_col1, indirect_col2, indirect_col3 = st.columns(3)
-
+
with indirect_col1:
overhead_percentage = st.slider(
"نسبة المصاريف العمومية والإدارية",
@@ -697,7 +698,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="overhead_percentage"
) * 100
-
+
with indirect_col2:
profit_percentage = st.slider(
"نسبة الربح",
@@ -708,7 +709,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="profit_percentage"
) * 100
-
+
with indirect_col3:
contingency_percentage = st.slider(
"نسبة الطوارئ",
@@ -719,14 +720,14 @@ class SmartPriceAnalysis:
format="%g%%",
key="contingency_percentage"
) * 100
-
+
# زر حفظ التحليل
submit_button = st.form_submit_button("حفظ التحليل")
-
+
if submit_button:
# التحقق من صحة البيانات
total_percentage = (materials_percentage + equipment_percentage + labor_percentage + subcontractors_percentage) / 100
-
+
if abs(total_percentage - 1.0) > 0.01:
st.error("مجموع نسب المكونات يجب أن يساوي 100%")
else:
@@ -735,18 +736,18 @@ class SmartPriceAnalysis:
equipment_total = sum([item["total"] for item in equipment_items]) if equipment_items else equipment_amount
labor_total = sum([item["total"] for item in labor_items]) if labor_items else labor_amount
subcontractors_total = sum([item["price"] for item in subcontractors_items]) if subcontractors_items else subcontractors_amount
-
+
# حساب التكاليف المباشرة
direct_cost = materials_total + equipment_total + labor_total + subcontractors_total
-
+
# حساب التكاليف غير المباشرة
overhead_amount = direct_cost * (overhead_percentage / 100)
profit_amount = direct_cost * (profit_percentage / 100)
contingency_amount = direct_cost * (contingency_percentage / 100)
-
+
# حساب إجمالي التكاليف
total_cost = direct_cost + overhead_amount + profit_amount + contingency_amount
-
+
# إنشاء مكونات البند
components = {
"materials": {
@@ -787,17 +788,17 @@ class SmartPriceAnalysis:
"total_cost": total_cost,
"analysis_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
-
+
# تحديث البند في جدول الكميات
boq_items = st.session_state.boq_items
item_index = boq_items[boq_items["id"] == item["id"]].index[0]
-
+
boq_items.at[item_index, "analyzed"] = True
boq_items.at[item_index, "components"] = components
-
+
# تحديث حالة الجلسة
st.session_state.boq_items = boq_items
-
+
# إضافة التحليل إلى سجل التحليلات
analysis_history = st.session_state.smart_price_analysis["analysis_history"]
analysis_history.append({
@@ -807,48 +808,48 @@ class SmartPriceAnalysis:
"components": components,
"analysis_date": components["analysis_date"]
})
-
+
st.session_state.smart_price_analysis["analysis_history"] = analysis_history
-
+
# إعادة تعيين البند الحالي
st.session_state.smart_price_analysis["current_item"] = None
-
+
# عرض رسالة نجاح
st.success(f"تم تحليل البند {item['id']} بنجاح!")
-
+
# إعادة توجيه إلى صفحة التحليل
- st.experimental_rerun()
-
+ st.rerun()
+
def _display_item_components(self, item):
"""عرض مكونات البند"""
-
+
# استخراج مكونات البند
components = item["components"]
-
+
if not components:
st.warning("لم يتم تحليل هذا البند بعد")
return
-
+
# عرض ملخص التحليل
st.markdown("#### ملخص التحليل")
-
+
# عرض تاريخ التحليل
st.markdown(f"**تاريخ التحليل:** {components['analysis_date']}")
-
+
# عرض التكاليف المباشرة وغير المباشرة
col1, col2 = st.columns(2)
-
+
with col1:
- st.markdown(f"**التكاليف المباشرة:** {components['direct_cost']:.2f} ريال")
+ st.markdown(f"**التكاليفالمباشرة:** {components['direct_cost']:.2f} ريال")
st.markdown(f"**التكاليف غير المباشرة:** {(components['total_cost'] - components['direct_cost']):.2f} ريال")
-
+
with col2:
st.markdown(f"**إجمالي التكاليف:** {components['total_cost']:.2f} ريال")
st.markdown(f"**سعر الوحدة:** {item['unit_price']:.2f} ريال")
-
+
# عرض نسب المكونات
st.markdown("#### نسب المكونات")
-
+
# إنشاء بيانات الرسم البياني
components_data = {
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
@@ -865,7 +866,7 @@ class SmartPriceAnalysis:
components["subcontractors"]["amount"]
]
}
-
+
# إنشاء رسم بياني دائري
fig = px.pie(
components_data,
@@ -875,82 +876,82 @@ class SmartPriceAnalysis:
color="المكون",
hover_data=["القيمة"]
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
# عرض تفاصيل المكونات
st.markdown("#### تفاصيل المكونات")
-
+
# إنشاء تبويبات لعرض تفاصيل المكونات
component_tabs = st.tabs(["المواد", "المعدات", "العمالة", "مقاولي الباطن", "التكاليف غير المباشرة"])
-
+
with component_tabs[0]:
# عرض تفاصيل المواد
st.markdown("##### المواد")
st.markdown(f"**نسبة المواد:** {components['materials']['percentage'] * 100:.2f}%")
st.markdown(f"**قيمة المواد:** {components['materials']['amount']:.2f} ريال")
-
+
# عرض قائمة المواد
if components["materials"]["items"]:
materials_df = pd.DataFrame(components["materials"]["items"])
materials_df.columns = ["اسم المادة", "الوحدة", "الكمية", "السعر", "الإجمالي"]
-
+
st.dataframe(materials_df, use_container_width=True)
else:
st.info("لم يتم إضافة مواد محددة")
-
+
with component_tabs[1]:
# عرض تفاصيل المعدات
st.markdown("##### المعدات")
st.markdown(f"**نسبة المعدات:** {components['equipment']['percentage'] * 100:.2f}%")
st.markdown(f"**قيمة المعدات:** {components['equipment']['amount']:.2f} ريال")
-
+
# عرض قائمة المعدات
if components["equipment"]["items"]:
equipment_df = pd.DataFrame(components["equipment"]["items"])
equipment_df.columns = ["اسم المعدة", "الوحدة", "الكمية", "السعر", "الإجمالي"]
-
+
st.dataframe(equipment_df, use_container_width=True)
else:
st.info("لم يتم إضافة معدات محددة")
-
+
with component_tabs[2]:
# عرض تفاصيل العمالة
st.markdown("##### العمالة")
st.markdown(f"**نسبة العمالة:** {components['labor']['percentage'] * 100:.2f}%")
st.markdown(f"**قيمة العمالة:** {components['labor']['amount']:.2f} ريال")
-
+
# عرض قائمة العمالة
if components["labor"]["items"]:
labor_df = pd.DataFrame(components["labor"]["items"])
labor_df.columns = ["المسمى الوظيفي", "الوحدة", "الكمية", "السعر", "الإجمالي"]
-
+
st.dataframe(labor_df, use_container_width=True)
else:
st.info("لم يتم إضافة عمالة محددة")
-
+
with component_tabs[3]:
# عرض تفاصيل مقاولي الباطن
st.markdown("##### مقاولي الباطن")
st.markdown(f"**نسبة مقاولي الباطن:** {components['subcontractors']['percentage'] * 100:.2f}%")
st.markdown(f"**قيمة مقاولي الباطن:** {components['subcontractors']['amount']:.2f} ريال")
-
+
# عرض قائمة مقاولي الباطن
if components["subcontractors"]["items"]:
subcontractors_df = pd.DataFrame(components["subcontractors"]["items"])
subcontractors_df.columns = ["اسم مقاول الباطن", "نوع العمل", "السعر"]
-
+
st.dataframe(subcontractors_df, use_container_width=True)
else:
st.info("لم يتم إضافة مقاولي باطن محددين")
-
+
with component_tabs[4]:
# عرض تفاصيل التكاليف غير المباشرة
st.markdown("##### التكاليف غير المباشرة")
-
+
# إنشاء بيانات التكاليف غير المباشرة
indirect_costs = components["indirect_costs"]
-
+
indirect_data = {
"البند": ["المصاريف العمومية والإدارية", "الربح", "الطوارئ"],
"النسبة": [
@@ -964,12 +965,12 @@ class SmartPriceAnalysis:
indirect_costs["contingency"]["amount"]
]
}
-
+
# إنشاء جدول للعرض
indirect_df = pd.DataFrame(indirect_data)
-
+
st.dataframe(indirect_df, use_container_width=True)
-
+
# إنشاء رسم بياني للتكاليف غير المباشرة
fig = px.bar(
indirect_df,
@@ -979,26 +980,26 @@ class SmartPriceAnalysis:
color="البند",
text_auto=True
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
def _render_analysis_settings_tab(self):
"""عرض تبويب إعدادات التحليل"""
-
+
st.markdown("### إعدادات التحليل الذكي للأسعار")
-
+
# استخراج إعدادات التحليل
price_components = st.session_state.smart_price_analysis["price_components"]
indirect_costs = st.session_state.smart_price_analysis["indirect_costs"]
productivity_factors = st.session_state.smart_price_analysis["productivity_factors"]
-
+
# إنشاء نموذج إعدادات التحليل
with st.form("analysis_settings_form"):
st.markdown("#### نسب مكونات السعر الافتراضية")
-
+
# نسب مكونات السعر
components_col1, components_col2 = st.columns(2)
-
+
with components_col1:
materials_percentage = st.slider(
"نسبة المواد من سعر الوحدة",
@@ -1009,7 +1010,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="settings_materials_percentage"
) * 100
-
+
equipment_percentage = st.slider(
"نسبة المعدات من سعر الوحدة",
min_value=0.0,
@@ -1019,7 +1020,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="settings_equipment_percentage"
) * 100
-
+
with components_col2:
labor_percentage = st.slider(
"نسبة العمالة من سعر الوحدة",
@@ -1030,7 +1031,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="settings_labor_percentage"
) * 100
-
+
subcontractors_percentage = st.slider(
"نسبة مقاولي الباطن من سعر الوحدة",
min_value=0.0,
@@ -1040,12 +1041,12 @@ class SmartPriceAnalysis:
format="%g%%",
key="settings_subcontractors_percentage"
) * 100
-
+
# التكاليف غير المباشرة
st.markdown("#### نسب التكاليف غير المباشرة الافتراضية")
-
+
indirect_col1, indirect_col2, indirect_col3 = st.columns(3)
-
+
with indirect_col1:
overhead_percentage = st.slider(
"نسبة المصاريف العمومية والإدارية",
@@ -1056,7 +1057,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="settings_overhead_percentage"
) * 100
-
+
with indirect_col2:
profit_percentage = st.slider(
"نسبة الربح",
@@ -1067,7 +1068,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="settings_profit_percentage"
) * 100
-
+
with indirect_col3:
contingency_percentage = st.slider(
"نسبة الطوارئ",
@@ -1078,12 +1079,12 @@ class SmartPriceAnalysis:
format="%g%%",
key="settings_contingency_percentage"
) * 100
-
+
# عوامل الإنتاجية
st.markdown("#### عوامل الإنتاجية")
-
+
productivity_col1, productivity_col2 = st.columns(2)
-
+
with productivity_col1:
weather_factor = st.slider(
"عامل الطقس",
@@ -1093,7 +1094,7 @@ class SmartPriceAnalysis:
step=0.1,
key="settings_weather_factor"
)
-
+
location_factor = st.slider(
"عامل الموقع",
min_value=0.5,
@@ -1102,7 +1103,7 @@ class SmartPriceAnalysis:
step=0.1,
key="settings_location_factor"
)
-
+
complexity_factor = st.slider(
"عامل التعقيد",
min_value=0.5,
@@ -1111,7 +1112,7 @@ class SmartPriceAnalysis:
step=0.1,
key="settings_complexity_factor"
)
-
+
with productivity_col2:
schedule_factor = st.slider(
"عامل الجدول الزمني",
@@ -1121,7 +1122,7 @@ class SmartPriceAnalysis:
step=0.1,
key="settings_schedule_factor"
)
-
+
resources_factor = st.slider(
"عامل الموارد",
min_value=0.5,
@@ -1130,14 +1131,14 @@ class SmartPriceAnalysis:
step=0.1,
key="settings_resources_factor"
)
-
+
# زر حفظ الإعدادات
submit_button = st.form_submit_button("حفظ الإعدادات")
-
+
if submit_button:
# التحقق من صحة البيانات
total_percentage = (materials_percentage + equipment_percentage + labor_percentage + subcontractors_percentage) / 100
-
+
if abs(total_percentage - 1.0) > 0.01:
st.error("مجموع نسب المكونات يجب أن يساوي 100%")
else:
@@ -1146,33 +1147,33 @@ class SmartPriceAnalysis:
price_components["equipment"] = equipment_percentage / 100
price_components["labor"] = labor_percentage / 100
price_components["subcontractors"] = subcontractors_percentage / 100
-
+
# تحديث نسب التكاليف غير المباشرة
indirect_costs["overhead"] = overhead_percentage / 100
indirect_costs["profit"] = profit_percentage / 100
indirect_costs["contingency"] = contingency_percentage / 100
-
+
# تحديث عوامل الإنتاجية
productivity_factors["weather"] = weather_factor
productivity_factors["location"] = location_factor
productivity_factors["complexity"] = complexity_factor
productivity_factors["schedule"] = schedule_factor
productivity_factors["resources"] = resources_factor
-
+
# تحديث حالة الجلسة
st.session_state.smart_price_analysis["price_components"] = price_components
st.session_state.smart_price_analysis["indirect_costs"] = indirect_costs
st.session_state.smart_price_analysis["productivity_factors"] = productivity_factors
-
+
# عرض رسالة نجاح
st.success("تم حفظ إعدادات التحليل بنجاح!")
-
+
# عرض الإعدادات الحالية
st.markdown("### الإعدادات الحالية")
-
+
# عرض نسب مكونات السعر
st.markdown("#### نسب مكونات السعر")
-
+
# إنشاء بيانات الرسم البياني
components_data = {
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
@@ -1183,7 +1184,7 @@ class SmartPriceAnalysis:
price_components["subcontractors"] * 100
]
}
-
+
# إنشاء رسم بياني دائري
fig = px.pie(
components_data,
@@ -1192,12 +1193,12 @@ class SmartPriceAnalysis:
title="توزيع مكونات سعر الوحدة",
color="المكون"
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
# عرض نسب التكاليف غير المباشرة
st.markdown("#### نسب التكاليف غير المباشرة")
-
+
# إنشاء بيانات الرسم البياني
indirect_data = {
"البند": ["المصاريف العمومية والإدارية", "الربح", "الطوارئ"],
@@ -1207,7 +1208,7 @@ class SmartPriceAnalysis:
indirect_costs["contingency"] * 100
]
}
-
+
# إنشاء رسم بياني شريطي
fig = px.bar(
indirect_data,
@@ -1217,12 +1218,12 @@ class SmartPriceAnalysis:
color="البند",
text_auto=True
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
# عرض عوامل الإنتاجية
st.markdown("#### عوامل الإنتاجية")
-
+
# إنشاء بيانات الرسم البياني
productivity_data = {
"العامل": ["الطقس", "الموقع", "التعقيد", "الجدول الزمني", "الموارد"],
@@ -1234,7 +1235,7 @@ class SmartPriceAnalysis:
productivity_factors["resources"]
]
}
-
+
# إنشاء رسم بياني شريطي
fig = px.bar(
productivity_data,
@@ -1244,7 +1245,7 @@ class SmartPriceAnalysis:
color="العامل",
text_auto=True
)
-
+
# إضافة خط أفقي عند القيمة 1.0
fig.add_shape(
type="line",
@@ -1258,42 +1259,42 @@ class SmartPriceAnalysis:
dash="dash"
)
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
def _render_analysis_reports_tab(self):
"""عرض تبويب تقارير التحليل"""
-
+
st.markdown("### تقارير التحليل الذكي للأسعار")
-
+
# استخراج البيانات
boq_items = st.session_state.boq_items
analysis_history = st.session_state.smart_price_analysis["analysis_history"]
-
+
# عرض ملخص التحليل
st.markdown("#### ملخص التحليل")
-
+
# حساب عدد البنود المحللة وغير المحللة
analyzed_count = len(boq_items[boq_items["analyzed"] == True])
not_analyzed_count = len(boq_items[boq_items["analyzed"] == False])
-
+
# عرض نسبة التحليل
analysis_percentage = analyzed_count / len(boq_items) * 100 if len(boq_items) > 0 else 0
-
+
st.markdown(f"**عدد البنود المحللة:** {analyzed_count} من أصل {len(boq_items)} ({analysis_percentage:.2f}%)")
-
+
# إنشاء مؤشر التقدم
st.progress(analysis_percentage / 100)
-
+
# عرض توزيع البنود المحللة حسب الفئة
st.markdown("#### توزيع البنود المحللة حسب الفئة")
-
+
# حساب عدد البنود المحللة لكل فئة
category_analysis = boq_items.groupby(["category", "analyzed"]).size().unstack(fill_value=0).reset_index()
-
+
if True in category_analysis.columns:
category_analysis.columns = ["الفئة", "غير محلل", "محلل"]
-
+
# إنشاء رسم بياني شريطي
fig = px.bar(
category_analysis,
@@ -1303,26 +1304,26 @@ class SmartPriceAnalysis:
barmode="stack",
color_discrete_map={"محلل": "green", "غير محلل": "red"}
)
-
+
st.plotly_chart(fig, use_container_width=True)
else:
st.info("لا يوجد بنود محللة بعد")
-
+
# عرض توزيع مكونات الأسعار
st.markdown("#### توزيع مكونات الأسعار")
-
+
# التحقق من وجود بنود محللة
if analyzed_count > 0:
# استخراج البنود المحللة
analyzed_items = boq_items[boq_items["analyzed"] == True]
-
+
# إنشاء قائمة لتخزين بيانات المكونات
components_data = []
-
+
# استخراج بيانات المكونات
for _, item in analyzed_items.iterrows():
components = item["components"]
-
+
components_data.append({
"id": item["id"],
"description": item["description"],
@@ -1336,19 +1337,19 @@ class SmartPriceAnalysis:
"labor_amount": components["labor"]["amount"],
"subcontractors_amount": components["subcontractors"]["amount"]
})
-
+
# إنشاء DataFrame
components_df = pd.DataFrame(components_data)
-
+
# حساب متوسط النسب
avg_materials_percentage = components_df["materials_percentage"].mean() * 100
avg_equipment_percentage = components_df["equipment_percentage"].mean() * 100
avg_labor_percentage = components_df["labor_percentage"].mean() * 100
avg_subcontractors_percentage = components_df["subcontractors_percentage"].mean() * 100
-
+
# عرض متوسط النسب
st.markdown("##### متوسط نسب المكونات")
-
+
avg_components_data = {
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
"النسبة": [
@@ -1358,7 +1359,7 @@ class SmartPriceAnalysis:
avg_subcontractors_percentage
]
}
-
+
# إنشاء رسم بياني دائري
fig = px.pie(
avg_components_data,
@@ -1367,15 +1368,15 @@ class SmartPriceAnalysis:
title="متوسط نسب مكونات الأسعار",
color="المكون"
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
# عرض توزيع النسب حسب البند
st.markdown("##### توزيع نسب المكونات حسب البند")
-
+
# إنشاء بيانات للرسم البياني
item_components_data = []
-
+
for _, row in components_df.iterrows():
item_components_data.extend([
{"البند": row["id"], "المكون": "المواد", "النسبة": row["materials_percentage"] * 100},
@@ -1383,10 +1384,10 @@ class SmartPriceAnalysis:
{"البند": row["id"], "المكون": "العمالة", "النسبة": row["labor_percentage"] * 100},
{"البند": row["id"], "المكون": "مقاولي الباطن", "النسبة": row["subcontractors_percentage"] * 100}
])
-
+
# إنشاء DataFrame
item_components_df = pd.DataFrame(item_components_data)
-
+
# إنشاء رسم بياني شريطي
fig = px.bar(
item_components_df,
@@ -1396,15 +1397,15 @@ class SmartPriceAnalysis:
title="توزيع نسب المكونات حسب البند",
barmode="stack"
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
# عرض مقارنة أسعار الوحدة
st.markdown("##### مقارنة أسعار الوحدة")
-
+
# إنشاء بيانات للرسم البياني
unit_price_data = []
-
+
for _, row in components_df.iterrows():
unit_price_data.extend([
{"البند": row["id"], "المكون": "المواد", "القيمة": row["materials_amount"]},
@@ -1412,10 +1413,10 @@ class SmartPriceAnalysis:
{"البند": row["id"], "المكون": "العمالة", "القيمة": row["labor_amount"]},
{"البند": row["id"], "المكون": "مقاولي الباطن", "القيمة": row["subcontractors_amount"]}
])
-
+
# إنشاء DataFrame
unit_price_df = pd.DataFrame(unit_price_data)
-
+
# إنشاء رسم بياني شريطي
fig = px.bar(
unit_price_df,
@@ -1425,7 +1426,7 @@ class SmartPriceAnalysis:
title="مقارنة مكونات أسعار الوحدة",
barmode="stack"
)
-
+
# إضافة خط لسعر الوحدة
for i, row in components_df.iterrows():
fig.add_shape(
@@ -1440,21 +1441,22 @@ class SmartPriceAnalysis:
dash="dash"
)
)
-
+
st.plotly_chart(fig, use_container_width=True)
+ self.render_cost_breakdown() #Added this line
else:
st.info("لا يوجد بنود محللة بعد")
-
+
# عرض سجل التحليلات
st.markdown("#### سجل التحليلات")
-
+
if analysis_history:
# عرض عدد التحليلات
st.markdown(f"**عدد التحليلات:** {len(analysis_history)}")
-
+
# عرض آخر 5 تحليلات
st.markdown("##### آخر 5 تحليلات")
-
+
for i, analysis in enumerate(analysis_history[-5:]):
st.markdown(f"**{i+1}. البند:** {analysis['item_id']} - {analysis['item_description']}")
st.markdown(f"**تاريخ التحليل:** {analysis['analysis_date']}")
@@ -1462,19 +1464,19 @@ class SmartPriceAnalysis:
st.markdown("---")
else:
st.info("لا يوجد سجل تحليلات بعد")
-
+
def _render_local_content_tab(self):
"""عرض تبويب المحتوى المحلي"""
-
+
st.markdown("### تحليل المحتوى المحلي")
-
+
# استخراج بيانات المحتوى المحلي
local_content = st.session_state.smart_price_analysis["local_content"]
-
+
# إنشاء نموذج إعدادات المحتوى المحلي
with st.form("local_content_form"):
st.markdown("#### إعدادات المحتوى المحلي")
-
+
# النسبة المستهدفة للمحتوى المحلي
target_percentage = st.slider(
"النسبة المستهدفة للمحتوى المحلي",
@@ -1485,12 +1487,12 @@ class SmartPriceAnalysis:
format="%g%%",
key="local_content_target"
) * 100
-
+
# نسب المحتوى المحلي لكل مكون
st.markdown("#### نسب المحتوى المحلي لكل مكون")
-
+
local_col1, local_col2 = st.columns(2)
-
+
with local_col1:
materials_local = st.slider(
"نسبة المواد المحلية",
@@ -1501,7 +1503,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="materials_local"
) * 100
-
+
equipment_local = st.slider(
"نسبة المعدات المحلية",
min_value=0.0,
@@ -1511,7 +1513,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="equipment_local"
) * 100
-
+
with local_col2:
labor_local = st.slider(
"نسبة العمالة المحلية",
@@ -1522,7 +1524,7 @@ class SmartPriceAnalysis:
format="%g%%",
key="labor_local"
) * 100
-
+
subcontractors_local = st.slider(
"نسبة مقاولي الباطن المحليين",
min_value=0.0,
@@ -1532,10 +1534,10 @@ class SmartPriceAnalysis:
format="%g%%",
key="subcontractors_local"
) * 100
-
+
# زر حفظ الإعدادات
submit_button = st.form_submit_button("حفظ إعدادات المحتوى المحلي")
-
+
if submit_button:
# تحديث إعدادات المحتوى المحلي
local_content["target"] = target_percentage / 100
@@ -1543,19 +1545,19 @@ class SmartPriceAnalysis:
local_content["equipment_local"] = equipment_local / 100
local_content["labor_local"] = labor_local / 100
local_content["subcontractors_local"] = subcontractors_local / 100
-
+
# تحديث حالة الجلسة
st.session_state.smart_price_analysis["local_content"] = local_content
-
+
# عرض رسالة نجاح
st.success("تم حفظ إعدادات المحتوى المحلي بنجاح!")
-
+
# حساب نسبة المحتوى المحلي الفعلية
st.markdown("#### حساب نسبة المحتوى المحلي الفعلية")
-
+
# استخراج نسب مكونات السعر
price_components = st.session_state.smart_price_analysis["price_components"]
-
+
# حساب نسبة المحتوى المحلي الفعلية
actual_local_content = (
price_components["materials"] * local_content["materials_local"] +
@@ -1563,31 +1565,31 @@ class SmartPriceAnalysis:
price_components["labor"] * local_content["labor_local"] +
price_components["subcontractors"] * local_content["subcontractors_local"]
)
-
+
# عرض نسبة المحتوى المحلي الفعلية
st.markdown(f"**نسبة المحتوى المحلي الفعلية:** {actual_local_content * 100:.2f}%")
st.markdown(f"**النسبة المستهدفة للمحتوى المحلي:** {local_content['target'] * 100:.2f}%")
-
+
# عرض مؤشر التقدم
progress_percentage = min(actual_local_content / local_content["target"], 1.0) if local_content["target"] > 0 else 0
-
+
st.progress(progress_percentage)
-
+
# عرض حالة المحتوى المحلي
if actual_local_content >= local_content["target"]:
st.success("تم تحقيق النسبة المستهدفة للمحتوى المحلي")
else:
st.warning("لم يتم تحقيق النسبة المستهدفة للمحتوى المحلي")
-
+
# عرض مساهمة كل مكون في المحتوى المحلي
st.markdown("#### مساهمة كل مكون في المحتوى المحلي")
-
+
# حساب مساهمة كل مكون
materials_contribution = price_components["materials"] * local_content["materials_local"]
equipment_contribution = price_components["equipment"] * local_content["equipment_local"]
labor_contribution = price_components["labor"] * local_content["labor_local"]
subcontractors_contribution = price_components["subcontractors"] * local_content["subcontractors_local"]
-
+
# إنشاء بيانات الرسم البياني
contribution_data = {
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
@@ -1598,7 +1600,7 @@ class SmartPriceAnalysis:
subcontractors_contribution * 100
]
}
-
+
# إنشاء رسم بياني شريطي
fig = px.bar(
contribution_data,
@@ -1608,21 +1610,21 @@ class SmartPriceAnalysis:
color="المكون",
text_auto=True
)
-
+
st.plotly_chart(fig, use_container_width=True)
-
+
# عرض توصيات لتحسين نسبة المحتوى المحلي
st.markdown("#### توصيات لتحسين نسبة المحتوى المحلي")
-
+
if actual_local_content < local_content["target"]:
# حساب الفجوة
gap = local_content["target"] - actual_local_content
-
+
st.markdown(f"**الفجوة الحالية:** {gap * 100:.2f}%")
-
+
# تحديد المكونات التي يمكن تحسينها
components_to_improve = []
-
+
if local_content["materials_local"] < 1.0:
components_to_improve.append({
"name": "المواد",
@@ -1630,7 +1632,7 @@ class SmartPriceAnalysis:
"weight": price_components["materials"],
"potential": price_components["materials"] * (1.0 - local_content["materials_local"])
})
-
+
if local_content["equipment_local"] < 1.0:
components_to_improve.append({
"name": "المعدات",
@@ -1638,7 +1640,7 @@ class SmartPriceAnalysis:
"weight": price_components["equipment"],
"potential": price_components["equipment"] * (1.0 - local_content["equipment_local"])
})
-
+
if local_content["labor_local"] < 1.0:
components_to_improve.append({
"name": "العمالة",
@@ -1646,7 +1648,7 @@ class SmartPriceAnalysis:
"weight": price_components["labor"],
"potential": price_components["labor"] * (1.0 - local_content["labor_local"])
})
-
+
if local_content["subcontractors_local"] < 1.0:
components_to_improve.append({
"name": "مقاولي الباطن",
@@ -1654,90 +1656,133 @@ class SmartPriceAnalysis:
"weight": price_components["subcontractors"],
"potential": price_components["subcontractors"] * (1.0 - local_content["subcontractors_local"])
})
-
+
# ترتيب المكونات حسب إمكانية التحسين
components_to_improve.sort(key=lambda x: x["potential"], reverse=True)
-
+
# عرض التوصيات
for component in components_to_improve:
st.markdown(f"**{component['name']}:** زيادة نسبة {component['name']} المحلية من {component['current'] * 100:.2f}% إلى {min(component['current'] + gap / component['weight'], 1.0) * 100:.2f}%")
else:
st.success("تم تحقيق النسبة المستهدفة للمحتوى المحلي")
-
+
def calculate_item_price(self, item_data):
"""حساب سعر البند بناءً على مكوناته"""
-
+
# استخراج مكونات البند
materials = item_data.get("materials", [])
equipment = item_data.get("equipment", [])
labor = item_data.get("labor", [])
subcontractors = item_data.get("subcontractors", [])
-
+
# حساب تكلفة المواد
materials_cost = sum([material["quantity"] * material["price"] for material in materials])
-
+
# حساب تكلفة المعدات
equipment_cost = sum([equipment_item["quantity"] * equipment_item["price"] for equipment_item in equipment])
-
+
# حساب تكلفة العمالة
labor_cost = sum([labor_item["quantity"] * labor_item["price"] for labor_item in labor])
-
+
# حساب تكلفة مقاولي الباطن
subcontractors_cost = sum([subcontractor["price"] for subcontractor in subcontractors])
-
+
# حساب التكاليف المباشرة
direct_cost = materials_cost + equipment_cost + labor_cost + subcontractors_cost
-
+
# استخراج نسب التكاليف غير المباشرة
indirect_costs = st.session_state.smart_price_analysis["indirect_costs"]
-
+
# حساب التكاليف غير المباشرة
overhead_amount = direct_cost * indirect_costs["overhead"]
profit_amount = direct_cost * indirect_costs["profit"]
contingency_amount = direct_cost * indirect_costs["contingency"]
-
+
# حساب إجمالي التكاليف
total_cost = direct_cost + overhead_amount + profit_amount + contingency_amount
-
+
return total_cost
-
+
def calculate_local_content(self, item_data):
"""حساب نسبة المحتوى المحلي للبند"""
-
+
# استخراج مكونات البند
materials = item_data.get("materials", [])
equipment = item_data.get("equipment", [])
labor = item_data.get("labor", [])
subcontractors = item_data.get("subcontractors", [])
-
+
# استخراج نسب المحتوى المحلي
local_content = st.session_state.smart_price_analysis["local_content"]
-
+
# حساب تكلفة المواد
materials_cost = sum([material["quantity"] * material["price"] for material in materials])
-
+
# حساب تكلفة المعدات
equipment_cost = sum([equipment_item["quantity"] * equipment_item["price"] for equipment_item in equipment])
-
+
# حساب تكلفة العمالة
labor_cost = sum([labor_item["quantity"] * labor_item["price"] for labor_item in labor])
-
+
# حساب تكلفة مقاولي الباطن
subcontractors_cost = sum([subcontractor["price"] for subcontractor in subcontractors])
-
+
# حساب التكاليف المباشرة
direct_cost = materials_cost + equipment_cost + labor_cost + subcontractors_cost
-
+
# حساب المحتوى المحلي
local_materials = materials_cost * local_content["materials_local"]
local_equipment = equipment_cost * local_content["equipment_local"]
local_labor = labor_cost * local_content["labor_local"]
local_subcontractors = subcontractors_cost * local_content["subcontractors_local"]
-
+
# حساب إجمالي المحتوى المحلي
total_local_content = local_materials + local_equipment + local_labor + local_subcontractors
-
+
# حساب نسبة المحتوى المحلي
local_content_percentage = total_local_content / direct_cost if direct_cost > 0 else 0
-
+
return local_content_percentage
+
+ def analyze_costs(self, items):
+ """تحليل التكاليف لبنود المشروع"""
+ total_cost = sum(item['total_price'] for item in items)
+ categories = {}
+
+ for item in items:
+ if item['category'] not in categories:
+ categories[item['category']] = 0
+ categories[item['category']] += item['total_price']
+
+ return {
+ 'total_cost': total_cost,
+ 'categories': categories
+ }
+
+ def render_cost_breakdown(self): #Added this function
+ """عرض تحليل التكاليف"""
+ if 'bill_of_quantities' not in st.session_state:
+ st.session_state.bill_of_quantities = []
+
+ if len(st.session_state.bill_of_quantities) > 0:
+ analysis = self.analyze_costs(st.session_state.bill_of_quantities)
+
+ st.metric("إجمالي التكاليف", f"{analysis['total_cost']:,.2f} ريال")
+
+ # عرض التكاليف حسب الفئة
+ st.subheader("التكاليف حسب الفئة")
+ categories_df = pd.DataFrame([
+ {"الفئة": cat, "التكلفة": cost}
+ for cat, cost in analysis['categories'].items()
+ ])
+
+ if not categories_df.empty:
+ fig = px.pie(
+ categories_df,
+ values="التكلفة",
+ names="الفئة",
+ title="توزيع التكاليف حسب الفئة"
+ )
+ st.plotly_chart(fig)
+ else:
+ st.warning("لا توجد بنود في جدول الكميات")
\ No newline at end of file
diff --git a/pricing_system/modules/catalogs/equipment_catalog.py b/pricing_system/modules/catalogs/equipment_catalog.py
index 20dff8ee3aaf066e50e7f2cc24ed8e65aeb32e3f..e17626b4e1132102cec5732d1eb67c07e10c01a2 100644
--- a/pricing_system/modules/catalogs/equipment_catalog.py
+++ b/pricing_system/modules/catalogs/equipment_catalog.py
@@ -1,1669 +1,1669 @@
-"""
-كتالوج المعدات - وحدة إدارة معدات المقاولات
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import plotly.express as px
-import os
-import json
-from datetime import datetime
-import io
-
-class EquipmentCatalog:
- """كتالوج المعدات"""
-
- def __init__(self):
- """تهيئة كتالوج المعدات"""
-
- # تهيئة حالة الجلسة لكتالوج المعدات
- if 'equipment_catalog' not in st.session_state:
- # إنشاء بيانات افتراضية للمعدات
- self._initialize_equipment_catalog()
-
- def _initialize_equipment_catalog(self):
- """تهيئة بيانات كتالوج المعدات"""
-
- # تعريف فئات المعدات
- equipment_categories = [
- "معدات الحفر والردم",
- "معدات النقل",
- "معدات الرفع",
- "معدات الخرسانة",
- "معدات الطرق",
- "معدات الصرف الصحي",
- "معدات السيول والكباري",
- "معدات الضغط والتثبيت",
- "معدات التوليد والطاقة",
- "معدات القياس والمساحة"
- ]
-
- # إنشاء قائمة المعدات
- equipment_data = []
-
- # 1. معدات الحفر والردم
- equipment_data.extend([
- {
- "id": "EQ-001",
- "name": "حفارة هيدروليكية كبيرة",
- "category": "معدات الحفر والردم",
- "subcategory": "حفارات",
- "brand": "كاتربيلر",
- "model": "CAT 336",
- "capacity": "2.5 م3",
- "production_rate": "150 م3/ساعة",
- "hourly_cost": 350,
- "daily_cost": 2800,
- "weekly_cost": 16800,
- "monthly_cost": 67200,
- "fuel_consumption": "35 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 5000,
- "operator_required": True,
- "description": "حفارة هيدروليكية كبيرة مناسبة لأعمال الحفر الثقيلة ومشاريع البنية التحتية الكبيرة",
- "image_url": "https://example.com/cat336.jpg"
- },
- {
- "id": "EQ-002",
- "name": "حفارة هيدروليكية متوسطة",
- "category": "معدات الحفر والردم",
- "subcategory": "حفارات",
- "brand": "كاتربيلر",
- "model": "CAT 320",
- "capacity": "1.5 م3",
- "production_rate": "100 م3/ساعة",
- "hourly_cost": 250,
- "daily_cost": 2000,
- "weekly_cost": 12000,
- "monthly_cost": 48000,
- "fuel_consumption": "25 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 3500,
- "operator_required": True,
- "description": "حفارة هيدروليكية متوسطة الحجم مناسبة لمعظم مشاريع البنية التحتية",
- "image_url": "https://example.com/cat320.jpg"
- },
- {
- "id": "EQ-003",
- "name": "حفارة هيدروليكية صغيرة",
- "category": "معدات الحفر والردم",
- "subcategory": "حفارات",
- "brand": "كاتربيلر",
- "model": "CAT 308",
- "capacity": "0.8 م3",
- "production_rate": "50 م3/ساعة",
- "hourly_cost": 150,
- "daily_cost": 1200,
- "weekly_cost": 7200,
- "monthly_cost": 28800,
- "fuel_consumption": "15 لتر/ساعة",
- "maintenance_period": "200 ساعة",
- "maintenance_cost": 2000,
- "operator_required": True,
- "description": "حفارة هيدروليكية صغيرة مناسبة للمشاريع الصغيرة والمساحات الضيقة",
- "image_url": "https://example.com/cat308.jpg"
- },
- {
- "id": "EQ-004",
- "name": "بلدوزر كبير",
- "category": "معدات الحفر والردم",
- "subcategory": "بلدوزرات",
- "brand": "كاتربيلر",
- "model": "D9",
- "capacity": "13.5 م3",
- "production_rate": "300 م3/ساعة",
- "hourly_cost": 400,
- "daily_cost": 3200,
- "weekly_cost": 19200,
- "monthly_cost": 76800,
- "fuel_consumption": "45 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 6000,
- "operator_required": True,
- "description": "بلدوزر كبير لأعمال التسوية والدفع في المشاريع الكبيرة",
- "image_url": "https://example.com/catd9.jpg"
- },
- {
- "id": "EQ-005",
- "name": "بلدوزر متوسط",
- "category": "معدات الحفر والردم",
- "subcategory": "بلدوزرات",
- "brand": "كاتربيلر",
- "model": "D7",
- "capacity": "8.5 م3",
- "production_rate": "200 م3/ساعة",
- "hourly_cost": 300,
- "daily_cost": 2400,
- "weekly_cost": 14400,
- "monthly_cost": 57600,
- "fuel_consumption": "35 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 4500,
- "operator_required": True,
- "description": "بلدوزر متوسط الحجم مناسب لمعظم مشاريع البنية التحتية",
- "image_url": "https://example.com/catd7.jpg"
- },
- {
- "id": "EQ-006",
- "name": "لودر أمامي كبير",
- "category": "معدات الحفر والردم",
- "subcategory": "لودرات",
- "brand": "كاتربيلر",
- "model": "980",
- "capacity": "5.5 م3",
- "production_rate": "250 م3/ساعة",
- "hourly_cost": 300,
- "daily_cost": 2400,
- "weekly_cost": 14400,
- "monthly_cost": 57600,
- "fuel_consumption": "30 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 4000,
- "operator_required": True,
- "description": "لودر أمامي كبير لأعمال التحميل في المشاريع الكبيرة",
- "image_url": "https://example.com/cat980.jpg"
- },
- {
- "id": "EQ-007",
- "name": "لودر أمامي متوسط",
- "category": "معدات الحفر والردم",
- "subcategory": "لودرات",
- "brand": "كاتربيلر",
- "model": "950",
- "capacity": "3.5 م3",
- "production_rate": "180 م3/ساعة",
- "hourly_cost": 250,
- "daily_cost": 2000,
- "weekly_cost": 12000,
- "monthly_cost": 48000,
- "fuel_consumption": "25 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 3500,
- "operator_required": True,
- "description": "لودر أمامي متوسط الحجم مناسب لمعظم مشاريع البنية التحتية",
- "image_url": "https://example.com/cat950.jpg"
- },
- {
- "id": "EQ-008",
- "name": "باكهو لودر",
- "category": "معدات الحفر والردم",
- "subcategory": "لودرات",
- "brand": "جي سي بي",
- "model": "3CX",
- "capacity": "1.0 م3",
- "production_rate": "60 م3/ساعة",
- "hourly_cost": 150,
- "daily_cost": 1200,
- "weekly_cost": 7200,
- "monthly_cost": 28800,
- "fuel_consumption": "12 لتر/ساعة",
- "maintenance_period": "200 ساعة",
- "maintenance_cost": 2000,
- "operator_required": True,
- "description": "باكهو لودر متعدد الاستخدامات للحفر والتحميل",
- "image_url": "https://example.com/jcb3cx.jpg"
- },
- {
- "id": "EQ-009",
- "name": "جريدر",
- "category": "معدات الحفر والردم",
- "subcategory": "معدات تسوية",
- "brand": "كاتربيلر",
- "model": "140",
- "capacity": "3.7 م عرض الشفرة",
- "production_rate": "2000 م2/ساعة",
- "hourly_cost": 250,
- "daily_cost": 2000,
- "weekly_cost": 12000,
- "monthly_cost": 48000,
- "fuel_consumption": "20 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 3000,
- "operator_required": True,
- "description": "جريدر لتسوية الطرق والمساحات",
- "image_url": "https://example.com/cat140.jpg"
- },
- {
- "id": "EQ-010",
- "name": "سكريبر",
- "category": "معدات الحفر والردم",
- "subcategory": "معدات تسوية",
- "brand": "كاتربيلر",
- "model": "621",
- "capacity": "21 م3",
- "production_rate": "400 م3/ساعة",
- "hourly_cost": 350,
- "daily_cost": 2800,
- "weekly_cost": 16800,
- "monthly_cost": 67200,
- "fuel_consumption": "35 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 5000,
- "operator_required": True,
- "description": "سكريبر لنقل وتسوية التربة لمسافات متوسطة",
- "image_url": "https://example.com/cat621.jpg"
- }
- ])
-
- # 2. معدات النقل
- equipment_data.extend([
- {
- "id": "EQ-011",
- "name": "شاحنة قلاب كبيرة",
- "category": "معدات النقل",
- "subcategory": "شاحنات قلاب",
- "brand": "مان",
- "model": "TGS 40.480",
- "capacity": "30 م3",
- "production_rate": "30 م3/رحلة",
- "hourly_cost": 200,
- "daily_cost": 1600,
- "weekly_cost": 9600,
- "monthly_cost": 38400,
- "fuel_consumption": "25 لتر/ساعة",
- "maintenance_period": "300 ساعة",
- "maintenance_cost": 3000,
- "operator_required": True,
- "description": "شاحنة قلاب كبيرة لنقل مواد الحفر والردم",
- "image_url": "https://example.com/mantgs.jpg"
- },
- {
- "id": "EQ-012",
- "name": "شاحنة قلاب متوسطة",
- "category": "معدات النقل",
- "subcategory": "شاحنات قلاب",
- "brand": "مرسيدس",
- "model": "Actros 3341",
- "capacity": "20 م3",
- "production_rate": "20 م3/رحلة",
- "hourly_cost": 180,
- "daily_cost": 1440,
- "weekly_cost": 8640,
- "monthly_cost": 34560,
- "fuel_consumption": "20 لتر/ساعة",
- "maintenance_period": "300 ساعة",
- "maintenance_cost": 2500,
- "operator_required": True,
- "description": "شاحنة قلاب متوسطة لنقل مواد الحفر والردم",
- "image_url": "https://example.com/actros.jpg"
- },
- {
- "id": "EQ-013",
- "name": "شاحنة خلاطة خرسانة",
- "category": "معدات النقل",
- "subcategory": "شاحنات خرسانة",
- "brand": "مرسيدس",
- "model": "Actros 3236",
- "capacity": "8 م3",
- "production_rate": "8 م3/رحلة",
- "hourly_cost": 200,
- "daily_cost": 1600,
- "weekly_cost": 9600,
- "monthly_cost": 38400,
- "fuel_consumption": "20 لتر/ساعة",
- "maintenance_period": "300 ساعة",
- "maintenance_cost": 3000,
- "operator_required": True,
- "description": "شاحنة خلاطة خرسانة لنقل الخرسانة الجاهزة",
- "image_url": "https://example.com/mixer.jpg"
- },
- {
- "id": "EQ-014",
- "name": "شاحنة نقل مياه",
- "category": "معدات النقل",
- "subcategory": "شاحنات مياه",
- "brand": "مان",
- "model": "TGS 33.360",
- "capacity": "20000 لتر",
- "production_rate": "20000 لتر/رحلة",
- "hourly_cost": 150,
- "daily_cost": 1200,
- "weekly_cost": 7200,
- "monthly_cost": 28800,
- "fuel_consumption": "18 لتر/ساعة",
- "maintenance_period": "300 ساعة",
- "maintenance_cost": 2000,
- "operator_required": True,
- "description": "شاحنة نقل مياه للمشاريع والرش",
- "image_url": "https://example.com/watertruck.jpg"
- },
- {
- "id": "EQ-015",
- "name": "شاحنة نقل معدات",
- "category": "معدات النقل",
- "subcategory": "شاحنات نقل",
- "brand": "فولفو",
- "model": "FH16",
- "capacity": "60 طن",
- "production_rate": "60 طن/رحلة",
- "hourly_cost": 250,
- "daily_cost": 2000,
- "weekly_cost": 12000,
- "monthly_cost": 48000,
- "fuel_consumption": "25 لتر/ساعة",
- "maintenance_period": "300 ساعة",
- "maintenance_cost": 3500,
- "operator_required": True,
- "description": "شاحنة نقل معدات ثقيلة (لوبد)",
- "image_url": "https://example.com/lowbed.jpg"
- }
- ])
-
- # 3. معدات الرفع
- equipment_data.extend([
- {
- "id": "EQ-016",
- "name": "رافعة برجية",
- "category": "معدات الرفع",
- "subcategory": "رافعات برجية",
- "brand": "ليبهر",
- "model": "200 EC-H",
- "capacity": "10 طن",
- "production_rate": "20 رفعة/ساعة",
- "hourly_cost": 400,
- "daily_cost": 3200,
- "weekly_cost": 19200,
- "monthly_cost": 76800,
- "fuel_consumption": "30 كيلوواط/ساعة",
- "maintenance_period": "300 ساعة",
- "maintenance_cost": 5000,
- "operator_required": True,
- "description": "رافعة برجية للمشاريع الإنشائية الكبيرة",
- "image_url": "https://example.com/towercrane.jpg"
- },
- {
- "id": "EQ-017",
- "name": "رافعة متحركة كبيرة",
- "category": "معدات الرفع",
- "subcategory": "رافعات متحركة",
- "brand": "ليبهر",
- "model": "LTM 1200",
- "capacity": "200 طن",
- "production_rate": "15 رفعة/ساعة",
- "hourly_cost": 600,
- "daily_cost": 4800,
- "weekly_cost": 28800,
- "monthly_cost": 115200,
- "fuel_consumption": "40 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 8000,
- "operator_required": True,
- "description": "رافعة متحركة كبيرة للأحمال الثقيلة",
- "image_url": "https://example.com/mobilecrane.jpg"
- },
- {
- "id": "EQ-018",
- "name": "رافعة متحركة متوسطة",
- "category": "معدات الرفع",
- "subcategory": "رافعات متحركة",
- "brand": "ليبهر",
- "model": "LTM 1070",
- "capacity": "70 طن",
- "production_rate": "15 رفعة/ساعة",
- "hourly_cost": 400,
- "daily_cost": 3200,
- "weekly_cost": 19200,
- "monthly_cost": 76800,
- "fuel_consumption": "30 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 6000,
- "operator_required": True,
- "description": "رافعة متحركة متوسطة للاستخدامات المتنوعة",
- "image_url": "https://example.com/mobilecrane2.jpg"
- },
- {
- "id": "EQ-019",
- "name": "رافعة شوكية",
- "category": "معدات الرفع",
- "subcategory": "رافعات شوكية",
- "brand": "كاتربيلر",
- "model": "DP70N",
- "capacity": "7 طن",
- "production_rate": "30 رفعة/ساعة",
- "hourly_cost": 150,
- "daily_cost": 1200,
- "weekly_cost": 7200,
- "monthly_cost": 28800,
- "fuel_consumption": "12 لتر/ساعة",
- "maintenance_period": "200 ساعة",
- "maintenance_cost": 2000,
- "operator_required": True,
- "description": "رافعة شوكية لنقل المواد في الموقع",
- "image_url": "https://example.com/forklift.jpg"
- },
- {
- "id": "EQ-020",
- "name": "رافعة سلة",
- "category": "معدات الرفع",
- "subcategory": "رافعات سلة",
- "brand": "جيني",
- "model": "S-85",
- "capacity": "227 كجم",
- "production_rate": "ارتفاع 26 متر",
- "hourly_cost": 200,
- "daily_cost": 1600,
- "weekly_cost": 9600,
- "monthly_cost": 38400,
- "fuel_consumption": "10 لتر/ساعة",
- "maintenance_period": "200 ساعة",
- "maintenance_cost": 2500,
- "operator_required": True,
- "description": "رافعة سلة للوصول إلى الارتفاعات",
- "image_url": "https://example.com/boomlift.jpg"
- }
- ])
-
- # 4. معدات الخرسانة
- equipment_data.extend([
- {
- "id": "EQ-021",
- "name": "خلاطة خرسانة مركزية",
- "category": "معدات الخرسانة",
- "subcategory": "خلاطات",
- "brand": "ليبهر",
- "model": "Betomix 3.0",
- "capacity": "120 م3/ساعة",
- "production_rate": "120 م3/ساعة",
- "hourly_cost": 800,
- "daily_cost": 6400,
- "weekly_cost": 38400,
- "monthly_cost": 153600,
- "fuel_consumption": "60 كيلوواط/ساعة",
- "maintenance_period": "500 ساعة",
- "maintenance_cost": 10000,
- "operator_required": True,
- "description": "محطة خلط خرسانة مركزية للمشاريع الكبيرة",
- "image_url": "https://example.com/batchplant.jpg"
- },
- {
- "id": "EQ-022",
- "name": "خلاطة خرسانة متنقلة",
- "category": "معدات الخرسانة",
- "subcategory": "خلاطات",
- "brand": "كارمكس",
- "model": "MCP-30",
- "capacity": "30 م3/ساعة",
- "production_rate": "30 م3/ساعة",
- "hourly_cost": 300,
- "daily_cost": 2400,
- "weekly_cost": 14400,
- "monthly_cost": 57600,
- "fuel_consumption": "25 كيلوواط/ساعة",
- "maintenance_period": "300 ساعة",
- "maintenance_cost": 5000,
- "operator_required": True,
- "description": "محطة خلط خرسانة متنقلة للمشاريع المتوسطة",
- "image_url": "https://example.com/mobilemixer.jpg"
- },
- {
- "id": "EQ-023",
- "name": "مضخة خرسانة ثابتة",
- "category": "معدات الخرسانة",
- "subcategory": "مضخات",
- "brand": "بوتزميستر",
- "model": "BSA 1409",
- "capacity": "90 م3/ساعة",
- "production_rate": "90 م3/ساعة",
- "hourly_cost": 350,
- "daily_cost": 2800,
- "weekly_cost": 16800,
- "monthly_cost": 67200,
- "fuel_consumption": "30 كيلوواط/ساعة",
- "maintenance_period": "300 ساعة",
- "maintenance_cost": 6000,
- "operator_required": True,
- "description": "مضخة خرسانة ثابتة للمشاريع الكبيرة",
- "image_url": "https://example.com/concretepump.jpg"
- },
- {
- "id": "EQ-024",
- "name": "مضخة خرسانة متحركة",
- "category": "معدات الخرسانة",
- "subcategory": "مضخات",
- "brand": "بوتزميستر",
- "model": "M42-5",
- "capacity": "160 م3/ساعة",
- "production_rate": "160 م3/ساعة",
- "hourly_cost": 500,
- "daily_cost": 4000,
- "weekly_cost": 24000,
- "monthly_cost": 96000,
- "fuel_consumption": "40 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 8000,
- "operator_required": True,
- "description": "مضخة خرسانة متحركة بذراع 42 متر",
- "image_url": "https://example.com/boomconcretepump.jpg"
- },
- {
- "id": "EQ-025",
- "name": "هزاز خرسانة",
- "category": "معدات الخرسانة",
- "subcategory": "هزازات",
- "brand": "واكر نيوسن",
- "model": "IREN",
- "capacity": "غير محدد",
- "production_rate": "غير محدد",
- "hourly_cost": 20,
- "daily_cost": 160,
- "weekly_cost": 960,
- "monthly_cost": 3840,
- "fuel_consumption": "غير محدد",
- "maintenance_period": "100 ساعة",
- "maintenance_cost": 500,
- "operator_required": True,
- "description": "هزاز خرسانة لدمك الخرسانة",
- "image_url": "https://example.com/vibrator.jpg"
- },
- {
- "id": "EQ-026",
- "name": "ماكينة تسوية الخرسانة",
- "category": "معدات الخرسانة",
- "subcategory": "معدات تشطيب",
- "brand": "سومرو",
- "model": "S-840",
- "capacity": "840 م2/ساعة",
- "production_rate": "840 م2/ساعة",
- "hourly_cost": 100,
- "daily_cost": 800,
- "weekly_cost": 4800,
- "monthly_cost": 19200,
- "fuel_consumption": "5 لتر/ساعة",
- "maintenance_period": "150 ساعة",
- "maintenance_cost": 1500,
- "operator_required": True,
- "description": "ماكينة تسوية الخرسانة (هليكوبتر)",
- "image_url": "https://example.com/powertrowel.jpg"
- }
- ])
-
- # 5. معدات الطرق
- equipment_data.extend([
- {
- "id": "EQ-027",
- "name": "فرادة أسفلت كبيرة",
- "category": "معدات الطرق",
- "subcategory": "فرادات",
- "brand": "فوجيلي",
- "model": "Super 2100-3i",
- "capacity": "1100 طن/ساعة",
- "production_rate": "1100 طن/ساعة",
- "hourly_cost": 600,
- "daily_cost": 4800,
- "weekly_cost": 28800,
- "monthly_cost": 115200,
- "fuel_consumption": "45 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 8000,
- "operator_required": True,
- "description": "فرادة أسفلت كبيرة للطرق السريعة",
- "image_url": "https://example.com/paver.jpg"
- },
- {
- "id": "EQ-028",
- "name": "فرادة أسفلت متوسطة",
- "category": "معدات الطرق",
- "subcategory": "فرادات",
- "brand": "فوجيلي",
- "model": "Super 1800-3i",
- "capacity": "700 طن/ساعة",
- "production_rate": "700 طن/ساعة",
- "hourly_cost": 450,
- "daily_cost": 3600,
- "weekly_cost": 21600,
- "monthly_cost": 86400,
- "fuel_consumption": "35 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 6000,
- "operator_required": True,
- "description": "فرادة أسفلت متوسطة للطرق العامة",
- "image_url": "https://example.com/paver2.jpg"
- },
- {
- "id": "EQ-029",
- "name": "مدحلة أسفلت ثقيلة",
- "category": "معدات الطرق",
- "subcategory": "مداحل",
- "brand": "بوماج",
- "model": "BW 203",
- "capacity": "غير محدد",
- "production_rate": "3000 م2/ساعة",
- "hourly_cost": 250,
- "daily_cost": 2000,
- "weekly_cost": 12000,
- "monthly_cost": 48000,
- "fuel_consumption": "20 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 3500,
- "operator_required": True,
- "description": "مدحلة أسفلت ثقيلة للطرق",
- "image_url": "https://example.com/roller.jpg"
- },
- {
- "id": "EQ-030",
- "name": "مدحلة أسفلت مطاطية",
- "category": "معدات الطرق",
- "subcategory": "مداحل",
- "brand": "بوماج",
- "model": "BW 27 RH",
- "capacity": "غير محدد",
- "production_rate": "3500 م2/ساعة",
- "hourly_cost": 200,
- "daily_cost": 1600,
- "weekly_cost": 9600,
- "monthly_cost": 38400,
- "fuel_consumption": "18 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 3000,
- "operator_required": True,
- "description": "مدحلة أسفلت مطاطية للطرق",
- "image_url": "https://example.com/rubberroller.jpg"
- },
- {
- "id": "EQ-031",
- "name": "قاشطة أسفلت",
- "category": "معدات الطرق",
- "subcategory": "قاشطات",
- "brand": "ويرتجن",
- "model": "W 210",
- "capacity": "غير محدد",
- "production_rate": "800 م2/ساعة",
- "hourly_cost": 500,
- "daily_cost": 4000,
- "weekly_cost": 24000,
- "monthly_cost": 96000,
- "fuel_consumption": "40 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 7000,
- "operator_required": True,
- "description": "قاشطة أسفلت لإزالة طبقات الأسفلت القديمة",
- "image_url": "https://example.com/milling.jpg"
- },
- {
- "id": "EQ-032",
- "name": "شاحنة رش البيتومين",
- "category": "معدات الطرق",
- "subcategory": "معدات رش",
- "brand": "روزنباور",
- "model": "S12000",
- "capacity": "12000 لتر",
- "production_rate": "15000 م2/ساعة",
- "hourly_cost": 200,
- "daily_cost": 1600,
- "weekly_cost": 9600,
- "monthly_cost": 38400,
- "fuel_consumption": "15 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 3000,
- "operator_required": True,
- "description": "شاحنة رش البيتومين للطرق",
- "image_url": "https://example.com/bitumensprayer.jpg"
- }
- ])
-
- # 6. معدات الصرف الصحي
- equipment_data.extend([
- {
- "id": "EQ-033",
- "name": "حفارة خنادق كبيرة",
- "category": "معدات الصرف الصحي",
- "subcategory": "حفارات خنادق",
- "brand": "فيرمير",
- "model": "T1255III",
- "capacity": "غير محدد",
- "production_rate": "300 م/ساعة",
- "hourly_cost": 400,
- "daily_cost": 3200,
- "weekly_cost": 19200,
- "monthly_cost": 76800,
- "fuel_consumption": "35 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 6000,
- "operator_required": True,
- "description": "حفارة خنادق كبيرة لمشاريع الصرف الصحي",
- "image_url": "https://example.com/trencher.jpg"
- },
- {
- "id": "EQ-034",
- "name": "ماكينة دفع أنابيب",
- "category": "معدات الصرف الصحي",
- "subcategory": "معدات دفع",
- "brand": "هيرينكنيشت",
- "model": "HK-500",
- "capacity": "غير محدد",
- "production_rate": "20 م/ساعة",
- "hourly_cost": 600,
- "daily_cost": 4800,
- "weekly_cost": 28800,
- "monthly_cost": 115200,
- "fuel_consumption": "40 كيلوواط/ساعة",
- "maintenance_period": "300 ساعة",
- "maintenance_cost": 8000,
- "operator_required": True,
- "description": "ماكينة دفع أنابيب بدون حفر مفتوح",
- "image_url": "https://example.com/pipejacking.jpg"
- },
- {
- "id": "EQ-035",
- "name": "سيارة شفط وتنظيف مجاري",
- "category": "معدات الصرف الصحي",
- "subcategory": "معدات تنظيف",
- "brand": "كايزر",
- "model": "AquaStar",
- "capacity": "12000 لتر",
- "production_rate": "غير محدد",
- "hourly_cost": 300,
- "daily_cost": 2400,
- "weekly_cost": 14400,
- "monthly_cost": 57600,
- "fuel_consumption": "20 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 4000,
- "operator_required": True,
- "description": "سيارة شفط وتنظيف مجاري بضغط عالي",
- "image_url": "https://example.com/sewercleaner.jpg"
- },
- {
- "id": "EQ-036",
- "name": "كاميرا فحص مجاري",
- "category": "معدات الصرف الصحي",
- "subcategory": "معدات فحص",
- "brand": "إيبوس",
- "model": "ROVION",
- "capacity": "غير محدد",
- "production_rate": "غير محدد",
- "hourly_cost": 150,
- "daily_cost": 1200,
- "weekly_cost": 7200,
- "monthly_cost": 28800,
- "fuel_consumption": "غير محدد",
- "maintenance_period": "200 ساعة",
- "maintenance_cost": 2000,
- "operator_required": True,
- "description": "كاميرا فحص مجاري للتفتيش والصيانة",
- "image_url": "https://example.com/sewercamera.jpg"
- },
- {
- "id": "EQ-037",
- "name": "مضخة مياه غاطسة كبيرة",
- "category": "معدات الصرف الصحي",
- "subcategory": "مضخات",
- "brand": "جرندفوس",
- "model": "S2",
- "capacity": "400 م3/ساعة",
- "production_rate": "400 م3/ساعة",
- "hourly_cost": 100,
- "daily_cost": 800,
- "weekly_cost": 4800,
- "monthly_cost": 19200,
- "fuel_consumption": "15 كيلوواط/ساعة",
- "maintenance_period": "500 ساعة",
- "maintenance_cost": 2500,
- "operator_required": False,
- "description": "مضخة مياه غاطسة كبيرة لمشاريع الصرف الصحي",
- "image_url": "https://example.com/submersiblepump.jpg"
- }
- ])
-
- # 7. معدات السيول والكباري
- equipment_data.extend([
- {
- "id": "EQ-038",
- "name": "معدات دق الخوازيق",
- "category": "معدات السيول والكباري",
- "subcategory": "معدات خوازيق",
- "brand": "بوير",
- "model": "BG 28",
- "capacity": "غير محدد",
- "production_rate": "10 خازوق/يوم",
- "hourly_cost": 800,
- "daily_cost": 6400,
- "weekly_cost": 38400,
- "monthly_cost": 153600,
- "fuel_consumption": "60 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 10000,
- "operator_required": True,
- "description": "معدات دق الخوازيق للكباري والأساسات العميقة",
- "image_url": "https://example.com/piling.jpg"
- },
- {
- "id": "EQ-039",
- "name": "رافعة جسرية",
- "category": "معدات السيول والكباري",
- "subcategory": "رافعات",
- "brand": "ليبهر",
- "model": "LG 1750",
- "capacity": "750 طن",
- "production_rate": "غير محدد",
- "hourly_cost": 1200,
- "daily_cost": 9600,
- "weekly_cost": 57600,
- "monthly_cost": 230400,
- "fuel_consumption": "80 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 15000,
- "operator_required": True,
- "description": "رافعة جسرية لتركيب عناصر الكباري الثقيلة",
- "image_url": "https://example.com/bridgecrane.jpg"
- },
- {
- "id": "EQ-040",
- "name": "معدات شد الكابلات",
- "category": "معدات السيول والكباري",
- "subcategory": "معدات شد",
- "brand": "فرايسينت",
- "model": "C500",
- "capacity": "500 طن",
- "production_rate": "غير محدد",
- "hourly_cost": 300,
- "daily_cost": 2400,
- "weekly_cost": 14400,
- "monthly_cost": 57600,
- "fuel_consumption": "غير محدد",
- "maintenance_period": "300 ساعة",
- "maintenance_cost": 5000,
- "operator_required": True,
- "description": "معدات شد الكابلات للكباري المعلقة",
- "image_url": "https://example.com/stressing.jpg"
- },
- {
- "id": "EQ-041",
- "name": "معدات حفر الأنفاق",
- "category": "معدات السيول والكباري",
- "subcategory": "معدات أنفاق",
- "brand": "هيرينكنيشت",
- "model": "S-500",
- "capacity": "غير محدد",
- "production_rate": "15 م/يوم",
- "hourly_cost": 1500,
- "daily_cost": 12000,
- "weekly_cost": 72000,
- "monthly_cost": 288000,
- "fuel_consumption": "100 كيلوواط/ساعة",
- "maintenance_period": "500 ساعة",
- "maintenance_cost": 20000,
- "operator_required": True,
- "description": "معدات حفر الأنفاق للمشاريع الكبيرة",
- "image_url": "https://example.com/tbm.jpg"
- },
- {
- "id": "EQ-042",
- "name": "سدود مؤقتة",
- "category": "معدات السيول والكباري",
- "subcategory": "معدات سيول",
- "brand": "أكواباريير",
- "model": "K-100",
- "capacity": "غير محدد",
- "production_rate": "100 م/يوم",
- "hourly_cost": 200,
- "daily_cost": 1600,
- "weekly_cost": 9600,
- "monthly_cost": 38400,
- "fuel_consumption": "غير محدد",
- "maintenance_period": "غير محدد",
- "maintenance_cost": 2000,
- "operator_required": True,
- "description": "سدود مؤقتة للحماية من السيول",
- "image_url": "https://example.com/cofferdam.jpg"
- }
- ])
-
- # 8. معدات الضغط والتثبيت
- equipment_data.extend([
- {
- "id": "EQ-043",
- "name": "مدحلة تربة ثقيلة",
- "category": "معدات الضغط والتثبيت",
- "subcategory": "مداحل",
- "brand": "بوماج",
- "model": "BW 226",
- "capacity": "غير محدد",
- "production_rate": "3000 م2/ساعة",
- "hourly_cost": 250,
- "daily_cost": 2000,
- "weekly_cost": 12000,
- "monthly_cost": 48000,
- "fuel_consumption": "20 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 3500,
- "operator_required": True,
- "description": "مدحلة تربة ثقيلة للمشاريع الكبيرة",
- "image_url": "https://example.com/soilroller.jpg"
- },
- {
- "id": "EQ-044",
- "name": "دكاكة قفازة",
- "category": "معدات الضغط والتثبيت",
- "subcategory": "دكاكات",
- "brand": "واكر نيوسن",
- "model": "BS 60-4",
- "capacity": "غير محدد",
- "production_rate": "150 م2/ساعة",
- "hourly_cost": 50,
- "daily_cost": 400,
- "weekly_cost": 2400,
- "monthly_cost": 9600,
- "fuel_consumption": "2 لتر/ساعة",
- "maintenance_period": "100 ساعة",
- "maintenance_cost": 800,
- "operator_required": True,
- "description": "دكاكة قفازة للمساحات الضيقة",
- "image_url": "https://example.com/rammer.jpg"
- },
- {
- "id": "EQ-045",
- "name": "دكاكة هزازة",
- "category": "معدات الضغط والتثبيت",
- "subcategory": "دكاكات",
- "brand": "واكر نيوسن",
- "model": "DPU 6555",
- "capacity": "غير محدد",
- "production_rate": "500 م2/ساعة",
- "hourly_cost": 80,
- "daily_cost": 640,
- "weekly_cost": 3840,
- "monthly_cost": 15360,
- "fuel_consumption": "3 لتر/ساعة",
- "maintenance_period": "100 ساعة",
- "maintenance_cost": 1000,
- "operator_required": True,
- "description": "دكاكة هزازة للمساحات المتوسطة",
- "image_url": "https://example.com/platecompactor.jpg"
- },
- {
- "id": "EQ-046",
- "name": "معدات تثبيت التربة",
- "category": "معدات الضغط والتثبيت",
- "subcategory": "معدات تثبيت",
- "brand": "ويرتجن",
- "model": "WR 250",
- "capacity": "غير محدد",
- "production_rate": "5000 م2/يوم",
- "hourly_cost": 500,
- "daily_cost": 4000,
- "weekly_cost": 24000,
- "monthly_cost": 96000,
- "fuel_consumption": "40 لتر/ساعة",
- "maintenance_period": "250 ساعة",
- "maintenance_cost": 7000,
- "operator_required": True,
- "description": "معدات تثبيت التربة بالإسمنت أو الجير",
- "image_url": "https://example.com/soilstabilizer.jpg"
- }
- ])
-
- # 9. معدات التوليد والطاقة
- equipment_data.extend([
- {
- "id": "EQ-047",
- "name": "مولد كهرباء كبير",
- "category": "معدات التوليد والطاقة",
- "subcategory": "مولدات",
- "brand": "كاتربيلر",
- "model": "C15",
- "capacity": "500 كيلوواط",
- "production_rate": "500 كيلوواط",
- "hourly_cost": 300,
- "daily_cost": 2400,
- "weekly_cost": 14400,
- "monthly_cost": 57600,
- "fuel_consumption": "80 لتر/ساعة",
- "maintenance_period": "500 ساعة",
- "maintenance_cost": 5000,
- "operator_required": False,
- "description": "مولد كهرباء كبير للمشاريع الكبيرة",
- "image_url": "https://example.com/generator.jpg"
- },
- {
- "id": "EQ-048",
- "name": "مولد كهرباء متوسط",
- "category": "معدات التوليد والطاقة",
- "subcategory": "مولدات",
- "brand": "كاتربيلر",
- "model": "C9",
- "capacity": "250 كيلوواط",
- "production_rate": "250 كيلوواط",
- "hourly_cost": 200,
- "daily_cost": 1600,
- "weekly_cost": 9600,
- "monthly_cost": 38400,
- "fuel_consumption": "45 لتر/ساعة",
- "maintenance_period": "500 ساعة",
- "maintenance_cost": 3500,
- "operator_required": False,
- "description": "مولد كهرباء متوسط للمشاريع المتوسطة",
- "image_url": "https://example.com/generator2.jpg"
- },
- {
- "id": "EQ-049",
- "name": "ضاغط هواء كبير",
- "category": "معدات التوليد والطاقة",
- "subcategory": "ضواغط",
- "brand": "أطلس كوبكو",
- "model": "XRVS 1000",
- "capacity": "1000 قدم مكعب/دقيقة",
- "production_rate": "1000 قدم مكعب/دقيقة",
- "hourly_cost": 200,
- "daily_cost": 1600,
- "weekly_cost": 9600,
- "monthly_cost": 38400,
- "fuel_consumption": "30 لتر/ساعة",
- "maintenance_period": "500 ساعة",
- "maintenance_cost": 3000,
- "operator_required": False,
- "description": "ضاغط هواء كبير للمشاريع الكبيرة",
- "image_url": "https://example.com/compressor.jpg"
- },
- {
- "id": "EQ-050",
- "name": "ضاغط هواء متوسط",
- "category": "معدات التوليد والطاقة",
- "subcategory": "ضواغط",
- "brand": "أطلس كوبكو",
- "model": "XRVS 500",
- "capacity": "500 قدم مكعب/دقيقة",
- "production_rate": "500 قدم مكعب/دقيقة",
- "hourly_cost": 150,
- "daily_cost": 1200,
- "weekly_cost": 7200,
- "monthly_cost": 28800,
- "fuel_consumption": "20 لتر/ساعة",
- "maintenance_period": "500 ساعة",
- "maintenance_cost": 2500,
- "operator_required": False,
- "description": "ضاغط هواء متوسط للمشاريع المتوسطة",
- "image_url": "https://example.com/compressor2.jpg"
- }
- ])
-
- # 10. معدات القياس والمساحة
- equipment_data.extend([
- {
- "id": "EQ-051",
- "name": "محطة رصد متكاملة",
- "category": "معدات القياس والمساحة",
- "subcategory": "محطات رصد",
- "brand": "ليكا",
- "model": "TS16",
- "capacity": "غير محدد",
- "production_rate": "غير محدد",
- "hourly_cost": 100,
- "daily_cost": 800,
- "weekly_cost": 4800,
- "monthly_cost": 19200,
- "fuel_consumption": "غير محدد",
- "maintenance_period": "1000 ساعة",
- "maintenance_cost": 2000,
- "operator_required": True,
- "description": "محطة رصد متكاملة للمساحة الدقيقة",
- "image_url": "https://example.com/totalstation.jpg"
- },
- {
- "id": "EQ-052",
- "name": "جهاز GPS مساحي",
- "category": "معدات القياس والمساحة",
- "subcategory": "أجهزة GPS",
- "brand": "ترمبل",
- "model": "R10",
- "capacity": "غير محدد",
- "production_rate": "غير محدد",
- "hourly_cost": 80,
- "daily_cost": 640,
- "weekly_cost": 3840,
- "monthly_cost": 15360,
- "fuel_consumption": "غير محدد",
- "maintenance_period": "1000 ساعة",
- "maintenance_cost": 1500,
- "operator_required": True,
- "description": "جهاز GPS مساحي دقيق",
- "image_url": "https://example.com/gps.jpg"
- },
- {
- "id": "EQ-053",
- "name": "جهاز مسح ليزري ثلاثي الأبعاد",
- "category": "معدات القياس والمساحة",
- "subcategory": "أجهزة مسح",
- "brand": "ليكا",
- "model": "RTC360",
- "capacity": "غير محدد",
- "production_rate": "غير محدد",
- "hourly_cost": 150,
- "daily_cost": 1200,
- "weekly_cost": 7200,
- "monthly_cost": 28800,
- "fuel_consumption": "غير محدد",
- "maintenance_period": "1000 ساعة",
- "maintenance_cost": 3000,
- "operator_required": True,
- "description": "جهاز مسح ليزري ثلاثي الأبعاد للمشاريع المعقدة",
- "image_url": "https://example.com/laserscanner.jpg"
- },
- {
- "id": "EQ-054",
- "name": "طائرة بدون طيار للمسح",
- "category": "معدات القياس والمساحة",
- "subcategory": "طائرات مسح",
- "brand": "دي جي آي",
- "model": "Phantom 4 RTK",
- "capacity": "غير محدد",
- "production_rate": "غير محدد",
- "hourly_cost": 100,
- "daily_cost": 800,
- "weekly_cost": 4800,
- "monthly_cost": 19200,
- "fuel_consumption": "غير محدد",
- "maintenance_period": "500 ساعة",
- "maintenance_cost": 1000,
- "operator_required": True,
- "description": "طائرة بدون طيار للمسح الجوي والتصوير",
- "image_url": "https://example.com/drone.jpg"
- }
- ])
-
- # تخزين البيانات في حالة الجلسة
- st.session_state.equipment_catalog = pd.DataFrame(equipment_data)
-
- def render(self):
- """عرض واجهة كتالوج المعدات"""
-
- st.markdown("## كتالوج المعدات")
-
- # إنشاء تبويبات لعرض الكتالوج
- tabs = st.tabs([
- "عرض الكتالوج",
- "إضافة معدة",
- "تحليل التكاليف",
- "استيراد/تصدير"
- ])
-
- with tabs[0]:
- self._render_catalog_view_tab()
-
- with tabs[1]:
- self._render_add_equipment_tab()
-
- with tabs[2]:
- self._render_cost_analysis_tab()
-
- with tabs[3]:
- self._render_import_export_tab()
-
- def _render_catalog_view_tab(self):
- """عرض تبويب عرض الكتالوج"""
-
- st.markdown("### عرض كتالوج المعدات")
-
- # استخراج البيانات
- equipment_df = st.session_state.equipment_catalog
-
- # إنشاء فلاتر للعرض
- col1, col2 = st.columns(2)
-
- with col1:
- # فلتر حسب الفئة
- categories = ["الكل"] + sorted(equipment_df["category"].unique().tolist())
- selected_category = st.selectbox("اختر فئة المعدات", categories)
-
- with col2:
- # فلتر حسب الفئة الفرعية
- if selected_category != "الكل":
- subcategories = ["الكل"] + sorted(equipment_df[equipment_df["category"] == selected_category]["subcategory"].unique().tolist())
- else:
- subcategories = ["الكل"] + sorted(equipment_df["subcategory"].unique().tolist())
-
- selected_subcategory = st.selectbox("اختر الفئة الفرعية", subcategories)
-
- # تطبيق الفلاتر
- filtered_df = equipment_df.copy()
-
- if selected_category != "الكل":
- filtered_df = filtered_df[filtered_df["category"] == selected_category]
-
- if selected_subcategory != "الكل":
- filtered_df = filtered_df[filtered_df["subcategory"] == selected_subcategory]
-
- # عرض البيانات
- if not filtered_df.empty:
- # عرض عدد النتائج
- st.info(f"تم العثور على {len(filtered_df)} معدة")
-
- # عرض المعدات في شكل بطاقات
- for i, (_, equipment) in enumerate(filtered_df.iterrows()):
- col1, col2 = st.columns([1, 2])
-
- with col1:
- # عرض صورة المعدة (استخدام صورة افتراضية)
- st.image("https://via.placeholder.com/150", caption=equipment["name"])
-
- with col2:
- # عرض معلومات المعدة
- st.markdown(f"**{equipment['name']}** (الكود: {equipment['id']})")
- st.markdown(f"الفئة: {equipment['category']} - {equipment['subcategory']}")
- st.markdown(f"الماركة: {equipment['brand']} | الموديل: {equipment['model']}")
- st.markdown(f"السعة: {equipment['capacity']} | معدل الإنتاج: {equipment['production_rate']}")
-
- # عرض التكاليف
- cost_col1, cost_col2, cost_col3, cost_col4 = st.columns(4)
- with cost_col1:
- st.metric("بالساعة", f"{equipment['hourly_cost']} ريال")
- with cost_col2:
- st.metric("باليوم", f"{equipment['daily_cost']} ريال")
- with cost_col3:
- st.metric("بالأسبوع", f"{equipment['weekly_cost']} ريال")
- with cost_col4:
- st.metric("بالشهر", f"{equipment['monthly_cost']} ريال")
-
- # إضافة زر لعرض التفاصيل
- if st.button(f"عرض التفاصيل الكاملة", key=f"details_{equipment['id']}"):
- st.session_state.selected_equipment = equipment['id']
- self._show_equipment_details(equipment)
-
- st.markdown("---")
- else:
- st.warning("لا توجد معدات تطابق معايير البحث")
-
- def _show_equipment_details(self, equipment):
- """عرض تفاصيل المعدة"""
-
- st.markdown(f"## تفاصيل المعدة: {equipment['name']}")
-
- col1, col2 = st.columns([1, 2])
-
- with col1:
- # عرض صورة المعدة (استخدام صورة افتراضية)
- st.image("https://via.placeholder.com/300", caption=equipment["name"])
-
- with col2:
- # عرض المعلومات الأساسية
- st.markdown("### المعلومات الأساسية")
- st.markdown(f"**الكود:** {equipment['id']}")
- st.markdown(f"**الفئة:** {equipment['category']} - {equipment['subcategory']}")
- st.markdown(f"**الماركة:** {equipment['brand']}")
- st.markdown(f"**الموديل:** {equipment['model']}")
- st.markdown(f"**السعة:** {equipment['capacity']}")
- st.markdown(f"**معدل الإنتاج:** {equipment['production_rate']}")
- st.markdown(f"**الوصف:** {equipment['description']}")
-
- # عرض معلومات التكلفة
- st.markdown("### معلومات التكلفة")
- cost_col1, cost_col2, cost_col3, cost_col4 = st.columns(4)
- with cost_col1:
- st.metric("التكلفة بالساعة", f"{equipment['hourly_cost']} ريال")
- with cost_col2:
- st.metric("التكلفة باليوم", f"{equipment['daily_cost']} ريال")
- with cost_col3:
- st.metric("التكلفة بالأسبوع", f"{equipment['weekly_cost']} ريال")
- with cost_col4:
- st.metric("التكلفة بالشهر", f"{equipment['monthly_cost']} ريال")
-
- # عرض معلومات التشغيل والصيانة
- st.markdown("### معلومات التشغيل والصيانة")
- maint_col1, maint_col2, maint_col3 = st.columns(3)
- with maint_col1:
- st.metric("استهلاك الوقود", f"{equipment['fuel_consumption']}")
- with maint_col2:
- st.metric("فترة الصيانة", f"{equipment['maintenance_period']}")
- with maint_col3:
- st.metric("تكلفة الصيانة", f"{equipment['maintenance_cost']} ريال")
-
- st.markdown(f"**يتطلب مشغل:** {'نعم' if equipment['operator_required'] else 'لا'}")
-
- # إضافة زر للتعديل
- if st.button("تعديل بيانات المعدة"):
- st.session_state.edit_equipment = equipment['id']
- # هنا يمكن إضافة منطق التعديل
-
- def _render_add_equipment_tab(self):
- """عرض تبويب إضافة معدة"""
-
- st.markdown("### إضافة معدة جديدة")
-
- # استخراج البيانات
- equipment_df = st.session_state.equipment_catalog
-
- # إنشاء نموذج إضافة معدة
- with st.form("add_equipment_form"):
- st.markdown("#### المعلومات الأساسية")
-
- # الصف الأول
- col1, col2 = st.columns(2)
- with col1:
- equipment_id = st.text_input("كود المعدة", value=f"EQ-{len(equipment_df) + 1:03d}")
- equipment_name = st.text_input("اسم المعدة", placeholder="مثال: حفارة هيدروليكية متوسطة")
-
- with col2:
- # استخراج الفئات والفئات الفرعية الموجودة
- categories = sorted(equipment_df["category"].unique().tolist())
- equipment_category = st.selectbox("فئة المعدة", categories)
-
- # استخراج الفئات الفرعية بناءً على الفئة المختارة
- subcategories = sorted(equipment_df[equipment_df["category"] == equipment_category]["subcategory"].unique().tolist())
- equipment_subcategory = st.selectbox("الفئة الفرعية", subcategories)
-
- # الصف الثاني
- col1, col2 = st.columns(2)
- with col1:
- equipment_brand = st.text_input("الماركة", placeholder="مثال: كاتربيلر")
- equipment_model = st.text_input("الموديل", placeholder="مثال: CAT 320")
-
- with col2:
- equipment_capacity = st.text_input("السعة", placeholder="مثال: 1.5 م3")
- equipment_production_rate = st.text_input("معدل الإنتاج", placeholder="مثال: 100 م3/ساعة")
-
- st.markdown("#### معلومات التكلفة")
-
- # الصف الثالث
- col1, col2, col3, col4 = st.columns(4)
- with col1:
- equipment_hourly_cost = st.number_input("التكلفة بالساعة (ريال)", min_value=0, step=10)
- with col2:
- equipment_daily_cost = st.number_input("التكلفة باليوم (ريال)", min_value=0, step=100)
- with col3:
- equipment_weekly_cost = st.number_input("التكلفة بالأسبوع (ريال)", min_value=0, step=500)
- with col4:
- equipment_monthly_cost = st.number_input("التكلفة بالشهر (ريال)", min_value=0, step=1000)
-
- st.markdown("#### معلومات التشغيل والصيانة")
-
- # الصف الرابع
- col1, col2, col3 = st.columns(3)
- with col1:
- equipment_fuel_consumption = st.text_input("استهلاك الوقود", placeholder="مثال: 25 لتر/ساعة")
- with col2:
- equipment_maintenance_period = st.text_input("فترة الصيانة", placeholder="مثال: 250 ساعة")
- with col3:
- equipment_maintenance_cost = st.number_input("تكلفة الصيانة (ريال)", min_value=0, step=500)
-
- # الصف الخامس
- col1, col2 = st.columns(2)
- with col1:
- equipment_operator_required = st.checkbox("يتطلب مشغل")
- with col2:
- equipment_image_url = st.text_input("رابط الصورة", placeholder="مثال: https://example.com/image.jpg")
-
- # وصف المعدة
- equipment_description = st.text_area("وصف المعدة", placeholder="أدخل وصفاً تفصيلياً للمعدة")
-
- # زر الإضافة
- submit_button = st.form_submit_button("إضافة المعدة")
-
- if submit_button:
- # التحقق من البيانات
- if not equipment_name or not equipment_category or not equipment_subcategory:
- st.error("يرجى إدخال المعلومات الأساسية للمعدة")
- else:
- # إنشاء معدة جديدة
- new_equipment = {
- "id": equipment_id,
- "name": equipment_name,
- "category": equipment_category,
- "subcategory": equipment_subcategory,
- "brand": equipment_brand,
- "model": equipment_model,
- "capacity": equipment_capacity,
- "production_rate": equipment_production_rate,
- "hourly_cost": equipment_hourly_cost,
- "daily_cost": equipment_daily_cost,
- "weekly_cost": equipment_weekly_cost,
- "monthly_cost": equipment_monthly_cost,
- "fuel_consumption": equipment_fuel_consumption,
- "maintenance_period": equipment_maintenance_period,
- "maintenance_cost": equipment_maintenance_cost,
- "operator_required": equipment_operator_required,
- "description": equipment_description,
- "image_url": equipment_image_url if equipment_image_url else "https://via.placeholder.com/150"
- }
-
- # إضافة المعدة إلى الكتالوج
- st.session_state.equipment_catalog = pd.concat([
- st.session_state.equipment_catalog,
- pd.DataFrame([new_equipment])
- ], ignore_index=True)
-
- st.success(f"تمت إضافة المعدة {equipment_name} بنجاح!")
-
- def _render_cost_analysis_tab(self):
- """عرض تبويب تحليل التكاليف"""
-
- st.markdown("### تحليل تكاليف المعدات")
-
- # استخراج البيانات
- equipment_df = st.session_state.equipment_catalog
-
- # تحليل متوسط التكاليف حسب الفئة
- st.markdown("#### متوسط التكاليف حسب الفئة")
-
- # حساب متوسط التكاليف لكل فئة
- category_costs = equipment_df.groupby("category").agg({
- "hourly_cost": "mean",
- "daily_cost": "mean",
- "weekly_cost": "mean",
- "monthly_cost": "mean"
- }).reset_index()
-
- # تغيير أسماء الأعمدة
- category_costs.columns = ["الفئة", "متوسط التكلفة بالساعة", "متوسط التكلفة باليوم", "متوسط التكلفة بالأسبوع", "متوسط التكلفة بالشهر"]
-
- # عرض الجدول
- st.dataframe(category_costs, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة
- st.markdown("#### مقارنة متوسط التكاليف اليومية حسب الفئة")
-
- fig = px.bar(
- category_costs,
- x="الفئة",
- y="متوسط التكلفة باليوم",
- title="متوسط التكاليف اليومية حسب فئة المعدات",
- color="الفئة",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل توزيع المعدات حسب الفئة
- st.markdown("#### توزيع المعدات حسب الفئة")
-
- # حساب عدد المعدات في كل فئة
- category_counts = equipment_df["category"].value_counts().reset_index()
- category_counts.columns = ["الفئة", "عدد المعدات"]
-
- # إنشاء رسم بياني دائري
- fig = px.pie(
- category_counts,
- values="عدد المعدات",
- names="الفئة",
- title="توزيع المعدات حسب الفئة",
- color="الفئة"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # حاسبة تكاليف المشروع
- st.markdown("#### حاسبة تكاليف المشروع")
-
- with st.form("project_cost_calculator"):
- st.markdown("أدخل المعدات المطلوبة للمشروع")
-
- # اختيار المعدات
- selected_equipment = st.multiselect(
- "اختر المعدات",
- options=equipment_df["name"].tolist(),
- format_func=lambda x: f"{x} ({equipment_df[equipment_df['name'] == x]['id'].iloc[0]})"
- )
-
- # اختيار مدة المشروع
- project_duration = st.number_input("مدة المشروع (بالأيام)", min_value=1, value=30)
-
- # زر الحساب
- calculate_button = st.form_submit_button("حساب التكاليف")
-
- if calculate_button:
- if not selected_equipment:
- st.error("يرجى اختيار معدة واحدة على الأقل")
- else:
- # حساب التكاليف
- project_costs = []
-
- for equipment_name in selected_equipment:
- equipment = equipment_df[equipment_df["name"] == equipment_name].iloc[0]
-
- # حساب التكلفة بناءً على المدة
- if project_duration <= 1:
- # تكلفة يوم واحد
- cost = equipment["daily_cost"]
- cost_type = "يومية"
- elif project_duration <= 7:
- # تكلفة أسبوع
- cost = equipment["weekly_cost"]
- cost_type = "أسبوعية"
- else:
- # تكلفة شهر أو أكثر
- months = project_duration / 30
- cost = equipment["monthly_cost"] * months
- cost_type = "شهرية"
-
- project_costs.append({
- "المعدة": equipment_name,
- "الكود": equipment["id"],
- "نوع التكلفة": cost_type,
- "التكلفة الإجمالية": cost
- })
-
- # عرض النتائج
- project_costs_df = pd.DataFrame(project_costs)
- st.dataframe(project_costs_df, use_container_width=True)
-
- # حساب إجمالي التكاليف
- total_cost = project_costs_df["التكلفة الإجمالية"].sum()
- st.metric("إجمالي تكاليف المعدات للمشروع", f"{total_cost:,.2f} ريال")
-
- def _render_import_export_tab(self):
- """عرض تبويب استيراد/تصدير"""
-
- st.markdown("### استيراد وتصدير بيانات المعدات")
-
- # استيراد البيانات
- st.markdown("#### استيراد البيانات")
-
- uploaded_file = st.file_uploader("اختر ملف Excel لاستيراد بيانات المعدات", type=["xlsx", "xls"])
-
- if uploaded_file is not None:
- try:
- # قراءة الملف
- imported_df = pd.read_excel(uploaded_file)
-
- # عرض البيانات المستوردة
- st.dataframe(imported_df, use_container_width=True)
-
- # زر الاستيراد
- if st.button("استيراد البيانات"):
- # التحقق من وجود الأعمدة المطلوبة
- required_columns = ["id", "name", "category", "subcategory"]
-
- if all(col in imported_df.columns for col in required_columns):
- # دمج البيانات المستوردة مع البيانات الحالية
- st.session_state.equipment_catalog = pd.concat([
- st.session_state.equipment_catalog,
- imported_df
- ], ignore_index=True).drop_duplicates(subset=["id"])
-
- st.success(f"تم استيراد {len(imported_df)} معدة بنجاح!")
- else:
- st.error("الملف المستورد لا يحتوي على الأعمدة المطلوبة")
-
- except Exception as e:
- st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}")
-
- # تصدير البيانات
- st.markdown("#### تصدير البيانات")
-
- # اختيار تنسيق التصدير
- export_format = st.radio("اختر تنسيق التصدير", ["Excel", "CSV", "JSON"], horizontal=True)
-
- if st.button("تصدير البيانات"):
- # استخراج البيانات
- equipment_df = st.session_state.equipment_catalog
-
- # تصدير البيانات حسب التنسيق المختار
- if export_format == "Excel":
- # تصدير إلى Excel
- output = io.BytesIO()
- with pd.ExcelWriter(output, engine="openpyxl") as writer:
- equipment_df.to_excel(writer, index=False, sheet_name="Equipment")
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف Excel",
- data=output.getvalue(),
- file_name="equipment_catalog.xlsx",
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
- )
-
- elif export_format == "CSV":
- # تصدير إلى CSV
- csv_data = equipment_df.to_csv(index=False)
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف CSV",
- data=csv_data,
- file_name="equipment_catalog.csv",
- mime="text/csv"
- )
-
- else: # JSON
- # تصدير إلى JSON
- json_data = equipment_df.to_json(orient="records", force_ascii=False)
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف JSON",
- data=json_data,
- file_name="equipment_catalog.json",
- mime="application/json"
- )
-
- def get_equipment_by_id(self, equipment_id):
- """الحصول على معدة بواسطة الكود"""
-
- equipment_df = st.session_state.equipment_catalog
- equipment = equipment_df[equipment_df["id"] == equipment_id]
-
- if not equipment.empty:
- return equipment.iloc[0].to_dict()
-
- return None
-
- def get_equipment_by_category(self, category):
- """الحصول على المعدات حسب الفئة"""
-
- equipment_df = st.session_state.equipment_catalog
- equipment = equipment_df[equipment_df["category"] == category]
-
- if not equipment.empty:
- return equipment.to_dict(orient="records")
-
- return []
-
- def calculate_equipment_cost(self, equipment_id, duration_days):
- """حساب تكلفة المعدة بناءً على المدة"""
-
- equipment = self.get_equipment_by_id(equipment_id)
-
- if equipment:
- # حساب التكلفة بناءً على المدة
- if duration_days <= 1:
- # تكلفة يوم واحد
- return equipment["daily_cost"]
- elif duration_days <= 7:
- # تكلفة أسبوع
- return equipment["weekly_cost"]
- else:
- # تكلفة شهر أو أكثر
- months = duration_days / 30
- return equipment["monthly_cost"] * months
-
- return 0
+"""
+كتالوج المعدات - وحدة إدارة معدات المقاولات
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import plotly.express as px
+import os
+import json
+from datetime import datetime
+import io
+
+class EquipmentCatalog:
+ """كتالوج المعدات"""
+
+ def __init__(self):
+ """تهيئة كتالوج المعدات"""
+
+ # تهيئة حالة الجلسة لكتالوج المعدات
+ if 'equipment_catalog' not in st.session_state:
+ # إنشاء بيانات افتراضية للمعدات
+ self._initialize_equipment_catalog()
+
+ def _initialize_equipment_catalog(self):
+ """تهيئة بيانات كتالوج المعدات"""
+
+ # تعريف فئات المعدات
+ equipment_categories = [
+ "معدات الحفر والردم",
+ "معدات النقل",
+ "معدات الرفع",
+ "معدات الخرسانة",
+ "معدات الطرق",
+ "معدات الصرف الصحي",
+ "معدات السيول والكباري",
+ "معدات الضغط والتثبيت",
+ "معدات التوليد والطاقة",
+ "معدات القياس والمساحة"
+ ]
+
+ # إنشاء قائمة المعدات
+ equipment_data = []
+
+ # 1. معدات الحفر والردم
+ equipment_data.extend([
+ {
+ "id": "EQ-001",
+ "name": "حفارة هيدروليكية كبيرة",
+ "category": "معدات الحفر والردم",
+ "subcategory": "حفارات",
+ "brand": "كاتربيلر",
+ "model": "CAT 336",
+ "capacity": "2.5 م3",
+ "production_rate": "150 م3/ساعة",
+ "hourly_cost": 350,
+ "daily_cost": 2800,
+ "weekly_cost": 16800,
+ "monthly_cost": 67200,
+ "fuel_consumption": "35 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 5000,
+ "operator_required": True,
+ "description": "حفارة هيدروليكية كبيرة مناسبة لأعمال الحفر الثقيلة ومشاريع البنية التحتية الكبيرة",
+ "image_url": "https://example.com/cat336.jpg"
+ },
+ {
+ "id": "EQ-002",
+ "name": "حفارة هيدروليكية متوسطة",
+ "category": "معدات الحفر والردم",
+ "subcategory": "حفارات",
+ "brand": "كاتربيلر",
+ "model": "CAT 320",
+ "capacity": "1.5 م3",
+ "production_rate": "100 م3/ساعة",
+ "hourly_cost": 250,
+ "daily_cost": 2000,
+ "weekly_cost": 12000,
+ "monthly_cost": 48000,
+ "fuel_consumption": "25 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 3500,
+ "operator_required": True,
+ "description": "حفارة هيدروليكية متوسطة الحجم مناسبة لمعظم مشاريع البنية التحتية",
+ "image_url": "https://example.com/cat320.jpg"
+ },
+ {
+ "id": "EQ-003",
+ "name": "حفارة هيدروليكية صغيرة",
+ "category": "معدات الحفر والردم",
+ "subcategory": "حفارات",
+ "brand": "كاتربيلر",
+ "model": "CAT 308",
+ "capacity": "0.8 م3",
+ "production_rate": "50 م3/ساعة",
+ "hourly_cost": 150,
+ "daily_cost": 1200,
+ "weekly_cost": 7200,
+ "monthly_cost": 28800,
+ "fuel_consumption": "15 لتر/ساعة",
+ "maintenance_period": "200 ساعة",
+ "maintenance_cost": 2000,
+ "operator_required": True,
+ "description": "حفارة هيدروليكية صغيرة مناسبة للمشاريع الصغيرة والمساحات الضيقة",
+ "image_url": "https://example.com/cat308.jpg"
+ },
+ {
+ "id": "EQ-004",
+ "name": "بلدوزر كبير",
+ "category": "معدات الحفر والردم",
+ "subcategory": "بلدوزرات",
+ "brand": "كاتربيلر",
+ "model": "D9",
+ "capacity": "13.5 م3",
+ "production_rate": "300 م3/ساعة",
+ "hourly_cost": 400,
+ "daily_cost": 3200,
+ "weekly_cost": 19200,
+ "monthly_cost": 76800,
+ "fuel_consumption": "45 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 6000,
+ "operator_required": True,
+ "description": "بلدوزر كبير لأعمال التسوية والدفع في المشاريع الكبيرة",
+ "image_url": "https://example.com/catd9.jpg"
+ },
+ {
+ "id": "EQ-005",
+ "name": "بلدوزر متوسط",
+ "category": "معدات الحفر والردم",
+ "subcategory": "بلدوزرات",
+ "brand": "كاتربيلر",
+ "model": "D7",
+ "capacity": "8.5 م3",
+ "production_rate": "200 م3/ساعة",
+ "hourly_cost": 300,
+ "daily_cost": 2400,
+ "weekly_cost": 14400,
+ "monthly_cost": 57600,
+ "fuel_consumption": "35 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 4500,
+ "operator_required": True,
+ "description": "بلدوزر متوسط الحجم مناسب لمعظم مشاريع البنية التحتية",
+ "image_url": "https://example.com/catd7.jpg"
+ },
+ {
+ "id": "EQ-006",
+ "name": "لودر أمامي كبير",
+ "category": "معدات الحفر والردم",
+ "subcategory": "لودرات",
+ "brand": "كاتربيلر",
+ "model": "980",
+ "capacity": "5.5 م3",
+ "production_rate": "250 م3/ساعة",
+ "hourly_cost": 300,
+ "daily_cost": 2400,
+ "weekly_cost": 14400,
+ "monthly_cost": 57600,
+ "fuel_consumption": "30 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 4000,
+ "operator_required": True,
+ "description": "لودر أمامي كبير لأعمال التحميل في المشاريع الكبيرة",
+ "image_url": "https://example.com/cat980.jpg"
+ },
+ {
+ "id": "EQ-007",
+ "name": "لودر أمامي متوسط",
+ "category": "معدات الحفر والردم",
+ "subcategory": "لودرات",
+ "brand": "كاتربيلر",
+ "model": "950",
+ "capacity": "3.5 م3",
+ "production_rate": "180 م3/ساعة",
+ "hourly_cost": 250,
+ "daily_cost": 2000,
+ "weekly_cost": 12000,
+ "monthly_cost": 48000,
+ "fuel_consumption": "25 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 3500,
+ "operator_required": True,
+ "description": "لودر أمامي متوسط الحجم مناسب لمعظم مشاريع البنية التحتية",
+ "image_url": "https://example.com/cat950.jpg"
+ },
+ {
+ "id": "EQ-008",
+ "name": "باكهو لودر",
+ "category": "معدات الحفر والردم",
+ "subcategory": "لودرات",
+ "brand": "جي سي بي",
+ "model": "3CX",
+ "capacity": "1.0 م3",
+ "production_rate": "60 م3/ساعة",
+ "hourly_cost": 150,
+ "daily_cost": 1200,
+ "weekly_cost": 7200,
+ "monthly_cost": 28800,
+ "fuel_consumption": "12 لتر/ساعة",
+ "maintenance_period": "200 ساعة",
+ "maintenance_cost": 2000,
+ "operator_required": True,
+ "description": "باكهو لودر متعدد الاستخدامات للحفر والتحميل",
+ "image_url": "https://example.com/jcb3cx.jpg"
+ },
+ {
+ "id": "EQ-009",
+ "name": "جريدر",
+ "category": "معدات الحفر والردم",
+ "subcategory": "معدات تسوية",
+ "brand": "كاتربيلر",
+ "model": "140",
+ "capacity": "3.7 م عرض الشفرة",
+ "production_rate": "2000 م2/ساعة",
+ "hourly_cost": 250,
+ "daily_cost": 2000,
+ "weekly_cost": 12000,
+ "monthly_cost": 48000,
+ "fuel_consumption": "20 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 3000,
+ "operator_required": True,
+ "description": "جريدر لتسوية الطرق والمساحات",
+ "image_url": "https://example.com/cat140.jpg"
+ },
+ {
+ "id": "EQ-010",
+ "name": "سكريبر",
+ "category": "معدات الحفر والردم",
+ "subcategory": "معدات تسوية",
+ "brand": "كاتربيلر",
+ "model": "621",
+ "capacity": "21 م3",
+ "production_rate": "400 م3/ساعة",
+ "hourly_cost": 350,
+ "daily_cost": 2800,
+ "weekly_cost": 16800,
+ "monthly_cost": 67200,
+ "fuel_consumption": "35 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 5000,
+ "operator_required": True,
+ "description": "سكريبر لنقل وتسوية التربة لمسافات متوسطة",
+ "image_url": "https://example.com/cat621.jpg"
+ }
+ ])
+
+ # 2. معدات النقل
+ equipment_data.extend([
+ {
+ "id": "EQ-011",
+ "name": "شاحنة قلاب كبيرة",
+ "category": "معدات النقل",
+ "subcategory": "شاحنات قلاب",
+ "brand": "مان",
+ "model": "TGS 40.480",
+ "capacity": "30 م3",
+ "production_rate": "30 م3/رحلة",
+ "hourly_cost": 200,
+ "daily_cost": 1600,
+ "weekly_cost": 9600,
+ "monthly_cost": 38400,
+ "fuel_consumption": "25 لتر/ساعة",
+ "maintenance_period": "300 ساعة",
+ "maintenance_cost": 3000,
+ "operator_required": True,
+ "description": "شاحنة قلاب كبيرة لنقل مواد الحفر والردم",
+ "image_url": "https://example.com/mantgs.jpg"
+ },
+ {
+ "id": "EQ-012",
+ "name": "شاحنة قلاب متوسطة",
+ "category": "معدات النقل",
+ "subcategory": "شاحنات قلاب",
+ "brand": "مرسيدس",
+ "model": "Actros 3341",
+ "capacity": "20 م3",
+ "production_rate": "20 م3/رحلة",
+ "hourly_cost": 180,
+ "daily_cost": 1440,
+ "weekly_cost": 8640,
+ "monthly_cost": 34560,
+ "fuel_consumption": "20 لتر/ساعة",
+ "maintenance_period": "300 ساعة",
+ "maintenance_cost": 2500,
+ "operator_required": True,
+ "description": "شاحنة قلاب متوسطة لنقل مواد الحفر والردم",
+ "image_url": "https://example.com/actros.jpg"
+ },
+ {
+ "id": "EQ-013",
+ "name": "شاحنة خلاطة خرسانة",
+ "category": "معدات النقل",
+ "subcategory": "شاحنات خرسانة",
+ "brand": "مرسيدس",
+ "model": "Actros 3236",
+ "capacity": "8 م3",
+ "production_rate": "8 م3/رحلة",
+ "hourly_cost": 200,
+ "daily_cost": 1600,
+ "weekly_cost": 9600,
+ "monthly_cost": 38400,
+ "fuel_consumption": "20 لتر/ساعة",
+ "maintenance_period": "300 ساعة",
+ "maintenance_cost": 3000,
+ "operator_required": True,
+ "description": "شاحنة خلاطة خرسانة لنقل الخرسانة الجاهزة",
+ "image_url": "https://example.com/mixer.jpg"
+ },
+ {
+ "id": "EQ-014",
+ "name": "شاحنة نقل مياه",
+ "category": "معدات النقل",
+ "subcategory": "شاحنات مياه",
+ "brand": "مان",
+ "model": "TGS 33.360",
+ "capacity": "20000 لتر",
+ "production_rate": "20000 لتر/رحلة",
+ "hourly_cost": 150,
+ "daily_cost": 1200,
+ "weekly_cost": 7200,
+ "monthly_cost": 28800,
+ "fuel_consumption": "18 لتر/ساعة",
+ "maintenance_period": "300 ساعة",
+ "maintenance_cost": 2000,
+ "operator_required": True,
+ "description": "شاحنة نقل مياه للمشاريع والرش",
+ "image_url": "https://example.com/watertruck.jpg"
+ },
+ {
+ "id": "EQ-015",
+ "name": "شاحنة نقل معدات",
+ "category": "معدات النقل",
+ "subcategory": "شاحنات نقل",
+ "brand": "فولفو",
+ "model": "FH16",
+ "capacity": "60 طن",
+ "production_rate": "60 طن/رحلة",
+ "hourly_cost": 250,
+ "daily_cost": 2000,
+ "weekly_cost": 12000,
+ "monthly_cost": 48000,
+ "fuel_consumption": "25 لتر/ساعة",
+ "maintenance_period": "300 ساعة",
+ "maintenance_cost": 3500,
+ "operator_required": True,
+ "description": "شاحنة نقل معدات ثقيلة (لوبد)",
+ "image_url": "https://example.com/lowbed.jpg"
+ }
+ ])
+
+ # 3. معدات الرفع
+ equipment_data.extend([
+ {
+ "id": "EQ-016",
+ "name": "رافعة برجية",
+ "category": "معدات الرفع",
+ "subcategory": "رافعات برجية",
+ "brand": "ليبهر",
+ "model": "200 EC-H",
+ "capacity": "10 طن",
+ "production_rate": "20 رفعة/ساعة",
+ "hourly_cost": 400,
+ "daily_cost": 3200,
+ "weekly_cost": 19200,
+ "monthly_cost": 76800,
+ "fuel_consumption": "30 كيلوواط/ساعة",
+ "maintenance_period": "300 ساعة",
+ "maintenance_cost": 5000,
+ "operator_required": True,
+ "description": "رافعة برجية للمشاريع الإنشائية الكبيرة",
+ "image_url": "https://example.com/towercrane.jpg"
+ },
+ {
+ "id": "EQ-017",
+ "name": "رافعة متحركة كبيرة",
+ "category": "معدات الرفع",
+ "subcategory": "رافعات متحركة",
+ "brand": "ليبهر",
+ "model": "LTM 1200",
+ "capacity": "200 طن",
+ "production_rate": "15 رفعة/ساعة",
+ "hourly_cost": 600,
+ "daily_cost": 4800,
+ "weekly_cost": 28800,
+ "monthly_cost": 115200,
+ "fuel_consumption": "40 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 8000,
+ "operator_required": True,
+ "description": "رافعة متحركة كبيرة للأحمال الثقيلة",
+ "image_url": "https://example.com/mobilecrane.jpg"
+ },
+ {
+ "id": "EQ-018",
+ "name": "رافعة متحركة متوسطة",
+ "category": "معدات الرفع",
+ "subcategory": "رافعات متحركة",
+ "brand": "ليبهر",
+ "model": "LTM 1070",
+ "capacity": "70 طن",
+ "production_rate": "15 رفعة/ساعة",
+ "hourly_cost": 400,
+ "daily_cost": 3200,
+ "weekly_cost": 19200,
+ "monthly_cost": 76800,
+ "fuel_consumption": "30 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 6000,
+ "operator_required": True,
+ "description": "رافعة متحركة متوسطة للاستخدامات المتنوعة",
+ "image_url": "https://example.com/mobilecrane2.jpg"
+ },
+ {
+ "id": "EQ-019",
+ "name": "رافعة شوكية",
+ "category": "معدات الرفع",
+ "subcategory": "رافعات شوكية",
+ "brand": "كاتربيلر",
+ "model": "DP70N",
+ "capacity": "7 طن",
+ "production_rate": "30 رفعة/ساعة",
+ "hourly_cost": 150,
+ "daily_cost": 1200,
+ "weekly_cost": 7200,
+ "monthly_cost": 28800,
+ "fuel_consumption": "12 لتر/ساعة",
+ "maintenance_period": "200 ساعة",
+ "maintenance_cost": 2000,
+ "operator_required": True,
+ "description": "رافعة شوكية لنقل المواد في الموقع",
+ "image_url": "https://example.com/forklift.jpg"
+ },
+ {
+ "id": "EQ-020",
+ "name": "رافعة سلة",
+ "category": "معدات الرفع",
+ "subcategory": "رافعات سلة",
+ "brand": "جيني",
+ "model": "S-85",
+ "capacity": "227 كجم",
+ "production_rate": "ارتفاع 26 متر",
+ "hourly_cost": 200,
+ "daily_cost": 1600,
+ "weekly_cost": 9600,
+ "monthly_cost": 38400,
+ "fuel_consumption": "10 لتر/ساعة",
+ "maintenance_period": "200 ساعة",
+ "maintenance_cost": 2500,
+ "operator_required": True,
+ "description": "رافعة سلة للوصول إلى الارتفاعات",
+ "image_url": "https://example.com/boomlift.jpg"
+ }
+ ])
+
+ # 4. معدات الخرسانة
+ equipment_data.extend([
+ {
+ "id": "EQ-021",
+ "name": "خلاطة خرسانة مركزية",
+ "category": "معدات الخرسانة",
+ "subcategory": "خلاطات",
+ "brand": "ليبهر",
+ "model": "Betomix 3.0",
+ "capacity": "120 م3/ساعة",
+ "production_rate": "120 م3/ساعة",
+ "hourly_cost": 800,
+ "daily_cost": 6400,
+ "weekly_cost": 38400,
+ "monthly_cost": 153600,
+ "fuel_consumption": "60 كيلوواط/ساعة",
+ "maintenance_period": "500 ساعة",
+ "maintenance_cost": 10000,
+ "operator_required": True,
+ "description": "محطة خلط خرسانة مركزية للمشاريع الكبيرة",
+ "image_url": "https://example.com/batchplant.jpg"
+ },
+ {
+ "id": "EQ-022",
+ "name": "خلاطة خرسانة متنقلة",
+ "category": "معدات الخرسانة",
+ "subcategory": "خلاطات",
+ "brand": "كارمكس",
+ "model": "MCP-30",
+ "capacity": "30 م3/ساعة",
+ "production_rate": "30 م3/ساعة",
+ "hourly_cost": 300,
+ "daily_cost": 2400,
+ "weekly_cost": 14400,
+ "monthly_cost": 57600,
+ "fuel_consumption": "25 كيلوواط/ساعة",
+ "maintenance_period": "300 ساعة",
+ "maintenance_cost": 5000,
+ "operator_required": True,
+ "description": "محطة خلط خرسانة متنقلة للمشاريع المتوسطة",
+ "image_url": "https://example.com/mobilemixer.jpg"
+ },
+ {
+ "id": "EQ-023",
+ "name": "مضخة خرسانة ثابتة",
+ "category": "معدات الخرسانة",
+ "subcategory": "مضخات",
+ "brand": "بوتزميستر",
+ "model": "BSA 1409",
+ "capacity": "90 م3/ساعة",
+ "production_rate": "90 م3/ساعة",
+ "hourly_cost": 350,
+ "daily_cost": 2800,
+ "weekly_cost": 16800,
+ "monthly_cost": 67200,
+ "fuel_consumption": "30 كيلوواط/ساعة",
+ "maintenance_period": "300 ساعة",
+ "maintenance_cost": 6000,
+ "operator_required": True,
+ "description": "مضخة خرسانة ثابتة للمشاريع الكبيرة",
+ "image_url": "https://example.com/concretepump.jpg"
+ },
+ {
+ "id": "EQ-024",
+ "name": "مضخة خرسانة متحركة",
+ "category": "معدات الخرسانة",
+ "subcategory": "مضخات",
+ "brand": "بوتزميستر",
+ "model": "M42-5",
+ "capacity": "160 م3/ساعة",
+ "production_rate": "160 م3/ساعة",
+ "hourly_cost": 500,
+ "daily_cost": 4000,
+ "weekly_cost": 24000,
+ "monthly_cost": 96000,
+ "fuel_consumption": "40 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 8000,
+ "operator_required": True,
+ "description": "مضخة خرسانة متحركة بذراع 42 متر",
+ "image_url": "https://example.com/boomconcretepump.jpg"
+ },
+ {
+ "id": "EQ-025",
+ "name": "هزاز خرسانة",
+ "category": "معدات الخرسانة",
+ "subcategory": "هزازات",
+ "brand": "واكر نيوسن",
+ "model": "IREN",
+ "capacity": "غير محدد",
+ "production_rate": "غير محدد",
+ "hourly_cost": 20,
+ "daily_cost": 160,
+ "weekly_cost": 960,
+ "monthly_cost": 3840,
+ "fuel_consumption": "غير محدد",
+ "maintenance_period": "100 ساعة",
+ "maintenance_cost": 500,
+ "operator_required": True,
+ "description": "هزاز خرسانة لدمك الخرسانة",
+ "image_url": "https://example.com/vibrator.jpg"
+ },
+ {
+ "id": "EQ-026",
+ "name": "ماكينة تسوية الخرسانة",
+ "category": "معدات الخرسانة",
+ "subcategory": "معدات تشطيب",
+ "brand": "سومرو",
+ "model": "S-840",
+ "capacity": "840 م2/ساعة",
+ "production_rate": "840 م2/ساعة",
+ "hourly_cost": 100,
+ "daily_cost": 800,
+ "weekly_cost": 4800,
+ "monthly_cost": 19200,
+ "fuel_consumption": "5 لتر/ساعة",
+ "maintenance_period": "150 ساعة",
+ "maintenance_cost": 1500,
+ "operator_required": True,
+ "description": "ماكينة تسوية الخرسانة (هليكوبتر)",
+ "image_url": "https://example.com/powertrowel.jpg"
+ }
+ ])
+
+ # 5. معدات الطرق
+ equipment_data.extend([
+ {
+ "id": "EQ-027",
+ "name": "فرادة أسفلت كبيرة",
+ "category": "معدات الطرق",
+ "subcategory": "فرادات",
+ "brand": "فوجيلي",
+ "model": "Super 2100-3i",
+ "capacity": "1100 طن/ساعة",
+ "production_rate": "1100 طن/ساعة",
+ "hourly_cost": 600,
+ "daily_cost": 4800,
+ "weekly_cost": 28800,
+ "monthly_cost": 115200,
+ "fuel_consumption": "45 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 8000,
+ "operator_required": True,
+ "description": "فرادة أسفلت كبيرة للطرق السريعة",
+ "image_url": "https://example.com/paver.jpg"
+ },
+ {
+ "id": "EQ-028",
+ "name": "فرادة أسفلت متوسطة",
+ "category": "معدات الطرق",
+ "subcategory": "فرادات",
+ "brand": "فوجيلي",
+ "model": "Super 1800-3i",
+ "capacity": "700 طن/ساعة",
+ "production_rate": "700 طن/ساعة",
+ "hourly_cost": 450,
+ "daily_cost": 3600,
+ "weekly_cost": 21600,
+ "monthly_cost": 86400,
+ "fuel_consumption": "35 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 6000,
+ "operator_required": True,
+ "description": "فرادة أسفلت متوسطة للطرق العامة",
+ "image_url": "https://example.com/paver2.jpg"
+ },
+ {
+ "id": "EQ-029",
+ "name": "مدحلة أسفلت ثقيلة",
+ "category": "معدات الطرق",
+ "subcategory": "مداحل",
+ "brand": "بوماج",
+ "model": "BW 203",
+ "capacity": "غير محدد",
+ "production_rate": "3000 م2/ساعة",
+ "hourly_cost": 250,
+ "daily_cost": 2000,
+ "weekly_cost": 12000,
+ "monthly_cost": 48000,
+ "fuel_consumption": "20 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 3500,
+ "operator_required": True,
+ "description": "مدحلة أسفلت ثقيلة للطرق",
+ "image_url": "https://example.com/roller.jpg"
+ },
+ {
+ "id": "EQ-030",
+ "name": "مدحلة أسفلت مطاطية",
+ "category": "معدات الطرق",
+ "subcategory": "مداحل",
+ "brand": "بوماج",
+ "model": "BW 27 RH",
+ "capacity": "غير محدد",
+ "production_rate": "3500 م2/ساعة",
+ "hourly_cost": 200,
+ "daily_cost": 1600,
+ "weekly_cost": 9600,
+ "monthly_cost": 38400,
+ "fuel_consumption": "18 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 3000,
+ "operator_required": True,
+ "description": "مدحلة أسفلت مطاطية للطرق",
+ "image_url": "https://example.com/rubberroller.jpg"
+ },
+ {
+ "id": "EQ-031",
+ "name": "قاشطة أسفلت",
+ "category": "معدات الطرق",
+ "subcategory": "قاشطات",
+ "brand": "ويرتجن",
+ "model": "W 210",
+ "capacity": "غير محدد",
+ "production_rate": "800 م2/ساعة",
+ "hourly_cost": 500,
+ "daily_cost": 4000,
+ "weekly_cost": 24000,
+ "monthly_cost": 96000,
+ "fuel_consumption": "40 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 7000,
+ "operator_required": True,
+ "description": "قاشطة أسفلت لإزالة طبقات الأسفلت القديمة",
+ "image_url": "https://example.com/milling.jpg"
+ },
+ {
+ "id": "EQ-032",
+ "name": "شاحنة رش البيتومين",
+ "category": "معدات الطرق",
+ "subcategory": "معدات رش",
+ "brand": "روزنباور",
+ "model": "S12000",
+ "capacity": "12000 لتر",
+ "production_rate": "15000 م2/ساعة",
+ "hourly_cost": 200,
+ "daily_cost": 1600,
+ "weekly_cost": 9600,
+ "monthly_cost": 38400,
+ "fuel_consumption": "15 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 3000,
+ "operator_required": True,
+ "description": "شاحنة رش البيتومين للطرق",
+ "image_url": "https://example.com/bitumensprayer.jpg"
+ }
+ ])
+
+ # 6. معدات الصرف الصحي
+ equipment_data.extend([
+ {
+ "id": "EQ-033",
+ "name": "حفارة خنادق كبيرة",
+ "category": "معدات الصرف الصحي",
+ "subcategory": "حفارات خنادق",
+ "brand": "فيرمير",
+ "model": "T1255III",
+ "capacity": "غير محدد",
+ "production_rate": "300 م/ساعة",
+ "hourly_cost": 400,
+ "daily_cost": 3200,
+ "weekly_cost": 19200,
+ "monthly_cost": 76800,
+ "fuel_consumption": "35 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 6000,
+ "operator_required": True,
+ "description": "حفارة خنادق كبيرة لمشاريع الصرف الصحي",
+ "image_url": "https://example.com/trencher.jpg"
+ },
+ {
+ "id": "EQ-034",
+ "name": "ماكينة دفع أنابيب",
+ "category": "معدات الصرف الصحي",
+ "subcategory": "معدات دفع",
+ "brand": "هيرينكنيشت",
+ "model": "HK-500",
+ "capacity": "غير محدد",
+ "production_rate": "20 م/ساعة",
+ "hourly_cost": 600,
+ "daily_cost": 4800,
+ "weekly_cost": 28800,
+ "monthly_cost": 115200,
+ "fuel_consumption": "40 كيلوواط/ساعة",
+ "maintenance_period": "300 ساعة",
+ "maintenance_cost": 8000,
+ "operator_required": True,
+ "description": "ماكينة دفع أنابيب بدون حفر مفتوح",
+ "image_url": "https://example.com/pipejacking.jpg"
+ },
+ {
+ "id": "EQ-035",
+ "name": "سيارة شفط وتنظيف مجاري",
+ "category": "معدات الصرف الصحي",
+ "subcategory": "معدات تنظيف",
+ "brand": "كايزر",
+ "model": "AquaStar",
+ "capacity": "12000 لتر",
+ "production_rate": "غير محدد",
+ "hourly_cost": 300,
+ "daily_cost": 2400,
+ "weekly_cost": 14400,
+ "monthly_cost": 57600,
+ "fuel_consumption": "20 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 4000,
+ "operator_required": True,
+ "description": "سيارة شفط وتنظيف مجاري بضغط عالي",
+ "image_url": "https://example.com/sewercleaner.jpg"
+ },
+ {
+ "id": "EQ-036",
+ "name": "كاميرا فحص مجاري",
+ "category": "معدات الصرف الصحي",
+ "subcategory": "معدات فحص",
+ "brand": "إيبوس",
+ "model": "ROVION",
+ "capacity": "غير محدد",
+ "production_rate": "غير محدد",
+ "hourly_cost": 150,
+ "daily_cost": 1200,
+ "weekly_cost": 7200,
+ "monthly_cost": 28800,
+ "fuel_consumption": "غير محدد",
+ "maintenance_period": "200 ساعة",
+ "maintenance_cost": 2000,
+ "operator_required": True,
+ "description": "كاميرا فحص مجاري للتفتيش والصيانة",
+ "image_url": "https://example.com/sewercamera.jpg"
+ },
+ {
+ "id": "EQ-037",
+ "name": "مضخة مياه غاطسة كبيرة",
+ "category": "معدات الصرف الصحي",
+ "subcategory": "مضخات",
+ "brand": "جرندفوس",
+ "model": "S2",
+ "capacity": "400 م3/ساعة",
+ "production_rate": "400 م3/ساعة",
+ "hourly_cost": 100,
+ "daily_cost": 800,
+ "weekly_cost": 4800,
+ "monthly_cost": 19200,
+ "fuel_consumption": "15 كيلوواط/ساعة",
+ "maintenance_period": "500 ساعة",
+ "maintenance_cost": 2500,
+ "operator_required": False,
+ "description": "مضخة مياه غاطسة كبيرة لمشاريع الصرف الصحي",
+ "image_url": "https://example.com/submersiblepump.jpg"
+ }
+ ])
+
+ # 7. معدات السيول والكباري
+ equipment_data.extend([
+ {
+ "id": "EQ-038",
+ "name": "معدات دق الخوازيق",
+ "category": "معدات السيول والكباري",
+ "subcategory": "معدات خوازيق",
+ "brand": "بوير",
+ "model": "BG 28",
+ "capacity": "غير محدد",
+ "production_rate": "10 خازوق/يوم",
+ "hourly_cost": 800,
+ "daily_cost": 6400,
+ "weekly_cost": 38400,
+ "monthly_cost": 153600,
+ "fuel_consumption": "60 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 10000,
+ "operator_required": True,
+ "description": "معدات دق الخوازيق للكباري والأساسات العميقة",
+ "image_url": "https://example.com/piling.jpg"
+ },
+ {
+ "id": "EQ-039",
+ "name": "رافعة جسرية",
+ "category": "معدات السيول والكباري",
+ "subcategory": "رافعات",
+ "brand": "ليبهر",
+ "model": "LG 1750",
+ "capacity": "750 طن",
+ "production_rate": "غير محدد",
+ "hourly_cost": 1200,
+ "daily_cost": 9600,
+ "weekly_cost": 57600,
+ "monthly_cost": 230400,
+ "fuel_consumption": "80 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 15000,
+ "operator_required": True,
+ "description": "رافعة جسرية لتركيب عناصر الكباري الثقيلة",
+ "image_url": "https://example.com/bridgecrane.jpg"
+ },
+ {
+ "id": "EQ-040",
+ "name": "معدات شد الكابلات",
+ "category": "معدات السيول والكباري",
+ "subcategory": "معدات شد",
+ "brand": "فرايسينت",
+ "model": "C500",
+ "capacity": "500 طن",
+ "production_rate": "غير محدد",
+ "hourly_cost": 300,
+ "daily_cost": 2400,
+ "weekly_cost": 14400,
+ "monthly_cost": 57600,
+ "fuel_consumption": "غير محدد",
+ "maintenance_period": "300 ساعة",
+ "maintenance_cost": 5000,
+ "operator_required": True,
+ "description": "معدات شد الكابلات للكباري المعلقة",
+ "image_url": "https://example.com/stressing.jpg"
+ },
+ {
+ "id": "EQ-041",
+ "name": "معدات حفر الأنفاق",
+ "category": "معدات السيول والكباري",
+ "subcategory": "معدات أنفاق",
+ "brand": "هيرينكنيشت",
+ "model": "S-500",
+ "capacity": "غير محدد",
+ "production_rate": "15 م/يوم",
+ "hourly_cost": 1500,
+ "daily_cost": 12000,
+ "weekly_cost": 72000,
+ "monthly_cost": 288000,
+ "fuel_consumption": "100 كيلوواط/ساعة",
+ "maintenance_period": "500 ساعة",
+ "maintenance_cost": 20000,
+ "operator_required": True,
+ "description": "معدات حفر الأنفاق للمشاريع الكبيرة",
+ "image_url": "https://example.com/tbm.jpg"
+ },
+ {
+ "id": "EQ-042",
+ "name": "سدود مؤقتة",
+ "category": "معدات السيول والكباري",
+ "subcategory": "معدات سيول",
+ "brand": "أكواباريير",
+ "model": "K-100",
+ "capacity": "غير محدد",
+ "production_rate": "100 م/يوم",
+ "hourly_cost": 200,
+ "daily_cost": 1600,
+ "weekly_cost": 9600,
+ "monthly_cost": 38400,
+ "fuel_consumption": "غير محدد",
+ "maintenance_period": "غير محدد",
+ "maintenance_cost": 2000,
+ "operator_required": True,
+ "description": "سدود مؤقتة للحماية من السيول",
+ "image_url": "https://example.com/cofferdam.jpg"
+ }
+ ])
+
+ # 8. معدات الضغط والتثبيت
+ equipment_data.extend([
+ {
+ "id": "EQ-043",
+ "name": "مدحلة تربة ثقيلة",
+ "category": "معدات الضغط والتثبيت",
+ "subcategory": "مداحل",
+ "brand": "بوماج",
+ "model": "BW 226",
+ "capacity": "غير محدد",
+ "production_rate": "3000 م2/ساعة",
+ "hourly_cost": 250,
+ "daily_cost": 2000,
+ "weekly_cost": 12000,
+ "monthly_cost": 48000,
+ "fuel_consumption": "20 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 3500,
+ "operator_required": True,
+ "description": "مدحلة تربة ثقيلة للمشاريع الكبيرة",
+ "image_url": "https://example.com/soilroller.jpg"
+ },
+ {
+ "id": "EQ-044",
+ "name": "دكاكة قفازة",
+ "category": "معدات الضغط والتثبيت",
+ "subcategory": "دكاكات",
+ "brand": "واكر نيوسن",
+ "model": "BS 60-4",
+ "capacity": "غير محدد",
+ "production_rate": "150 م2/ساعة",
+ "hourly_cost": 50,
+ "daily_cost": 400,
+ "weekly_cost": 2400,
+ "monthly_cost": 9600,
+ "fuel_consumption": "2 لتر/ساعة",
+ "maintenance_period": "100 ساعة",
+ "maintenance_cost": 800,
+ "operator_required": True,
+ "description": "دكاكة قفازة للمساحات الضيقة",
+ "image_url": "https://example.com/rammer.jpg"
+ },
+ {
+ "id": "EQ-045",
+ "name": "دكاكة هزازة",
+ "category": "معدات الضغط والتثبيت",
+ "subcategory": "دكاكات",
+ "brand": "واكر نيوسن",
+ "model": "DPU 6555",
+ "capacity": "غير محدد",
+ "production_rate": "500 م2/ساعة",
+ "hourly_cost": 80,
+ "daily_cost": 640,
+ "weekly_cost": 3840,
+ "monthly_cost": 15360,
+ "fuel_consumption": "3 لتر/ساعة",
+ "maintenance_period": "100 ساعة",
+ "maintenance_cost": 1000,
+ "operator_required": True,
+ "description": "دكاكة هزازة للمساحات المتوسطة",
+ "image_url": "https://example.com/platecompactor.jpg"
+ },
+ {
+ "id": "EQ-046",
+ "name": "معدات تثبيت التربة",
+ "category": "معدات الضغط والتثبيت",
+ "subcategory": "معدات تثبيت",
+ "brand": "ويرتجن",
+ "model": "WR 250",
+ "capacity": "غير محدد",
+ "production_rate": "5000 م2/يوم",
+ "hourly_cost": 500,
+ "daily_cost": 4000,
+ "weekly_cost": 24000,
+ "monthly_cost": 96000,
+ "fuel_consumption": "40 لتر/ساعة",
+ "maintenance_period": "250 ساعة",
+ "maintenance_cost": 7000,
+ "operator_required": True,
+ "description": "معدات تثبيت التربة بالإسمنت أو الجير",
+ "image_url": "https://example.com/soilstabilizer.jpg"
+ }
+ ])
+
+ # 9. معدات التوليد والطاقة
+ equipment_data.extend([
+ {
+ "id": "EQ-047",
+ "name": "مولد كهرباء كبير",
+ "category": "معدات التوليد والطاقة",
+ "subcategory": "مولدات",
+ "brand": "كاتربيلر",
+ "model": "C15",
+ "capacity": "500 كيلوواط",
+ "production_rate": "500 كيلوواط",
+ "hourly_cost": 300,
+ "daily_cost": 2400,
+ "weekly_cost": 14400,
+ "monthly_cost": 57600,
+ "fuel_consumption": "80 لتر/ساعة",
+ "maintenance_period": "500 ساعة",
+ "maintenance_cost": 5000,
+ "operator_required": False,
+ "description": "مولد كهرباء كبير للمشاريع الكبيرة",
+ "image_url": "https://example.com/generator.jpg"
+ },
+ {
+ "id": "EQ-048",
+ "name": "مولد كهرباء متوسط",
+ "category": "معدات التوليد والطاقة",
+ "subcategory": "مولدات",
+ "brand": "كاتربيلر",
+ "model": "C9",
+ "capacity": "250 كيلوواط",
+ "production_rate": "250 كيلوواط",
+ "hourly_cost": 200,
+ "daily_cost": 1600,
+ "weekly_cost": 9600,
+ "monthly_cost": 38400,
+ "fuel_consumption": "45 لتر/ساعة",
+ "maintenance_period": "500 ساعة",
+ "maintenance_cost": 3500,
+ "operator_required": False,
+ "description": "مولد كهرباء متوسط للمشاريع المتوسطة",
+ "image_url": "https://example.com/generator2.jpg"
+ },
+ {
+ "id": "EQ-049",
+ "name": "ضاغط هواء كبير",
+ "category": "معدات التوليد والطاقة",
+ "subcategory": "ضواغط",
+ "brand": "أطلس كوبكو",
+ "model": "XRVS 1000",
+ "capacity": "1000 قدم مكعب/دقيقة",
+ "production_rate": "1000 قدم مكعب/دقيقة",
+ "hourly_cost": 200,
+ "daily_cost": 1600,
+ "weekly_cost": 9600,
+ "monthly_cost": 38400,
+ "fuel_consumption": "30 لتر/ساعة",
+ "maintenance_period": "500 ساعة",
+ "maintenance_cost": 3000,
+ "operator_required": False,
+ "description": "ضاغط هواء كبير للمشاريع الكبيرة",
+ "image_url": "https://example.com/compressor.jpg"
+ },
+ {
+ "id": "EQ-050",
+ "name": "ضاغط هواء متوسط",
+ "category": "معدات التوليد والطاقة",
+ "subcategory": "ضواغط",
+ "brand": "أطلس كوبكو",
+ "model": "XRVS 500",
+ "capacity": "500 قدم مكعب/دقيقة",
+ "production_rate": "500 قدم مكعب/دقيقة",
+ "hourly_cost": 150,
+ "daily_cost": 1200,
+ "weekly_cost": 7200,
+ "monthly_cost": 28800,
+ "fuel_consumption": "20 لتر/ساعة",
+ "maintenance_period": "500 ساعة",
+ "maintenance_cost": 2500,
+ "operator_required": False,
+ "description": "ضاغط هواء متوسط للمشاريع المتوسطة",
+ "image_url": "https://example.com/compressor2.jpg"
+ }
+ ])
+
+ # 10. معدات القياس والمساحة
+ equipment_data.extend([
+ {
+ "id": "EQ-051",
+ "name": "محطة رصد متكاملة",
+ "category": "معدات القياس والمساحة",
+ "subcategory": "محطات رصد",
+ "brand": "ليكا",
+ "model": "TS16",
+ "capacity": "غير محدد",
+ "production_rate": "غير محدد",
+ "hourly_cost": 100,
+ "daily_cost": 800,
+ "weekly_cost": 4800,
+ "monthly_cost": 19200,
+ "fuel_consumption": "غير محدد",
+ "maintenance_period": "1000 ساعة",
+ "maintenance_cost": 2000,
+ "operator_required": True,
+ "description": "محطة رصد متكاملة للمساحة الدقيقة",
+ "image_url": "https://example.com/totalstation.jpg"
+ },
+ {
+ "id": "EQ-052",
+ "name": "جهاز GPS مساحي",
+ "category": "معدات القياس والمساحة",
+ "subcategory": "أجهزة GPS",
+ "brand": "ترمبل",
+ "model": "R10",
+ "capacity": "غير محدد",
+ "production_rate": "غير محدد",
+ "hourly_cost": 80,
+ "daily_cost": 640,
+ "weekly_cost": 3840,
+ "monthly_cost": 15360,
+ "fuel_consumption": "غير محدد",
+ "maintenance_period": "1000 ساعة",
+ "maintenance_cost": 1500,
+ "operator_required": True,
+ "description": "جهاز GPS مساحي دقيق",
+ "image_url": "https://example.com/gps.jpg"
+ },
+ {
+ "id": "EQ-053",
+ "name": "جهاز مسح ليزري ثلاثي الأبعاد",
+ "category": "معدات القياس والمساحة",
+ "subcategory": "أجهزة مسح",
+ "brand": "ليكا",
+ "model": "RTC360",
+ "capacity": "غير محدد",
+ "production_rate": "غير محدد",
+ "hourly_cost": 150,
+ "daily_cost": 1200,
+ "weekly_cost": 7200,
+ "monthly_cost": 28800,
+ "fuel_consumption": "غير محدد",
+ "maintenance_period": "1000 ساعة",
+ "maintenance_cost": 3000,
+ "operator_required": True,
+ "description": "جهاز مسح ليزري ثلاثي الأبعاد للمشاريع المعقدة",
+ "image_url": "https://example.com/laserscanner.jpg"
+ },
+ {
+ "id": "EQ-054",
+ "name": "طائرة بدون طيار للمسح",
+ "category": "معدات القياس والمساحة",
+ "subcategory": "طائرات مسح",
+ "brand": "دي جي آي",
+ "model": "Phantom 4 RTK",
+ "capacity": "غير محدد",
+ "production_rate": "غير محدد",
+ "hourly_cost": 100,
+ "daily_cost": 800,
+ "weekly_cost": 4800,
+ "monthly_cost": 19200,
+ "fuel_consumption": "غير محدد",
+ "maintenance_period": "500 ساعة",
+ "maintenance_cost": 1000,
+ "operator_required": True,
+ "description": "طائرة بدون طيار للمسح الجوي والتصوير",
+ "image_url": "https://example.com/drone.jpg"
+ }
+ ])
+
+ # تخزين البيانات في حالة الجلسة
+ st.session_state.equipment_catalog = pd.DataFrame(equipment_data)
+
+ def render(self):
+ """عرض واجهة كتالوج المعدات"""
+
+ st.markdown("## كتالوج المعدات")
+
+ # إنشاء تبويبات لعرض الكتالوج
+ tabs = st.tabs([
+ "عرض الكتالوج",
+ "إضافة معدة",
+ "تحليل التكاليف",
+ "استيراد/تصدير"
+ ])
+
+ with tabs[0]:
+ self._render_catalog_view_tab()
+
+ with tabs[1]:
+ self._render_add_equipment_tab()
+
+ with tabs[2]:
+ self._render_cost_analysis_tab()
+
+ with tabs[3]:
+ self._render_import_export_tab()
+
+ def _render_catalog_view_tab(self):
+ """عرض تبويب عرض الكتالوج"""
+
+ st.markdown("### عرض كتالوج المعدات")
+
+ # استخراج البيانات
+ equipment_df = st.session_state.equipment_catalog
+
+ # إنشاء فلاتر للعرض
+ col1, col2 = st.columns(2)
+
+ with col1:
+ # فلتر حسب الفئة
+ categories = ["الكل"] + sorted(equipment_df["category"].unique().tolist())
+ selected_category = st.selectbox("اختر فئة المعدات", categories)
+
+ with col2:
+ # فلتر حسب الفئة الفرعية
+ if selected_category != "الكل":
+ subcategories = ["الكل"] + sorted(equipment_df[equipment_df["category"] == selected_category]["subcategory"].unique().tolist())
+ else:
+ subcategories = ["الكل"] + sorted(equipment_df["subcategory"].unique().tolist())
+
+ selected_subcategory = st.selectbox("اختر الفئة الفرعية", subcategories)
+
+ # تطبيق الفلاتر
+ filtered_df = equipment_df.copy()
+
+ if selected_category != "الكل":
+ filtered_df = filtered_df[filtered_df["category"] == selected_category]
+
+ if selected_subcategory != "الكل":
+ filtered_df = filtered_df[filtered_df["subcategory"] == selected_subcategory]
+
+ # عرض البيانات
+ if not filtered_df.empty:
+ # عرض عدد النتائج
+ st.info(f"تم العثور على {len(filtered_df)} معدة")
+
+ # عرض المعدات في شكل بطاقات
+ for i, (_, equipment) in enumerate(filtered_df.iterrows()):
+ col1, col2 = st.columns([1, 2])
+
+ with col1:
+ # عرض صورة المعدة (استخدام صورة افتراضية)
+ st.image("https://via.placeholder.com/150", caption=equipment["name"])
+
+ with col2:
+ # عرض معلومات المعدة
+ st.markdown(f"**{equipment['name']}** (الكود: {equipment['id']})")
+ st.markdown(f"الفئة: {equipment['category']} - {equipment['subcategory']}")
+ st.markdown(f"الماركة: {equipment['brand']} | الموديل: {equipment['model']}")
+ st.markdown(f"السعة: {equipment['capacity']} | معدل الإنتاج: {equipment['production_rate']}")
+
+ # عرض التكاليف
+ cost_col1, cost_col2, cost_col3, cost_col4 = st.columns(4)
+ with cost_col1:
+ st.metric("بالساعة", f"{equipment['hourly_cost']} ريال")
+ with cost_col2:
+ st.metric("باليوم", f"{equipment['daily_cost']} ريال")
+ with cost_col3:
+ st.metric("بالأسبوع", f"{equipment['weekly_cost']} ريال")
+ with cost_col4:
+ st.metric("بالشهر", f"{equipment['monthly_cost']} ريال")
+
+ # إضافة زر لعرض التفاصيل
+ if st.button(f"عرض التفاصيل الكاملة", key=f"details_{equipment['id']}"):
+ st.session_state.selected_equipment = equipment['id']
+ self._show_equipment_details(equipment)
+
+ st.markdown("---")
+ else:
+ st.warning("لا توجد معدات تطابق معايير البحث")
+
+ def _show_equipment_details(self, equipment):
+ """عرض تفاصيل المعدة"""
+
+ st.markdown(f"## تفاصيل المعدة: {equipment['name']}")
+
+ col1, col2 = st.columns([1, 2])
+
+ with col1:
+ # عرض صورة المعدة (استخدام صورة افتراضية)
+ st.image("https://via.placeholder.com/300", caption=equipment["name"])
+
+ with col2:
+ # عرض المعلومات الأساسية
+ st.markdown("### المعلومات الأساسية")
+ st.markdown(f"**الكود:** {equipment['id']}")
+ st.markdown(f"**الفئة:** {equipment['category']} - {equipment['subcategory']}")
+ st.markdown(f"**الماركة:** {equipment['brand']}")
+ st.markdown(f"**الموديل:** {equipment['model']}")
+ st.markdown(f"**السعة:** {equipment['capacity']}")
+ st.markdown(f"**معدل الإنتاج:** {equipment['production_rate']}")
+ st.markdown(f"**الوصف:** {equipment['description']}")
+
+ # عرض معلومات التكلفة
+ st.markdown("### معلومات التكلفة")
+ cost_col1, cost_col2, cost_col3, cost_col4 = st.columns(4)
+ with cost_col1:
+ st.metric("التكلفة بالساعة", f"{equipment['hourly_cost']} ريال")
+ with cost_col2:
+ st.metric("التكلفة باليوم", f"{equipment['daily_cost']} ريال")
+ with cost_col3:
+ st.metric("التكلفة بالأسبوع", f"{equipment['weekly_cost']} ريال")
+ with cost_col4:
+ st.metric("التكلفة بالشهر", f"{equipment['monthly_cost']} ريال")
+
+ # عرض معلومات التشغيل والصيانة
+ st.markdown("### معلومات التشغيل والصيانة")
+ maint_col1, maint_col2, maint_col3 = st.columns(3)
+ with maint_col1:
+ st.metric("استهلاك الوقود", f"{equipment['fuel_consumption']}")
+ with maint_col2:
+ st.metric("فترة الصيانة", f"{equipment['maintenance_period']}")
+ with maint_col3:
+ st.metric("تكلفة الصيانة", f"{equipment['maintenance_cost']} ريال")
+
+ st.markdown(f"**يتطلب مشغل:** {'نعم' if equipment['operator_required'] else 'لا'}")
+
+ # إضافة زر للتعديل
+ if st.button("تعديل بيانات المعدة"):
+ st.session_state.edit_equipment = equipment['id']
+ # هنا يمكن إضافة منطق التعديل
+
+ def _render_add_equipment_tab(self):
+ """عرض تبويب إضافة معدة"""
+
+ st.markdown("### إضافة معدة جديدة")
+
+ # استخراج البيانات
+ equipment_df = st.session_state.equipment_catalog
+
+ # إنشاء نموذج إضافة معدة
+ with st.form("add_equipment_form"):
+ st.markdown("#### المعلومات الأساسية")
+
+ # الصف الأول
+ col1, col2 = st.columns(2)
+ with col1:
+ equipment_id = st.text_input("كود المعدة", value=f"EQ-{len(equipment_df) + 1:03d}")
+ equipment_name = st.text_input("اسم المعدة", placeholder="مثال: حفارة هيدروليكية متوسطة")
+
+ with col2:
+ # استخراج الفئات والفئات الفرعية الموجودة
+ categories = sorted(equipment_df["category"].unique().tolist())
+ equipment_category = st.selectbox("فئة المعدة", categories)
+
+ # استخراج الفئات الفرعية بناءً على الفئة المختارة
+ subcategories = sorted(equipment_df[equipment_df["category"] == equipment_category]["subcategory"].unique().tolist())
+ equipment_subcategory = st.selectbox("الفئة الفرعية", subcategories)
+
+ # الصف الثاني
+ col1, col2 = st.columns(2)
+ with col1:
+ equipment_brand = st.text_input("الماركة", placeholder="مثال: كاتربيلر")
+ equipment_model = st.text_input("الموديل", placeholder="مثال: CAT 320")
+
+ with col2:
+ equipment_capacity = st.text_input("السعة", placeholder="مثال: 1.5 م3")
+ equipment_production_rate = st.text_input("معدل الإنتاج", placeholder="مثال: 100 م3/ساعة")
+
+ st.markdown("#### معلومات التكلفة")
+
+ # الصف الثالث
+ col1, col2, col3, col4 = st.columns(4)
+ with col1:
+ equipment_hourly_cost = st.number_input("التكلفة بالساعة (ريال)", min_value=0, step=10)
+ with col2:
+ equipment_daily_cost = st.number_input("التكلفة باليوم (ريال)", min_value=0, step=100)
+ with col3:
+ equipment_weekly_cost = st.number_input("التكلفة بالأسبوع (ريال)", min_value=0, step=500)
+ with col4:
+ equipment_monthly_cost = st.number_input("التكلفة بالشهر (ريال)", min_value=0, step=1000)
+
+ st.markdown("#### معلومات التشغيل والصيانة")
+
+ # الصف الرابع
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ equipment_fuel_consumption = st.text_input("استهلاك الوقود", placeholder="مثال: 25 لتر/ساعة")
+ with col2:
+ equipment_maintenance_period = st.text_input("فترة الصيانة", placeholder="مثال: 250 ساعة")
+ with col3:
+ equipment_maintenance_cost = st.number_input("تكلفة الصيانة (ريال)", min_value=0, step=500)
+
+ # الصف الخامس
+ col1, col2 = st.columns(2)
+ with col1:
+ equipment_operator_required = st.checkbox("يتطلب مشغل")
+ with col2:
+ equipment_image_url = st.text_input("رابط الصورة", placeholder="مثال: https://example.com/image.jpg")
+
+ # وصف المعدة
+ equipment_description = st.text_area("وصف المعدة", placeholder="أدخل وصفاً تفصيلياً للمعدة")
+
+ # زر الإضافة
+ submit_button = st.form_submit_button("إضافة المعدة")
+
+ if submit_button:
+ # التحقق من البيانات
+ if not equipment_name or not equipment_category or not equipment_subcategory:
+ st.error("يرجى إدخال المعلومات الأساسية للمعدة")
+ else:
+ # إنشاء معدة جديدة
+ new_equipment = {
+ "id": equipment_id,
+ "name": equipment_name,
+ "category": equipment_category,
+ "subcategory": equipment_subcategory,
+ "brand": equipment_brand,
+ "model": equipment_model,
+ "capacity": equipment_capacity,
+ "production_rate": equipment_production_rate,
+ "hourly_cost": equipment_hourly_cost,
+ "daily_cost": equipment_daily_cost,
+ "weekly_cost": equipment_weekly_cost,
+ "monthly_cost": equipment_monthly_cost,
+ "fuel_consumption": equipment_fuel_consumption,
+ "maintenance_period": equipment_maintenance_period,
+ "maintenance_cost": equipment_maintenance_cost,
+ "operator_required": equipment_operator_required,
+ "description": equipment_description,
+ "image_url": equipment_image_url if equipment_image_url else "https://via.placeholder.com/150"
+ }
+
+ # إضافة المعدة إلى الكتالوج
+ st.session_state.equipment_catalog = pd.concat([
+ st.session_state.equipment_catalog,
+ pd.DataFrame([new_equipment])
+ ], ignore_index=True)
+
+ st.success(f"تمت إضافة المعدة {equipment_name} بنجاح!")
+
+ def _render_cost_analysis_tab(self):
+ """عرض تبويب تحليل التكاليف"""
+
+ st.markdown("### تحليل تكاليف المعدات")
+
+ # استخراج البيانات
+ equipment_df = st.session_state.equipment_catalog
+
+ # تحليل متوسط التكاليف حسب الفئة
+ st.markdown("#### متوسط التكاليف حسب الفئة")
+
+ # حساب متوسط التكاليف لكل فئة
+ category_costs = equipment_df.groupby("category").agg({
+ "hourly_cost": "mean",
+ "daily_cost": "mean",
+ "weekly_cost": "mean",
+ "monthly_cost": "mean"
+ }).reset_index()
+
+ # تغيير أسماء الأعمدة
+ category_costs.columns = ["الفئة", "متوسط التكلفة بالساعة", "متوسط التكلفة باليوم", "متوسط التكلفة بالأسبوع", "متوسط التكلفة بالشهر"]
+
+ # عرض الجدول
+ st.dataframe(category_costs, use_container_width=True)
+
+ # إنشاء رسم بياني للمقارنة
+ st.markdown("#### مقارنة متوسط التكاليف اليومية حسب الفئة")
+
+ fig = px.bar(
+ category_costs,
+ x="الفئة",
+ y="متوسط التكلفة باليوم",
+ title="متوسط التكاليف اليومية حسب فئة المعدات",
+ color="الفئة",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل توزيع المعدات حسب الفئة
+ st.markdown("#### توزيع المعدات حسب الفئة")
+
+ # حساب عدد المعدات في كل فئة
+ category_counts = equipment_df["category"].value_counts().reset_index()
+ category_counts.columns = ["الفئة", "عدد المعدات"]
+
+ # إنشاء رسم بياني دائري
+ fig = px.pie(
+ category_counts,
+ values="عدد المعدات",
+ names="الفئة",
+ title="توزيع المعدات حسب الفئة",
+ color="الفئة"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # حاسبة تكاليف المشروع
+ st.markdown("#### حاسبة تكاليف المشروع")
+
+ with st.form("project_cost_calculator"):
+ st.markdown("أدخل المعدات المطلوبة للمشروع")
+
+ # اختيار المعدات
+ selected_equipment = st.multiselect(
+ "اختر المعدات",
+ options=equipment_df["name"].tolist(),
+ format_func=lambda x: f"{x} ({equipment_df[equipment_df['name'] == x]['id'].iloc[0]})"
+ )
+
+ # اختيار مدة المشروع
+ project_duration = st.number_input("مدة المشروع (بالأيام)", min_value=1, value=30)
+
+ # زر الحساب
+ calculate_button = st.form_submit_button("حساب التكاليف")
+
+ if calculate_button:
+ if not selected_equipment:
+ st.error("يرجى اختيار معدة واحدة على الأقل")
+ else:
+ # حساب التكاليف
+ project_costs = []
+
+ for equipment_name in selected_equipment:
+ equipment = equipment_df[equipment_df["name"] == equipment_name].iloc[0]
+
+ # حساب التكلفة بناءً على المدة
+ if project_duration <= 1:
+ # تكلفة يوم واحد
+ cost = equipment["daily_cost"]
+ cost_type = "يومية"
+ elif project_duration <= 7:
+ # تكلفة أسبوع
+ cost = equipment["weekly_cost"]
+ cost_type = "أسبوعية"
+ else:
+ # تكلفة شهر أو أكثر
+ months = project_duration / 30
+ cost = equipment["monthly_cost"] * months
+ cost_type = "شهرية"
+
+ project_costs.append({
+ "المعدة": equipment_name,
+ "الكود": equipment["id"],
+ "نوع التكلفة": cost_type,
+ "التكلفة الإجمالية": cost
+ })
+
+ # عرض النتائج
+ project_costs_df = pd.DataFrame(project_costs)
+ st.dataframe(project_costs_df, use_container_width=True)
+
+ # حساب إجمالي التكاليف
+ total_cost = project_costs_df["التكلفة الإجمالية"].sum()
+ st.metric("إجمالي تكاليف المعدات للمشروع", f"{total_cost:,.2f} ريال")
+
+ def _render_import_export_tab(self):
+ """عرض تبويب استيراد/تصدير"""
+
+ st.markdown("### استيراد وتصدير بيانات المعدات")
+
+ # استيراد البيانات
+ st.markdown("#### استيراد البيانات")
+
+ uploaded_file = st.file_uploader("اختر ملف Excel لاستيراد بيانات المعدات", type=["xlsx", "xls"])
+
+ if uploaded_file is not None:
+ try:
+ # قراءة الملف
+ imported_df = pd.read_excel(uploaded_file)
+
+ # عرض البيانات المستوردة
+ st.dataframe(imported_df, use_container_width=True)
+
+ # زر الاستيراد
+ if st.button("استيراد البيانات"):
+ # التحقق من وجود الأعمدة المطلوبة
+ required_columns = ["id", "name", "category", "subcategory"]
+
+ if all(col in imported_df.columns for col in required_columns):
+ # دمج البيانات المستوردة مع البيانات الحالية
+ st.session_state.equipment_catalog = pd.concat([
+ st.session_state.equipment_catalog,
+ imported_df
+ ], ignore_index=True).drop_duplicates(subset=["id"])
+
+ st.success(f"تم استيراد {len(imported_df)} معدة بنجاح!")
+ else:
+ st.error("الملف المستورد لا يحتوي على الأعمدة المطلوبة")
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}")
+
+ # تصدير البيانات
+ st.markdown("#### تصدير البيانات")
+
+ # اختيار تنسيق التصدير
+ export_format = st.radio("اختر تنسيق التصدير", ["Excel", "CSV", "JSON"], horizontal=True)
+
+ if st.button("تصدير البيانات"):
+ # استخراج البيانات
+ equipment_df = st.session_state.equipment_catalog
+
+ # تصدير البيانات حسب التنسيق المختار
+ if export_format == "Excel":
+ # تصدير إلى Excel
+ output = io.BytesIO()
+ with pd.ExcelWriter(output, engine="openpyxl") as writer:
+ equipment_df.to_excel(writer, index=False, sheet_name="Equipment")
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف Excel",
+ data=output.getvalue(),
+ file_name="equipment_catalog.xlsx",
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ )
+
+ elif export_format == "CSV":
+ # تصدير إلى CSV
+ csv_data = equipment_df.to_csv(index=False)
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف CSV",
+ data=csv_data,
+ file_name="equipment_catalog.csv",
+ mime="text/csv"
+ )
+
+ else: # JSON
+ # تصدير إلى JSON
+ json_data = equipment_df.to_json(orient="records", force_ascii=False)
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف JSON",
+ data=json_data,
+ file_name="equipment_catalog.json",
+ mime="application/json"
+ )
+
+ def get_equipment_by_id(self, equipment_id):
+ """الحصول على معدة بواسطة الكود"""
+
+ equipment_df = st.session_state.equipment_catalog
+ equipment = equipment_df[equipment_df["id"] == equipment_id]
+
+ if not equipment.empty:
+ return equipment.iloc[0].to_dict()
+
+ return None
+
+ def get_equipment_by_category(self, category):
+ """الحصول على المعدات حسب الفئة"""
+
+ equipment_df = st.session_state.equipment_catalog
+ equipment = equipment_df[equipment_df["category"] == category]
+
+ if not equipment.empty:
+ return equipment.to_dict(orient="records")
+
+ return []
+
+ def calculate_equipment_cost(self, equipment_id, duration_days):
+ """حساب تكلفة المعدة بناءً على المدة"""
+
+ equipment = self.get_equipment_by_id(equipment_id)
+
+ if equipment:
+ # حساب التكلفة بناءً على المدة
+ if duration_days <= 1:
+ # تكلفة يوم واحد
+ return equipment["daily_cost"]
+ elif duration_days <= 7:
+ # تكلفة أسبوع
+ return equipment["weekly_cost"]
+ else:
+ # تكلفة شهر أو أكثر
+ months = duration_days / 30
+ return equipment["monthly_cost"] * months
+
+ return 0
diff --git a/pricing_system/modules/catalogs/labor_catalog.py b/pricing_system/modules/catalogs/labor_catalog.py
index ea07fd43b17b0d5a89b7f763b571b18d1b5f4bbf..98843ac772695bca88577ed5b54f8e4edffb320a 100644
--- a/pricing_system/modules/catalogs/labor_catalog.py
+++ b/pricing_system/modules/catalogs/labor_catalog.py
@@ -1,1527 +1,1527 @@
-"""
-كتالوج العمالة - وحدة إدارة العمالة والمهندسين
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import plotly.express as px
-import os
-import json
-from datetime import datetime
-import io
-
-class LaborCatalog:
- """كتالوج العمالة"""
-
- def __init__(self):
- """تهيئة كتالوج العمالة"""
-
- # تهيئة حالة الجلسة لكتالوج العمالة
- if 'labor_catalog' not in st.session_state:
- # إنشاء بيانات افتراضية للعمالة
- self._initialize_labor_catalog()
-
- def _initialize_labor_catalog(self):
- """تهيئة بيانات كتالوج العمالة"""
-
- # تعريف فئات العمالة
- labor_categories = [
- "مهندسين",
- "فنيين",
- "عمال مهرة",
- "عمال عاديين",
- "سائقين",
- "مشغلي معدات",
- "إداريين"
- ]
-
- # إنشاء قائمة العمالة
- labor_data = []
-
- # 1. مهندسين
- labor_data.extend([
- {
- "id": "ENG-001",
- "name": "مهندس مدني (خبرة 15+ سنة)",
- "category": "مهندسين",
- "subcategory": "مدني",
- "hourly_rate": 150,
- "daily_rate": 1200,
- "weekly_rate": 6000,
- "monthly_rate": 25000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "إدارة مشاريع، تصميم إنشائي، إشراف على التنفيذ",
- "certifications": "عضوية الهيئة السعودية للمهندسين، PMP",
- "description": "مهندس مدني ذو خبرة 15+ سنة في إدارة وتنفيذ مشاريع البنية التحتية والطرق والجسور"
- },
- {
- "id": "ENG-002",
- "name": "مهندس مدني (خبرة 10-15 سنة)",
- "category": "مهندسين",
- "subcategory": "مدني",
- "hourly_rate": 120,
- "daily_rate": 960,
- "weekly_rate": 4800,
- "monthly_rate": 20000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "إدارة مشاريع، تصميم إنشائي، إشراف على التنفيذ",
- "certifications": "عضوية الهيئة السعودية للمهندسين",
- "description": "مهندس مدني ذو خبرة 10-15 سنة في إدارة وتنفيذ مشاريع البنية التحتية والطرق والجسور"
- },
- {
- "id": "ENG-003",
- "name": "مهندس مدني (خبرة 5-10 سنوات)",
- "category": "مهندسين",
- "subcategory": "مدني",
- "hourly_rate": 100,
- "daily_rate": 800,
- "weekly_rate": 4000,
- "monthly_rate": 16000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تصميم إنشائي، إشراف على التنفيذ، حساب كميات",
- "certifications": "عضوية الهيئة السعودية للمهندسين",
- "description": "مهندس مدني ذو خبرة 5-10 سنوات في تنفيذ مشاريع البنية التحتية والطرق والجسور"
- },
- {
- "id": "ENG-004",
- "name": "مهندس مدني (خبرة 1-5 سنوات)",
- "category": "مهندسين",
- "subcategory": "مدني",
- "hourly_rate": 80,
- "daily_rate": 640,
- "weekly_rate": 3200,
- "monthly_rate": 13000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "إشراف على التنفيذ، حساب كميات، رسم هندسي",
- "certifications": "عضوية الهيئة السعودية للمهندسين",
- "description": "مهندس مدني حديث الخبرة في تنفيذ مشاريع البنية التحتية والطرق والجسور"
- },
- {
- "id": "ENG-005",
- "name": "مهندس ميكانيكي (خبرة 10+ سنة)",
- "category": "مهندسين",
- "subcategory": "ميكانيكي",
- "hourly_rate": 130,
- "daily_rate": 1040,
- "weekly_rate": 5200,
- "monthly_rate": 22000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تصميم أنظمة ميكانيكية، إدارة صيانة المعدات، إشراف على التنفيذ",
- "certifications": "عضوية الهيئة السعودية للمهندسين",
- "description": "مهندس ميكانيكي ذو خبرة 10+ سنة في تصميم وتنفيذ الأنظمة الميكانيكية وصيانة المعدات"
- },
- {
- "id": "ENG-006",
- "name": "مهندس ميكانيكي (خبرة 5-10 سنوات)",
- "category": "مهندسين",
- "subcategory": "ميكانيكي",
- "hourly_rate": 100,
- "daily_rate": 800,
- "weekly_rate": 4000,
- "monthly_rate": 16000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تصميم أنظمة ميكانيكية، صيانة المعدات، إشراف على التنفيذ",
- "certifications": "عضوية الهيئة السعودية للمهندسين",
- "description": "مهندس ميكانيكي ذو خبرة 5-10 سنوات في تنفيذ الأنظمة الميكانيكية وصيانة المعدات"
- },
- {
- "id": "ENG-007",
- "name": "مهندس كهربائي (خبرة 10+ سنة)",
- "category": "مهندسين",
- "subcategory": "كهربائي",
- "hourly_rate": 130,
- "daily_rate": 1040,
- "weekly_rate": 5200,
- "monthly_rate": 22000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تصميم أنظمة كهربائية، إدارة مشاريع كهربائية، إشراف على التنفيذ",
- "certifications": "عضوية الهيئة السعودية للمهندسين",
- "description": "مهندس كهربائي ذو خبرة 10+ سنة في تصميم وتنفيذ الأنظمة الكهربائية للمشاريع الكبرى"
- },
- {
- "id": "ENG-008",
- "name": "مهندس كهربائي (خبرة 5-10 سنوات)",
- "category": "مهندسين",
- "subcategory": "كهربائي",
- "hourly_rate": 100,
- "daily_rate": 800,
- "weekly_rate": 4000,
- "monthly_rate": 16000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تصميم أنظمة كهربائية، إشراف على التنفيذ، اختبار وتشغيل",
- "certifications": "عضوية الهيئة السعودية للمهندسين",
- "description": "مهندس كهربائي ذو خبرة 5-10 سنوات في تنفيذ الأنظمة الكهربائية للمشاريع"
- },
- {
- "id": "ENG-009",
- "name": "مهندس مساحة (خبرة 10+ سنة)",
- "category": "مهندسين",
- "subcategory": "مساحة",
- "hourly_rate": 120,
- "daily_rate": 960,
- "weekly_rate": 4800,
- "monthly_rate": 20000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "مسح طبوغرافي، نظم معلومات جغرافية، حساب كميات",
- "certifications": "عضوية الهيئة السعودية للمهندسين",
- "description": "مهندس مساحة ذو خبرة 10+ سنة في المسح الطبوغرافي ونظم المعلومات الجغرافية"
- },
- {
- "id": "ENG-010",
- "name": "مهندس مساحة (خبرة 5-10 سنوات)",
- "category": "مهندسين",
- "subcategory": "مساحة",
- "hourly_rate": 90,
- "daily_rate": 720,
- "weekly_rate": 3600,
- "monthly_rate": 15000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "مسح طبوغرافي، نظم معلومات جغرافية، حساب كميات",
- "certifications": "عضوية الهيئة السعودية للمهندسين",
- "description": "مهندس مساحة ذو خبرة 5-10 سنوات في المسح الطبوغرافي ونظم المعلومات الجغرافية"
- }
- ])
-
- # 2. فنيين
- labor_data.extend([
- {
- "id": "TECH-001",
- "name": "فني مدني (خبرة 10+ سنة)",
- "category": "فنيين",
- "subcategory": "مدني",
- "hourly_rate": 60,
- "daily_rate": 480,
- "weekly_rate": 2400,
- "monthly_rate": 9500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "قراءة مخططات، إشراف على التنفيذ، فحص جودة",
- "certifications": "دبلوم فني",
- "description": "فني مدني ذو خبرة 10+ سنة في الإشراف على تنفيذ الأعمال المدنية"
- },
- {
- "id": "TECH-002",
- "name": "فني مدني (خبرة 5-10 سنوات)",
- "category": "فنيين",
- "subcategory": "مدني",
- "hourly_rate": 50,
- "daily_rate": 400,
- "weekly_rate": 2000,
- "monthly_rate": 8000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "قراءة مخططات، إشراف على التنفيذ، فحص جودة",
- "certifications": "دبلوم فني",
- "description": "فني مدني ذو خبرة 5-10 سنوات في الإشراف على تنفيذ الأعمال المدنية"
- },
- {
- "id": "TECH-003",
- "name": "فني مساحة (خبرة 10+ سنة)",
- "category": "فنيين",
- "subcategory": "مساحة",
- "hourly_rate": 55,
- "daily_rate": 440,
- "weekly_rate": 2200,
- "monthly_rate": 9000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "استخدام أجهزة المساحة، قراءة مخططات، حساب كميات",
- "certifications": "دبلوم فني",
- "description": "فني مساحة ذو خبرة 10+ سنة في أعمال المساحة وحساب الكميات"
- },
- {
- "id": "TECH-004",
- "name": "فني مساحة (خبرة 5-10 سنوات)",
- "category": "فنيين",
- "subcategory": "مساحة",
- "hourly_rate": 45,
- "daily_rate": 360,
- "weekly_rate": 1800,
- "monthly_rate": 7500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "استخدام أجهزة المساحة، قراءة مخططات، حساب كميات",
- "certifications": "دبلوم فني",
- "description": "فني مساحة ذو خبرة 5-10 سنوات في أعمال المساحة وحساب الكميات"
- },
- {
- "id": "TECH-005",
- "name": "فني كهربائي (خبرة 10+ سنة)",
- "category": "فنيين",
- "subcategory": "كهربائي",
- "hourly_rate": 55,
- "daily_rate": 440,
- "weekly_rate": 2200,
- "monthly_rate": 9000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تمديدات كهربائية، قراءة مخططات، صيانة",
- "certifications": "دبلوم فني",
- "description": "فني كهربائي ذو خبرة 10+ سنة في التمديدات الكهربائية والصيانة"
- },
- {
- "id": "TECH-006",
- "name": "فني كهربائي (خبرة 5-10 سنوات)",
- "category": "فنيين",
- "subcategory": "كهربائي",
- "hourly_rate": 45,
- "daily_rate": 360,
- "weekly_rate": 1800,
- "monthly_rate": 7500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تمديدات كهربائية، قراءة مخططات، صيانة",
- "certifications": "دبلوم فني",
- "description": "فني كهربائي ذو خبرة 5-10 سنوات في التمديدات الكهربائية والصيانة"
- },
- {
- "id": "TECH-007",
- "name": "فني ميكانيكي (خبرة 10+ سنة)",
- "category": "فنيين",
- "subcategory": "ميكانيكي",
- "hourly_rate": 55,
- "daily_rate": 440,
- "weekly_rate": 2200,
- "monthly_rate": 9000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "صيانة معدات، تركيب أنظمة ميكانيكية، قراءة مخططات",
- "certifications": "دبلوم فني",
- "description": "فني ميكانيكي ذو خبرة 10+ سنة في صيانة المعدات وتركيب الأنظمة الميكانيكية"
- },
- {
- "id": "TECH-008",
- "name": "فني ميكانيكي (خبرة 5-10 سنوات)",
- "category": "فنيين",
- "subcategory": "ميكانيكي",
- "hourly_rate": 45,
- "daily_rate": 360,
- "weekly_rate": 1800,
- "monthly_rate": 7500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "صيانة معدات، تركيب أنظمة ميكانيكية، قراءة مخططات",
- "certifications": "دبلوم فني",
- "description": "فني ميكانيكي ذو خبرة 5-10 سنوات في صيانة المعدات وتركيب الأنظمة الميكانيكية"
- },
- {
- "id": "TECH-009",
- "name": "فني سباكة (خبرة 10+ سنة)",
- "category": "فنيين",
- "subcategory": "سباكة",
- "hourly_rate": 50,
- "daily_rate": 400,
- "weekly_rate": 2000,
- "monthly_rate": 8000,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "تمديدات صحية، تركيب أنظمة صرف، قراءة مخططات",
- "certifications": "شهادة مهنية",
- "description": "فني سباكة ذو خبرة 10+ سنة في التمديدات الصحية وأنظمة الصرف"
- },
- {
- "id": "TECH-010",
- "name": "فني سباكة (خبرة 5-10 سنوات)",
- "category": "فنيين",
- "subcategory": "سباكة",
- "hourly_rate": 40,
- "daily_rate": 320,
- "weekly_rate": 1600,
- "monthly_rate": 6500,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "تمديدات صحية، تركيب أنظمة صرف، قراءة مخططات",
- "certifications": "شهادة مهنية",
- "description": "فني سباكة ذو خبرة 5-10 سنوات في التمديدات الصحية وأنظمة الصرف"
- }
- ])
-
- # 3. عمال مهرة
- labor_data.extend([
- {
- "id": "SKILL-001",
- "name": "حداد مسلح (خبرة 10+ سنة)",
- "category": "عمال مهرة",
- "subcategory": "حداد",
- "hourly_rate": 40,
- "daily_rate": 320,
- "weekly_rate": 1600,
- "monthly_rate": 6500,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "تجهيز وتركيب حديد التسليح، قراءة مخططات",
- "certifications": "شهادة مهنية",
- "description": "حداد مسلح ذو خبرة 10+ سنة في تجهيز وتركيب حديد التسليح للمنشآت الخرسانية"
- },
- {
- "id": "SKILL-002",
- "name": "حداد مسلح (خبرة 5-10 سنوات)",
- "category": "عمال مهرة",
- "subcategory": "حداد",
- "hourly_rate": 35,
- "daily_rate": 280,
- "weekly_rate": 1400,
- "monthly_rate": 5500,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "تجهيز وتركيب حديد التسليح، قراءة مخططات",
- "certifications": "شهادة مهنية",
- "description": "حداد مسلح ذو خبرة 5-10 سنوات في تجهيز وتركيب حديد التسليح للمنشآت الخرسانية"
- },
- {
- "id": "SKILL-003",
- "name": "نجار مسلح (خبرة 10+ سنة)",
- "category": "عمال مهرة",
- "subcategory": "نجار",
- "hourly_rate": 40,
- "daily_rate": 320,
- "weekly_rate": 1600,
- "monthly_rate": 6500,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "تجهيز وتركيب الشدات الخشبية، قراءة مخططات",
- "certifications": "شهادة مهنية",
- "description": "نجار مسلح ذو خبرة 10+ سنة في تجهيز وتركيب الشدات الخشبية للمنشآت الخرسانية"
- },
- {
- "id": "SKILL-004",
- "name": "نجار مسلح (خبرة 5-10 سنوات)",
- "category": "عمال مهرة",
- "subcategory": "نجار",
- "hourly_rate": 35,
- "daily_rate": 280,
- "weekly_rate": 1400,
- "monthly_rate": 5500,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "تجهيز وتركيب الشدات الخشبية، قراءة مخططات",
- "certifications": "شهادة مهنية",
- "description": "نجار مسلح ذو خبرة 5-10 سنوات في تجهيز وتركيب الشدات الخشبية للمنشآت الخرسانية"
- },
- {
- "id": "SKILL-005",
- "name": "بناء (خبرة 10+ سنة)",
- "category": "عمال مهرة",
- "subcategory": "بناء",
- "hourly_rate": 35,
- "daily_rate": 280,
- "weekly_rate": 1400,
- "monthly_rate": 5500,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "بناء طابوق، بناء حجر، تشطيبات",
- "certifications": "شهادة مهنية",
- "description": "بناء ذو خبرة 10+ سنة في أعمال البناء بالطابوق والحجر والتشطيبات"
- },
- {
- "id": "SKILL-006",
- "name": "بناء (خبرة 5-10 سنوات)",
- "category": "عمال مهرة",
- "subcategory": "بناء",
- "hourly_rate": 30,
- "daily_rate": 240,
- "weekly_rate": 1200,
- "monthly_rate": 4800,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "بناء طابوق، بناء حجر، تشطيبات",
- "certifications": "شهادة مهنية",
- "description": "بناء ذو خبرة 5-10 سنوات في أعمال البناء بالطابوق والحجر والتشطيبات"
- },
- {
- "id": "SKILL-007",
- "name": "لحام (خبرة 10+ سنة)",
- "category": "عمال مهرة",
- "subcategory": "لحام",
- "hourly_rate": 40,
- "daily_rate": 320,
- "weekly_rate": 1600,
- "monthly_rate": 6500,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "لحام كهربائي، لحام أرجون، قراءة مخططات",
- "certifications": "شهادة مهنية",
- "description": "لحام ذو خبرة 10+ سنة في أعمال اللحام الكهربائي والأرجون"
- },
- {
- "id": "SKILL-008",
- "name": "لحام (خبرة 5-10 سنوات)",
- "category": "عمال مهرة",
- "subcategory": "لحام",
- "hourly_rate": 35,
- "daily_rate": 280,
- "weekly_rate": 1400,
- "monthly_rate": 5500,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "لحام كهربائي، لحام أرجون، قراءة مخططات",
- "certifications": "شهادة مهنية",
- "description": "لحام ذو خبرة 5-10 سنوات في أعمال اللحام الكهربائي والأرجون"
- },
- {
- "id": "SKILL-009",
- "name": "كهربائي (خبرة 10+ سنة)",
- "category": "عمال مهرة",
- "subcategory": "كهربائي",
- "hourly_rate": 35,
- "daily_rate": 280,
- "weekly_rate": 1400,
- "monthly_rate": 5500,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "تمديدات كهربائية، تركيب لوحات، صيانة",
- "certifications": "شهادة مهنية",
- "description": "كهربائي ذو خبرة 10+ سنة في التمديدات الكهربائية وتركيب اللوحات والصيانة"
- },
- {
- "id": "SKILL-010",
- "name": "كهربائي (خبرة 5-10 سنوات)",
- "category": "عمال مهرة",
- "subcategory": "كهربائي",
- "hourly_rate": 30,
- "daily_rate": 240,
- "weekly_rate": 1200,
- "monthly_rate": 4800,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "تمديدات كهربائية، تركيب لوحات، صيانة",
- "certifications": "شهادة مهنية",
- "description": "كهربائي ذو خبرة 5-10 سنوات في التمديدات الكهربائية وتركيب اللوحات والصيانة"
- }
- ])
-
- # 4. عمال عاديين
- labor_data.extend([
- {
- "id": "LABOR-001",
- "name": "عامل عادي (خبرة 5+ سنة)",
- "category": "عمال عاديين",
- "subcategory": "عامل",
- "hourly_rate": 20,
- "daily_rate": 160,
- "weekly_rate": 800,
- "monthly_rate": 3200,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "أعمال يدوية، مناولة مواد، تنظيف",
- "certifications": "لا يوجد",
- "description": "عامل عادي ذو خبرة 5+ سنة في الأعمال اليدوية ومناولة المواد والتنظيف"
- },
- {
- "id": "LABOR-002",
- "name": "عامل عادي (خبرة 1-5 سنوات)",
- "category": "عمال عاديين",
- "subcategory": "عامل",
- "hourly_rate": 15,
- "daily_rate": 120,
- "weekly_rate": 600,
- "monthly_rate": 2400,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "أعمال يدوية، مناولة مواد، تنظيف",
- "certifications": "لا يوجد",
- "description": "عامل عادي ذو خبرة 1-5 سنوات في الأعمال اليدوية ومناولة المواد والتنظيف"
- },
- {
- "id": "LABOR-003",
- "name": "عامل نظافة (خبرة 3+ سنة)",
- "category": "عمال عاديين",
- "subcategory": "نظافة",
- "hourly_rate": 15,
- "daily_rate": 120,
- "weekly_rate": 600,
- "monthly_rate": 2400,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "تنظيف، ترتيب، إزالة مخلفات",
- "certifications": "لا يوجد",
- "description": "عامل نظافة ذو خبرة 3+ سنة في أعمال التنظيف والترتيب وإزالة المخلفات"
- },
- {
- "id": "LABOR-004",
- "name": "عامل نظافة (خبرة 1-3 سنوات)",
- "category": "عمال عاديين",
- "subcategory": "نظافة",
- "hourly_rate": 12,
- "daily_rate": 96,
- "weekly_rate": 480,
- "monthly_rate": 1900,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "تنظيف، ترتيب، إزالة مخلفات",
- "certifications": "لا يوجد",
- "description": "عامل نظافة ذو خبرة 1-3 سنوات في أعمال التنظيف والترتيب وإزالة المخلفات"
- },
- {
- "id": "LABOR-005",
- "name": "عامل مساعد (خبرة 3+ سنة)",
- "category": "عمال عاديين",
- "subcategory": "مساعد",
- "hourly_rate": 18,
- "daily_rate": 144,
- "weekly_rate": 720,
- "monthly_rate": 2900,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "مساعدة في أعمال البناء، مناولة مواد، تنظيف",
- "certifications": "لا يوجد",
- "description": "عامل مساعد ذو خبرة 3+ سنة في مساعدة العمال المهرة ومناولة المواد"
- },
- {
- "id": "LABOR-006",
- "name": "عامل مساعد (خبرة 1-3 سنوات)",
- "category": "عمال عاديين",
- "subcategory": "مساعد",
- "hourly_rate": 15,
- "daily_rate": 120,
- "weekly_rate": 600,
- "monthly_rate": 2400,
- "nationality": "مقيم",
- "availability": "متاح",
- "skills": "مساعدة في أعمال البناء، مناولة مواد، تنظيف",
- "certifications": "لا يوجد",
- "description": "عامل مساعد ذو خبرة 1-3 سنوات في مساعدة العمال المهرة ومناولة المواد"
- }
- ])
-
- # 5. سائقين
- labor_data.extend([
- {
- "id": "DRIVER-001",
- "name": "سائق شاحنة ثقيلة (خبرة 10+ سنة)",
- "category": "سائقين",
- "subcategory": "شاحنة ثقيلة",
- "hourly_rate": 35,
- "daily_rate": 280,
- "weekly_rate": 1400,
- "monthly_rate": 5500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "قيادة شاحنات ثقيلة، صيانة أولية، سجل نظيف",
- "certifications": "رخصة قيادة شاحنات ثقيلة",
- "description": "سائق شاحنة ثقيلة ذو خبرة 10+ سنة في قيادة الشاحنات الثقيلة ونقل المواد"
- },
- {
- "id": "DRIVER-002",
- "name": "سائق شاحنة ثقيلة (خبرة 5-10 سنوات)",
- "category": "سائقين",
- "subcategory": "شاحنة ثقيلة",
- "hourly_rate": 30,
- "daily_rate": 240,
- "weekly_rate": 1200,
- "monthly_rate": 4800,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "قيادة شاحنات ثقيلة، صيانة أولية، سجل نظيف",
- "certifications": "رخصة قيادة شاحنات ثقيلة",
- "description": "سائق شاحنة ثقيلة ذو خبرة 5-10 سنوات في قيادة الشاحنات الثقيلة ونقل المواد"
- },
- {
- "id": "DRIVER-003",
- "name": "سائق شاحنة خلاطة خرسانة (خبرة 10+ سنة)",
- "category": "سائقين",
- "subcategory": "خلاطة خرسانة",
- "hourly_rate": 40,
- "daily_rate": 320,
- "weekly_rate": 1600,
- "monthly_rate": 6500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "قيادة خلاطات الخرسانة، صيانة أولية، سجل نظيف",
- "certifications": "رخصة قيادة شاحنات ثقيلة",
- "description": "سائق شاحنة خلاطة خرسانة ذو خبرة 10+ سنة في قيادة خلاطات الخرسانة وصب الخرسانة"
- },
- {
- "id": "DRIVER-004",
- "name": "سائق شاحنة خلاطة خرسانة (خبرة 5-10 سنوات)",
- "category": "سائقين",
- "subcategory": "خلاطة خرسانة",
- "hourly_rate": 35,
- "daily_rate": 280,
- "weekly_rate": 1400,
- "monthly_rate": 5500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "قيادة خلاطات الخرسانة، صيانة أولية، سجل نظيف",
- "certifications": "رخصة قيادة شاحنات ثقيلة",
- "description": "سائق شاحنة خلاطة خرسانة ذو خبرة 5-10 سنوات في قيادة خلاطات الخرسانة وصب الخرسانة"
- },
- {
- "id": "DRIVER-005",
- "name": "سائق شاحنة نقل مياه (خبرة 5+ سنة)",
- "category": "سائقين",
- "subcategory": "نقل مياه",
- "hourly_rate": 30,
- "daily_rate": 240,
- "weekly_rate": 1200,
- "monthly_rate": 4800,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "قيادة شاحنات نقل المياه، صيانة أولية، سجل نظيف",
- "certifications": "رخصة قيادة شاحنات ثقيلة",
- "description": "سائق شاحنة نقل مياه ذو خبرة 5+ سنة في قيادة شاحنات نقل المياه وتوزيع المياه"
- },
- {
- "id": "DRIVER-006",
- "name": "سائق سيارة نقل خفيف (خبرة 5+ سنة)",
- "category": "سائقين",
- "subcategory": "نقل خفيف",
- "hourly_rate": 25,
- "daily_rate": 200,
- "weekly_rate": 1000,
- "monthly_rate": 4000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "قيادة سيارات النقل الخفيف، صيانة أولية، سجل نظيف",
- "certifications": "رخصة قيادة",
- "description": "سائق سيارة نقل خفيف ذو خبرة 5+ سنة في قيادة سيارات النقل الخفيف ونقل المواد والعمال"
- }
- ])
-
- # 6. مشغلي معدات
- labor_data.extend([
- {
- "id": "OPER-001",
- "name": "مشغل حفارة (خبرة 10+ سنة)",
- "category": "مشغلي معدات",
- "subcategory": "حفارة",
- "hourly_rate": 45,
- "daily_rate": 360,
- "weekly_rate": 1800,
- "monthly_rate": 7500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تشغيل حفارات، صيانة أولية، حفر دقيق",
- "certifications": "رخصة تشغيل معدات ثقيلة",
- "description": "مشغل حفارة ذو خبرة 10+ سنة في تشغيل الحفارات وأعمال الحفر الدقيق"
- },
- {
- "id": "OPER-002",
- "name": "مشغل حفارة (خبرة 5-10 سنوات)",
- "category": "مشغلي معدات",
- "subcategory": "حفارة",
- "hourly_rate": 40,
- "daily_rate": 320,
- "weekly_rate": 1600,
- "monthly_rate": 6500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تشغيل حفارات، صيانة أولية، حفر دقيق",
- "certifications": "رخصة تشغيل معدات ثقيلة",
- "description": "مشغل حفارة ذو خبرة 5-10 سنوات في تشغيل الحفارات وأعمال الحفر الدقيق"
- },
- {
- "id": "OPER-003",
- "name": "مشغل لودر (خبرة 10+ سنة)",
- "category": "مشغلي معدات",
- "subcategory": "لودر",
- "hourly_rate": 40,
- "daily_rate": 320,
- "weekly_rate": 1600,
- "monthly_rate": 6500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تشغيل لودر، صيانة أولية، تحميل دقيق",
- "certifications": "رخصة تشغيل معدات ثقيلة",
- "description": "مشغل لودر ذو خبرة 10+ سنة في تشغيل اللودر وأعمال التحميل الدقيق"
- },
- {
- "id": "OPER-004",
- "name": "مشغل لودر (خبرة 5-10 سنوات)",
- "category": "مشغلي معدات",
- "subcategory": "لودر",
- "hourly_rate": 35,
- "daily_rate": 280,
- "weekly_rate": 1400,
- "monthly_rate": 5500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تشغيل لودر، صيانة أولية، تحميل دقيق",
- "certifications": "رخصة تشغيل معدات ثقيلة",
- "description": "مشغل لودر ذو خبرة 5-10 سنوات في تشغيل اللودر وأعمال التحميل الدقيق"
- },
- {
- "id": "OPER-005",
- "name": "مشغل بلدوزر (خبرة 10+ سنة)",
- "category": "مشغلي معدات",
- "subcategory": "بلدوزر",
- "hourly_rate": 45,
- "daily_rate": 360,
- "weekly_rate": 1800,
- "monthly_rate": 7500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تشغيل بلدوزر، صيانة أولية، تسوية دقيقة",
- "certifications": "رخصة تشغيل معدات ثقيلة",
- "description": "مشغل بلدوزر ذو خبرة 10+ سنة في تشغيل البلدوزر وأعمال التسوية الدقيقة"
- },
- {
- "id": "OPER-006",
- "name": "مشغل بلدوزر (خبرة 5-10 سنوات)",
- "category": "مشغلي معدات",
- "subcategory": "بلدوزر",
- "hourly_rate": 40,
- "daily_rate": 320,
- "weekly_rate": 1600,
- "monthly_rate": 6500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تشغيل بلدوزر، صيانة أولية، تسوية دقيقة",
- "certifications": "رخصة تشغيل معدات ثقيلة",
- "description": "مشغل بلدوزر ذو خبرة 5-10 سنوات في تشغيل البلدوزر وأعمال التسوية الدقيقة"
- },
- {
- "id": "OPER-007",
- "name": "مشغل جريدر (خبرة 10+ سنة)",
- "category": "مشغلي معدات",
- "subcategory": "جريدر",
- "hourly_rate": 45,
- "daily_rate": 360,
- "weekly_rate": 1800,
- "monthly_rate": 7500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تشغيل جريدر، صيانة أولية، تسوية دقيقة",
- "certifications": "رخصة تشغيل معدات ثقيلة",
- "description": "مشغل جريدر ذو خبرة 10+ سنة في تشغيل الجريدر وأعمال التسوية الدقيقة للطرق"
- },
- {
- "id": "OPER-008",
- "name": "مشغل جريدر (خبرة 5-10 سنوات)",
- "category": "مشغلي معدات",
- "subcategory": "جريدر",
- "hourly_rate": 40,
- "daily_rate": 320,
- "weekly_rate": 1600,
- "monthly_rate": 6500,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تشغيل جريدر، صيانة أولية، تسوية دقيقة",
- "certifications": "رخصة تشغيل معدات ثقيلة",
- "description": "مشغل جريدر ذو خبرة 5-10 سنوات في تشغيل الجريدر وأعمال التسوية الدقيقة للطرق"
- },
- {
- "id": "OPER-009",
- "name": "مشغل رافعة (خبرة 10+ سنة)",
- "category": "مشغلي معدات",
- "subcategory": "رافعة",
- "hourly_rate": 50,
- "daily_rate": 400,
- "weekly_rate": 2000,
- "monthly_rate": 8000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تشغيل رافعات، صيانة أولية، رفع دقيق",
- "certifications": "رخصة تشغيل رافعات",
- "description": "مشغل رافعة ذو خبرة 10+ سنة في تشغيل الرافعات وأعمال الرفع الدقيق"
- },
- {
- "id": "OPER-010",
- "name": "مشغل رافعة (خبرة 5-10 سنوات)",
- "category": "مشغلي معدات",
- "subcategory": "رافعة",
- "hourly_rate": 45,
- "daily_rate": 360,
- "weekly_rate": 1800,
- "monthly_rate": 7000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تشغيل رافعات، صيانة أولية، رفع دقيق",
- "certifications": "رخصة تشغيل رافعات",
- "description": "مشغل رافعة ذو خبرة 5-10 سنوات في تشغيل الرافعات وأعمال الرفع الدقيق"
- }
- ])
-
- # 7. إداريين
- labor_data.extend([
- {
- "id": "ADMIN-001",
- "name": "مدير مشروع (خبرة 15+ سنة)",
- "category": "إداريين",
- "subcategory": "مدير مشروع",
- "hourly_rate": 200,
- "daily_rate": 1600,
- "weekly_rate": 8000,
- "monthly_rate": 32000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "إدارة مشاريع، تخطيط، متابعة، إعداد تقارير",
- "certifications": "PMP، عضوية الهيئة السعودية للمهندسين",
- "description": "مدير مشروع ذو خبرة 15+ سنة في إدارة مشاريع البنية التحتية والطرق والجسور"
- },
- {
- "id": "ADMIN-002",
- "name": "مدير مشروع (خبرة 10-15 سنة)",
- "category": "إداريين",
- "subcategory": "مدير مشروع",
- "hourly_rate": 150,
- "daily_rate": 1200,
- "weekly_rate": 6000,
- "monthly_rate": 25000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "إدارة مشاريع، تخطيط، متابعة، إعداد تقارير",
- "certifications": "PMP، عضوية الهيئة السعودية للمهندسين",
- "description": "مدير مشروع ذو خبرة 10-15 سنة في إدارة مشاريع البنية التحتية والطرق والجسور"
- },
- {
- "id": "ADMIN-003",
- "name": "مهندس تخطيط (خبرة 10+ سنة)",
- "category": "إداريين",
- "subcategory": "تخطيط",
- "hourly_rate": 120,
- "daily_rate": 960,
- "weekly_rate": 4800,
- "monthly_rate": 20000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تخطيط مشاريع، جدولة، متابعة، إعداد تقارير",
- "certifications": "PMP، Primavera P6، MS Project",
- "description": "مهندس تخطيط ذو خبرة 10+ سنة في تخطيط وجدولة ومتابعة مشاريع البنية التحتية"
- },
- {
- "id": "ADMIN-004",
- "name": "مهندس تخطيط (خبرة 5-10 سنوات)",
- "category": "إداريين",
- "subcategory": "تخطيط",
- "hourly_rate": 100,
- "daily_rate": 800,
- "weekly_rate": 4000,
- "monthly_rate": 16000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "تخطيط مشاريع، جدولة، متابعة، إعداد تقارير",
- "certifications": "Primavera P6، MS Project",
- "description": "مهندس تخطيط ذو خبرة 5-10 سنوات في تخطيط وجدولة ومتابعة مشاريع البنية التحتية"
- },
- {
- "id": "ADMIN-005",
- "name": "مهندس مراقبة جودة (خبرة 10+ سنة)",
- "category": "إداريين",
- "subcategory": "مراقبة جودة",
- "hourly_rate": 120,
- "daily_rate": 960,
- "weekly_rate": 4800,
- "monthly_rate": 20000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "مراقبة جودة، اختبارات، إعداد تقارير، تطبيق معايير",
- "certifications": "ISO 9001، عضوية الهيئة السعودية للمهندسين",
- "description": "مهندس مراقبة جودة ذو خبرة 10+ سنة في مراقبة جودة مشاريع البنية التحتية"
- },
- {
- "id": "ADMIN-006",
- "name": "مهندس مراقبة جودة (خبرة 5-10 سنوات)",
- "category": "إداريين",
- "subcategory": "مراقبة جودة",
- "hourly_rate": 100,
- "daily_rate": 800,
- "weekly_rate": 4000,
- "monthly_rate": 16000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "مراقبة جودة، اختبارات، إعداد تقارير، تطبيق معايير",
- "certifications": "ISO 9001، عضوية الهيئة السعودية للمهندسين",
- "description": "مهندس مراقبة جودة ذو خبرة 5-10 سنوات في مراقبة جودة مشاريع البنية التحتية"
- },
- {
- "id": "ADMIN-007",
- "name": "مهندس سلامة (خبرة 10+ سنة)",
- "category": "إداريين",
- "subcategory": "سلامة",
- "hourly_rate": 120,
- "daily_rate": 960,
- "weekly_rate": 4800,
- "monthly_rate": 20000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "إدارة السلامة، تدريب، تفتيش، إعداد تقارير",
- "certifications": "NEBOSH، OSHA",
- "description": "مهندس سلامة ذو خبرة 10+ سنة في إدارة السلامة في مشاريع البنية التحتية"
- },
- {
- "id": "ADMIN-008",
- "name": "مهندس سلامة (خبرة 5-10 سنوات)",
- "category": "إداريين",
- "subcategory": "سلامة",
- "hourly_rate": 100,
- "daily_rate": 800,
- "weekly_rate": 4000,
- "monthly_rate": 16000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "إدارة السلامة، تدريب، تفتيش، إعداد تقارير",
- "certifications": "NEBOSH، OSHA",
- "description": "مهندس سلامة ذو خبرة 5-10 سنوات في إدارة السلامة في مشاريع البنية التحتية"
- },
- {
- "id": "ADMIN-009",
- "name": "محاسب مشاريع (خبرة 10+ سنة)",
- "category": "إداريين",
- "subcategory": "محاسبة",
- "hourly_rate": 100,
- "daily_rate": 800,
- "weekly_rate": 4000,
- "monthly_rate": 16000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "محاسبة مشاريع، إعداد تقارير مالية، متابعة مصروفات",
- "certifications": "SOCPA",
- "description": "محاسب مشاريع ذو خبرة 10+ سنة في محاسبة مشاريع البنية التحتية"
- },
- {
- "id": "ADMIN-010",
- "name": "محاسب مشاريع (خبرة 5-10 سنوات)",
- "category": "إداريين",
- "subcategory": "محاسبة",
- "hourly_rate": 80,
- "daily_rate": 640,
- "weekly_rate": 3200,
- "monthly_rate": 13000,
- "nationality": "سعودي",
- "availability": "متاح",
- "skills": "محاسبة مشاريع، إعداد تقارير مالية، متابعة مصروفات",
- "certifications": "SOCPA",
- "description": "محاسب مشاريع ذو خبرة 5-10 سنوات في محاسبة مشاريع البنية التحتية"
- }
- ])
-
- # تخزين البيانات في حالة الجلسة
- st.session_state.labor_catalog = pd.DataFrame(labor_data)
-
- def render(self):
- """عرض واجهة كتالوج العمالة"""
-
- st.markdown("## كتالوج العمالة والمهندسين")
-
- # إنشاء تبويبات لعرض الكتالوج
- tabs = st.tabs([
- "عرض الكتالوج",
- "إضافة عامل",
- "تحليل الأسعار",
- "استيراد/تصدير"
- ])
-
- with tabs[0]:
- self._render_catalog_view_tab()
-
- with tabs[1]:
- self._render_add_labor_tab()
-
- with tabs[2]:
- self._render_price_analysis_tab()
-
- with tabs[3]:
- self._render_import_export_tab()
-
- def _render_catalog_view_tab(self):
- """عرض تبويب عرض الكتالوج"""
-
- st.markdown("### عرض كتالوج العمالة والمهندسين")
-
- # استخراج البيانات
- labor_df = st.session_state.labor_catalog
-
- # إنشاء فلاتر للعرض
- col1, col2, col3 = st.columns(3)
-
- with col1:
- # فلتر حسب الفئة
- categories = ["الكل"] + sorted(labor_df["category"].unique().tolist())
- selected_category = st.selectbox("اختر فئة العمالة", categories)
-
- with col2:
- # فلتر حسب الفئة الفرعية
- if selected_category != "الكل":
- subcategories = ["الكل"] + sorted(labor_df[labor_df["category"] == selected_category]["subcategory"].unique().tolist())
- else:
- subcategories = ["الكل"] + sorted(labor_df["subcategory"].unique().tolist())
-
- selected_subcategory = st.selectbox("اختر التخصص", subcategories)
-
- with col3:
- # فلتر حسب الجنسية
- nationalities = ["الكل"] + sorted(labor_df["nationality"].unique().tolist())
- selected_nationality = st.selectbox("اختر الجنسية", nationalities)
-
- # تطبيق الفلاتر
- filtered_df = labor_df.copy()
-
- if selected_category != "الكل":
- filtered_df = filtered_df[filtered_df["category"] == selected_category]
-
- if selected_subcategory != "الكل":
- filtered_df = filtered_df[filtered_df["subcategory"] == selected_subcategory]
-
- if selected_nationality != "الكل":
- filtered_df = filtered_df[filtered_df["nationality"] == selected_nationality]
-
- # عرض البيانات
- if not filtered_df.empty:
- # عرض عدد النتائج
- st.info(f"تم العثور على {len(filtered_df)} عامل/مهندس")
-
- # عرض العمالة في شكل بطاقات
- for i, (_, labor) in enumerate(filtered_df.iterrows()):
- col1, col2 = st.columns([1, 3])
-
- with col1:
- # عرض صورة العامل (استخدام صورة افتراضية)
- st.image("https://via.placeholder.com/150", caption=labor["name"])
-
- with col2:
- # عرض معلومات العامل
- st.markdown(f"**{labor['name']}** (الكود: {labor['id']})")
- st.markdown(f"الفئة: {labor['category']} - {labor['subcategory']}")
- st.markdown(f"الجنسية: {labor['nationality']} | الحالة: {labor['availability']}")
- st.markdown(f"الأسعار: {labor['hourly_rate']} ريال/ساعة | {labor['daily_rate']} ريال/يوم | {labor['monthly_rate']} ريال/شهر")
- st.markdown(f"المهارات: {labor['skills']}")
-
- # إضافة زر لعرض التفاصيل
- if st.button(f"عرض التفاصيل الكاملة", key=f"details_{labor['id']}"):
- st.session_state.selected_labor = labor['id']
- self._show_labor_details(labor)
-
- st.markdown("---")
- else:
- st.warning("لا توجد عمالة تطابق معايير البحث")
-
- def _show_labor_details(self, labor):
- """عرض تفاصيل العامل"""
-
- st.markdown(f"## تفاصيل العامل/المهندس: {labor['name']}")
-
- col1, col2 = st.columns([1, 2])
-
- with col1:
- # عرض صورة العامل (استخدام صورة افتراضية)
- st.image("https://via.placeholder.com/300", caption=labor["name"])
-
- with col2:
- # عرض المعلومات الأساسية
- st.markdown("### المعلومات الأساسية")
- st.markdown(f"**الكود:** {labor['id']}")
- st.markdown(f"**الفئة:** {labor['category']} - {labor['subcategory']}")
- st.markdown(f"**الجنسية:** {labor['nationality']}")
- st.markdown(f"**الحالة:** {labor['availability']}")
- st.markdown(f"**المهارات:** {labor['skills']}")
- st.markdown(f"**الشهادات:** {labor['certifications']}")
- st.markdown(f"**الوصف:** {labor['description']}")
-
- # عرض معلومات الأسعار
- st.markdown("### معلومات الأسعار")
-
- price_data = {
- "وحدة الزمن": ["ساعة", "يوم", "أسبوع", "شهر"],
- "السعر (ريال)": [
- labor['hourly_rate'],
- labor['daily_rate'],
- labor['weekly_rate'],
- labor['monthly_rate']
- ]
- }
-
- price_df = pd.DataFrame(price_data)
- st.dataframe(price_df, use_container_width=True)
-
- # إضافة زر للتعديل
- if st.button("تعديل بيانات العامل"):
- st.session_state.edit_labor = labor['id']
- # هنا يمكن إضافة منطق التعديل
-
- def _render_add_labor_tab(self):
- """عرض تبويب إضافة عامل"""
-
- st.markdown("### إضافة عامل/مهندس جديد")
-
- # استخراج البيانات
- labor_df = st.session_state.labor_catalog
-
- # إنشاء نموذج إضافة عامل
- with st.form("add_labor_form"):
- st.markdown("#### المعلومات الأساسية")
-
- # الصف الأول
- col1, col2 = st.columns(2)
- with col1:
- labor_id = st.text_input("كود العامل", value=f"LABOR-{len(labor_df) + 1:03d}")
- labor_name = st.text_input("اسم العامل", placeholder="مثال: مهندس مدني (خبرة 10+ سنة)")
-
- with col2:
- # استخراج الفئات والفئات الفرعية الموجودة
- categories = sorted(labor_df["category"].unique().tolist())
- labor_category = st.selectbox("فئة العامل", categories)
-
- # استخراج الفئات الفرعية بناءً على الفئة المختارة
- subcategories = sorted(labor_df[labor_df["category"] == labor_category]["subcategory"].unique().tolist())
- labor_subcategory = st.selectbox("التخصص", subcategories)
-
- # الصف الثاني
- col1, col2 = st.columns(2)
- with col1:
- labor_nationality = st.selectbox("الجنسية", ["سعودي", "مقيم"])
- with col2:
- labor_availability = st.selectbox("الحالة", ["متاح", "غير متاح"])
-
- # الصف الثالث - الأسعار
- st.markdown("#### معلومات الأسعار")
-
- col1, col2, col3, col4 = st.columns(4)
- with col1:
- labor_hourly_rate = st.number_input("السعر بالساعة (ريال)", min_value=0, step=5)
- with col2:
- labor_daily_rate = st.number_input("السعر باليوم (ريال)", min_value=0, step=40)
- with col3:
- labor_weekly_rate = st.number_input("السعر بالأسبوع (ريال)", min_value=0, step=200)
- with col4:
- labor_monthly_rate = st.number_input("السعر بالشهر (ريال)", min_value=0, step=1000)
-
- # المهارات والشهادات
- st.markdown("#### المهارات والشهادات")
-
- labor_skills = st.text_area("المهارات", placeholder="مثال: إدارة مشاريع، تصميم إنشائي، إشراف على التنفيذ")
- labor_certifications = st.text_input("الشهادات", placeholder="مثال: عضوية الهيئة السعودية للمهندسين، PMP")
-
- # وصف العامل
- labor_description = st.text_area("وصف العامل", placeholder="أدخل وصفاً تفصيلياً للعامل/المهندس")
-
- # زر الإضافة
- submit_button = st.form_submit_button("إضافة العامل")
-
- if submit_button:
- # التحقق من البيانات
- if not labor_name or not labor_category or not labor_subcategory:
- st.error("يرجى إدخال المعلومات الأساسية للعامل")
- else:
- # إنشاء عامل جديد
- new_labor = {
- "id": labor_id,
- "name": labor_name,
- "category": labor_category,
- "subcategory": labor_subcategory,
- "hourly_rate": labor_hourly_rate,
- "daily_rate": labor_daily_rate,
- "weekly_rate": labor_weekly_rate,
- "monthly_rate": labor_monthly_rate,
- "nationality": labor_nationality,
- "availability": labor_availability,
- "skills": labor_skills,
- "certifications": labor_certifications,
- "description": labor_description
- }
-
- # إضافة العامل إلى الكتالوج
- st.session_state.labor_catalog = pd.concat([
- st.session_state.labor_catalog,
- pd.DataFrame([new_labor])
- ], ignore_index=True)
-
- st.success(f"تمت إضافة العامل {labor_name} بنجاح!")
-
- def _render_price_analysis_tab(self):
- """عرض تبويب تحليل الأسعار"""
-
- st.markdown("### تحليل أسعار العمالة والمهندسين")
-
- # استخراج البيانات
- labor_df = st.session_state.labor_catalog
-
- # تحليل متوسط الأسعار حسب الفئة
- st.markdown("#### متوسط الأسعار حسب الفئة")
-
- # حساب متوسط الأسعار لكل فئة
- category_prices = labor_df.groupby("category").agg({
- "hourly_rate": "mean",
- "daily_rate": "mean",
- "monthly_rate": "mean"
- }).reset_index()
-
- # تغيير أسماء الأعمدة
- category_prices.columns = ["الفئة", "متوسط السعر بالساعة", "متوسط السعر باليوم", "متوسط السعر بالشهر"]
-
- # عرض الجدول
- st.dataframe(category_prices, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة
- st.markdown("#### مقارنة متوسط الأسعار الشهرية حسب الفئة")
-
- fig = px.bar(
- category_prices,
- x="الفئة",
- y="متوسط السعر بالشهر",
- title="متوسط أسعار العمالة والمهندسين الشهرية حسب الفئة",
- color="الفئة",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل توزيع العمالة حسب الجنسية
- st.markdown("#### توزيع العمالة حسب الجنسية")
-
- # حساب عدد العمالة حسب الجنسية
- nationality_counts = labor_df["nationality"].value_counts().reset_index()
- nationality_counts.columns = ["الجنسية", "عدد العمالة"]
-
- # إنشاء رسم بياني دائري
- fig = px.pie(
- nationality_counts,
- values="عدد العمالة",
- names="الجنسية",
- title="توزيع العمالة حسب الجنسية",
- color="الجنسية"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل متوسط الأسعار حسب التخصص
- st.markdown("#### متوسط الأسعار حسب التخصص")
-
- # اختيار الفئة للتحليل
- selected_category_for_analysis = st.selectbox(
- "اختر الفئة للتحليل",
- sorted(labor_df["category"].unique().tolist())
- )
-
- # حساب متوسط الأسعار لكل تخصص ضمن الفئة المختارة
- subcategory_prices = labor_df[labor_df["category"] == selected_category_for_analysis].groupby("subcategory").agg({
- "hourly_rate": "mean",
- "daily_rate": "mean",
- "monthly_rate": "mean"
- }).reset_index()
-
- # تغيير أسماء الأعمدة
- subcategory_prices.columns = ["التخصص", "متوسط السعر بالساعة", "متوسط السعر باليوم", "متوسط السعر بالشهر"]
-
- # عرض الجدول
- st.dataframe(subcategory_prices, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة
- fig = px.bar(
- subcategory_prices,
- x="التخصص",
- y="متوسط السعر بالشهر",
- title=f"متوسط أسعار {selected_category_for_analysis} الشهرية حسب التخصص",
- color="التخصص",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # حاسبة تكاليف العمالة للمشروع
- st.markdown("#### حاسبة تكاليف العمالة للمشروع")
-
- with st.form("project_labor_calculator"):
- st.markdown("أدخل العمالة المطلوبة للمشروع")
-
- # اختيار العمالة
- selected_labor = st.multiselect(
- "اختر العمالة",
- options=labor_df["name"].tolist(),
- format_func=lambda x: f"{x} ({labor_df[labor_df['name'] == x]['id'].iloc[0]})"
- )
-
- # اختيار وحدة الزمن
- time_unit = st.radio("اختر وحدة الزمن", ["ساعة", "يوم", "أسبوع", "شهر"], horizontal=True)
-
- # إنشاء حقول إدخال الكميات
- quantities = {}
-
- if selected_labor:
- st.markdown("أدخل المدة المطلوبة")
-
- for labor_name in selected_labor:
- quantities[labor_name] = st.number_input(
- f"{labor_name}",
- min_value=0,
- step=1,
- key=f"qty_{labor_df[labor_df['name'] == labor_name]['id'].iloc[0]}"
- )
-
- # زر الحساب
- calculate_button = st.form_submit_button("حساب التكاليف")
-
- if calculate_button:
- if not selected_labor:
- st.error("يرجى اختيار عامل واحد على الأقل")
- else:
- # حساب التكاليف
- project_costs = []
-
- for labor_name in selected_labor:
- labor = labor_df[labor_df["name"] == labor_name].iloc[0]
- quantity = quantities[labor_name]
-
- if quantity > 0:
- # تحديد السعر بناءً على وحدة الزمن
- if time_unit == "ساعة":
- rate = labor["hourly_rate"]
- rate_name = "السعر بالساعة"
- elif time_unit == "يوم":
- rate = labor["daily_rate"]
- rate_name = "السعر باليوم"
- elif time_unit == "أسبوع":
- rate = labor["weekly_rate"]
- rate_name = "السعر بالأسبوع"
- else: # شهر
- rate = labor["monthly_rate"]
- rate_name = "السعر بالشهر"
-
- cost = rate * quantity
-
- project_costs.append({
- "العامل": labor_name,
- "الكود": labor["id"],
- "الفئة": labor["category"],
- "التخصص": labor["subcategory"],
- rate_name: rate,
- f"عدد {time_unit}ات": quantity,
- "التكلفة الإجمالية": cost
- })
-
- if project_costs:
- # عرض النتائج
- project_costs_df = pd.DataFrame(project_costs)
- st.dataframe(project_costs_df, use_container_width=True)
-
- # حساب إجمالي التكاليف
- total_cost = project_costs_df["التكلفة الإجمالية"].sum()
- st.metric("إجمالي تكاليف العمالة للمشروع", f"{total_cost:,.2f} ريال")
- else:
- st.warning("يرجى إدخال مدة أكبر من صفر")
-
- def _render_import_export_tab(self):
- """عرض تبويب استيراد/تصدير"""
-
- st.markdown("### استيراد وتصدير بيانات العمالة والمهندسين")
-
- # استيراد البيانات
- st.markdown("#### استيراد البيانات")
-
- uploaded_file = st.file_uploader("اختر ملف Excel لاستيراد بيانات العمالة", type=["xlsx", "xls"])
-
- if uploaded_file is not None:
- try:
- # قراءة الملف
- imported_df = pd.read_excel(uploaded_file)
-
- # عرض البيانات المستوردة
- st.dataframe(imported_df, use_container_width=True)
-
- # زر الاستيراد
- if st.button("استيراد البيانات"):
- # التحقق من وجود الأعمدة المطلوبة
- required_columns = ["id", "name", "category", "subcategory", "hourly_rate", "daily_rate", "weekly_rate", "monthly_rate"]
-
- if all(col in imported_df.columns for col in required_columns):
- # دمج البيانات المستوردة مع البيانات الحالية
- st.session_state.labor_catalog = pd.concat([
- st.session_state.labor_catalog,
- imported_df
- ], ignore_index=True).drop_duplicates(subset=["id"])
-
- st.success(f"تم استيراد {len(imported_df)} عامل/مهندس بنجاح!")
- else:
- st.error("الملف المستورد لا يحتوي على الأعمدة المطلوبة")
-
- except Exception as e:
- st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}")
-
- # تصدير البيانات
- st.markdown("#### تصدير البيانات")
-
- # اختيار تنسيق التصدير
- export_format = st.radio("اختر تنسيق التصدير", ["Excel", "CSV", "JSON"], horizontal=True)
-
- if st.button("تصدير البيانات"):
- # استخراج البيانات
- labor_df = st.session_state.labor_catalog
-
- # تصدير البيانات حسب التنسيق المختار
- if export_format == "Excel":
- # تصدير إلى Excel
- output = io.BytesIO()
- with pd.ExcelWriter(output, engine="openpyxl") as writer:
- labor_df.to_excel(writer, index=False, sheet_name="Labor")
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف Excel",
- data=output.getvalue(),
- file_name="labor_catalog.xlsx",
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
- )
-
- elif export_format == "CSV":
- # تصدير إلى CSV
- csv_data = labor_df.to_csv(index=False)
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف CSV",
- data=csv_data,
- file_name="labor_catalog.csv",
- mime="text/csv"
- )
-
- else: # JSON
- # تصدير إلى JSON
- json_data = labor_df.to_json(orient="records", force_ascii=False)
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف JSON",
- data=json_data,
- file_name="labor_catalog.json",
- mime="application/json"
- )
-
- def get_labor_by_id(self, labor_id):
- """الحصول على عامل بواسطة الكود"""
-
- labor_df = st.session_state.labor_catalog
- labor = labor_df[labor_df["id"] == labor_id]
-
- if not labor.empty:
- return labor.iloc[0].to_dict()
-
- return None
-
- def get_labor_by_category(self, category):
- """الحصول على العمالة حسب الفئة"""
-
- labor_df = st.session_state.labor_catalog
- labor = labor_df[labor_df["category"] == category]
-
- if not labor.empty:
- return labor.to_dict(orient="records")
-
- return []
-
- def calculate_labor_cost(self, labor_id, quantity, time_unit="day"):
- """حساب تكلفة العامل بناءً على الكمية ووحدة الزمن"""
-
- labor = self.get_labor_by_id(labor_id)
-
- if labor:
- if time_unit == "hour":
- return labor["hourly_rate"] * quantity
- elif time_unit == "day":
- return labor["daily_rate"] * quantity
- elif time_unit == "week":
- return labor["weekly_rate"] * quantity
- elif time_unit == "month":
- return labor["monthly_rate"] * quantity
-
- return 0
+"""
+كتالوج العمالة - وحدة إدارة العمالة والمهندسين
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import plotly.express as px
+import os
+import json
+from datetime import datetime
+import io
+
+class LaborCatalog:
+ """كتالوج العمالة"""
+
+ def __init__(self):
+ """تهيئة كتالوج العمالة"""
+
+ # تهيئة حالة الجلسة لكتالوج العمالة
+ if 'labor_catalog' not in st.session_state:
+ # إنشاء بيانات افتراضية للعمالة
+ self._initialize_labor_catalog()
+
+ def _initialize_labor_catalog(self):
+ """تهيئة بيانات كتالوج العمالة"""
+
+ # تعريف فئات العمالة
+ labor_categories = [
+ "مهندسين",
+ "فنيين",
+ "عمال مهرة",
+ "عمال عاديين",
+ "سائقين",
+ "مشغلي معدات",
+ "إداريين"
+ ]
+
+ # إنشاء قائمة العمالة
+ labor_data = []
+
+ # 1. مهندسين
+ labor_data.extend([
+ {
+ "id": "ENG-001",
+ "name": "مهندس مدني (خبرة 15+ سنة)",
+ "category": "مهندسين",
+ "subcategory": "مدني",
+ "hourly_rate": 150,
+ "daily_rate": 1200,
+ "weekly_rate": 6000,
+ "monthly_rate": 25000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "إدارة مشاريع، تصميم إنشائي، إشراف على التنفيذ",
+ "certifications": "عضوية الهيئة السعودية للمهندسين، PMP",
+ "description": "مهندس مدني ذو خبرة 15+ سنة في إدارة وتنفيذ مشاريع البنية التحتية والطرق والجسور"
+ },
+ {
+ "id": "ENG-002",
+ "name": "مهندس مدني (خبرة 10-15 سنة)",
+ "category": "مهندسين",
+ "subcategory": "مدني",
+ "hourly_rate": 120,
+ "daily_rate": 960,
+ "weekly_rate": 4800,
+ "monthly_rate": 20000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "إدارة مشاريع، تصميم إنشائي، إشراف على التنفيذ",
+ "certifications": "عضوية الهيئة السعودية للمهندسين",
+ "description": "مهندس مدني ذو خبرة 10-15 سنة في إدارة وتنفيذ مشاريع البنية التحتية والطرق والجسور"
+ },
+ {
+ "id": "ENG-003",
+ "name": "مهندس مدني (خبرة 5-10 سنوات)",
+ "category": "مهندسين",
+ "subcategory": "مدني",
+ "hourly_rate": 100,
+ "daily_rate": 800,
+ "weekly_rate": 4000,
+ "monthly_rate": 16000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تصميم إنشائي، إشراف على التنفيذ، حساب كميات",
+ "certifications": "عضوية الهيئة السعودية للمهندسين",
+ "description": "مهندس مدني ذو خبرة 5-10 سنوات في تنفيذ مشاريع البنية التحتية والطرق والجسور"
+ },
+ {
+ "id": "ENG-004",
+ "name": "مهندس مدني (خبرة 1-5 سنوات)",
+ "category": "مهندسين",
+ "subcategory": "مدني",
+ "hourly_rate": 80,
+ "daily_rate": 640,
+ "weekly_rate": 3200,
+ "monthly_rate": 13000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "إشراف على التنفيذ، حساب كميات، رسم هندسي",
+ "certifications": "عضوية الهيئة السعودية للمهندسين",
+ "description": "مهندس مدني حديث الخبرة في تنفيذ مشاريع البنية التحتية والطرق والجسور"
+ },
+ {
+ "id": "ENG-005",
+ "name": "مهندس ميكانيكي (خبرة 10+ سنة)",
+ "category": "مهندسين",
+ "subcategory": "ميكانيكي",
+ "hourly_rate": 130,
+ "daily_rate": 1040,
+ "weekly_rate": 5200,
+ "monthly_rate": 22000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تصميم أنظمة ميكانيكية، إدارة صيانة المعدات، إشراف على التنفيذ",
+ "certifications": "عضوية الهيئة السعودية للمهندسين",
+ "description": "مهندس ميكانيكي ذو خبرة 10+ سنة في تصميم وتنفيذ الأنظمة الميكانيكية وصيانة المعدات"
+ },
+ {
+ "id": "ENG-006",
+ "name": "مهندس ميكانيكي (خبرة 5-10 سنوات)",
+ "category": "مهندسين",
+ "subcategory": "ميكانيكي",
+ "hourly_rate": 100,
+ "daily_rate": 800,
+ "weekly_rate": 4000,
+ "monthly_rate": 16000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تصميم أنظمة ميكانيكية، صيانة المعدات، إشراف على التنفيذ",
+ "certifications": "عضوية الهيئة السعودية للمهندسين",
+ "description": "مهندس ميكانيكي ذو خبرة 5-10 سنوات في تنفيذ الأنظمة الميكانيكية وصيانة المعدات"
+ },
+ {
+ "id": "ENG-007",
+ "name": "مهندس كهربائي (خبرة 10+ سنة)",
+ "category": "مهندسين",
+ "subcategory": "كهربائي",
+ "hourly_rate": 130,
+ "daily_rate": 1040,
+ "weekly_rate": 5200,
+ "monthly_rate": 22000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تصميم أنظمة كهربائية، إدارة مشاريع كهربائية، إشراف على التنفيذ",
+ "certifications": "عضوية الهيئة السعودية للمهندسين",
+ "description": "مهندس كهربائي ذو خبرة 10+ سنة في تصميم وتنفيذ الأنظمة الكهربائية للمشاريع الكبرى"
+ },
+ {
+ "id": "ENG-008",
+ "name": "مهندس كهربائي (خبرة 5-10 سنوات)",
+ "category": "مهندسين",
+ "subcategory": "كهربائي",
+ "hourly_rate": 100,
+ "daily_rate": 800,
+ "weekly_rate": 4000,
+ "monthly_rate": 16000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تصميم أنظمة كهربائية، إشراف على التنفيذ، اختبار وتشغيل",
+ "certifications": "عضوية الهيئة السعودية للمهندسين",
+ "description": "مهندس كهربائي ذو خبرة 5-10 سنوات في تنفيذ الأنظمة الكهربائية للمشاريع"
+ },
+ {
+ "id": "ENG-009",
+ "name": "مهندس مساحة (خبرة 10+ سنة)",
+ "category": "مهندسين",
+ "subcategory": "مساحة",
+ "hourly_rate": 120,
+ "daily_rate": 960,
+ "weekly_rate": 4800,
+ "monthly_rate": 20000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "مسح طبوغرافي، نظم معلومات جغرافية، حساب كميات",
+ "certifications": "عضوية الهيئة السعودية للمهندسين",
+ "description": "مهندس مساحة ذو خبرة 10+ سنة في المسح الطبوغرافي ونظم المعلومات الجغرافية"
+ },
+ {
+ "id": "ENG-010",
+ "name": "مهندس مساحة (خبرة 5-10 سنوات)",
+ "category": "مهندسين",
+ "subcategory": "مساحة",
+ "hourly_rate": 90,
+ "daily_rate": 720,
+ "weekly_rate": 3600,
+ "monthly_rate": 15000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "مسح طبوغرافي، نظم معلومات جغرافية، حساب كميات",
+ "certifications": "عضوية الهيئة السعودية للمهندسين",
+ "description": "مهندس مساحة ذو خبرة 5-10 سنوات في المسح الطبوغرافي ونظم المعلومات الجغرافية"
+ }
+ ])
+
+ # 2. فنيين
+ labor_data.extend([
+ {
+ "id": "TECH-001",
+ "name": "فني مدني (خبرة 10+ سنة)",
+ "category": "فنيين",
+ "subcategory": "مدني",
+ "hourly_rate": 60,
+ "daily_rate": 480,
+ "weekly_rate": 2400,
+ "monthly_rate": 9500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "قراءة مخططات، إشراف على التنفيذ، فحص جودة",
+ "certifications": "دبلوم فني",
+ "description": "فني مدني ذو خبرة 10+ سنة في الإشراف على تنفيذ الأعمال المدنية"
+ },
+ {
+ "id": "TECH-002",
+ "name": "فني مدني (خبرة 5-10 سنوات)",
+ "category": "فنيين",
+ "subcategory": "مدني",
+ "hourly_rate": 50,
+ "daily_rate": 400,
+ "weekly_rate": 2000,
+ "monthly_rate": 8000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "قراءة مخططات، إشراف على التنفيذ، فحص جودة",
+ "certifications": "دبلوم فني",
+ "description": "فني مدني ذو خبرة 5-10 سنوات في الإشراف على تنفيذ الأعمال المدنية"
+ },
+ {
+ "id": "TECH-003",
+ "name": "فني مساحة (خبرة 10+ سنة)",
+ "category": "فنيين",
+ "subcategory": "مساحة",
+ "hourly_rate": 55,
+ "daily_rate": 440,
+ "weekly_rate": 2200,
+ "monthly_rate": 9000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "استخدام أجهزة المساحة، قراءة مخططات، حساب كميات",
+ "certifications": "دبلوم فني",
+ "description": "فني مساحة ذو خبرة 10+ سنة في أعمال المساحة وحساب الكميات"
+ },
+ {
+ "id": "TECH-004",
+ "name": "فني مساحة (خبرة 5-10 سنوات)",
+ "category": "فنيين",
+ "subcategory": "مساحة",
+ "hourly_rate": 45,
+ "daily_rate": 360,
+ "weekly_rate": 1800,
+ "monthly_rate": 7500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "استخدام أجهزة المساحة، قراءة مخططات، حساب كميات",
+ "certifications": "دبلوم فني",
+ "description": "فني مساحة ذو خبرة 5-10 سنوات في أعمال المساحة وحساب الكميات"
+ },
+ {
+ "id": "TECH-005",
+ "name": "فني كهربائي (خبرة 10+ سنة)",
+ "category": "فنيين",
+ "subcategory": "كهربائي",
+ "hourly_rate": 55,
+ "daily_rate": 440,
+ "weekly_rate": 2200,
+ "monthly_rate": 9000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تمديدات كهربائية، قراءة مخططات، صيانة",
+ "certifications": "دبلوم فني",
+ "description": "فني كهربائي ذو خبرة 10+ سنة في التمديدات الكهربائية والصيانة"
+ },
+ {
+ "id": "TECH-006",
+ "name": "فني كهربائي (خبرة 5-10 سنوات)",
+ "category": "فنيين",
+ "subcategory": "كهربائي",
+ "hourly_rate": 45,
+ "daily_rate": 360,
+ "weekly_rate": 1800,
+ "monthly_rate": 7500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تمديدات كهربائية، قراءة مخططات، صيانة",
+ "certifications": "دبلوم فني",
+ "description": "فني كهربائي ذو خبرة 5-10 سنوات في التمديدات الكهربائية والصيانة"
+ },
+ {
+ "id": "TECH-007",
+ "name": "فني ميكانيكي (خبرة 10+ سنة)",
+ "category": "فنيين",
+ "subcategory": "ميكانيكي",
+ "hourly_rate": 55,
+ "daily_rate": 440,
+ "weekly_rate": 2200,
+ "monthly_rate": 9000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "صيانة معدات، تركيب أنظمة ميكانيكية، قراءة مخططات",
+ "certifications": "دبلوم فني",
+ "description": "فني ميكانيكي ذو خبرة 10+ سنة في صيانة المعدات وتركيب الأنظمة الميكانيكية"
+ },
+ {
+ "id": "TECH-008",
+ "name": "فني ميكانيكي (خبرة 5-10 سنوات)",
+ "category": "فنيين",
+ "subcategory": "ميكانيكي",
+ "hourly_rate": 45,
+ "daily_rate": 360,
+ "weekly_rate": 1800,
+ "monthly_rate": 7500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "صيانة معدات، تركيب أنظمة ميكانيكية، قراءة مخططات",
+ "certifications": "دبلوم فني",
+ "description": "فني ميكانيكي ذو خبرة 5-10 سنوات في صيانة المعدات وتركيب الأنظمة الميكانيكية"
+ },
+ {
+ "id": "TECH-009",
+ "name": "فني سباكة (خبرة 10+ سنة)",
+ "category": "فنيين",
+ "subcategory": "سباكة",
+ "hourly_rate": 50,
+ "daily_rate": 400,
+ "weekly_rate": 2000,
+ "monthly_rate": 8000,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "تمديدات صحية، تركيب أنظمة صرف، قراءة مخططات",
+ "certifications": "شهادة مهنية",
+ "description": "فني سباكة ذو خبرة 10+ سنة في التمديدات الصحية وأنظمة الصرف"
+ },
+ {
+ "id": "TECH-010",
+ "name": "فني سباكة (خبرة 5-10 سنوات)",
+ "category": "فنيين",
+ "subcategory": "سباكة",
+ "hourly_rate": 40,
+ "daily_rate": 320,
+ "weekly_rate": 1600,
+ "monthly_rate": 6500,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "تمديدات صحية، تركيب أنظمة صرف، قراءة مخططات",
+ "certifications": "شهادة مهنية",
+ "description": "فني سباكة ذو خبرة 5-10 سنوات في التمديدات الصحية وأنظمة الصرف"
+ }
+ ])
+
+ # 3. عمال مهرة
+ labor_data.extend([
+ {
+ "id": "SKILL-001",
+ "name": "حداد مسلح (خبرة 10+ سنة)",
+ "category": "عمال مهرة",
+ "subcategory": "حداد",
+ "hourly_rate": 40,
+ "daily_rate": 320,
+ "weekly_rate": 1600,
+ "monthly_rate": 6500,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "تجهيز وتركيب حديد التسليح، قراءة مخططات",
+ "certifications": "شهادة مهنية",
+ "description": "حداد مسلح ذو خبرة 10+ سنة في تجهيز وتركيب حديد التسليح للمنشآت الخرسانية"
+ },
+ {
+ "id": "SKILL-002",
+ "name": "حداد مسلح (خبرة 5-10 سنوات)",
+ "category": "عمال مهرة",
+ "subcategory": "حداد",
+ "hourly_rate": 35,
+ "daily_rate": 280,
+ "weekly_rate": 1400,
+ "monthly_rate": 5500,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "تجهيز وتركيب حديد التسليح، قراءة مخططات",
+ "certifications": "شهادة مهنية",
+ "description": "حداد مسلح ذو خبرة 5-10 سنوات في تجهيز وتركيب حديد التسليح للمنشآت الخرسانية"
+ },
+ {
+ "id": "SKILL-003",
+ "name": "نجار مسلح (خبرة 10+ سنة)",
+ "category": "عمال مهرة",
+ "subcategory": "نجار",
+ "hourly_rate": 40,
+ "daily_rate": 320,
+ "weekly_rate": 1600,
+ "monthly_rate": 6500,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "تجهيز وتركيب الشدات الخشبية، قراءة مخططات",
+ "certifications": "شهادة مهنية",
+ "description": "نجار مسلح ذو خبرة 10+ سنة في تجهيز وتركيب الشدات الخشبية للمنشآت الخرسانية"
+ },
+ {
+ "id": "SKILL-004",
+ "name": "نجار مسلح (خبرة 5-10 سنوات)",
+ "category": "عمال مهرة",
+ "subcategory": "نجار",
+ "hourly_rate": 35,
+ "daily_rate": 280,
+ "weekly_rate": 1400,
+ "monthly_rate": 5500,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "تجهيز وتركيب الشدات الخشبية، قراءة مخططات",
+ "certifications": "شهادة مهنية",
+ "description": "نجار مسلح ذو خبرة 5-10 سنوات في تجهيز وتركيب الشدات الخشبية للمنشآت الخرسانية"
+ },
+ {
+ "id": "SKILL-005",
+ "name": "بناء (خبرة 10+ سنة)",
+ "category": "عمال مهرة",
+ "subcategory": "بناء",
+ "hourly_rate": 35,
+ "daily_rate": 280,
+ "weekly_rate": 1400,
+ "monthly_rate": 5500,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "بناء طابوق، بناء حجر، تشطيبات",
+ "certifications": "شهادة مهنية",
+ "description": "بناء ذو خبرة 10+ سنة في أعمال البناء بالطابوق والحجر والتشطيبات"
+ },
+ {
+ "id": "SKILL-006",
+ "name": "بناء (خبرة 5-10 سنوات)",
+ "category": "عمال مهرة",
+ "subcategory": "بناء",
+ "hourly_rate": 30,
+ "daily_rate": 240,
+ "weekly_rate": 1200,
+ "monthly_rate": 4800,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "بناء طابوق، بناء حجر، تشطيبات",
+ "certifications": "شهادة مهنية",
+ "description": "بناء ذو خبرة 5-10 سنوات في أعمال البناء بالطابوق والحجر والتشطيبات"
+ },
+ {
+ "id": "SKILL-007",
+ "name": "لحام (خبرة 10+ سنة)",
+ "category": "عمال مهرة",
+ "subcategory": "لحام",
+ "hourly_rate": 40,
+ "daily_rate": 320,
+ "weekly_rate": 1600,
+ "monthly_rate": 6500,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "لحام كهربائي، لحام أرجون، قراءة مخططات",
+ "certifications": "شهادة مهنية",
+ "description": "لحام ذو خبرة 10+ سنة في أعمال اللحام الكهربائي والأرجون"
+ },
+ {
+ "id": "SKILL-008",
+ "name": "لحام (خبرة 5-10 سنوات)",
+ "category": "عمال مهرة",
+ "subcategory": "لحام",
+ "hourly_rate": 35,
+ "daily_rate": 280,
+ "weekly_rate": 1400,
+ "monthly_rate": 5500,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "لحام كهربائي، لحام أرجون، قراءة مخططات",
+ "certifications": "شهادة مهنية",
+ "description": "لحام ذو خبرة 5-10 سنوات في أعمال اللحام الكهربائي والأرجون"
+ },
+ {
+ "id": "SKILL-009",
+ "name": "كهربائي (خبرة 10+ سنة)",
+ "category": "عمال مهرة",
+ "subcategory": "كهربائي",
+ "hourly_rate": 35,
+ "daily_rate": 280,
+ "weekly_rate": 1400,
+ "monthly_rate": 5500,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "تمديدات كهربائية، تركيب لوحات، صيانة",
+ "certifications": "شهادة مهنية",
+ "description": "كهربائي ذو خبرة 10+ سنة في التمديدات الكهربائية وتركيب اللوحات والصيانة"
+ },
+ {
+ "id": "SKILL-010",
+ "name": "كهربائي (خبرة 5-10 سنوات)",
+ "category": "عمال مهرة",
+ "subcategory": "كهربائي",
+ "hourly_rate": 30,
+ "daily_rate": 240,
+ "weekly_rate": 1200,
+ "monthly_rate": 4800,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "تمديدات كهربائية، تركيب لوحات، صيانة",
+ "certifications": "شهادة مهنية",
+ "description": "كهربائي ذو خبرة 5-10 سنوات في التمديدات الكهربائية وتركيب اللوحات والصيانة"
+ }
+ ])
+
+ # 4. عمال عاديين
+ labor_data.extend([
+ {
+ "id": "LABOR-001",
+ "name": "عامل عادي (خبرة 5+ سنة)",
+ "category": "عمال عاديين",
+ "subcategory": "عامل",
+ "hourly_rate": 20,
+ "daily_rate": 160,
+ "weekly_rate": 800,
+ "monthly_rate": 3200,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "أعمال يدوية، مناولة مواد، تنظيف",
+ "certifications": "لا يوجد",
+ "description": "عامل عادي ذو خبرة 5+ سنة في الأعمال اليدوية ومناولة المواد والتنظيف"
+ },
+ {
+ "id": "LABOR-002",
+ "name": "عامل عادي (خبرة 1-5 سنوات)",
+ "category": "عمال عاديين",
+ "subcategory": "عامل",
+ "hourly_rate": 15,
+ "daily_rate": 120,
+ "weekly_rate": 600,
+ "monthly_rate": 2400,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "أعمال يدوية، مناولة مواد، تنظيف",
+ "certifications": "لا يوجد",
+ "description": "عامل عادي ذو خبرة 1-5 سنوات في الأعمال اليدوية ومناولة المواد والتنظيف"
+ },
+ {
+ "id": "LABOR-003",
+ "name": "عامل نظافة (خبرة 3+ سنة)",
+ "category": "عمال عاديين",
+ "subcategory": "نظافة",
+ "hourly_rate": 15,
+ "daily_rate": 120,
+ "weekly_rate": 600,
+ "monthly_rate": 2400,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "تنظيف، ترتيب، إزالة مخلفات",
+ "certifications": "لا يوجد",
+ "description": "عامل نظافة ذو خبرة 3+ سنة في أعمال التنظيف والترتيب وإزالة المخلفات"
+ },
+ {
+ "id": "LABOR-004",
+ "name": "عامل نظافة (خبرة 1-3 سنوات)",
+ "category": "عمال عاديين",
+ "subcategory": "نظافة",
+ "hourly_rate": 12,
+ "daily_rate": 96,
+ "weekly_rate": 480,
+ "monthly_rate": 1900,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "تنظيف، ترتيب، إزالة مخلفات",
+ "certifications": "لا يوجد",
+ "description": "عامل نظافة ذو خبرة 1-3 سنوات في أعمال التنظيف والترتيب وإزالة المخلفات"
+ },
+ {
+ "id": "LABOR-005",
+ "name": "عامل مساعد (خبرة 3+ سنة)",
+ "category": "عمال عاديين",
+ "subcategory": "مساعد",
+ "hourly_rate": 18,
+ "daily_rate": 144,
+ "weekly_rate": 720,
+ "monthly_rate": 2900,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "مساعدة في أعمال البناء، مناولة مواد، تنظيف",
+ "certifications": "لا يوجد",
+ "description": "عامل مساعد ذو خبرة 3+ سنة في مساعدة العمال المهرة ومناولة المواد"
+ },
+ {
+ "id": "LABOR-006",
+ "name": "عامل مساعد (خبرة 1-3 سنوات)",
+ "category": "عمال عاديين",
+ "subcategory": "مساعد",
+ "hourly_rate": 15,
+ "daily_rate": 120,
+ "weekly_rate": 600,
+ "monthly_rate": 2400,
+ "nationality": "مقيم",
+ "availability": "متاح",
+ "skills": "مساعدة في أعمال البناء، مناولة مواد، تنظيف",
+ "certifications": "لا يوجد",
+ "description": "عامل مساعد ذو خبرة 1-3 سنوات في مساعدة العمال المهرة ومناولة المواد"
+ }
+ ])
+
+ # 5. سائقين
+ labor_data.extend([
+ {
+ "id": "DRIVER-001",
+ "name": "سائق شاحنة ثقيلة (خبرة 10+ سنة)",
+ "category": "سائقين",
+ "subcategory": "شاحنة ثقيلة",
+ "hourly_rate": 35,
+ "daily_rate": 280,
+ "weekly_rate": 1400,
+ "monthly_rate": 5500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "قيادة شاحنات ثقيلة، صيانة أولية، سجل نظيف",
+ "certifications": "رخصة قيادة شاحنات ثقيلة",
+ "description": "سائق شاحنة ثقيلة ذو خبرة 10+ سنة في قيادة الشاحنات الثقيلة ونقل المواد"
+ },
+ {
+ "id": "DRIVER-002",
+ "name": "سائق شاحنة ثقيلة (خبرة 5-10 سنوات)",
+ "category": "سائقين",
+ "subcategory": "شاحنة ثقيلة",
+ "hourly_rate": 30,
+ "daily_rate": 240,
+ "weekly_rate": 1200,
+ "monthly_rate": 4800,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "قيادة شاحنات ثقيلة، صيانة أولية، سجل نظيف",
+ "certifications": "رخصة قيادة شاحنات ثقيلة",
+ "description": "سائق شاحنة ثقيلة ذو خبرة 5-10 سنوات في قيادة الشاحنات الثقيلة ونقل المواد"
+ },
+ {
+ "id": "DRIVER-003",
+ "name": "سائق شاحنة خلاطة خرسانة (خبرة 10+ سنة)",
+ "category": "سائقين",
+ "subcategory": "خلاطة خرسانة",
+ "hourly_rate": 40,
+ "daily_rate": 320,
+ "weekly_rate": 1600,
+ "monthly_rate": 6500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "قيادة خلاطات الخرسانة، صيانة أولية، سجل نظيف",
+ "certifications": "رخصة قيادة شاحنات ثقيلة",
+ "description": "سائق شاحنة خلاطة خرسانة ذو خبرة 10+ سنة في قيادة خلاطات الخرسانة وصب الخرسانة"
+ },
+ {
+ "id": "DRIVER-004",
+ "name": "سائق شاحنة خلاطة خرسانة (خبرة 5-10 سنوات)",
+ "category": "سائقين",
+ "subcategory": "خلاطة خرسانة",
+ "hourly_rate": 35,
+ "daily_rate": 280,
+ "weekly_rate": 1400,
+ "monthly_rate": 5500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "قيادة خلاطات الخرسانة، صيانة أولية، سجل نظيف",
+ "certifications": "رخصة قيادة شاحنات ثقيلة",
+ "description": "سائق شاحنة خلاطة خرسانة ذو خبرة 5-10 سنوات في قيادة خلاطات الخرسانة وصب الخرسانة"
+ },
+ {
+ "id": "DRIVER-005",
+ "name": "سائق شاحنة نقل مياه (خبرة 5+ سنة)",
+ "category": "سائقين",
+ "subcategory": "نقل مياه",
+ "hourly_rate": 30,
+ "daily_rate": 240,
+ "weekly_rate": 1200,
+ "monthly_rate": 4800,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "قيادة شاحنات نقل المياه، صيانة أولية، سجل نظيف",
+ "certifications": "رخصة قيادة شاحنات ثقيلة",
+ "description": "سائق شاحنة نقل مياه ذو خبرة 5+ سنة في قيادة شاحنات نقل المياه وتوزيع المياه"
+ },
+ {
+ "id": "DRIVER-006",
+ "name": "سائق سيارة نقل خفيف (خبرة 5+ سنة)",
+ "category": "سائقين",
+ "subcategory": "نقل خفيف",
+ "hourly_rate": 25,
+ "daily_rate": 200,
+ "weekly_rate": 1000,
+ "monthly_rate": 4000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "قيادة سيارات النقل الخفيف، صيانة أولية، سجل نظيف",
+ "certifications": "رخصة قيادة",
+ "description": "سائق سيارة نقل خفيف ذو خبرة 5+ سنة في قيادة سيارات النقل الخفيف ونقل المواد والعمال"
+ }
+ ])
+
+ # 6. مشغلي معدات
+ labor_data.extend([
+ {
+ "id": "OPER-001",
+ "name": "مشغل حفارة (خبرة 10+ سنة)",
+ "category": "مشغلي معدات",
+ "subcategory": "حفارة",
+ "hourly_rate": 45,
+ "daily_rate": 360,
+ "weekly_rate": 1800,
+ "monthly_rate": 7500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تشغيل حفارات، صيانة أولية، حفر دقيق",
+ "certifications": "رخصة تشغيل معدات ثقيلة",
+ "description": "مشغل حفارة ذو خبرة 10+ سنة في تشغيل الحفارات وأعمال الحفر الدقيق"
+ },
+ {
+ "id": "OPER-002",
+ "name": "مشغل حفارة (خبرة 5-10 سنوات)",
+ "category": "مشغلي معدات",
+ "subcategory": "حفارة",
+ "hourly_rate": 40,
+ "daily_rate": 320,
+ "weekly_rate": 1600,
+ "monthly_rate": 6500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تشغيل حفارات، صيانة أولية، حفر دقيق",
+ "certifications": "رخصة تشغيل معدات ثقيلة",
+ "description": "مشغل حفارة ذو خبرة 5-10 سنوات في تشغيل الحفارات وأعمال الحفر الدقيق"
+ },
+ {
+ "id": "OPER-003",
+ "name": "مشغل لودر (خبرة 10+ سنة)",
+ "category": "مشغلي معدات",
+ "subcategory": "لودر",
+ "hourly_rate": 40,
+ "daily_rate": 320,
+ "weekly_rate": 1600,
+ "monthly_rate": 6500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تشغيل لودر، صيانة أولية، تحميل دقيق",
+ "certifications": "رخصة تشغيل معدات ثقيلة",
+ "description": "مشغل لودر ذو خبرة 10+ سنة في تشغيل اللودر وأعمال التحميل الدقيق"
+ },
+ {
+ "id": "OPER-004",
+ "name": "مشغل لودر (خبرة 5-10 سنوات)",
+ "category": "مشغلي معدات",
+ "subcategory": "لودر",
+ "hourly_rate": 35,
+ "daily_rate": 280,
+ "weekly_rate": 1400,
+ "monthly_rate": 5500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تشغيل لودر، صيانة أولية، تحميل دقيق",
+ "certifications": "رخصة تشغيل معدات ثقيلة",
+ "description": "مشغل لودر ذو خبرة 5-10 سنوات في تشغيل اللودر وأعمال التحميل الدقيق"
+ },
+ {
+ "id": "OPER-005",
+ "name": "مشغل بلدوزر (خبرة 10+ سنة)",
+ "category": "مشغلي معدات",
+ "subcategory": "بلدوزر",
+ "hourly_rate": 45,
+ "daily_rate": 360,
+ "weekly_rate": 1800,
+ "monthly_rate": 7500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تشغيل بلدوزر، صيانة أولية، تسوية دقيقة",
+ "certifications": "رخصة تشغيل معدات ثقيلة",
+ "description": "مشغل بلدوزر ذو خبرة 10+ سنة في تشغيل البلدوزر وأعمال التسوية الدقيقة"
+ },
+ {
+ "id": "OPER-006",
+ "name": "مشغل بلدوزر (خبرة 5-10 سنوات)",
+ "category": "مشغلي معدات",
+ "subcategory": "بلدوزر",
+ "hourly_rate": 40,
+ "daily_rate": 320,
+ "weekly_rate": 1600,
+ "monthly_rate": 6500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تشغيل بلدوزر، صيانة أولية، تسوية دقيقة",
+ "certifications": "رخصة تشغيل معدات ثقيلة",
+ "description": "مشغل بلدوزر ذو خبرة 5-10 سنوات في تشغيل البلدوزر وأعمال التسوية الدقيقة"
+ },
+ {
+ "id": "OPER-007",
+ "name": "مشغل جريدر (خبرة 10+ سنة)",
+ "category": "مشغلي معدات",
+ "subcategory": "جريدر",
+ "hourly_rate": 45,
+ "daily_rate": 360,
+ "weekly_rate": 1800,
+ "monthly_rate": 7500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تشغيل جريدر، صيانة أولية، تسوية دقيقة",
+ "certifications": "رخصة تشغيل معدات ثقيلة",
+ "description": "مشغل جريدر ذو خبرة 10+ سنة في تشغيل الجريدر وأعمال التسوية الدقيقة للطرق"
+ },
+ {
+ "id": "OPER-008",
+ "name": "مشغل جريدر (خبرة 5-10 سنوات)",
+ "category": "مشغلي معدات",
+ "subcategory": "جريدر",
+ "hourly_rate": 40,
+ "daily_rate": 320,
+ "weekly_rate": 1600,
+ "monthly_rate": 6500,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تشغيل جريدر، صيانة أولية، تسوية دقيقة",
+ "certifications": "رخصة تشغيل معدات ثقيلة",
+ "description": "مشغل جريدر ذو خبرة 5-10 سنوات في تشغيل الجريدر وأعمال التسوية الدقيقة للطرق"
+ },
+ {
+ "id": "OPER-009",
+ "name": "مشغل رافعة (خبرة 10+ سنة)",
+ "category": "مشغلي معدات",
+ "subcategory": "رافعة",
+ "hourly_rate": 50,
+ "daily_rate": 400,
+ "weekly_rate": 2000,
+ "monthly_rate": 8000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تشغيل رافعات، صيانة أولية، رفع دقيق",
+ "certifications": "رخصة تشغيل رافعات",
+ "description": "مشغل رافعة ذو خبرة 10+ سنة في تشغيل الرافعات وأعمال الرفع الدقيق"
+ },
+ {
+ "id": "OPER-010",
+ "name": "مشغل رافعة (خبرة 5-10 سنوات)",
+ "category": "مشغلي معدات",
+ "subcategory": "رافعة",
+ "hourly_rate": 45,
+ "daily_rate": 360,
+ "weekly_rate": 1800,
+ "monthly_rate": 7000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تشغيل رافعات، صيانة أولية، رفع دقيق",
+ "certifications": "رخصة تشغيل رافعات",
+ "description": "مشغل رافعة ذو خبرة 5-10 سنوات في تشغيل الرافعات وأعمال الرفع الدقيق"
+ }
+ ])
+
+ # 7. إداريين
+ labor_data.extend([
+ {
+ "id": "ADMIN-001",
+ "name": "مدير مشروع (خبرة 15+ سنة)",
+ "category": "إداريين",
+ "subcategory": "مدير مشروع",
+ "hourly_rate": 200,
+ "daily_rate": 1600,
+ "weekly_rate": 8000,
+ "monthly_rate": 32000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "إدارة مشاريع، تخطيط، متابعة، إعداد تقارير",
+ "certifications": "PMP، عضوية الهيئة السعودية للمهندسين",
+ "description": "مدير مشروع ذو خبرة 15+ سنة في إدارة مشاريع البنية التحتية والطرق والجسور"
+ },
+ {
+ "id": "ADMIN-002",
+ "name": "مدير مشروع (خبرة 10-15 سنة)",
+ "category": "إداريين",
+ "subcategory": "مدير مشروع",
+ "hourly_rate": 150,
+ "daily_rate": 1200,
+ "weekly_rate": 6000,
+ "monthly_rate": 25000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "إدارة مشاريع، تخطيط، متابعة، إعداد تقارير",
+ "certifications": "PMP، عضوية الهيئة السعودية للمهندسين",
+ "description": "مدير مشروع ذو خبرة 10-15 سنة في إدارة مشاريع البنية التحتية والطرق والجسور"
+ },
+ {
+ "id": "ADMIN-003",
+ "name": "مهندس تخطيط (خبرة 10+ سنة)",
+ "category": "إداريين",
+ "subcategory": "تخطيط",
+ "hourly_rate": 120,
+ "daily_rate": 960,
+ "weekly_rate": 4800,
+ "monthly_rate": 20000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تخطيط مشاريع، جدولة، متابعة، إعداد تقارير",
+ "certifications": "PMP، Primavera P6، MS Project",
+ "description": "مهندس تخطيط ذو خبرة 10+ سنة في تخطيط وجدولة ومتابعة مشاريع البنية التحتية"
+ },
+ {
+ "id": "ADMIN-004",
+ "name": "مهندس تخطيط (خبرة 5-10 سنوات)",
+ "category": "إداريين",
+ "subcategory": "تخطيط",
+ "hourly_rate": 100,
+ "daily_rate": 800,
+ "weekly_rate": 4000,
+ "monthly_rate": 16000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "تخطيط مشاريع، جدولة، متابعة، إعداد تقارير",
+ "certifications": "Primavera P6، MS Project",
+ "description": "مهندس تخطيط ذو خبرة 5-10 سنوات في تخطيط وجدولة ومتابعة مشاريع البنية التحتية"
+ },
+ {
+ "id": "ADMIN-005",
+ "name": "مهندس مراقبة جودة (خبرة 10+ سنة)",
+ "category": "إداريين",
+ "subcategory": "مراقبة جودة",
+ "hourly_rate": 120,
+ "daily_rate": 960,
+ "weekly_rate": 4800,
+ "monthly_rate": 20000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "مراقبة جودة، اختبارات، إعداد تقارير، تطبيق معايير",
+ "certifications": "ISO 9001، عضوية الهيئة السعودية للمهندسين",
+ "description": "مهندس مراقبة جودة ذو خبرة 10+ سنة في مراقبة جودة مشاريع البنية التحتية"
+ },
+ {
+ "id": "ADMIN-006",
+ "name": "مهندس مراقبة جودة (خبرة 5-10 سنوات)",
+ "category": "إداريين",
+ "subcategory": "مراقبة جودة",
+ "hourly_rate": 100,
+ "daily_rate": 800,
+ "weekly_rate": 4000,
+ "monthly_rate": 16000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "مراقبة جودة، اختبارات، إعداد تقارير، تطبيق معايير",
+ "certifications": "ISO 9001، عضوية الهيئة السعودية للمهندسين",
+ "description": "مهندس مراقبة جودة ذو خبرة 5-10 سنوات في مراقبة جودة مشاريع البنية التحتية"
+ },
+ {
+ "id": "ADMIN-007",
+ "name": "مهندس سلامة (خبرة 10+ سنة)",
+ "category": "إداريين",
+ "subcategory": "سلامة",
+ "hourly_rate": 120,
+ "daily_rate": 960,
+ "weekly_rate": 4800,
+ "monthly_rate": 20000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "إدارة السلامة، تدريب، تفتيش، إعداد تقارير",
+ "certifications": "NEBOSH، OSHA",
+ "description": "مهندس سلامة ذو خبرة 10+ سنة في إدارة السلامة في مشاريع البنية التحتية"
+ },
+ {
+ "id": "ADMIN-008",
+ "name": "مهندس سلامة (خبرة 5-10 سنوات)",
+ "category": "إداريين",
+ "subcategory": "سلامة",
+ "hourly_rate": 100,
+ "daily_rate": 800,
+ "weekly_rate": 4000,
+ "monthly_rate": 16000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "إدارة السلامة، تدريب، تفتيش، إعداد تقارير",
+ "certifications": "NEBOSH، OSHA",
+ "description": "مهندس سلامة ذو خبرة 5-10 سنوات في إدارة السلامة في مشاريع البنية التحتية"
+ },
+ {
+ "id": "ADMIN-009",
+ "name": "محاسب مشاريع (خبرة 10+ سنة)",
+ "category": "إداريين",
+ "subcategory": "محاسبة",
+ "hourly_rate": 100,
+ "daily_rate": 800,
+ "weekly_rate": 4000,
+ "monthly_rate": 16000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "محاسبة مشاريع، إعداد تقارير مالية، متابعة مصروفات",
+ "certifications": "SOCPA",
+ "description": "محاسب مشاريع ذو خبرة 10+ سنة في محاسبة مشاريع البنية التحتية"
+ },
+ {
+ "id": "ADMIN-010",
+ "name": "محاسب مشاريع (خبرة 5-10 سنوات)",
+ "category": "إداريين",
+ "subcategory": "محاسبة",
+ "hourly_rate": 80,
+ "daily_rate": 640,
+ "weekly_rate": 3200,
+ "monthly_rate": 13000,
+ "nationality": "سعودي",
+ "availability": "متاح",
+ "skills": "محاسبة مشاريع، إعداد تقارير مالية، متابعة مصروفات",
+ "certifications": "SOCPA",
+ "description": "محاسب مشاريع ذو خبرة 5-10 سنوات في محاسبة مشاريع البنية التحتية"
+ }
+ ])
+
+ # تخزين البيانات في حالة الجلسة
+ st.session_state.labor_catalog = pd.DataFrame(labor_data)
+
+ def render(self):
+ """عرض واجهة كتالوج العمالة"""
+
+ st.markdown("## كتالوج العمالة والمهندسين")
+
+ # إنشاء تبويبات لعرض الكتالوج
+ tabs = st.tabs([
+ "عرض الكتالوج",
+ "إضافة عامل",
+ "تحليل الأسعار",
+ "استيراد/تصدير"
+ ])
+
+ with tabs[0]:
+ self._render_catalog_view_tab()
+
+ with tabs[1]:
+ self._render_add_labor_tab()
+
+ with tabs[2]:
+ self._render_price_analysis_tab()
+
+ with tabs[3]:
+ self._render_import_export_tab()
+
+ def _render_catalog_view_tab(self):
+ """عرض تبويب عرض الكتالوج"""
+
+ st.markdown("### عرض كتالوج العمالة والمهندسين")
+
+ # استخراج البيانات
+ labor_df = st.session_state.labor_catalog
+
+ # إنشاء فلاتر للعرض
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ # فلتر حسب الفئة
+ categories = ["الكل"] + sorted(labor_df["category"].unique().tolist())
+ selected_category = st.selectbox("اختر فئة العمالة", categories)
+
+ with col2:
+ # فلتر حسب الفئة الفرعية
+ if selected_category != "الكل":
+ subcategories = ["الكل"] + sorted(labor_df[labor_df["category"] == selected_category]["subcategory"].unique().tolist())
+ else:
+ subcategories = ["الكل"] + sorted(labor_df["subcategory"].unique().tolist())
+
+ selected_subcategory = st.selectbox("اختر التخصص", subcategories)
+
+ with col3:
+ # فلتر حسب الجنسية
+ nationalities = ["الكل"] + sorted(labor_df["nationality"].unique().tolist())
+ selected_nationality = st.selectbox("اختر الجنسية", nationalities)
+
+ # تطبيق الفلاتر
+ filtered_df = labor_df.copy()
+
+ if selected_category != "الكل":
+ filtered_df = filtered_df[filtered_df["category"] == selected_category]
+
+ if selected_subcategory != "الكل":
+ filtered_df = filtered_df[filtered_df["subcategory"] == selected_subcategory]
+
+ if selected_nationality != "الكل":
+ filtered_df = filtered_df[filtered_df["nationality"] == selected_nationality]
+
+ # عرض البيانات
+ if not filtered_df.empty:
+ # عرض عدد النتائج
+ st.info(f"تم العثور على {len(filtered_df)} عامل/مهندس")
+
+ # عرض العمالة في شكل بطاقات
+ for i, (_, labor) in enumerate(filtered_df.iterrows()):
+ col1, col2 = st.columns([1, 3])
+
+ with col1:
+ # عرض صورة العامل (استخدام صورة افتراضية)
+ st.image("https://via.placeholder.com/150", caption=labor["name"])
+
+ with col2:
+ # عرض معلومات العامل
+ st.markdown(f"**{labor['name']}** (الكود: {labor['id']})")
+ st.markdown(f"الفئة: {labor['category']} - {labor['subcategory']}")
+ st.markdown(f"الجنسية: {labor['nationality']} | الحالة: {labor['availability']}")
+ st.markdown(f"الأسعار: {labor['hourly_rate']} ريال/ساعة | {labor['daily_rate']} ريال/يوم | {labor['monthly_rate']} ريال/شهر")
+ st.markdown(f"المهارات: {labor['skills']}")
+
+ # إضافة زر لعرض التفاصيل
+ if st.button(f"عرض التفاصيل الكاملة", key=f"details_{labor['id']}"):
+ st.session_state.selected_labor = labor['id']
+ self._show_labor_details(labor)
+
+ st.markdown("---")
+ else:
+ st.warning("لا توجد عمالة تطابق معايير البحث")
+
+ def _show_labor_details(self, labor):
+ """عرض تفاصيل العامل"""
+
+ st.markdown(f"## تفاصيل العامل/المهندس: {labor['name']}")
+
+ col1, col2 = st.columns([1, 2])
+
+ with col1:
+ # عرض صورة العامل (استخدام صورة افتراضية)
+ st.image("https://via.placeholder.com/300", caption=labor["name"])
+
+ with col2:
+ # عرض المعلومات الأساسية
+ st.markdown("### المعلومات الأساسية")
+ st.markdown(f"**الكود:** {labor['id']}")
+ st.markdown(f"**الفئة:** {labor['category']} - {labor['subcategory']}")
+ st.markdown(f"**الجنسية:** {labor['nationality']}")
+ st.markdown(f"**الحالة:** {labor['availability']}")
+ st.markdown(f"**المهارات:** {labor['skills']}")
+ st.markdown(f"**الشهادات:** {labor['certifications']}")
+ st.markdown(f"**الوصف:** {labor['description']}")
+
+ # عرض معلومات الأسعار
+ st.markdown("### معلومات الأسعار")
+
+ price_data = {
+ "وحدة الزمن": ["ساعة", "يوم", "أسبوع", "شهر"],
+ "السعر (ريال)": [
+ labor['hourly_rate'],
+ labor['daily_rate'],
+ labor['weekly_rate'],
+ labor['monthly_rate']
+ ]
+ }
+
+ price_df = pd.DataFrame(price_data)
+ st.dataframe(price_df, use_container_width=True)
+
+ # إضافة زر للتعديل
+ if st.button("تعديل بيانات العامل"):
+ st.session_state.edit_labor = labor['id']
+ # هنا يمكن إضافة منطق التعديل
+
+ def _render_add_labor_tab(self):
+ """عرض تبويب إضافة عامل"""
+
+ st.markdown("### إضافة عامل/مهندس جديد")
+
+ # استخراج البيانات
+ labor_df = st.session_state.labor_catalog
+
+ # إنشاء نموذج إضافة عامل
+ with st.form("add_labor_form"):
+ st.markdown("#### المعلومات الأساسية")
+
+ # الصف الأول
+ col1, col2 = st.columns(2)
+ with col1:
+ labor_id = st.text_input("كود العامل", value=f"LABOR-{len(labor_df) + 1:03d}")
+ labor_name = st.text_input("اسم العامل", placeholder="مثال: مهندس مدني (خبرة 10+ سنة)")
+
+ with col2:
+ # استخراج الفئات والفئات الفرعية الموجودة
+ categories = sorted(labor_df["category"].unique().tolist())
+ labor_category = st.selectbox("فئة العامل", categories)
+
+ # استخراج الفئات الفرعية بناءً على الفئة المختارة
+ subcategories = sorted(labor_df[labor_df["category"] == labor_category]["subcategory"].unique().tolist())
+ labor_subcategory = st.selectbox("التخصص", subcategories)
+
+ # الصف الثاني
+ col1, col2 = st.columns(2)
+ with col1:
+ labor_nationality = st.selectbox("الجنسية", ["سعودي", "مقيم"])
+ with col2:
+ labor_availability = st.selectbox("الحالة", ["متاح", "غير متاح"])
+
+ # الصف الثالث - الأسعار
+ st.markdown("#### معلومات الأسعار")
+
+ col1, col2, col3, col4 = st.columns(4)
+ with col1:
+ labor_hourly_rate = st.number_input("السعر بالساعة (ريال)", min_value=0, step=5)
+ with col2:
+ labor_daily_rate = st.number_input("السعر باليوم (ريال)", min_value=0, step=40)
+ with col3:
+ labor_weekly_rate = st.number_input("السعر بالأسبوع (ريال)", min_value=0, step=200)
+ with col4:
+ labor_monthly_rate = st.number_input("السعر بالشهر (ريال)", min_value=0, step=1000)
+
+ # المهارات والشهادات
+ st.markdown("#### المهارات والشهادات")
+
+ labor_skills = st.text_area("المهارات", placeholder="مثال: إدارة مشاريع، تصميم إنشائي، إشراف على التنفيذ")
+ labor_certifications = st.text_input("الشهادات", placeholder="مثال: عضوية الهيئة السعودية للمهندسين، PMP")
+
+ # وصف العامل
+ labor_description = st.text_area("وصف العامل", placeholder="أدخل وصفاً تفصيلياً للعامل/المهندس")
+
+ # زر الإضافة
+ submit_button = st.form_submit_button("إضافة العامل")
+
+ if submit_button:
+ # التحقق من البيانات
+ if not labor_name or not labor_category or not labor_subcategory:
+ st.error("يرجى إدخال المعلومات الأساسية للعامل")
+ else:
+ # إنشاء عامل جديد
+ new_labor = {
+ "id": labor_id,
+ "name": labor_name,
+ "category": labor_category,
+ "subcategory": labor_subcategory,
+ "hourly_rate": labor_hourly_rate,
+ "daily_rate": labor_daily_rate,
+ "weekly_rate": labor_weekly_rate,
+ "monthly_rate": labor_monthly_rate,
+ "nationality": labor_nationality,
+ "availability": labor_availability,
+ "skills": labor_skills,
+ "certifications": labor_certifications,
+ "description": labor_description
+ }
+
+ # إضافة العامل إلى الكتالوج
+ st.session_state.labor_catalog = pd.concat([
+ st.session_state.labor_catalog,
+ pd.DataFrame([new_labor])
+ ], ignore_index=True)
+
+ st.success(f"تمت إضافة العامل {labor_name} بنجاح!")
+
+ def _render_price_analysis_tab(self):
+ """عرض تبويب تحليل الأسعار"""
+
+ st.markdown("### تحليل أسعار العمالة والمهندسين")
+
+ # استخراج البيانات
+ labor_df = st.session_state.labor_catalog
+
+ # تحليل متوسط الأسعار حسب الفئة
+ st.markdown("#### متوسط الأسعار حسب الفئة")
+
+ # حساب متوسط الأسعار لكل فئة
+ category_prices = labor_df.groupby("category").agg({
+ "hourly_rate": "mean",
+ "daily_rate": "mean",
+ "monthly_rate": "mean"
+ }).reset_index()
+
+ # تغيير أسماء الأعمدة
+ category_prices.columns = ["الفئة", "متوسط السعر بالساعة", "متوسط السعر باليوم", "متوسط السعر بالشهر"]
+
+ # عرض الجدول
+ st.dataframe(category_prices, use_container_width=True)
+
+ # إنشاء رسم بياني للمقارنة
+ st.markdown("#### مقارنة متوسط الأسعار الشهرية حسب الفئة")
+
+ fig = px.bar(
+ category_prices,
+ x="الفئة",
+ y="متوسط السعر بالشهر",
+ title="متوسط أسعار العمالة والمهندسين الشهرية حسب الفئة",
+ color="الفئة",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل توزيع العمالة حسب الجنسية
+ st.markdown("#### توزيع العمالة حسب الجنسية")
+
+ # حساب عدد العمالة حسب الجنسية
+ nationality_counts = labor_df["nationality"].value_counts().reset_index()
+ nationality_counts.columns = ["الجنسية", "عدد العمالة"]
+
+ # إنشاء رسم بياني دائري
+ fig = px.pie(
+ nationality_counts,
+ values="عدد العمالة",
+ names="الجنسية",
+ title="توزيع العمالة حسب الجنسية",
+ color="الجنسية"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل متوسط الأسعار حسب التخصص
+ st.markdown("#### متوسط الأسعار حسب التخصص")
+
+ # اختيار الفئة للتحليل
+ selected_category_for_analysis = st.selectbox(
+ "اختر الفئة للتحليل",
+ sorted(labor_df["category"].unique().tolist())
+ )
+
+ # حساب متوسط الأسعار لكل تخصص ضمن الفئة المختارة
+ subcategory_prices = labor_df[labor_df["category"] == selected_category_for_analysis].groupby("subcategory").agg({
+ "hourly_rate": "mean",
+ "daily_rate": "mean",
+ "monthly_rate": "mean"
+ }).reset_index()
+
+ # تغيير أسماء الأعمدة
+ subcategory_prices.columns = ["التخصص", "متوسط السعر بالساعة", "متوسط السعر باليوم", "متوسط السعر بالشهر"]
+
+ # عرض الجدول
+ st.dataframe(subcategory_prices, use_container_width=True)
+
+ # إنشاء رسم بياني للمقارنة
+ fig = px.bar(
+ subcategory_prices,
+ x="التخصص",
+ y="متوسط السعر بالشهر",
+ title=f"متوسط أسعار {selected_category_for_analysis} الشهرية حسب التخصص",
+ color="التخصص",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # حاسبة تكاليف العمالة للمشروع
+ st.markdown("#### حاسبة تكاليف العمالة للمشروع")
+
+ with st.form("project_labor_calculator"):
+ st.markdown("أدخل العمالة المطلوبة للمشروع")
+
+ # اختيار العمالة
+ selected_labor = st.multiselect(
+ "اختر العمالة",
+ options=labor_df["name"].tolist(),
+ format_func=lambda x: f"{x} ({labor_df[labor_df['name'] == x]['id'].iloc[0]})"
+ )
+
+ # اختيار وحدة الزمن
+ time_unit = st.radio("اختر وحدة الزمن", ["ساعة", "يوم", "أسبوع", "شهر"], horizontal=True)
+
+ # إنشاء حقول إدخال الكميات
+ quantities = {}
+
+ if selected_labor:
+ st.markdown("أدخل المدة المطلوبة")
+
+ for labor_name in selected_labor:
+ quantities[labor_name] = st.number_input(
+ f"{labor_name}",
+ min_value=0,
+ step=1,
+ key=f"qty_{labor_df[labor_df['name'] == labor_name]['id'].iloc[0]}"
+ )
+
+ # زر الحساب
+ calculate_button = st.form_submit_button("حساب التكاليف")
+
+ if calculate_button:
+ if not selected_labor:
+ st.error("يرجى اختيار عامل واحد على الأقل")
+ else:
+ # حساب التكاليف
+ project_costs = []
+
+ for labor_name in selected_labor:
+ labor = labor_df[labor_df["name"] == labor_name].iloc[0]
+ quantity = quantities[labor_name]
+
+ if quantity > 0:
+ # تحديد السعر بناءً على وحدة الزمن
+ if time_unit == "ساعة":
+ rate = labor["hourly_rate"]
+ rate_name = "السعر بالساعة"
+ elif time_unit == "يوم":
+ rate = labor["daily_rate"]
+ rate_name = "السعر باليوم"
+ elif time_unit == "أسبوع":
+ rate = labor["weekly_rate"]
+ rate_name = "السعر بالأسبوع"
+ else: # شهر
+ rate = labor["monthly_rate"]
+ rate_name = "السعر بالشهر"
+
+ cost = rate * quantity
+
+ project_costs.append({
+ "العامل": labor_name,
+ "الكود": labor["id"],
+ "الفئة": labor["category"],
+ "التخصص": labor["subcategory"],
+ rate_name: rate,
+ f"عدد {time_unit}ات": quantity,
+ "التكلفة الإجمالية": cost
+ })
+
+ if project_costs:
+ # عرض النتائج
+ project_costs_df = pd.DataFrame(project_costs)
+ st.dataframe(project_costs_df, use_container_width=True)
+
+ # حساب إجمالي التكاليف
+ total_cost = project_costs_df["التكلفة الإجمالية"].sum()
+ st.metric("إجمالي تكاليف العمالة للمشروع", f"{total_cost:,.2f} ريال")
+ else:
+ st.warning("يرجى إدخال مدة أكبر من صفر")
+
+ def _render_import_export_tab(self):
+ """عرض تبويب استيراد/تصدير"""
+
+ st.markdown("### استيراد وتصدير بيانات العمالة والمهندسين")
+
+ # استيراد البيانات
+ st.markdown("#### استيراد البيانات")
+
+ uploaded_file = st.file_uploader("اختر ملف Excel لاستيراد بيانات العمالة", type=["xlsx", "xls"])
+
+ if uploaded_file is not None:
+ try:
+ # قراءة الملف
+ imported_df = pd.read_excel(uploaded_file)
+
+ # عرض البيانات المستوردة
+ st.dataframe(imported_df, use_container_width=True)
+
+ # زر الاستيراد
+ if st.button("استيراد البيانات"):
+ # التحقق من وجود الأعمدة المطلوبة
+ required_columns = ["id", "name", "category", "subcategory", "hourly_rate", "daily_rate", "weekly_rate", "monthly_rate"]
+
+ if all(col in imported_df.columns for col in required_columns):
+ # دمج البيانات المستوردة مع البيانات الحالية
+ st.session_state.labor_catalog = pd.concat([
+ st.session_state.labor_catalog,
+ imported_df
+ ], ignore_index=True).drop_duplicates(subset=["id"])
+
+ st.success(f"تم استيراد {len(imported_df)} عامل/مهندس بنجاح!")
+ else:
+ st.error("الملف المستورد لا يحتوي على الأعمدة المطلوبة")
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}")
+
+ # تصدير البيانات
+ st.markdown("#### تصدير البيانات")
+
+ # اختيار تنسيق التصدير
+ export_format = st.radio("اختر تنسيق التصدير", ["Excel", "CSV", "JSON"], horizontal=True)
+
+ if st.button("تصدير البيانات"):
+ # استخراج البيانات
+ labor_df = st.session_state.labor_catalog
+
+ # تصدير البيانات حسب التنسيق المختار
+ if export_format == "Excel":
+ # تصدير إلى Excel
+ output = io.BytesIO()
+ with pd.ExcelWriter(output, engine="openpyxl") as writer:
+ labor_df.to_excel(writer, index=False, sheet_name="Labor")
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف Excel",
+ data=output.getvalue(),
+ file_name="labor_catalog.xlsx",
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ )
+
+ elif export_format == "CSV":
+ # تصدير إلى CSV
+ csv_data = labor_df.to_csv(index=False)
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف CSV",
+ data=csv_data,
+ file_name="labor_catalog.csv",
+ mime="text/csv"
+ )
+
+ else: # JSON
+ # تصدير إلى JSON
+ json_data = labor_df.to_json(orient="records", force_ascii=False)
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف JSON",
+ data=json_data,
+ file_name="labor_catalog.json",
+ mime="application/json"
+ )
+
+ def get_labor_by_id(self, labor_id):
+ """الحصول على عامل بواسطة الكود"""
+
+ labor_df = st.session_state.labor_catalog
+ labor = labor_df[labor_df["id"] == labor_id]
+
+ if not labor.empty:
+ return labor.iloc[0].to_dict()
+
+ return None
+
+ def get_labor_by_category(self, category):
+ """الحصول على العمالة حسب الفئة"""
+
+ labor_df = st.session_state.labor_catalog
+ labor = labor_df[labor_df["category"] == category]
+
+ if not labor.empty:
+ return labor.to_dict(orient="records")
+
+ return []
+
+ def calculate_labor_cost(self, labor_id, quantity, time_unit="day"):
+ """حساب تكلفة العامل بناءً على الكمية ووحدة الزمن"""
+
+ labor = self.get_labor_by_id(labor_id)
+
+ if labor:
+ if time_unit == "hour":
+ return labor["hourly_rate"] * quantity
+ elif time_unit == "day":
+ return labor["daily_rate"] * quantity
+ elif time_unit == "week":
+ return labor["weekly_rate"] * quantity
+ elif time_unit == "month":
+ return labor["monthly_rate"] * quantity
+
+ return 0
diff --git a/pricing_system/modules/catalogs/materials_catalog.py b/pricing_system/modules/catalogs/materials_catalog.py
index 72aa28a5b6be27e850dc3ab231bfc91181ce4cfd..c976795bf76258bfe96893c07fa478d30e227a6a 100644
--- a/pricing_system/modules/catalogs/materials_catalog.py
+++ b/pricing_system/modules/catalogs/materials_catalog.py
@@ -1,1930 +1,1930 @@
-"""
-كتالوج المواد - وحدة إدارة مواد المقاولات
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import plotly.express as px
-import os
-import json
-from datetime import datetime
-import io
-
-class MaterialsCatalog:
- """كتالوج المواد"""
-
- def __init__(self):
- """تهيئة كتالوج المواد"""
-
- # تهيئة حالة الجلسة لكتالوج المواد
- if 'materials_catalog' not in st.session_state:
- # إنشاء بيانات افتراضية للمواد
- self._initialize_materials_catalog()
-
- def _initialize_materials_catalog(self):
- """تهيئة بيانات كتالوج المواد"""
-
- # تعريف فئات المواد
- material_categories = [
- "مواد الخرسانة",
- "مواد البناء",
- "مواد الطرق",
- "مواد الصرف الصحي",
- "مواد العزل",
- "مواد التشطيبات",
- "مواد كهربائية",
- "مواد ميكانيكية",
- "مواد الري والزراعة",
- "مواد متنوعة"
- ]
-
- # إنشاء قائمة المواد
- materials_data = []
-
- # 1. مواد الخرسانة
- materials_data.extend([
- {
- "id": "MAT-001",
- "name": "أسمنت بورتلاندي عادي",
- "category": "مواد الخرسانة",
- "subcategory": "أسمنت",
- "unit": "طن",
- "price": 600,
- "supplier": "شركة أسمنت اليمامة",
- "origin": "محلي",
- "lead_time": 2,
- "min_order": 10,
- "description": "أسمنت بورتلاندي عادي مطابق للمواصفات السعودية",
- "image_url": "https://example.com/cement.jpg"
- },
- {
- "id": "MAT-002",
- "name": "أسمنت مقاوم للكبريتات",
- "category": "مواد الخرسانة",
- "subcategory": "أسمنت",
- "unit": "طن",
- "price": 650,
- "supplier": "شركة أسمنت ينبع",
- "origin": "محلي",
- "lead_time": 2,
- "min_order": 10,
- "description": "أسمنت مقاوم للكبريتات للمناطق ذات التربة الكبريتية",
- "image_url": "https://example.com/srcement.jpg"
- },
- {
- "id": "MAT-003",
- "name": "رمل خشن",
- "category": "مواد الخرسانة",
- "subcategory": "ركام",
- "unit": "م3",
- "price": 80,
- "supplier": "كسارات الرياض",
- "origin": "محلي",
- "lead_time": 1,
- "min_order": 20,
- "description": "رمل خشن للخرسانة مطابق للمواصفات",
- "image_url": "https://example.com/sand.jpg"
- },
- {
- "id": "MAT-004",
- "name": "بحص مقاس 3/4 بوصة",
- "category": "مواد الخرسانة",
- "subcategory": "ركام",
- "unit": "م3",
- "price": 120,
- "supplier": "كسارات الرياض",
- "origin": "محلي",
- "lead_time": 1,
- "min_order": 20,
- "description": "بحص مقاس 3/4 بوصة للخرسانة مطابق للمواصفات",
- "image_url": "https://example.com/gravel.jpg"
- },
- {
- "id": "MAT-005",
- "name": "بحص مقاس 3/8 بوصة",
- "category": "مواد الخرسانة",
- "subcategory": "ركام",
- "unit": "م3",
- "price": 130,
- "supplier": "كسارات الرياض",
- "origin": "محلي",
- "lead_time": 1,
- "min_order": 20,
- "description": "بحص مقاس 3/8 بوصة للخرسانة مطابق للمواصفات",
- "image_url": "https://example.com/gravel2.jpg"
- },
- {
- "id": "MAT-006",
- "name": "ماء",
- "category": "مواد الخرسانة",
- "subcategory": "سوائل",
- "unit": "م3",
- "price": 5,
- "supplier": "متعدد",
- "origin": "محلي",
- "lead_time": 1,
- "min_order": 10,
- "description": "ماء صالح للخلط في الخرسانة",
- "image_url": "https://example.com/water.jpg"
- },
- {
- "id": "MAT-007",
- "name": "إضافة ملدنة للخرسانة",
- "category": "مواد الخرسانة",
- "subcategory": "إضافات",
- "unit": "لتر",
- "price": 15,
- "supplier": "سيكا",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 200,
- "description": "إضافة ملدنة لتحسين قابلية تشغيل الخرسانة",
- "image_url": "https://example.com/plasticizer.jpg"
- },
- {
- "id": "MAT-008",
- "name": "إضافة مؤخرة للشك",
- "category": "مواد الخرسانة",
- "subcategory": "إضافات",
- "unit": "لتر",
- "price": 18,
- "supplier": "سيكا",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 200,
- "description": "إضافة مؤخرة للشك للصب في الأجواء الحارة",
- "image_url": "https://example.com/retarder.jpg"
- },
- {
- "id": "MAT-009",
- "name": "إضافة معجلة للشك",
- "category": "مواد الخرسانة",
- "subcategory": "إضافات",
- "unit": "لتر",
- "price": 20,
- "supplier": "سيكا",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 200,
- "description": "إضافة معجلة للشك للصب في الأجواء الباردة",
- "image_url": "https://example.com/accelerator.jpg"
- },
- {
- "id": "MAT-010",
- "name": "ألياف بوليبروبلين",
- "category": "مواد الخرسانة",
- "subcategory": "إضافات",
- "unit": "كجم",
- "price": 35,
- "supplier": "سيكا",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 100,
- "description": "ألياف بوليبروبلين لتقليل التشققات في الخرسانة",
- "image_url": "https://example.com/fibers.jpg"
- }
- ])
-
- # 2. مواد البناء
- materials_data.extend([
- {
- "id": "MAT-011",
- "name": "طابوق أسمنتي مقاس 20×20×40 سم",
- "category": "مواد البناء",
- "subcategory": "طابوق",
- "unit": "قطعة",
- "price": 3.5,
- "supplier": "مصنع الرياض للطابوق",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 1000,
- "description": "طابوق أسمنتي مقاس 20×20×40 سم للجدران الخارجية",
- "image_url": "https://example.com/block.jpg"
- },
- {
- "id": "MAT-012",
- "name": "طابوق أسمنتي مقاس 15×20×40 سم",
- "category": "مواد البناء",
- "subcategory": "طابوق",
- "unit": "قطعة",
- "price": 3,
- "supplier": "مصنع الرياض للطابوق",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 1000,
- "description": "طابوق أسمنتي مقاس 15×20×40 سم للجدران الداخلية",
- "image_url": "https://example.com/block2.jpg"
- },
- {
- "id": "MAT-013",
- "name": "طابوق أسمنتي مقاس 10×20×40 سم",
- "category": "مواد البناء",
- "subcategory": "طابوق",
- "unit": "قطعة",
- "price": 2.5,
- "supplier": "مصنع الرياض للطابوق",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 1000,
- "description": "طابوق أسمنتي مقاس 10×20×40 سم للجدران الداخلية",
- "image_url": "https://example.com/block3.jpg"
- },
- {
- "id": "MAT-014",
- "name": "حديد تسليح قطر 8 مم",
- "category": "مواد البناء",
- "subcategory": "حديد تسليح",
- "unit": "طن",
- "price": 3200,
- "supplier": "شركة حديد الراجحي",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 5,
- "description": "حديد تسليح قطر 8 مم مطابق للمواصفات السعودية",
- "image_url": "https://example.com/rebar8.jpg"
- },
- {
- "id": "MAT-015",
- "name": "حديد تسليح قطر 10 مم",
- "category": "مواد البناء",
- "subcategory": "حديد تسليح",
- "unit": "طن",
- "price": 3150,
- "supplier": "شركة حديد الراجحي",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 5,
- "description": "حديد تسليح قطر 10 مم مطابق للمواصفات السعودية",
- "image_url": "https://example.com/rebar10.jpg"
- },
- {
- "id": "MAT-016",
- "name": "حديد تسليح قطر 12 مم",
- "category": "مواد البناء",
- "subcategory": "حديد تسليح",
- "unit": "طن",
- "price": 3100,
- "supplier": "شركة حديد الراجحي",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 5,
- "description": "حديد تسليح قطر 12 مم مطابق للمواصفات السعودية",
- "image_url": "https://example.com/rebar12.jpg"
- },
- {
- "id": "MAT-017",
- "name": "حديد تسليح قطر 16 مم",
- "category": "مواد البناء",
- "subcategory": "حديد تسليح",
- "unit": "طن",
- "price": 3050,
- "supplier": "شركة حديد الراجحي",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 5,
- "description": "حديد تسليح قطر 16 مم مطابق للمواصفات السعودية",
- "image_url": "https://example.com/rebar16.jpg"
- },
- {
- "id": "MAT-018",
- "name": "حديد تسليح قطر 20 مم",
- "category": "مواد البناء",
- "subcategory": "حديد تسليح",
- "unit": "طن",
- "price": 3000,
- "supplier": "شركة حديد الراجحي",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 5,
- "description": "حديد تسليح قطر 20 مم مطابق للمواصفات السعودية",
- "image_url": "https://example.com/rebar20.jpg"
- },
- {
- "id": "MAT-019",
- "name": "حديد تسليح قطر 25 مم",
- "category": "مواد البناء",
- "subcategory": "حديد تسليح",
- "unit": "طن",
- "price": 2950,
- "supplier": "شركة حديد الراجحي",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 5,
- "description": "حديد تسليح قطر 25 مم مطابق للمواصفات السعودية",
- "image_url": "https://example.com/rebar25.jpg"
- },
- {
- "id": "MAT-020",
- "name": "شبك حديد ملحوم 6 مم",
- "category": "مواد البناء",
- "subcategory": "حديد تسليح",
- "unit": "م2",
- "price": 25,
- "supplier": "شركة حديد الراجحي",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 100,
- "description": "شبك حديد ملحوم 6 مم للأرضيات والأسقف",
- "image_url": "https://example.com/wiremesh.jpg"
- }
- ])
-
- # 3. مواد الطرق
- materials_data.extend([
- {
- "id": "MAT-021",
- "name": "بيس كورس",
- "category": "مواد الطرق",
- "subcategory": "طبقات أساس",
- "unit": "م3",
- "price": 70,
- "supplier": "كسارات الرياض",
- "origin": "محلي",
- "lead_time": 2,
- "min_order": 50,
- "description": "مادة بيس كورس لطبقات الأساس في الطرق",
- "image_url": "https://example.com/basecourse.jpg"
- },
- {
- "id": "MAT-022",
- "name": "ساب بيس",
- "category": "مواد الطرق",
- "subcategory": "طبقات أساس",
- "unit": "م3",
- "price": 60,
- "supplier": "كسارات الرياض",
- "origin": "محلي",
- "lead_time": 2,
- "min_order": 50,
- "description": "مادة ساب بيس لطبقات ما تحت الأساس في الطرق",
- "image_url": "https://example.com/subbase.jpg"
- },
- {
- "id": "MAT-023",
- "name": "بيتومين MC-70",
- "category": "مواد الطرق",
- "subcategory": "بيتومين",
- "unit": "لتر",
- "price": 3.5,
- "supplier": "أرامكو",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 10000,
- "description": "بيتومين MC-70 للطبقة التأسيسية",
- "image_url": "https://example.com/bitumen1.jpg"
- },
- {
- "id": "MAT-024",
- "name": "بيتومين RC-250",
- "category": "مواد الطرق",
- "subcategory": "بيتومين",
- "unit": "لتر",
- "price": 3.8,
- "supplier": "أرامكو",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 10000,
- "description": "بيتومين RC-250 للطبقة اللاصقة",
- "image_url": "https://example.com/bitumen2.jpg"
- },
- {
- "id": "MAT-025",
- "name": "خلطة إسفلتية ساخنة",
- "category": "مواد الطرق",
- "subcategory": "إسفلت",
- "unit": "طن",
- "price": 250,
- "supplier": "مصنع الإسفلت المركزي",
- "origin": "محلي",
- "lead_time": 1,
- "min_order": 20,
- "description": "خلطة إسفلتية ساخنة لطبقات الرصف",
- "image_url": "https://example.com/asphalt.jpg"
- },
- {
- "id": "MAT-026",
- "name": "بردورات خرسانية",
- "category": "مواد الطرق",
- "subcategory": "بردورات",
- "unit": "متر طولي",
- "price": 35,
- "supplier": "مصنع الخرسانة الجاهزة",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "بردورات خرسانية مقاس 15×30×50 سم",
- "image_url": "https://example.com/curb.jpg"
- },
- {
- "id": "MAT-027",
- "name": "انترلوك خرساني",
- "category": "مواد الطرق",
- "subcategory": "رصف",
- "unit": "م2",
- "price": 45,
- "supplier": "مصنع الخرسانة الجاهزة",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "انترلوك خرساني سماكة 8 سم للأرصفة",
- "image_url": "https://example.com/interlock.jpg"
- },
- {
- "id": "MAT-028",
- "name": "دهان خطوط طرق أبيض",
- "category": "مواد الطرق",
- "subcategory": "دهانات",
- "unit": "لتر",
- "price": 25,
- "supplier": "شركة الدهانات السعودية",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 200,
- "description": "دهان خطوط طرق أبيض عاكس",
- "image_url": "https://example.com/roadpaint.jpg"
- },
- {
- "id": "MAT-029",
- "name": "دهان خطوط طرق أصفر",
- "category": "مواد الطرق",
- "subcategory": "دهانات",
- "unit": "لتر",
- "price": 25,
- "supplier": "شركة الدهانات السعودية",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 200,
- "description": "دهان خطوط طرق أصفر عاكس",
- "image_url": "https://example.com/roadpaint2.jpg"
- },
- {
- "id": "MAT-030",
- "name": "حبيبات زجاجية عاكسة",
- "category": "مواد الطرق",
- "subcategory": "دهانات",
- "unit": "كجم",
- "price": 15,
- "supplier": "شركة الدهانات السعودية",
- "origin": "مستورد",
- "lead_time": 10,
- "min_order": 100,
- "description": "حبيبات زجاجية عاكسة لدهانات الطرق",
- "image_url": "https://example.com/reflective.jpg"
- }
- ])
-
- # 4. مواد الصرف الصحي
- materials_data.extend([
- {
- "id": "MAT-031",
- "name": "أنابيب PVC قطر 110 مم",
- "category": "مواد الصرف الصحي",
- "subcategory": "أنابيب",
- "unit": "متر طولي",
- "price": 35,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 100,
- "description": "أنابيب PVC قطر 110 مم للصرف الصحي الداخلي",
- "image_url": "https://example.com/pvcpipe.jpg"
- },
- {
- "id": "MAT-032",
- "name": "أنابيب PVC قطر 160 مم",
- "category": "مواد الصرف الصحي",
- "subcategory": "أنابيب",
- "unit": "متر طولي",
- "price": 55,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 100,
- "description": "أنابيب PVC قطر 160 مم للصرف الصحي الخارجي",
- "image_url": "https://example.com/pvcpipe2.jpg"
- },
- {
- "id": "MAT-033",
- "name": "أنابيب UPVC قطر 200 مم",
- "category": "مواد الصرف الصحي",
- "subcategory": "أنابيب",
- "unit": "متر طولي",
- "price": 80,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 50,
- "description": "أنابيب UPVC قطر 200 مم للصرف الصحي الرئيسي",
- "image_url": "https://example.com/upvcpipe.jpg"
- },
- {
- "id": "MAT-034",
- "name": "أنابيب UPVC قطر 315 مم",
- "category": "مواد الصرف الصحي",
- "subcategory": "أنابيب",
- "unit": "متر طولي",
- "price": 150,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 50,
- "description": "أنابيب UPVC قطر 315 مم للصرف الصحي الرئيسي",
- "image_url": "https://example.com/upvcpipe2.jpg"
- },
- {
- "id": "MAT-035",
- "name": "أنابيب خرسانية مسلحة قطر 600 مم",
- "category": "مواد الصرف الصحي",
- "subcategory": "أنابيب",
- "unit": "متر طولي",
- "price": 450,
- "supplier": "مصنع الأنابيب الخرسانية",
- "origin": "محلي",
- "lead_time": 10,
- "min_order": 20,
- "description": "أنابيب خرسانية مسلحة قطر 600 مم للصرف الصحي الرئيسي",
- "image_url": "https://example.com/concretepipe.jpg"
- },
- {
- "id": "MAT-036",
- "name": "أنابيب خرسانية مسلحة قطر 1000 مم",
- "category": "مواد الصرف الصحي",
- "subcategory": "أنابيب",
- "unit": "متر طولي",
- "price": 850,
- "supplier": "مصنع الأنابيب الخرسانية",
- "origin": "محلي",
- "lead_time": 14,
- "min_order": 10,
- "description": "أنابيب خرسانية مسلحة قطر 1000 مم للصرف الصحي الرئيسي",
- "image_url": "https://example.com/concretepipe2.jpg"
- },
- {
- "id": "MAT-037",
- "name": "غرفة تفتيش خرسانية مسبقة الصب 80×80 سم",
- "category": "مواد الصرف الصحي",
- "subcategory": "غرف تفتيش",
- "unit": "قطعة",
- "price": 650,
- "supplier": "مصنع الخرسانة الجاهزة",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 5,
- "description": "غرفة تفتيش خرسانية مسبقة الصب 80×80 سم",
- "image_url": "https://example.com/manhole.jpg"
- },
- {
- "id": "MAT-038",
- "name": "غطاء غرفة تفتيش حديد زهر ثقيل",
- "category": "مواد الصرف الصحي",
- "subcategory": "غرف تفتيش",
- "unit": "قطعة",
- "price": 450,
- "supplier": "مصنع المسبوكات الحديدية",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 10,
- "description": "غطاء غرفة تفتيش حديد زهر ثقيل للطرق",
- "image_url": "https://example.com/manholecoverheavy.jpg"
- },
- {
- "id": "MAT-039",
- "name": "غطاء غرفة تفتيش حديد زهر خفيف",
- "category": "مواد الصرف الصحي",
- "subcategory": "غرف تفتيش",
- "unit": "قطعة",
- "price": 300,
- "supplier": "مصنع المسبوكات الحديدية",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 10,
- "description": "غطاء غرفة تفتيش حديد زهر خفيف للأرصفة",
- "image_url": "https://example.com/manholecoverlight.jpg"
- },
- {
- "id": "MAT-040",
- "name": "مصافي مطر حديد زهر",
- "category": "مواد الصرف الصحي",
- "subcategory": "مصافي",
- "unit": "قطعة",
- "price": 350,
- "supplier": "مصنع المسبوكات الحديدية",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 10,
- "description": "مصافي مطر حديد زهر للطرق",
- "image_url": "https://example.com/gully.jpg"
- }
- ])
-
- # 5. مواد العزل
- materials_data.extend([
- {
- "id": "MAT-041",
- "name": "رولات عزل مائي بيتوميني 4 مم",
- "category": "مواد العزل",
- "subcategory": "عزل مائي",
- "unit": "م2",
- "price": 25,
- "supplier": "شركة العزل السعودية",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 200,
- "description": "رولات عزل مائي بيتوميني 4 مم للأسطح",
- "image_url": "https://example.com/waterproofing.jpg"
- },
- {
- "id": "MAT-042",
- "name": "دهان عزل مائي أكريليك",
- "category": "مواد العزل",
- "subcategory": "عزل مائي",
- "unit": "لتر",
- "price": 18,
- "supplier": "شركة العزل السعودية",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "دهان عزل مائي أكريليك للأسطح",
- "image_url": "https://example.com/waterproofingpaint.jpg"
- },
- {
- "id": "MAT-043",
- "name": "مادة عزل مائي بوليمرية",
- "category": "مواد العزل",
- "subcategory": "عزل مائي",
- "unit": "كجم",
- "price": 35,
- "supplier": "سيكا",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 50,
- "description": "مادة عزل مائي بوليمرية للحمامات والمطابخ",
- "image_url": "https://example.com/polymerwaterproofing.jpg"
- },
- {
- "id": "MAT-044",
- "name": "ألواح بوليسترين للعزل الحراري 5 سم",
- "category": "مواد العزل",
- "subcategory": "عزل حراري",
- "unit": "م2",
- "price": 20,
- "supplier": "شركة العزل السعودية",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 100,
- "description": "ألواح بوليسترين للعزل الحراري سماكة 5 سم",
- "image_url": "https://example.com/polystyrene.jpg"
- },
- {
- "id": "MAT-045",
- "name": "ألواح صوف صخري 5 سم",
- "category": "مواد العزل",
- "subcategory": "عزل حراري",
- "unit": "م2",
- "price": 35,
- "supplier": "شركة العزل السعودية",
- "origin": "مستورد",
- "lead_time": 10,
- "min_order": 100,
- "description": "ألواح صوف صخري للعزل الحراري والصوتي سماكة 5 سم",
- "image_url": "https://example.com/rockwool.jpg"
- },
- {
- "id": "MAT-046",
- "name": "ألواح عزل صوتي 2 سم",
- "category": "مواد العزل",
- "subcategory": "عزل صوتي",
- "unit": "م2",
- "price": 45,
- "supplier": "شركة العزل السعودية",
- "origin": "مستورد",
- "lead_time": 10,
- "min_order": 50,
- "description": "ألواح عزل صوتي سماكة 2 سم",
- "image_url": "https://example.com/acousticinsulation.jpg"
- },
- {
- "id": "MAT-047",
- "name": "شريط عزل مطاطي",
- "category": "مواد العزل",
- "subcategory": "عزل مائي",
- "unit": "متر طولي",
- "price": 15,
- "supplier": "سيكا",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 100,
- "description": "شريط عزل مطاطي للفواصل الإنشائية",
- "image_url": "https://example.com/rubberstrip.jpg"
- },
- {
- "id": "MAT-048",
- "name": "مادة حشو فواصل بوليوريثان",
- "category": "مواد العزل",
- "subcategory": "عزل مائي",
- "unit": "لتر",
- "price": 40,
- "supplier": "سيكا",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 20,
- "description": "مادة حشو فواصل بوليوريثان للفواصل الإنشائية",
- "image_url": "https://example.com/sealant.jpg"
- }
- ])
-
- # 6. مواد التشطيبات
- materials_data.extend([
- {
- "id": "MAT-049",
- "name": "بلاط سيراميك للأرضيات 60×60 سم",
- "category": "مواد التشطيبات",
- "subcategory": "بلاط",
- "unit": "م2",
- "price": 65,
- "supplier": "شركة السيراميك السعودية",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 100,
- "description": "بلاط سيراميك للأرضيات مقاس 60×60 سم",
- "image_url": "https://example.com/ceramictile.jpg"
- },
- {
- "id": "MAT-050",
- "name": "بلاط بورسلين للأرضيات 60×60 سم",
- "category": "مواد التشطيبات",
- "subcategory": "بلاط",
- "unit": "م2",
- "price": 85,
- "supplier": "شركة السيراميك السعودية",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 100,
- "description": "بلاط بورسلين للأرضيات مقاس 60×60 سم",
- "image_url": "https://example.com/porcelaintile.jpg"
- },
- {
- "id": "MAT-051",
- "name": "بلاط سيراميك للجدران 30×60 سم",
- "category": "مواد التشطيبات",
- "subcategory": "بلاط",
- "unit": "م2",
- "price": 60,
- "supplier": "شركة السيراميك السعودية",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 100,
- "description": "بلاط سيراميك للجدران مقاس 30×60 سم",
- "image_url": "https://example.com/walltile.jpg"
- },
- {
- "id": "MAT-052",
- "name": "رخام أبيض كرارة",
- "category": "مواد التشطيبات",
- "subcategory": "رخام",
- "unit": "م2",
- "price": 350,
- "supplier": "شركة الرخام السعودية",
- "origin": "مستورد",
- "lead_time": 14,
- "min_order": 20,
- "description": "رخام أبيض كرارة للأرضيات سماكة 2 سم",
- "image_url": "https://example.com/marble.jpg"
- },
- {
- "id": "MAT-053",
- "name": "جرانيت أسود",
- "category": "مواد التشطيبات",
- "subcategory": "جرانيت",
- "unit": "م2",
- "price": 450,
- "supplier": "شركة الرخام السعودية",
- "origin": "مستورد",
- "lead_time": 14,
- "min_order": 20,
- "description": "جرانيت أسود للأرضيات سماكة 2 سم",
- "image_url": "https://example.com/granite.jpg"
- },
- {
- "id": "MAT-054",
- "name": "دهان أساس للجدران الداخلية",
- "category": "مواد التشطيبات",
- "subcategory": "دهانات",
- "unit": "لتر",
- "price": 15,
- "supplier": "شركة الدهانات السعودية",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "دهان أساس للجدران الداخلية",
- "image_url": "https://example.com/primer.jpg"
- },
- {
- "id": "MAT-055",
- "name": "دهان بلاستيك للجدران الداخلية",
- "category": "مواد التشطيبات",
- "subcategory": "دهانات",
- "unit": "لتر",
- "price": 25,
- "supplier": "شركة الدهانات السعودية",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "دهان بلاستيك للجدران الداخلية",
- "image_url": "https://example.com/paint.jpg"
- },
- {
- "id": "MAT-056",
- "name": "دهان خارجي مقاوم للعوامل الجوية",
- "category": "مواد التشطيبات",
- "subcategory": "دهانات",
- "unit": "لتر",
- "price": 35,
- "supplier": "شركة الدهانات السعودية",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "دهان خارجي مقاوم للعوامل الجوية",
- "image_url": "https://example.com/exteriorpaint.jpg"
- },
- {
- "id": "MAT-057",
- "name": "ألواح جبس 12 مم",
- "category": "مواد التشطيبات",
- "subcategory": "جبس",
- "unit": "م2",
- "price": 18,
- "supplier": "شركة الجبس السعودية",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 100,
- "description": "ألواح جبس سماكة 12 مم للأسقف والجدران",
- "image_url": "https://example.com/gypsum.jpg"
- },
- {
- "id": "MAT-058",
- "name": "ألواح جبس مقاومة للرطوبة 12 مم",
- "category": "مواد التشطيبات",
- "subcategory": "جبس",
- "unit": "م2",
- "price": 25,
- "supplier": "شركة الجبس السعودية",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 100,
- "description": "ألواح جبس مقاومة للرطوبة سماكة 12 مم للحمامات والمطابخ",
- "image_url": "https://example.com/moistureresistantgypsum.jpg"
- }
- ])
-
- # 7. مواد كهربائية
- materials_data.extend([
- {
- "id": "MAT-059",
- "name": "كابل نحاس 1×10 مم2",
- "category": "مواد كهربائية",
- "subcategory": "كابلات",
- "unit": "متر طولي",
- "price": 15,
- "supplier": "الشركة السعودية للكابلات",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 100,
- "description": "كابل نحاس 1×10 مم2 للتمديدات الكهربائية",
- "image_url": "https://example.com/coppercable.jpg"
- },
- {
- "id": "MAT-060",
- "name": "كابل نحاس 1×6 مم2",
- "category": "مواد كهربائية",
- "subcategory": "كابلات",
- "unit": "متر طولي",
- "price": 10,
- "supplier": "الشركة السعودية للكابلات",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 100,
- "description": "كابل نحاس 1×6 مم2 للتمديدات الكهربائية",
- "image_url": "https://example.com/coppercable2.jpg"
- },
- {
- "id": "MAT-061",
- "name": "كابل نحاس 1×2.5 مم2",
- "category": "مواد كهربائية",
- "subcategory": "كابلات",
- "unit": "متر طولي",
- "price": 5,
- "supplier": "الشركة السعودية للكابلات",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 100,
- "description": "كابل نحاس 1×2.5 مم2 للتمديدات الكهربائية",
- "image_url": "https://example.com/coppercable3.jpg"
- },
- {
- "id": "MAT-062",
- "name": "كابل نحاس 1×1.5 مم2",
- "category": "مواد كهربائية",
- "subcategory": "كابلات",
- "unit": "متر طولي",
- "price": 3,
- "supplier": "الشركة السعودية للكابلات",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 100,
- "description": "كابل نحاس 1×1.5 مم2 للتمديدات الكهربائية",
- "image_url": "https://example.com/coppercable4.jpg"
- },
- {
- "id": "MAT-063",
- "name": "أنابيب بلاستيكية للتمديدات الكهربائية 20 مم",
- "category": "مواد كهربائية",
- "subcategory": "أنابيب",
- "unit": "متر طولي",
- "price": 3,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "أنابيب بلاستيكية للتمديدات الكهربائية قطر 20 مم",
- "image_url": "https://example.com/conduit.jpg"
- },
- {
- "id": "MAT-064",
- "name": "أنابيب بلاستيكية للتمديدات الكهربائية 25 مم",
- "category": "مواد كهربائية",
- "subcategory": "أنابيب",
- "unit": "متر طولي",
- "price": 4,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "أنابيب بلاستيكية للتمديدات الكهربائية قطر 25 مم",
- "image_url": "https://example.com/conduit2.jpg"
- },
- {
- "id": "MAT-065",
- "name": "علب كهربائية بلاستيكية",
- "category": "مواد كهربائية",
- "subcategory": "علب",
- "unit": "قطعة",
- "price": 2,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "علب كهربائية بلاستيكية للمفاتيح والبرايز",
- "image_url": "https://example.com/electricalbox.jpg"
- },
- {
- "id": "MAT-066",
- "name": "مفتاح إنارة مفرد",
- "category": "مواد كهربائية",
- "subcategory": "مفاتيح",
- "unit": "قطعة",
- "price": 15,
- "supplier": "شركة الأدوات الكهربائية",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 50,
- "description": "مفتاح إنارة مفرد",
- "image_url": "https://example.com/switch.jpg"
- },
- {
- "id": "MAT-067",
- "name": "مفتاح إنارة ثنائي",
- "category": "مواد كهربائية",
- "subcategory": "مفاتيح",
- "unit": "قطعة",
- "price": 20,
- "supplier": "شركة الأدوات الكهربائية",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 50,
- "description": "مفتاح إنارة ثنائي",
- "image_url": "https://example.com/switch2.jpg"
- },
- {
- "id": "MAT-068",
- "name": "بريزة كهربائية",
- "category": "مواد كهربائية",
- "subcategory": "برايز",
- "unit": "قطعة",
- "price": 15,
- "supplier": "شركة الأدوات الكهربائية",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 50,
- "description": "بريزة كهربائية",
- "image_url": "https://example.com/socket.jpg"
- }
- ])
-
- # 8. مواد ميكانيكية
- materials_data.extend([
- {
- "id": "MAT-069",
- "name": "أنابيب مياه PPR قطر 20 مم",
- "category": "مواد ميكانيكية",
- "subcategory": "أنابيب مياه",
- "unit": "متر طولي",
- "price": 8,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "أنابيب مياه PPR قطر 20 مم",
- "image_url": "https://example.com/pprpipe.jpg"
- },
- {
- "id": "MAT-070",
- "name": "أنابيب مياه PPR قطر 25 مم",
- "category": "مواد ميكانيكية",
- "subcategory": "أنابيب مياه",
- "unit": "متر طولي",
- "price": 12,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "أنابيب مياه PPR قطر 25 مم",
- "image_url": "https://example.com/pprpipe2.jpg"
- },
- {
- "id": "MAT-071",
- "name": "أنابيب مياه PPR قطر 32 مم",
- "category": "مواد ميكانيكية",
- "subcategory": "أنابيب مياه",
- "unit": "متر طولي",
- "price": 18,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "أنابيب مياه PPR قطر 32 مم",
- "image_url": "https://example.com/pprpipe3.jpg"
- },
- {
- "id": "MAT-072",
- "name": "أنابيب حديد مجلفن قطر 2 بوصة",
- "category": "مواد ميكانيكية",
- "subcategory": "أنابيب حريق",
- "unit": "متر طولي",
- "price": 65,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 50,
- "description": "أنابيب حديد مجلفن قطر 2 بوصة لأنظمة مكافحة الحريق",
- "image_url": "https://example.com/galvanizedpipe.jpg"
- },
- {
- "id": "MAT-073",
- "name": "أنابيب حديد مجلفن قطر 4 بوصة",
- "category": "مواد ميكانيكية",
- "subcategory": "أنابيب حريق",
- "unit": "متر طولي",
- "price": 120,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 50,
- "description": "أنابيب حديد مجلفن قطر 4 بوصة لأنظمة مكافحة الحريق",
- "image_url": "https://example.com/galvanizedpipe2.jpg"
- },
- {
- "id": "MAT-074",
- "name": "أنابيب نحاس قطر 1/2 بوصة",
- "category": "مواد ميكانيكية",
- "subcategory": "أنابيب تكييف",
- "unit": "متر طولي",
- "price": 45,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "مستورد",
- "lead_time": 10,
- "min_order": 50,
- "description": "أنابيب نحاس قطر 1/2 بوصة لأنظمة التكييف",
- "image_url": "https://example.com/copperpipe.jpg"
- },
- {
- "id": "MAT-075",
- "name": "أنابيب نحاس قطر 3/4 بوصة",
- "category": "مواد ميكانيكية",
- "subcategory": "أنابيب تكييف",
- "unit": "متر طولي",
- "price": 65,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "مستورد",
- "lead_time": 10,
- "min_order": 50,
- "description": "أنابيب نحاس قطر 3/4 بوصة لأنظمة التكييف",
- "image_url": "https://example.com/copperpipe2.jpg"
- },
- {
- "id": "MAT-076",
- "name": "مضخة مياه 1 حصان",
- "category": "مواد ميكانيكية",
- "subcategory": "مضخات",
- "unit": "قطعة",
- "price": 850,
- "supplier": "شركة المضخات السعودية",
- "origin": "مستورد",
- "lead_time": 10,
- "min_order": 2,
- "description": "مضخة مياه 1 حصان",
- "image_url": "https://example.com/waterpump.jpg"
- },
- {
- "id": "MAT-077",
- "name": "مضخة مياه 2 حصان",
- "category": "مواد ميكانيكية",
- "subcategory": "مضخات",
- "unit": "قطعة",
- "price": 1200,
- "supplier": "شركة المضخات السعودية",
- "origin": "مستورد",
- "lead_time": 10,
- "min_order": 2,
- "description": "مضخة مياه 2 حصان",
- "image_url": "https://example.com/waterpump2.jpg"
- },
- {
- "id": "MAT-078",
- "name": "خزان مياه بلاستيكي 1000 لتر",
- "category": "مواد ميكانيكية",
- "subcategory": "خزانات",
- "unit": "قطعة",
- "price": 650,
- "supplier": "شركة الخزانات السعودية",
- "origin": "محلي",
- "lead_time": 5,
- "min_order": 2,
- "description": "خزان مياه بلاستيكي سعة 1000 لتر",
- "image_url": "https://example.com/watertank.jpg"
- }
- ])
-
- # 9. مواد الري والزراعة
- materials_data.extend([
- {
- "id": "MAT-079",
- "name": "أنابيب بولي إيثيلين قطر 32 مم",
- "category": "مواد الري والزراعة",
- "subcategory": "أنابيب ري",
- "unit": "متر طولي",
- "price": 8,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "أنابيب بولي إيثيلين قطر 32 مم لأنظمة الري",
- "image_url": "https://example.com/pepipe.jpg"
- },
- {
- "id": "MAT-080",
- "name": "أنابيب بولي إيثيلين قطر 63 مم",
- "category": "مواد الري والزراعة",
- "subcategory": "أنابيب ري",
- "unit": "متر طولي",
- "price": 15,
- "supplier": "الشركة السعودية للأنابيب",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 100,
- "description": "أنابيب بولي إيثيلين قطر 63 مم لأنظمة الري",
- "image_url": "https://example.com/pepipe2.jpg"
- },
- {
- "id": "MAT-081",
- "name": "نقاطات ري 4 لتر/ساعة",
- "category": "مواد الري والزراعة",
- "subcategory": "نقاطات",
- "unit": "قطعة",
- "price": 1,
- "supplier": "شركة أنظمة الري",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 1000,
- "description": "نقاطات ري 4 لتر/ساعة",
- "image_url": "https://example.com/dripper.jpg"
- },
- {
- "id": "MAT-082",
- "name": "نقاطات ري 8 لتر/ساعة",
- "category": "مواد الري والزراعة",
- "subcategory": "نقاطات",
- "unit": "قطعة",
- "price": 1.2,
- "supplier": "شركة أنظمة الري",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 1000,
- "description": "نقاطات ري 8 لتر/ساعة",
- "image_url": "https://example.com/dripper2.jpg"
- },
- {
- "id": "MAT-083",
- "name": "رشاشات ري دوارة",
- "category": "مواد الري والزراعة",
- "subcategory": "رشاشات",
- "unit": "قطعة",
- "price": 25,
- "supplier": "شركة أنظمة الري",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 100,
- "description": "رشاشات ري دوارة للمسطحات الخضراء",
- "image_url": "https://example.com/sprinkler.jpg"
- },
- {
- "id": "MAT-084",
- "name": "محابس بلاستيكية قطر 32 مم",
- "category": "مواد الري والزراعة",
- "subcategory": "محابس",
- "unit": "قطعة",
- "price": 15,
- "supplier": "شركة أنظمة الري",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 50,
- "description": "محابس بلاستيكية قطر 32 مم لأنظمة الري",
- "image_url": "https://example.com/valve.jpg"
- },
- {
- "id": "MAT-085",
- "name": "محابس بلاستيكية قطر 63 مم",
- "category": "مواد الري والزراعة",
- "subcategory": "محابس",
- "unit": "قطعة",
- "price": 35,
- "supplier": "شركة أنظمة الري",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 50,
- "description": "محابس بلاستيكية قطر 63 مم لأنظمة الري",
- "image_url": "https://example.com/valve2.jpg"
- },
- {
- "id": "MAT-086",
- "name": "وحدة تحكم ري 6 محطات",
- "category": "مواد الري والزراعة",
- "subcategory": "وحدات تحكم",
- "unit": "قطعة",
- "price": 450,
- "supplier": "شركة أنظمة الري",
- "origin": "مستورد",
- "lead_time": 10,
- "min_order": 5,
- "description": "وحدة تحكم ري 6 محطات",
- "image_url": "https://example.com/controller.jpg"
- },
- {
- "id": "MAT-087",
- "name": "وحدة تحكم ري 12 محطة",
- "category": "مواد الري والزراعة",
- "subcategory": "وحدات تحكم",
- "unit": "قطعة",
- "price": 750,
- "supplier": "شركة أنظمة الري",
- "origin": "مستورد",
- "lead_time": 10,
- "min_order": 5,
- "description": "وحدة تحكم ري 12 محطة",
- "image_url": "https://example.com/controller2.jpg"
- },
- {
- "id": "MAT-088",
- "name": "تربة زراعية",
- "category": "مواد الري والزراعة",
- "subcategory": "تربة",
- "unit": "م3",
- "price": 120,
- "supplier": "شركة المواد الزراعية",
- "origin": "محلي",
- "lead_time": 2,
- "min_order": 10,
- "description": "تربة زراعية للزراعة",
- "image_url": "https://example.com/soil.jpg"
- }
- ])
-
- # 10. مواد متنوعة
- materials_data.extend([
- {
- "id": "MAT-089",
- "name": "أسلاك تربيط حديد التسليح",
- "category": "مواد متنوعة",
- "subcategory": "أسلاك",
- "unit": "كجم",
- "price": 12,
- "supplier": "شركة حديد الراجحي",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 50,
- "description": "أسلاك تربيط حديد التسليح",
- "image_url": "https://example.com/bindingwire.jpg"
- },
- {
- "id": "MAT-090",
- "name": "مسامير متنوعة",
- "category": "مواد متنوعة",
- "subcategory": "مسامير",
- "unit": "كجم",
- "price": 15,
- "supplier": "شركة الأدوات",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 20,
- "description": "مسامير متنوعة للأعمال الإنشائية",
- "image_url": "https://example.com/nails.jpg"
- },
- {
- "id": "MAT-091",
- "name": "شدات معدنية",
- "category": "مواد متنوعة",
- "subcategory": "شدات",
- "unit": "م2",
- "price": 120,
- "supplier": "شركة معدات البناء",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 50,
- "description": "شدات معدنية للأعمال الخرسانية",
- "image_url": "https://example.com/formwork.jpg"
- },
- {
- "id": "MAT-092",
- "name": "سقالات معدنية",
- "category": "مواد متنوعة",
- "subcategory": "سقالات",
- "unit": "م2",
- "price": 85,
- "supplier": "شركة معدات البناء",
- "origin": "محلي",
- "lead_time": 7,
- "min_order": 50,
- "description": "سقالات معدنية للأعمال الإنشائية",
- "image_url": "https://example.com/scaffold.jpg"
- },
- {
- "id": "MAT-093",
- "name": "خشب أبلكاش 18 مم",
- "category": "مواد متنوعة",
- "subcategory": "خشب",
- "unit": "م2",
- "price": 65,
- "supplier": "شركة الأخشاب",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 20,
- "description": "خشب أبلكاش سماكة 18 مم للشدات الخشبية",
- "image_url": "https://example.com/plywood.jpg"
- },
- {
- "id": "MAT-094",
- "name": "عوارض خشبية 10×10 سم",
- "category": "مواد متنوعة",
- "subcategory": "خشب",
- "unit": "متر طولي",
- "price": 25,
- "supplier": "شركة الأخشاب",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 50,
- "description": "عوارض خشبية 10×10 سم للشدات الخشبية",
- "image_url": "https://example.com/timber.jpg"
- },
- {
- "id": "MAT-095",
- "name": "مواد لاصقة للبلاط",
- "category": "مواد متنوعة",
- "subcategory": "مواد لاصقة",
- "unit": "كجم",
- "price": 2.5,
- "supplier": "شركة مواد البناء",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 500,
- "description": "مواد لاصقة للبلاط",
- "image_url": "https://example.com/tileadhesive.jpg"
- },
- {
- "id": "MAT-096",
- "name": "روبة للبلاط",
- "category": "مواد متنوعة",
- "subcategory": "مواد لاصقة",
- "unit": "كجم",
- "price": 3,
- "supplier": "شركة مواد البناء",
- "origin": "محلي",
- "lead_time": 3,
- "min_order": 200,
- "description": "روبة للبلاط",
- "image_url": "https://example.com/grout.jpg"
- },
- {
- "id": "MAT-097",
- "name": "مواد معالجة الخرسانة",
- "category": "مواد متنوعة",
- "subcategory": "مواد معالجة",
- "unit": "لتر",
- "price": 12,
- "supplier": "سيكا",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 100,
- "description": "مواد معالجة الخرسانة",
- "image_url": "https://example.com/curing.jpg"
- },
- {
- "id": "MAT-098",
- "name": "مواد إصلاح الخرسانة",
- "category": "مواد متنوعة",
- "subcategory": "مواد معالجة",
- "unit": "كجم",
- "price": 18,
- "supplier": "سيكا",
- "origin": "مستورد",
- "lead_time": 7,
- "min_order": 50,
- "description": "مواد إصلاح الخرسانة",
- "image_url": "https://example.com/repair.jpg"
- }
- ])
-
- # تخزين البيانات في حالة الجلسة
- st.session_state.materials_catalog = pd.DataFrame(materials_data)
-
- def render(self):
- """عرض واجهة كتالوج المواد"""
-
- st.markdown("## كتالوج المواد")
-
- # إنشاء تبويبات لعرض الكتالوج
- tabs = st.tabs([
- "عرض الكتالوج",
- "إضافة مادة",
- "تحليل الأسعار",
- "استيراد/تصدير"
- ])
-
- with tabs[0]:
- self._render_catalog_view_tab()
-
- with tabs[1]:
- self._render_add_material_tab()
-
- with tabs[2]:
- self._render_price_analysis_tab()
-
- with tabs[3]:
- self._render_import_export_tab()
-
- def _render_catalog_view_tab(self):
- """عرض تبويب عرض الكتالوج"""
-
- st.markdown("### عرض كتالوج المواد")
-
- # استخراج البيانات
- materials_df = st.session_state.materials_catalog
-
- # إنشاء فلاتر للعرض
- col1, col2, col3 = st.columns(3)
-
- with col1:
- # فلتر حسب الفئة
- categories = ["الكل"] + sorted(materials_df["category"].unique().tolist())
- selected_category = st.selectbox("اختر فئة المواد", categories)
-
- with col2:
- # فلتر حسب الفئة الفرعية
- if selected_category != "الكل":
- subcategories = ["الكل"] + sorted(materials_df[materials_df["category"] == selected_category]["subcategory"].unique().tolist())
- else:
- subcategories = ["الكل"] + sorted(materials_df["subcategory"].unique().tolist())
-
- selected_subcategory = st.selectbox("اختر الفئة الفرعية", subcategories)
-
- with col3:
- # فلتر حسب المورد
- suppliers = ["الكل"] + sorted(materials_df["supplier"].unique().tolist())
- selected_supplier = st.selectbox("اختر المورد", suppliers)
-
- # تطبيق الفلاتر
- filtered_df = materials_df.copy()
-
- if selected_category != "الكل":
- filtered_df = filtered_df[filtered_df["category"] == selected_category]
-
- if selected_subcategory != "الكل":
- filtered_df = filtered_df[filtered_df["subcategory"] == selected_subcategory]
-
- if selected_supplier != "الكل":
- filtered_df = filtered_df[filtered_df["supplier"] == selected_supplier]
-
- # عرض البيانات
- if not filtered_df.empty:
- # عرض عدد النتائج
- st.info(f"تم العثور على {len(filtered_df)} مادة")
-
- # عرض المواد في شكل بطاقات
- for i, (_, material) in enumerate(filtered_df.iterrows()):
- col1, col2 = st.columns([1, 2])
-
- with col1:
- # عرض صورة المادة (استخدام صورة افتراضية)
- st.image("https://via.placeholder.com/150", caption=material["name"])
-
- with col2:
- # عرض معلومات المادة
- st.markdown(f"**{material['name']}** (الكود: {material['id']})")
- st.markdown(f"الفئة: {material['category']} - {material['subcategory']}")
- st.markdown(f"الوحدة: {material['unit']} | السعر: {material['price']} ريال/{material['unit']}")
- st.markdown(f"المورد: {material['supplier']} | المنشأ: {material['origin']}")
- st.markdown(f"مدة التوريد: {material['lead_time']} يوم | الحد الأدنى للطلب: {material['min_order']} {material['unit']}")
-
- # إضافة زر لعرض التفاصيل
- if st.button(f"عرض التفاصيل الكاملة", key=f"details_{material['id']}"):
- st.session_state.selected_material = material['id']
- self._show_material_details(material)
-
- st.markdown("---")
- else:
- st.warning("لا توجد مواد تطابق معايير البحث")
-
- def _show_material_details(self, material):
- """عرض تفاصيل المادة"""
-
- st.markdown(f"## تفاصيل المادة: {material['name']}")
-
- col1, col2 = st.columns([1, 2])
-
- with col1:
- # عرض صورة المادة (استخدام صورة افتراضية)
- st.image("https://via.placeholder.com/300", caption=material["name"])
-
- with col2:
- # عرض المعلومات الأساسية
- st.markdown("### المعلومات الأساسية")
- st.markdown(f"**الكود:** {material['id']}")
- st.markdown(f"**الفئة:** {material['category']} - {material['subcategory']}")
- st.markdown(f"**الوحدة:** {material['unit']}")
- st.markdown(f"**السعر:** {material['price']} ريال/{material['unit']}")
- st.markdown(f"**المورد:** {material['supplier']}")
- st.markdown(f"**المنشأ:** {material['origin']}")
- st.markdown(f"**مدة التوريد:** {material['lead_time']} يوم")
- st.markdown(f"**الحد الأدنى للطلب:** {material['min_order']} {material['unit']}")
- st.markdown(f"**الوصف:** {material['description']}")
-
- # إضافة زر للتعديل
- if st.button("تعديل بيانات المادة"):
- st.session_state.edit_material = material['id']
- # هنا يمكن إضافة منطق التعديل
-
- def _render_add_material_tab(self):
- """عرض تبويب إضافة مادة"""
-
- st.markdown("### إضافة مادة جديدة")
-
- # استخراج البيانات
- materials_df = st.session_state.materials_catalog
-
- # إنشاء نموذج إضافة مادة
- with st.form("add_material_form"):
- st.markdown("#### المعلومات الأساسية")
-
- # الصف الأول
- col1, col2 = st.columns(2)
- with col1:
- material_id = st.text_input("كود المادة", value=f"MAT-{len(materials_df) + 1:03d}")
- material_name = st.text_input("اسم المادة", placeholder="مثال: أسمنت بورتلاندي عادي")
-
- with col2:
- # استخراج الفئات والفئات الفرعية الموجودة
- categories = sorted(materials_df["category"].unique().tolist())
- material_category = st.selectbox("فئة المادة", categories)
-
- # استخراج الفئات الفرعية بناءً على الفئة المختارة
- subcategories = sorted(materials_df[materials_df["category"] == material_category]["subcategory"].unique().tolist())
- material_subcategory = st.selectbox("الفئة الفرعية", subcategories)
-
- # الصف الثاني
- col1, col2, col3 = st.columns(3)
- with col1:
- material_unit = st.text_input("وحدة القياس", placeholder="مثال: م3، طن، قطعة")
- with col2:
- material_price = st.number_input("السعر (ريال)", min_value=0.0, step=0.5)
- with col3:
- material_min_order = st.number_input("الحد الأدنى للطلب", min_value=1, step=1)
-
- # الصف الثالث
- col1, col2, col3 = st.columns(3)
- with col1:
- material_supplier = st.text_input("المورد", placeholder="مثال: شركة الإسمنت السعودية")
- with col2:
- material_origin = st.selectbox("المنشأ", ["محلي", "مستورد"])
- with col3:
- material_lead_time = st.number_input("مدة التوريد (يوم)", min_value=1, step=1)
-
- # وصف المادة
- material_description = st.text_area("وصف المادة", placeholder="أدخل وصفاً تفصيلياً للمادة")
-
- # رابط الصورة
- material_image_url = st.text_input("رابط الصورة", placeholder="مثال: https://example.com/image.jpg")
-
- # زر الإضافة
- submit_button = st.form_submit_button("إضافة المادة")
-
- if submit_button:
- # التحقق من البيانات
- if not material_name or not material_category or not material_subcategory or not material_unit:
- st.error("يرجى إدخال المعلومات الأساسية للمادة")
- else:
- # إنشاء مادة جديدة
- new_material = {
- "id": material_id,
- "name": material_name,
- "category": material_category,
- "subcategory": material_subcategory,
- "unit": material_unit,
- "price": material_price,
- "supplier": material_supplier,
- "origin": material_origin,
- "lead_time": material_lead_time,
- "min_order": material_min_order,
- "description": material_description,
- "image_url": material_image_url if material_image_url else "https://via.placeholder.com/150"
- }
-
- # إضافة المادة إلى الكتالوج
- st.session_state.materials_catalog = pd.concat([
- st.session_state.materials_catalog,
- pd.DataFrame([new_material])
- ], ignore_index=True)
-
- st.success(f"تمت إضافة المادة {material_name} بنجاح!")
-
- def _render_price_analysis_tab(self):
- """عرض تبويب تحليل الأسعار"""
-
- st.markdown("### تحليل أسعار المواد")
-
- # استخراج البيانات
- materials_df = st.session_state.materials_catalog
-
- # تحليل متوسط الأسعار حسب الفئة
- st.markdown("#### متوسط الأسعار حسب الفئة")
-
- # حساب متوسط الأسعار لكل فئة
- category_prices = materials_df.groupby("category").agg({
- "price": "mean"
- }).reset_index()
-
- # تغيير أسماء الأعمدة
- category_prices.columns = ["الفئة", "متوسط السعر"]
-
- # عرض الجدول
- st.dataframe(category_prices, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة
- st.markdown("#### مقارنة متوسط الأسعار حسب الفئة")
-
- fig = px.bar(
- category_prices,
- x="الفئة",
- y="متوسط السعر",
- title="متوسط أسعار المواد حسب الفئة",
- color="الفئة",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل توزيع المواد حسب المنشأ
- st.markdown("#### توزيع المواد حسب المنشأ")
-
- # حساب عدد المواد حسب المنشأ
- origin_counts = materials_df["origin"].value_counts().reset_index()
- origin_counts.columns = ["المنشأ", "عدد المواد"]
-
- # إنشاء رسم بياني دائري
- fig = px.pie(
- origin_counts,
- values="عدد المواد",
- names="المنشأ",
- title="توزيع المواد حسب المنشأ",
- color="المنشأ"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل مدة التوريد
- st.markdown("#### تحليل مدة التوريد")
-
- # حساب متوسط مدة التوريد حسب المنشأ
- lead_time_by_origin = materials_df.groupby("origin").agg({
- "lead_time": "mean"
- }).reset_index()
-
- # تغيير أسماء الأعمدة
- lead_time_by_origin.columns = ["المنشأ", "متوسط مدة التوريد (يوم)"]
-
- # عرض الجدول
- st.dataframe(lead_time_by_origin, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة
- fig = px.bar(
- lead_time_by_origin,
- x="المنشأ",
- y="متوسط مدة التوريد (يوم)",
- title="متوسط مدة التوريد حسب المنشأ",
- color="المنشأ",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # حاسبة تكاليف المشروع
- st.markdown("#### حاسبة تكاليف المشروع")
-
- with st.form("project_cost_calculator"):
- st.markdown("أدخل المواد المطلوبة للمشروع")
-
- # اختيار المواد
- selected_materials = st.multiselect(
- "اختر المواد",
- options=materials_df["name"].tolist(),
- format_func=lambda x: f"{x} ({materials_df[materials_df['name'] == x]['id'].iloc[0]})"
- )
-
- # إنشاء حقول إدخال الكميات
- quantities = {}
-
- if selected_materials:
- st.markdown("أدخل الكميات المطلوبة")
-
- for material_name in selected_materials:
- material = materials_df[materials_df["name"] == material_name].iloc[0]
- quantities[material_name] = st.number_input(
- f"{material_name} ({material['unit']})",
- min_value=0.0,
- step=0.5,
- key=f"qty_{material['id']}"
- )
-
- # زر الحساب
- calculate_button = st.form_submit_button("حساب التكاليف")
-
- if calculate_button:
- if not selected_materials:
- st.error("يرجى اختيار مادة واحدة على الأقل")
- else:
- # حساب التكاليف
- project_costs = []
-
- for material_name in selected_materials:
- material = materials_df[materials_df["name"] == material_name].iloc[0]
- quantity = quantities[material_name]
-
- if quantity > 0:
- cost = material["price"] * quantity
-
- project_costs.append({
- "المادة": material_name,
- "الكود": material["id"],
- "الوحدة": material["unit"],
- "الكمية": quantity,
- "سعر الوحدة": material["price"],
- "التكلفة الإجمالية": cost
- })
-
- if project_costs:
- # عرض النتائج
- project_costs_df = pd.DataFrame(project_costs)
- st.dataframe(project_costs_df, use_container_width=True)
-
- # حساب إجمالي التكاليف
- total_cost = project_costs_df["التكلفة الإجمالية"].sum()
- st.metric("إجمالي تكاليف المواد للمشروع", f"{total_cost:,.2f} ريال")
- else:
- st.warning("يرجى إدخال كميات أكبر من صفر")
-
- def _render_import_export_tab(self):
- """عرض تبويب استيراد/تصدير"""
-
- st.markdown("### استيراد وتصدير بيانات المواد")
-
- # استيراد البيانات
- st.markdown("#### استيراد البيانات")
-
- uploaded_file = st.file_uploader("اختر ملف Excel لاستيراد بيانات المواد", type=["xlsx", "xls"])
-
- if uploaded_file is not None:
- try:
- # قراءة الملف
- imported_df = pd.read_excel(uploaded_file)
-
- # عرض البيانات المستوردة
- st.dataframe(imported_df, use_container_width=True)
-
- # زر الاستيراد
- if st.button("استيراد البيانات"):
- # التحقق من وجود الأعمدة المطلوبة
- required_columns = ["id", "name", "category", "subcategory", "unit", "price"]
-
- if all(col in imported_df.columns for col in required_columns):
- # دمج البيانات المستوردة مع البيانات الحالية
- st.session_state.materials_catalog = pd.concat([
- st.session_state.materials_catalog,
- imported_df
- ], ignore_index=True).drop_duplicates(subset=["id"])
-
- st.success(f"تم استيراد {len(imported_df)} مادة بنجاح!")
- else:
- st.error("الملف المستورد لا يحتوي على الأعمدة المطلوبة")
-
- except Exception as e:
- st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}")
-
- # تصدير البيانات
- st.markdown("#### تصدير البيانات")
-
- # اختيار تنسيق التصدير
- export_format = st.radio("اختر تنسيق التصدير", ["Excel", "CSV", "JSON"], horizontal=True)
-
- if st.button("تصدير البيانات"):
- # استخراج البيانات
- materials_df = st.session_state.materials_catalog
-
- # تصدير البيانات حسب التنسيق المختار
- if export_format == "Excel":
- # تصدير إلى Excel
- output = io.BytesIO()
- with pd.ExcelWriter(output, engine="openpyxl") as writer:
- materials_df.to_excel(writer, index=False, sheet_name="Materials")
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف Excel",
- data=output.getvalue(),
- file_name="materials_catalog.xlsx",
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
- )
-
- elif export_format == "CSV":
- # تصدير إلى CSV
- csv_data = materials_df.to_csv(index=False)
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف CSV",
- data=csv_data,
- file_name="materials_catalog.csv",
- mime="text/csv"
- )
-
- else: # JSON
- # تصدير إلى JSON
- json_data = materials_df.to_json(orient="records", force_ascii=False)
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف JSON",
- data=json_data,
- file_name="materials_catalog.json",
- mime="application/json"
- )
-
- def get_material_by_id(self, material_id):
- """الحصول على مادة بواسطة الكود"""
-
- materials_df = st.session_state.materials_catalog
- material = materials_df[materials_df["id"] == material_id]
-
- if not material.empty:
- return material.iloc[0].to_dict()
-
- return None
-
- def get_materials_by_category(self, category):
- """الحصول على المواد حسب الفئة"""
-
- materials_df = st.session_state.materials_catalog
- materials = materials_df[materials_df["category"] == category]
-
- if not materials.empty:
- return materials.to_dict(orient="records")
-
- return []
-
- def calculate_material_cost(self, material_id, quantity):
- """حساب تكلفة المادة بناءً على الكمية"""
-
- material = self.get_material_by_id(material_id)
-
- if material:
- return material["price"] * quantity
-
- return 0
+"""
+كتالوج المواد - وحدة إدارة مواد المقاولات
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import plotly.express as px
+import os
+import json
+from datetime import datetime
+import io
+
+class MaterialsCatalog:
+ """كتالوج المواد"""
+
+ def __init__(self):
+ """تهيئة كتالوج المواد"""
+
+ # تهيئة حالة الجلسة لكتالوج المواد
+ if 'materials_catalog' not in st.session_state:
+ # إنشاء بيانات افتراضية للمواد
+ self._initialize_materials_catalog()
+
+ def _initialize_materials_catalog(self):
+ """تهيئة بيانات كتالوج المواد"""
+
+ # تعريف فئات المواد
+ material_categories = [
+ "مواد الخرسانة",
+ "مواد البناء",
+ "مواد الطرق",
+ "مواد الصرف الصحي",
+ "مواد العزل",
+ "مواد التشطيبات",
+ "مواد كهربائية",
+ "مواد ميكانيكية",
+ "مواد الري والزراعة",
+ "مواد متنوعة"
+ ]
+
+ # إنشاء قائمة المواد
+ materials_data = []
+
+ # 1. مواد الخرسانة
+ materials_data.extend([
+ {
+ "id": "MAT-001",
+ "name": "أسمنت بورتلاندي عادي",
+ "category": "مواد الخرسانة",
+ "subcategory": "أسمنت",
+ "unit": "طن",
+ "price": 600,
+ "supplier": "شركة أسمنت اليمامة",
+ "origin": "محلي",
+ "lead_time": 2,
+ "min_order": 10,
+ "description": "أسمنت بورتلاندي عادي مطابق للمواصفات السعودية",
+ "image_url": "https://example.com/cement.jpg"
+ },
+ {
+ "id": "MAT-002",
+ "name": "أسمنت مقاوم للكبريتات",
+ "category": "مواد الخرسانة",
+ "subcategory": "أسمنت",
+ "unit": "طن",
+ "price": 650,
+ "supplier": "شركة أسمنت ينبع",
+ "origin": "محلي",
+ "lead_time": 2,
+ "min_order": 10,
+ "description": "أسمنت مقاوم للكبريتات للمناطق ذات التربة الكبريتية",
+ "image_url": "https://example.com/srcement.jpg"
+ },
+ {
+ "id": "MAT-003",
+ "name": "رمل خشن",
+ "category": "مواد الخرسانة",
+ "subcategory": "ركام",
+ "unit": "م3",
+ "price": 80,
+ "supplier": "كسارات الرياض",
+ "origin": "محلي",
+ "lead_time": 1,
+ "min_order": 20,
+ "description": "رمل خشن للخرسانة مطابق للمواصفات",
+ "image_url": "https://example.com/sand.jpg"
+ },
+ {
+ "id": "MAT-004",
+ "name": "بحص مقاس 3/4 بوصة",
+ "category": "مواد الخرسانة",
+ "subcategory": "ركام",
+ "unit": "م3",
+ "price": 120,
+ "supplier": "كسارات الرياض",
+ "origin": "محلي",
+ "lead_time": 1,
+ "min_order": 20,
+ "description": "بحص مقاس 3/4 بوصة للخرسانة مطابق للمواصفات",
+ "image_url": "https://example.com/gravel.jpg"
+ },
+ {
+ "id": "MAT-005",
+ "name": "بحص مقاس 3/8 بوصة",
+ "category": "مواد الخرسانة",
+ "subcategory": "ركام",
+ "unit": "م3",
+ "price": 130,
+ "supplier": "كسارات الرياض",
+ "origin": "محلي",
+ "lead_time": 1,
+ "min_order": 20,
+ "description": "بحص مقاس 3/8 بوصة للخرسانة مطابق للمواصفات",
+ "image_url": "https://example.com/gravel2.jpg"
+ },
+ {
+ "id": "MAT-006",
+ "name": "ماء",
+ "category": "مواد الخرسانة",
+ "subcategory": "سوائل",
+ "unit": "م3",
+ "price": 5,
+ "supplier": "متعدد",
+ "origin": "محلي",
+ "lead_time": 1,
+ "min_order": 10,
+ "description": "ماء صالح للخلط في الخرسانة",
+ "image_url": "https://example.com/water.jpg"
+ },
+ {
+ "id": "MAT-007",
+ "name": "إضافة ملدنة للخرسانة",
+ "category": "مواد الخرسانة",
+ "subcategory": "إضافات",
+ "unit": "لتر",
+ "price": 15,
+ "supplier": "سيكا",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 200,
+ "description": "إضافة ملدنة لتحسين قابلية تشغيل الخرسانة",
+ "image_url": "https://example.com/plasticizer.jpg"
+ },
+ {
+ "id": "MAT-008",
+ "name": "إضافة مؤخرة للشك",
+ "category": "مواد الخرسانة",
+ "subcategory": "إضافات",
+ "unit": "لتر",
+ "price": 18,
+ "supplier": "سيكا",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 200,
+ "description": "إضافة مؤخرة للشك للصب في الأجواء الحارة",
+ "image_url": "https://example.com/retarder.jpg"
+ },
+ {
+ "id": "MAT-009",
+ "name": "إضافة معجلة للشك",
+ "category": "مواد الخرسانة",
+ "subcategory": "إضافات",
+ "unit": "لتر",
+ "price": 20,
+ "supplier": "سيكا",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 200,
+ "description": "إضافة معجلة للشك للصب في الأجواء الباردة",
+ "image_url": "https://example.com/accelerator.jpg"
+ },
+ {
+ "id": "MAT-010",
+ "name": "ألياف بوليبروبلين",
+ "category": "مواد الخرسانة",
+ "subcategory": "إضافات",
+ "unit": "كجم",
+ "price": 35,
+ "supplier": "سيكا",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 100,
+ "description": "ألياف بوليبروبلين لتقليل التشققات في الخرسانة",
+ "image_url": "https://example.com/fibers.jpg"
+ }
+ ])
+
+ # 2. مواد البناء
+ materials_data.extend([
+ {
+ "id": "MAT-011",
+ "name": "طابوق أسمنتي مقاس 20×20×40 سم",
+ "category": "مواد البناء",
+ "subcategory": "طابوق",
+ "unit": "قطعة",
+ "price": 3.5,
+ "supplier": "مصنع الرياض للطابوق",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 1000,
+ "description": "طابوق أسمنتي مقاس 20×20×40 سم للجدران الخارجية",
+ "image_url": "https://example.com/block.jpg"
+ },
+ {
+ "id": "MAT-012",
+ "name": "طابوق أسمنتي مقاس 15×20×40 سم",
+ "category": "مواد البناء",
+ "subcategory": "طابوق",
+ "unit": "قطعة",
+ "price": 3,
+ "supplier": "مصنع الرياض للطابوق",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 1000,
+ "description": "طابوق أسمنتي مقاس 15×20×40 سم للجدران الداخلية",
+ "image_url": "https://example.com/block2.jpg"
+ },
+ {
+ "id": "MAT-013",
+ "name": "طابوق أسمنتي مقاس 10×20×40 سم",
+ "category": "مواد البناء",
+ "subcategory": "طابوق",
+ "unit": "قطعة",
+ "price": 2.5,
+ "supplier": "مصنع الرياض للطابوق",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 1000,
+ "description": "طابوق أسمنتي مقاس 10×20×40 سم للجدران الداخلية",
+ "image_url": "https://example.com/block3.jpg"
+ },
+ {
+ "id": "MAT-014",
+ "name": "حديد تسليح قطر 8 مم",
+ "category": "مواد البناء",
+ "subcategory": "حديد تسليح",
+ "unit": "طن",
+ "price": 3200,
+ "supplier": "شركة حديد الراجحي",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 5,
+ "description": "حديد تسليح قطر 8 مم مطابق للمواصفات السعودية",
+ "image_url": "https://example.com/rebar8.jpg"
+ },
+ {
+ "id": "MAT-015",
+ "name": "حديد تسليح قطر 10 مم",
+ "category": "مواد البناء",
+ "subcategory": "حديد تسليح",
+ "unit": "طن",
+ "price": 3150,
+ "supplier": "شركة حديد الراجحي",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 5,
+ "description": "حديد تسليح قطر 10 مم مطابق للمواصفات السعودية",
+ "image_url": "https://example.com/rebar10.jpg"
+ },
+ {
+ "id": "MAT-016",
+ "name": "حديد تسليح قطر 12 مم",
+ "category": "مواد البناء",
+ "subcategory": "حديد تسليح",
+ "unit": "طن",
+ "price": 3100,
+ "supplier": "شركة حديد الراجحي",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 5,
+ "description": "حديد تسليح قطر 12 مم مطابق للمواصفات السعودية",
+ "image_url": "https://example.com/rebar12.jpg"
+ },
+ {
+ "id": "MAT-017",
+ "name": "حديد تسليح قطر 16 مم",
+ "category": "مواد البناء",
+ "subcategory": "حديد تسليح",
+ "unit": "طن",
+ "price": 3050,
+ "supplier": "شركة حديد الراجحي",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 5,
+ "description": "حديد تسليح قطر 16 مم مطابق للمواصفات السعودية",
+ "image_url": "https://example.com/rebar16.jpg"
+ },
+ {
+ "id": "MAT-018",
+ "name": "حديد تسليح قطر 20 مم",
+ "category": "مواد البناء",
+ "subcategory": "حديد تسليح",
+ "unit": "طن",
+ "price": 3000,
+ "supplier": "شركة حديد الراجحي",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 5,
+ "description": "حديد تسليح قطر 20 مم مطابق للمواصفات السعودية",
+ "image_url": "https://example.com/rebar20.jpg"
+ },
+ {
+ "id": "MAT-019",
+ "name": "حديد تسليح قطر 25 مم",
+ "category": "مواد البناء",
+ "subcategory": "حديد تسليح",
+ "unit": "طن",
+ "price": 2950,
+ "supplier": "شركة حديد الراجحي",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 5,
+ "description": "حديد تسليح قطر 25 مم مطابق للمواصفات السعودية",
+ "image_url": "https://example.com/rebar25.jpg"
+ },
+ {
+ "id": "MAT-020",
+ "name": "شبك حديد ملحوم 6 مم",
+ "category": "مواد البناء",
+ "subcategory": "حديد تسليح",
+ "unit": "م2",
+ "price": 25,
+ "supplier": "شركة حديد الراجحي",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 100,
+ "description": "شبك حديد ملحوم 6 مم للأرضيات والأسقف",
+ "image_url": "https://example.com/wiremesh.jpg"
+ }
+ ])
+
+ # 3. مواد الطرق
+ materials_data.extend([
+ {
+ "id": "MAT-021",
+ "name": "بيس كورس",
+ "category": "مواد الطرق",
+ "subcategory": "طبقات أساس",
+ "unit": "م3",
+ "price": 70,
+ "supplier": "كسارات الرياض",
+ "origin": "محلي",
+ "lead_time": 2,
+ "min_order": 50,
+ "description": "مادة بيس كورس لطبقات الأساس في الطرق",
+ "image_url": "https://example.com/basecourse.jpg"
+ },
+ {
+ "id": "MAT-022",
+ "name": "ساب بيس",
+ "category": "مواد الطرق",
+ "subcategory": "طبقات أساس",
+ "unit": "م3",
+ "price": 60,
+ "supplier": "كسارات الرياض",
+ "origin": "محلي",
+ "lead_time": 2,
+ "min_order": 50,
+ "description": "مادة ساب بيس لطبقات ما تحت الأساس في الطرق",
+ "image_url": "https://example.com/subbase.jpg"
+ },
+ {
+ "id": "MAT-023",
+ "name": "بيتومين MC-70",
+ "category": "مواد الطرق",
+ "subcategory": "بيتومين",
+ "unit": "لتر",
+ "price": 3.5,
+ "supplier": "أرامكو",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 10000,
+ "description": "بيتومين MC-70 للطبقة التأسيسية",
+ "image_url": "https://example.com/bitumen1.jpg"
+ },
+ {
+ "id": "MAT-024",
+ "name": "بيتومين RC-250",
+ "category": "مواد الطرق",
+ "subcategory": "بيتومين",
+ "unit": "لتر",
+ "price": 3.8,
+ "supplier": "أرامكو",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 10000,
+ "description": "بيتومين RC-250 للطبقة اللاصقة",
+ "image_url": "https://example.com/bitumen2.jpg"
+ },
+ {
+ "id": "MAT-025",
+ "name": "خلطة إسفلتية ساخنة",
+ "category": "مواد الطرق",
+ "subcategory": "إسفلت",
+ "unit": "طن",
+ "price": 250,
+ "supplier": "مصنع الإسفلت المركزي",
+ "origin": "محلي",
+ "lead_time": 1,
+ "min_order": 20,
+ "description": "خلطة إسفلتية ساخنة لطبقات الرصف",
+ "image_url": "https://example.com/asphalt.jpg"
+ },
+ {
+ "id": "MAT-026",
+ "name": "بردورات خرسانية",
+ "category": "مواد الطرق",
+ "subcategory": "بردورات",
+ "unit": "متر طولي",
+ "price": 35,
+ "supplier": "مصنع الخرسانة الجاهزة",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "بردورات خرسانية مقاس 15×30×50 سم",
+ "image_url": "https://example.com/curb.jpg"
+ },
+ {
+ "id": "MAT-027",
+ "name": "انترلوك خرساني",
+ "category": "مواد الطرق",
+ "subcategory": "رصف",
+ "unit": "م2",
+ "price": 45,
+ "supplier": "مصنع الخرسانة الجاهزة",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "انترلوك خرساني سماكة 8 سم للأرصفة",
+ "image_url": "https://example.com/interlock.jpg"
+ },
+ {
+ "id": "MAT-028",
+ "name": "دهان خطوط طرق أبيض",
+ "category": "مواد الطرق",
+ "subcategory": "دهانات",
+ "unit": "لتر",
+ "price": 25,
+ "supplier": "شركة الدهانات السعودية",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 200,
+ "description": "دهان خطوط طرق أبيض عاكس",
+ "image_url": "https://example.com/roadpaint.jpg"
+ },
+ {
+ "id": "MAT-029",
+ "name": "دهان خطوط طرق أصفر",
+ "category": "مواد الطرق",
+ "subcategory": "دهانات",
+ "unit": "لتر",
+ "price": 25,
+ "supplier": "شركة الدهانات السعودية",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 200,
+ "description": "دهان خطوط طرق أصفر عاكس",
+ "image_url": "https://example.com/roadpaint2.jpg"
+ },
+ {
+ "id": "MAT-030",
+ "name": "حبيبات زجاجية عاكسة",
+ "category": "مواد الطرق",
+ "subcategory": "دهانات",
+ "unit": "كجم",
+ "price": 15,
+ "supplier": "شركة الدهانات السعودية",
+ "origin": "مستورد",
+ "lead_time": 10,
+ "min_order": 100,
+ "description": "حبيبات زجاجية عاكسة لدهانات الطرق",
+ "image_url": "https://example.com/reflective.jpg"
+ }
+ ])
+
+ # 4. مواد الصرف الصحي
+ materials_data.extend([
+ {
+ "id": "MAT-031",
+ "name": "أنابيب PVC قطر 110 مم",
+ "category": "مواد الصرف الصحي",
+ "subcategory": "أنابيب",
+ "unit": "متر طولي",
+ "price": 35,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 100,
+ "description": "أنابيب PVC قطر 110 مم للصرف الصحي الداخلي",
+ "image_url": "https://example.com/pvcpipe.jpg"
+ },
+ {
+ "id": "MAT-032",
+ "name": "أنابيب PVC قطر 160 مم",
+ "category": "مواد الصرف الصحي",
+ "subcategory": "أنابيب",
+ "unit": "متر طولي",
+ "price": 55,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 100,
+ "description": "أنابيب PVC قطر 160 مم للصرف الصحي الخارجي",
+ "image_url": "https://example.com/pvcpipe2.jpg"
+ },
+ {
+ "id": "MAT-033",
+ "name": "أنابيب UPVC قطر 200 مم",
+ "category": "مواد الصرف الصحي",
+ "subcategory": "أنابيب",
+ "unit": "متر طولي",
+ "price": 80,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 50,
+ "description": "أنابيب UPVC قطر 200 مم للصرف الصحي الرئيسي",
+ "image_url": "https://example.com/upvcpipe.jpg"
+ },
+ {
+ "id": "MAT-034",
+ "name": "أنابيب UPVC قطر 315 مم",
+ "category": "مواد الصرف الصحي",
+ "subcategory": "أنابيب",
+ "unit": "متر طولي",
+ "price": 150,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 50,
+ "description": "أنابيب UPVC قطر 315 مم للصرف الصحي الرئيسي",
+ "image_url": "https://example.com/upvcpipe2.jpg"
+ },
+ {
+ "id": "MAT-035",
+ "name": "أنابيب خرسانية مسلحة قطر 600 مم",
+ "category": "مواد الصرف الصحي",
+ "subcategory": "أنابيب",
+ "unit": "متر طولي",
+ "price": 450,
+ "supplier": "مصنع الأنابيب الخرسانية",
+ "origin": "محلي",
+ "lead_time": 10,
+ "min_order": 20,
+ "description": "أنابيب خرسانية مسلحة قطر 600 مم للصرف الصحي الرئيسي",
+ "image_url": "https://example.com/concretepipe.jpg"
+ },
+ {
+ "id": "MAT-036",
+ "name": "أنابيب خرسانية مسلحة قطر 1000 مم",
+ "category": "مواد الصرف الصحي",
+ "subcategory": "أنابيب",
+ "unit": "متر طولي",
+ "price": 850,
+ "supplier": "مصنع الأنابيب الخرسانية",
+ "origin": "محلي",
+ "lead_time": 14,
+ "min_order": 10,
+ "description": "أنابيب خرسانية مسلحة قطر 1000 مم للصرف الصحي الرئيسي",
+ "image_url": "https://example.com/concretepipe2.jpg"
+ },
+ {
+ "id": "MAT-037",
+ "name": "غرفة تفتيش خرسانية مسبقة الصب 80×80 سم",
+ "category": "مواد الصرف الصحي",
+ "subcategory": "غرف تفتيش",
+ "unit": "قطعة",
+ "price": 650,
+ "supplier": "مصنع الخرسانة الجاهزة",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 5,
+ "description": "غرفة تفتيش خرسانية مسبقة الصب 80×80 سم",
+ "image_url": "https://example.com/manhole.jpg"
+ },
+ {
+ "id": "MAT-038",
+ "name": "غطاء غرفة تفتيش حديد زهر ثقيل",
+ "category": "مواد الصرف الصحي",
+ "subcategory": "غرف تفتيش",
+ "unit": "قطعة",
+ "price": 450,
+ "supplier": "مصنع المسبوكات الحديدية",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 10,
+ "description": "غطاء غرفة تفتيش حديد زهر ثقيل للطرق",
+ "image_url": "https://example.com/manholecoverheavy.jpg"
+ },
+ {
+ "id": "MAT-039",
+ "name": "غطاء غرفة تفتيش حديد زهر خفيف",
+ "category": "مواد الصرف الصحي",
+ "subcategory": "غرف تفتيش",
+ "unit": "قطعة",
+ "price": 300,
+ "supplier": "مصنع المسبوكات الحديدية",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 10,
+ "description": "غطاء غرفة تفتيش حديد زهر خفيف للأرصفة",
+ "image_url": "https://example.com/manholecoverlight.jpg"
+ },
+ {
+ "id": "MAT-040",
+ "name": "مصافي مطر حديد زهر",
+ "category": "مواد الصرف الصحي",
+ "subcategory": "مصافي",
+ "unit": "قطعة",
+ "price": 350,
+ "supplier": "مصنع المسبوكات الحديدية",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 10,
+ "description": "مصافي مطر حديد زهر للطرق",
+ "image_url": "https://example.com/gully.jpg"
+ }
+ ])
+
+ # 5. مواد العزل
+ materials_data.extend([
+ {
+ "id": "MAT-041",
+ "name": "رولات عزل مائي بيتوميني 4 مم",
+ "category": "مواد العزل",
+ "subcategory": "عزل مائي",
+ "unit": "م2",
+ "price": 25,
+ "supplier": "شركة العزل السعودية",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 200,
+ "description": "رولات عزل مائي بيتوميني 4 مم للأسطح",
+ "image_url": "https://example.com/waterproofing.jpg"
+ },
+ {
+ "id": "MAT-042",
+ "name": "دهان عزل مائي أكريليك",
+ "category": "مواد العزل",
+ "subcategory": "عزل مائي",
+ "unit": "لتر",
+ "price": 18,
+ "supplier": "شركة العزل السعودية",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "دهان عزل مائي أكريليك للأسطح",
+ "image_url": "https://example.com/waterproofingpaint.jpg"
+ },
+ {
+ "id": "MAT-043",
+ "name": "مادة عزل مائي بوليمرية",
+ "category": "مواد العزل",
+ "subcategory": "عزل مائي",
+ "unit": "كجم",
+ "price": 35,
+ "supplier": "سيكا",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 50,
+ "description": "مادة عزل مائي بوليمرية للحمامات والمطابخ",
+ "image_url": "https://example.com/polymerwaterproofing.jpg"
+ },
+ {
+ "id": "MAT-044",
+ "name": "ألواح بوليسترين للعزل الحراري 5 سم",
+ "category": "مواد العزل",
+ "subcategory": "عزل حراري",
+ "unit": "م2",
+ "price": 20,
+ "supplier": "شركة العزل السعودية",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 100,
+ "description": "ألواح بوليسترين للعزل الحراري سماكة 5 سم",
+ "image_url": "https://example.com/polystyrene.jpg"
+ },
+ {
+ "id": "MAT-045",
+ "name": "ألواح صوف صخري 5 سم",
+ "category": "مواد العزل",
+ "subcategory": "عزل حراري",
+ "unit": "م2",
+ "price": 35,
+ "supplier": "شركة العزل السعودية",
+ "origin": "مستورد",
+ "lead_time": 10,
+ "min_order": 100,
+ "description": "ألواح صوف صخري للعزل الحراري والصوتي سماكة 5 سم",
+ "image_url": "https://example.com/rockwool.jpg"
+ },
+ {
+ "id": "MAT-046",
+ "name": "ألواح عزل صوتي 2 سم",
+ "category": "مواد العزل",
+ "subcategory": "عزل صوتي",
+ "unit": "م2",
+ "price": 45,
+ "supplier": "شركة العزل السعودية",
+ "origin": "مستورد",
+ "lead_time": 10,
+ "min_order": 50,
+ "description": "ألواح عزل صوتي سماكة 2 سم",
+ "image_url": "https://example.com/acousticinsulation.jpg"
+ },
+ {
+ "id": "MAT-047",
+ "name": "شريط عزل مطاطي",
+ "category": "مواد العزل",
+ "subcategory": "عزل مائي",
+ "unit": "متر طولي",
+ "price": 15,
+ "supplier": "سيكا",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 100,
+ "description": "شريط عزل مطاطي للفواصل الإنشائية",
+ "image_url": "https://example.com/rubberstrip.jpg"
+ },
+ {
+ "id": "MAT-048",
+ "name": "مادة حشو فواصل بوليوريثان",
+ "category": "مواد العزل",
+ "subcategory": "عزل مائي",
+ "unit": "لتر",
+ "price": 40,
+ "supplier": "سيكا",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 20,
+ "description": "مادة حشو فواصل بوليوريثان للفواصل الإنشائية",
+ "image_url": "https://example.com/sealant.jpg"
+ }
+ ])
+
+ # 6. مواد التشطيبات
+ materials_data.extend([
+ {
+ "id": "MAT-049",
+ "name": "بلاط سيراميك للأرضيات 60×60 سم",
+ "category": "مواد التشطيبات",
+ "subcategory": "بلاط",
+ "unit": "م2",
+ "price": 65,
+ "supplier": "شركة السيراميك السعودية",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 100,
+ "description": "بلاط سيراميك للأرضيات مقاس 60×60 سم",
+ "image_url": "https://example.com/ceramictile.jpg"
+ },
+ {
+ "id": "MAT-050",
+ "name": "بلاط بورسلين للأرضيات 60×60 سم",
+ "category": "مواد التشطيبات",
+ "subcategory": "بلاط",
+ "unit": "م2",
+ "price": 85,
+ "supplier": "شركة السيراميك السعودية",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 100,
+ "description": "بلاط بورسلين للأرضيات مقاس 60×60 سم",
+ "image_url": "https://example.com/porcelaintile.jpg"
+ },
+ {
+ "id": "MAT-051",
+ "name": "بلاط سيراميك للجدران 30×60 سم",
+ "category": "مواد التشطيبات",
+ "subcategory": "بلاط",
+ "unit": "م2",
+ "price": 60,
+ "supplier": "شركة السيراميك السعودية",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 100,
+ "description": "بلاط سيراميك للجدران مقاس 30×60 سم",
+ "image_url": "https://example.com/walltile.jpg"
+ },
+ {
+ "id": "MAT-052",
+ "name": "رخام أبيض كرارة",
+ "category": "مواد التشطيبات",
+ "subcategory": "رخام",
+ "unit": "م2",
+ "price": 350,
+ "supplier": "شركة الرخام السعودية",
+ "origin": "مستورد",
+ "lead_time": 14,
+ "min_order": 20,
+ "description": "رخام أبيض كرارة للأرضيات سماكة 2 سم",
+ "image_url": "https://example.com/marble.jpg"
+ },
+ {
+ "id": "MAT-053",
+ "name": "جرانيت أسود",
+ "category": "مواد التشطيبات",
+ "subcategory": "جرانيت",
+ "unit": "م2",
+ "price": 450,
+ "supplier": "شركة الرخام السعودية",
+ "origin": "مستورد",
+ "lead_time": 14,
+ "min_order": 20,
+ "description": "جرانيت أسود للأرضيات سماكة 2 سم",
+ "image_url": "https://example.com/granite.jpg"
+ },
+ {
+ "id": "MAT-054",
+ "name": "دهان أساس للجدران الداخلية",
+ "category": "مواد التشطيبات",
+ "subcategory": "دهانات",
+ "unit": "لتر",
+ "price": 15,
+ "supplier": "شركة الدهانات السعودية",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "دهان أساس للجدران الداخلية",
+ "image_url": "https://example.com/primer.jpg"
+ },
+ {
+ "id": "MAT-055",
+ "name": "دهان بلاستيك للجدران الداخلية",
+ "category": "مواد التشطيبات",
+ "subcategory": "دهانات",
+ "unit": "لتر",
+ "price": 25,
+ "supplier": "شركة الدهانات السعودية",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "دهان بلاستيك للجدران الداخلية",
+ "image_url": "https://example.com/paint.jpg"
+ },
+ {
+ "id": "MAT-056",
+ "name": "دهان خارجي مقاوم للعوامل الجوية",
+ "category": "مواد التشطيبات",
+ "subcategory": "دهانات",
+ "unit": "لتر",
+ "price": 35,
+ "supplier": "شركة الدهانات السعودية",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "دهان خارجي مقاوم للعوامل الجوية",
+ "image_url": "https://example.com/exteriorpaint.jpg"
+ },
+ {
+ "id": "MAT-057",
+ "name": "ألواح جبس 12 مم",
+ "category": "مواد التشطيبات",
+ "subcategory": "جبس",
+ "unit": "م2",
+ "price": 18,
+ "supplier": "شركة الجبس السعودية",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 100,
+ "description": "ألواح جبس سماكة 12 مم للأسقف والجدران",
+ "image_url": "https://example.com/gypsum.jpg"
+ },
+ {
+ "id": "MAT-058",
+ "name": "ألواح جبس مقاومة للرطوبة 12 مم",
+ "category": "مواد التشطيبات",
+ "subcategory": "جبس",
+ "unit": "م2",
+ "price": 25,
+ "supplier": "شركة الجبس السعودية",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 100,
+ "description": "ألواح جبس مقاومة للرطوبة سماكة 12 مم للحمامات والمطابخ",
+ "image_url": "https://example.com/moistureresistantgypsum.jpg"
+ }
+ ])
+
+ # 7. مواد كهربائية
+ materials_data.extend([
+ {
+ "id": "MAT-059",
+ "name": "كابل نحاس 1×10 مم2",
+ "category": "مواد كهربائية",
+ "subcategory": "كابلات",
+ "unit": "متر طولي",
+ "price": 15,
+ "supplier": "الشركة السعودية للكابلات",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 100,
+ "description": "كابل نحاس 1×10 مم2 للتمديدات الكهربائية",
+ "image_url": "https://example.com/coppercable.jpg"
+ },
+ {
+ "id": "MAT-060",
+ "name": "كابل نحاس 1×6 مم2",
+ "category": "مواد كهربائية",
+ "subcategory": "كابلات",
+ "unit": "متر طولي",
+ "price": 10,
+ "supplier": "الشركة السعودية للكابلات",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 100,
+ "description": "كابل نحاس 1×6 مم2 للتمديدات الكهربائية",
+ "image_url": "https://example.com/coppercable2.jpg"
+ },
+ {
+ "id": "MAT-061",
+ "name": "كابل نحاس 1×2.5 مم2",
+ "category": "مواد كهربائية",
+ "subcategory": "كابلات",
+ "unit": "متر طولي",
+ "price": 5,
+ "supplier": "الشركة السعودية للكابلات",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 100,
+ "description": "كابل نحاس 1×2.5 مم2 للتمديدات الكهربائية",
+ "image_url": "https://example.com/coppercable3.jpg"
+ },
+ {
+ "id": "MAT-062",
+ "name": "كابل نحاس 1×1.5 مم2",
+ "category": "مواد كهربائية",
+ "subcategory": "كابلات",
+ "unit": "متر طولي",
+ "price": 3,
+ "supplier": "الشركة السعودية للكابلات",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 100,
+ "description": "كابل نحاس 1×1.5 مم2 للتمديدات الكهربائية",
+ "image_url": "https://example.com/coppercable4.jpg"
+ },
+ {
+ "id": "MAT-063",
+ "name": "أنابيب بلاستيكية للتمديدات الكهربائية 20 مم",
+ "category": "مواد كهربائية",
+ "subcategory": "أنابيب",
+ "unit": "متر طولي",
+ "price": 3,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "أنابيب بلاستيكية للتمديدات الكهربائية قطر 20 مم",
+ "image_url": "https://example.com/conduit.jpg"
+ },
+ {
+ "id": "MAT-064",
+ "name": "أنابيب بلاستيكية للتمديدات الكهربائية 25 مم",
+ "category": "مواد كهربائية",
+ "subcategory": "أنابيب",
+ "unit": "متر طولي",
+ "price": 4,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "أنابيب بلاستيكية للتمديدات الكهربائية قطر 25 مم",
+ "image_url": "https://example.com/conduit2.jpg"
+ },
+ {
+ "id": "MAT-065",
+ "name": "علب كهربائية بلاستيكية",
+ "category": "مواد كهربائية",
+ "subcategory": "علب",
+ "unit": "قطعة",
+ "price": 2,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "علب كهربائية بلاستيكية للمفاتيح والبرايز",
+ "image_url": "https://example.com/electricalbox.jpg"
+ },
+ {
+ "id": "MAT-066",
+ "name": "مفتاح إنارة مفرد",
+ "category": "مواد كهربائية",
+ "subcategory": "مفاتيح",
+ "unit": "قطعة",
+ "price": 15,
+ "supplier": "شركة الأدوات الكهربائية",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 50,
+ "description": "مفتاح إنارة مفرد",
+ "image_url": "https://example.com/switch.jpg"
+ },
+ {
+ "id": "MAT-067",
+ "name": "مفتاح إنارة ثنائي",
+ "category": "مواد كهربائية",
+ "subcategory": "مفاتيح",
+ "unit": "قطعة",
+ "price": 20,
+ "supplier": "شركة الأدوات الكهربائية",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 50,
+ "description": "مفتاح إنارة ثنائي",
+ "image_url": "https://example.com/switch2.jpg"
+ },
+ {
+ "id": "MAT-068",
+ "name": "بريزة كهربائية",
+ "category": "مواد كهربائية",
+ "subcategory": "برايز",
+ "unit": "قطعة",
+ "price": 15,
+ "supplier": "شركة الأدوات الكهربائية",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 50,
+ "description": "بريزة كهربائية",
+ "image_url": "https://example.com/socket.jpg"
+ }
+ ])
+
+ # 8. مواد ميكانيكية
+ materials_data.extend([
+ {
+ "id": "MAT-069",
+ "name": "أنابيب مياه PPR قطر 20 مم",
+ "category": "مواد ميكانيكية",
+ "subcategory": "أنابيب مياه",
+ "unit": "متر طولي",
+ "price": 8,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "أنابيب مياه PPR قطر 20 مم",
+ "image_url": "https://example.com/pprpipe.jpg"
+ },
+ {
+ "id": "MAT-070",
+ "name": "أنابيب مياه PPR قطر 25 مم",
+ "category": "مواد ميكانيكية",
+ "subcategory": "أنابيب مياه",
+ "unit": "متر طولي",
+ "price": 12,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "أنابيب مياه PPR قطر 25 مم",
+ "image_url": "https://example.com/pprpipe2.jpg"
+ },
+ {
+ "id": "MAT-071",
+ "name": "أنابيب مياه PPR قطر 32 مم",
+ "category": "مواد ميكانيكية",
+ "subcategory": "أنابيب مياه",
+ "unit": "متر طولي",
+ "price": 18,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "أنابيب مياه PPR قطر 32 مم",
+ "image_url": "https://example.com/pprpipe3.jpg"
+ },
+ {
+ "id": "MAT-072",
+ "name": "أنابيب حديد مجلفن قطر 2 بوصة",
+ "category": "مواد ميكانيكية",
+ "subcategory": "أنابيب حريق",
+ "unit": "متر طولي",
+ "price": 65,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 50,
+ "description": "أنابيب حديد مجلفن قطر 2 بوصة لأنظمة مكافحة الحريق",
+ "image_url": "https://example.com/galvanizedpipe.jpg"
+ },
+ {
+ "id": "MAT-073",
+ "name": "أنابيب حديد مجلفن قطر 4 بوصة",
+ "category": "مواد ميكانيكية",
+ "subcategory": "أنابيب حريق",
+ "unit": "متر طولي",
+ "price": 120,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 50,
+ "description": "أنابيب حديد مجلفن قطر 4 بوصة لأنظمة مكافحة الحريق",
+ "image_url": "https://example.com/galvanizedpipe2.jpg"
+ },
+ {
+ "id": "MAT-074",
+ "name": "أنابيب نحاس قطر 1/2 بوصة",
+ "category": "مواد ميكانيكية",
+ "subcategory": "أنابيب تكييف",
+ "unit": "متر طولي",
+ "price": 45,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "مستورد",
+ "lead_time": 10,
+ "min_order": 50,
+ "description": "أنابيب نحاس قطر 1/2 بوصة لأنظمة التكييف",
+ "image_url": "https://example.com/copperpipe.jpg"
+ },
+ {
+ "id": "MAT-075",
+ "name": "أنابيب نحاس قطر 3/4 بوصة",
+ "category": "مواد ميكانيكية",
+ "subcategory": "أنابيب تكييف",
+ "unit": "متر طولي",
+ "price": 65,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "مستورد",
+ "lead_time": 10,
+ "min_order": 50,
+ "description": "أنابيب نحاس قطر 3/4 بوصة لأنظمة التكييف",
+ "image_url": "https://example.com/copperpipe2.jpg"
+ },
+ {
+ "id": "MAT-076",
+ "name": "مضخة مياه 1 حصان",
+ "category": "مواد ميكانيكية",
+ "subcategory": "مضخات",
+ "unit": "قطعة",
+ "price": 850,
+ "supplier": "شركة المضخات السعودية",
+ "origin": "مستورد",
+ "lead_time": 10,
+ "min_order": 2,
+ "description": "مضخة مياه 1 حصان",
+ "image_url": "https://example.com/waterpump.jpg"
+ },
+ {
+ "id": "MAT-077",
+ "name": "مضخة مياه 2 حصان",
+ "category": "مواد ميكانيكية",
+ "subcategory": "مضخات",
+ "unit": "قطعة",
+ "price": 1200,
+ "supplier": "شركة المضخات السعودية",
+ "origin": "مستورد",
+ "lead_time": 10,
+ "min_order": 2,
+ "description": "مضخة مياه 2 حصان",
+ "image_url": "https://example.com/waterpump2.jpg"
+ },
+ {
+ "id": "MAT-078",
+ "name": "خزان مياه بلاستيكي 1000 لتر",
+ "category": "مواد ميكانيكية",
+ "subcategory": "خزانات",
+ "unit": "قطعة",
+ "price": 650,
+ "supplier": "شركة الخزانات السعودية",
+ "origin": "محلي",
+ "lead_time": 5,
+ "min_order": 2,
+ "description": "خزان مياه بلاستيكي سعة 1000 لتر",
+ "image_url": "https://example.com/watertank.jpg"
+ }
+ ])
+
+ # 9. مواد الري والزراعة
+ materials_data.extend([
+ {
+ "id": "MAT-079",
+ "name": "أنابيب بولي إيثيلين قطر 32 مم",
+ "category": "مواد الري والزراعة",
+ "subcategory": "أنابيب ري",
+ "unit": "متر طولي",
+ "price": 8,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "أنابيب بولي إيثيلين قطر 32 مم لأنظمة الري",
+ "image_url": "https://example.com/pepipe.jpg"
+ },
+ {
+ "id": "MAT-080",
+ "name": "أنابيب بولي إيثيلين قطر 63 مم",
+ "category": "مواد الري والزراعة",
+ "subcategory": "أنابيب ري",
+ "unit": "متر طولي",
+ "price": 15,
+ "supplier": "الشركة السعودية للأنابيب",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 100,
+ "description": "أنابيب بولي إيثيلين قطر 63 مم لأنظمة الري",
+ "image_url": "https://example.com/pepipe2.jpg"
+ },
+ {
+ "id": "MAT-081",
+ "name": "نقاطات ري 4 لتر/ساعة",
+ "category": "مواد الري والزراعة",
+ "subcategory": "نقاطات",
+ "unit": "قطعة",
+ "price": 1,
+ "supplier": "شركة أنظمة الري",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 1000,
+ "description": "نقاطات ري 4 لتر/ساعة",
+ "image_url": "https://example.com/dripper.jpg"
+ },
+ {
+ "id": "MAT-082",
+ "name": "نقاطات ري 8 لتر/ساعة",
+ "category": "مواد الري والزراعة",
+ "subcategory": "نقاطات",
+ "unit": "قطعة",
+ "price": 1.2,
+ "supplier": "شركة أنظمة الري",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 1000,
+ "description": "نقاطات ري 8 لتر/ساعة",
+ "image_url": "https://example.com/dripper2.jpg"
+ },
+ {
+ "id": "MAT-083",
+ "name": "رشاشات ري دوارة",
+ "category": "مواد الري والزراعة",
+ "subcategory": "رشاشات",
+ "unit": "قطعة",
+ "price": 25,
+ "supplier": "شركة أنظمة الري",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 100,
+ "description": "رشاشات ري دوارة للمسطحات الخضراء",
+ "image_url": "https://example.com/sprinkler.jpg"
+ },
+ {
+ "id": "MAT-084",
+ "name": "محابس بلاستيكية قطر 32 مم",
+ "category": "مواد الري والزراعة",
+ "subcategory": "محابس",
+ "unit": "قطعة",
+ "price": 15,
+ "supplier": "شركة أنظمة الري",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 50,
+ "description": "محابس بلاستيكية قطر 32 مم لأنظمة الري",
+ "image_url": "https://example.com/valve.jpg"
+ },
+ {
+ "id": "MAT-085",
+ "name": "محابس بلاستيكية قطر 63 مم",
+ "category": "مواد الري والزراعة",
+ "subcategory": "محابس",
+ "unit": "قطعة",
+ "price": 35,
+ "supplier": "شركة أنظمة الري",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 50,
+ "description": "محابس بلاستيكية قطر 63 مم لأنظمة الري",
+ "image_url": "https://example.com/valve2.jpg"
+ },
+ {
+ "id": "MAT-086",
+ "name": "وحدة تحكم ري 6 محطات",
+ "category": "مواد الري والزراعة",
+ "subcategory": "وحدات تحكم",
+ "unit": "قطعة",
+ "price": 450,
+ "supplier": "شركة أنظمة الري",
+ "origin": "مستورد",
+ "lead_time": 10,
+ "min_order": 5,
+ "description": "وحدة تحكم ري 6 محطات",
+ "image_url": "https://example.com/controller.jpg"
+ },
+ {
+ "id": "MAT-087",
+ "name": "وحدة تحكم ري 12 محطة",
+ "category": "مواد الري والزراعة",
+ "subcategory": "وحدات تحكم",
+ "unit": "قطعة",
+ "price": 750,
+ "supplier": "شركة أنظمة الري",
+ "origin": "مستورد",
+ "lead_time": 10,
+ "min_order": 5,
+ "description": "وحدة تحكم ري 12 محطة",
+ "image_url": "https://example.com/controller2.jpg"
+ },
+ {
+ "id": "MAT-088",
+ "name": "تربة زراعية",
+ "category": "مواد الري والزراعة",
+ "subcategory": "تربة",
+ "unit": "م3",
+ "price": 120,
+ "supplier": "شركة المواد الزراعية",
+ "origin": "محلي",
+ "lead_time": 2,
+ "min_order": 10,
+ "description": "تربة زراعية للزراعة",
+ "image_url": "https://example.com/soil.jpg"
+ }
+ ])
+
+ # 10. مواد متنوعة
+ materials_data.extend([
+ {
+ "id": "MAT-089",
+ "name": "أسلاك تربيط حديد التسليح",
+ "category": "مواد متنوعة",
+ "subcategory": "أسلاك",
+ "unit": "كجم",
+ "price": 12,
+ "supplier": "شركة حديد الراجحي",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 50,
+ "description": "أسلاك تربيط حديد التسليح",
+ "image_url": "https://example.com/bindingwire.jpg"
+ },
+ {
+ "id": "MAT-090",
+ "name": "مسامير متنوعة",
+ "category": "مواد متنوعة",
+ "subcategory": "مسامير",
+ "unit": "كجم",
+ "price": 15,
+ "supplier": "شركة الأدوات",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 20,
+ "description": "مسامير متنوعة للأعمال الإنشائية",
+ "image_url": "https://example.com/nails.jpg"
+ },
+ {
+ "id": "MAT-091",
+ "name": "شدات معدنية",
+ "category": "مواد متنوعة",
+ "subcategory": "شدات",
+ "unit": "م2",
+ "price": 120,
+ "supplier": "شركة معدات البناء",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 50,
+ "description": "شدات معدنية للأعمال الخرسانية",
+ "image_url": "https://example.com/formwork.jpg"
+ },
+ {
+ "id": "MAT-092",
+ "name": "سقالات معدنية",
+ "category": "مواد متنوعة",
+ "subcategory": "سقالات",
+ "unit": "م2",
+ "price": 85,
+ "supplier": "شركة معدات البناء",
+ "origin": "محلي",
+ "lead_time": 7,
+ "min_order": 50,
+ "description": "سقالات معدنية للأعمال الإنشائية",
+ "image_url": "https://example.com/scaffold.jpg"
+ },
+ {
+ "id": "MAT-093",
+ "name": "خشب أبلكاش 18 مم",
+ "category": "مواد متنوعة",
+ "subcategory": "خشب",
+ "unit": "م2",
+ "price": 65,
+ "supplier": "شركة الأخشاب",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 20,
+ "description": "خشب أبلكاش سماكة 18 مم للشدات الخشبية",
+ "image_url": "https://example.com/plywood.jpg"
+ },
+ {
+ "id": "MAT-094",
+ "name": "عوارض خشبية 10×10 سم",
+ "category": "مواد متنوعة",
+ "subcategory": "خشب",
+ "unit": "متر طولي",
+ "price": 25,
+ "supplier": "شركة الأخشاب",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 50,
+ "description": "عوارض خشبية 10×10 سم للشدات الخشبية",
+ "image_url": "https://example.com/timber.jpg"
+ },
+ {
+ "id": "MAT-095",
+ "name": "مواد لاصقة للبلاط",
+ "category": "مواد متنوعة",
+ "subcategory": "مواد لاصقة",
+ "unit": "كجم",
+ "price": 2.5,
+ "supplier": "شركة مواد البناء",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 500,
+ "description": "مواد لاصقة للبلاط",
+ "image_url": "https://example.com/tileadhesive.jpg"
+ },
+ {
+ "id": "MAT-096",
+ "name": "روبة للبلاط",
+ "category": "مواد متنوعة",
+ "subcategory": "مواد لاصقة",
+ "unit": "كجم",
+ "price": 3,
+ "supplier": "شركة مواد البناء",
+ "origin": "محلي",
+ "lead_time": 3,
+ "min_order": 200,
+ "description": "روبة للبلاط",
+ "image_url": "https://example.com/grout.jpg"
+ },
+ {
+ "id": "MAT-097",
+ "name": "مواد معالجة الخرسانة",
+ "category": "مواد متنوعة",
+ "subcategory": "مواد معالجة",
+ "unit": "لتر",
+ "price": 12,
+ "supplier": "سيكا",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 100,
+ "description": "مواد معالجة الخرسانة",
+ "image_url": "https://example.com/curing.jpg"
+ },
+ {
+ "id": "MAT-098",
+ "name": "مواد إصلاح الخرسانة",
+ "category": "مواد متنوعة",
+ "subcategory": "مواد معالجة",
+ "unit": "كجم",
+ "price": 18,
+ "supplier": "سيكا",
+ "origin": "مستورد",
+ "lead_time": 7,
+ "min_order": 50,
+ "description": "مواد إصلاح الخرسانة",
+ "image_url": "https://example.com/repair.jpg"
+ }
+ ])
+
+ # تخزين البيانات في حالة الجلسة
+ st.session_state.materials_catalog = pd.DataFrame(materials_data)
+
+ def render(self):
+ """عرض واجهة كتالوج المواد"""
+
+ st.markdown("## كتالوج المواد")
+
+ # إنشاء تبويبات لعرض الكتالوج
+ tabs = st.tabs([
+ "عرض الكتالوج",
+ "إضافة مادة",
+ "تحليل الأسعار",
+ "استيراد/تصدير"
+ ])
+
+ with tabs[0]:
+ self._render_catalog_view_tab()
+
+ with tabs[1]:
+ self._render_add_material_tab()
+
+ with tabs[2]:
+ self._render_price_analysis_tab()
+
+ with tabs[3]:
+ self._render_import_export_tab()
+
+ def _render_catalog_view_tab(self):
+ """عرض تبويب عرض الكتالوج"""
+
+ st.markdown("### عرض كتالوج المواد")
+
+ # استخراج البيانات
+ materials_df = st.session_state.materials_catalog
+
+ # إنشاء فلاتر للعرض
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ # فلتر حسب الفئة
+ categories = ["الكل"] + sorted(materials_df["category"].unique().tolist())
+ selected_category = st.selectbox("اختر فئة المواد", categories)
+
+ with col2:
+ # فلتر حسب الفئة الفرعية
+ if selected_category != "الكل":
+ subcategories = ["الكل"] + sorted(materials_df[materials_df["category"] == selected_category]["subcategory"].unique().tolist())
+ else:
+ subcategories = ["الكل"] + sorted(materials_df["subcategory"].unique().tolist())
+
+ selected_subcategory = st.selectbox("اختر الفئة الفرعية", subcategories)
+
+ with col3:
+ # فلتر حسب المورد
+ suppliers = ["الكل"] + sorted(materials_df["supplier"].unique().tolist())
+ selected_supplier = st.selectbox("اختر المورد", suppliers)
+
+ # تطبيق الفلاتر
+ filtered_df = materials_df.copy()
+
+ if selected_category != "الكل":
+ filtered_df = filtered_df[filtered_df["category"] == selected_category]
+
+ if selected_subcategory != "الكل":
+ filtered_df = filtered_df[filtered_df["subcategory"] == selected_subcategory]
+
+ if selected_supplier != "الكل":
+ filtered_df = filtered_df[filtered_df["supplier"] == selected_supplier]
+
+ # عرض البيانات
+ if not filtered_df.empty:
+ # عرض عدد النتائج
+ st.info(f"تم العثور على {len(filtered_df)} مادة")
+
+ # عرض المواد في شكل بطاقات
+ for i, (_, material) in enumerate(filtered_df.iterrows()):
+ col1, col2 = st.columns([1, 2])
+
+ with col1:
+ # عرض صورة المادة (استخدام صورة افتراضية)
+ st.image("https://via.placeholder.com/150", caption=material["name"])
+
+ with col2:
+ # عرض معلومات المادة
+ st.markdown(f"**{material['name']}** (الكود: {material['id']})")
+ st.markdown(f"الفئة: {material['category']} - {material['subcategory']}")
+ st.markdown(f"الوحدة: {material['unit']} | السعر: {material['price']} ريال/{material['unit']}")
+ st.markdown(f"المورد: {material['supplier']} | المنشأ: {material['origin']}")
+ st.markdown(f"مدة التوريد: {material['lead_time']} يوم | الحد الأدنى للطلب: {material['min_order']} {material['unit']}")
+
+ # إضافة زر لعرض التفاصيل
+ if st.button(f"عرض التفاصيل الكاملة", key=f"details_{material['id']}"):
+ st.session_state.selected_material = material['id']
+ self._show_material_details(material)
+
+ st.markdown("---")
+ else:
+ st.warning("لا توجد مواد تطابق معايير البحث")
+
+ def _show_material_details(self, material):
+ """عرض تفاصيل المادة"""
+
+ st.markdown(f"## تفاصيل المادة: {material['name']}")
+
+ col1, col2 = st.columns([1, 2])
+
+ with col1:
+ # عرض صورة المادة (استخدام صورة افتراضية)
+ st.image("https://via.placeholder.com/300", caption=material["name"])
+
+ with col2:
+ # عرض المعلومات الأساسية
+ st.markdown("### المعلومات الأساسية")
+ st.markdown(f"**الكود:** {material['id']}")
+ st.markdown(f"**الفئة:** {material['category']} - {material['subcategory']}")
+ st.markdown(f"**الوحدة:** {material['unit']}")
+ st.markdown(f"**السعر:** {material['price']} ريال/{material['unit']}")
+ st.markdown(f"**المورد:** {material['supplier']}")
+ st.markdown(f"**المنشأ:** {material['origin']}")
+ st.markdown(f"**مدة التوريد:** {material['lead_time']} يوم")
+ st.markdown(f"**الحد الأدنى للطلب:** {material['min_order']} {material['unit']}")
+ st.markdown(f"**الوصف:** {material['description']}")
+
+ # إضافة زر للتعديل
+ if st.button("تعديل بيانات المادة"):
+ st.session_state.edit_material = material['id']
+ # هنا يمكن إضافة منطق التعديل
+
+ def _render_add_material_tab(self):
+ """عرض تبويب إضافة مادة"""
+
+ st.markdown("### إضافة مادة جديدة")
+
+ # استخراج البيانات
+ materials_df = st.session_state.materials_catalog
+
+ # إنشاء نموذج إضافة مادة
+ with st.form("add_material_form"):
+ st.markdown("#### المعلومات الأساسية")
+
+ # الصف الأول
+ col1, col2 = st.columns(2)
+ with col1:
+ material_id = st.text_input("كود المادة", value=f"MAT-{len(materials_df) + 1:03d}")
+ material_name = st.text_input("اسم المادة", placeholder="مثال: أسمنت بورتلاندي عادي")
+
+ with col2:
+ # استخراج الفئات والفئات الفرعية الموجودة
+ categories = sorted(materials_df["category"].unique().tolist())
+ material_category = st.selectbox("فئة المادة", categories)
+
+ # استخراج الفئات الفرعية بناءً على الفئة المختارة
+ subcategories = sorted(materials_df[materials_df["category"] == material_category]["subcategory"].unique().tolist())
+ material_subcategory = st.selectbox("الفئة الفرعية", subcategories)
+
+ # الصف الثاني
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ material_unit = st.text_input("وحدة القياس", placeholder="مثال: م3، طن، قطعة")
+ with col2:
+ material_price = st.number_input("السعر (ريال)", min_value=0.0, step=0.5)
+ with col3:
+ material_min_order = st.number_input("الحد الأدنى للطلب", min_value=1, step=1)
+
+ # الصف الثالث
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ material_supplier = st.text_input("المورد", placeholder="مثال: شركة الإسمنت السعودية")
+ with col2:
+ material_origin = st.selectbox("المنشأ", ["محلي", "مستورد"])
+ with col3:
+ material_lead_time = st.number_input("مدة التوريد (يوم)", min_value=1, step=1)
+
+ # وصف المادة
+ material_description = st.text_area("وصف المادة", placeholder="أدخل وصفاً تفصيلياً للمادة")
+
+ # رابط الصورة
+ material_image_url = st.text_input("رابط الصورة", placeholder="مثال: https://example.com/image.jpg")
+
+ # زر الإضافة
+ submit_button = st.form_submit_button("إضافة المادة")
+
+ if submit_button:
+ # التحقق من البيانات
+ if not material_name or not material_category or not material_subcategory or not material_unit:
+ st.error("يرجى إدخال المعلومات الأساسية للمادة")
+ else:
+ # إنشاء مادة جديدة
+ new_material = {
+ "id": material_id,
+ "name": material_name,
+ "category": material_category,
+ "subcategory": material_subcategory,
+ "unit": material_unit,
+ "price": material_price,
+ "supplier": material_supplier,
+ "origin": material_origin,
+ "lead_time": material_lead_time,
+ "min_order": material_min_order,
+ "description": material_description,
+ "image_url": material_image_url if material_image_url else "https://via.placeholder.com/150"
+ }
+
+ # إضافة المادة إلى الكتالوج
+ st.session_state.materials_catalog = pd.concat([
+ st.session_state.materials_catalog,
+ pd.DataFrame([new_material])
+ ], ignore_index=True)
+
+ st.success(f"تمت إضافة المادة {material_name} بنجاح!")
+
+ def _render_price_analysis_tab(self):
+ """عرض تبويب تحليل الأسعار"""
+
+ st.markdown("### تحليل أسعار المواد")
+
+ # استخراج البيانات
+ materials_df = st.session_state.materials_catalog
+
+ # تحليل متوسط الأسعار حسب الفئة
+ st.markdown("#### متوسط الأسعار حسب الفئة")
+
+ # حساب متوسط الأسعار لكل فئة
+ category_prices = materials_df.groupby("category").agg({
+ "price": "mean"
+ }).reset_index()
+
+ # تغيير أسماء الأعمدة
+ category_prices.columns = ["الفئة", "متوسط السعر"]
+
+ # عرض الجدول
+ st.dataframe(category_prices, use_container_width=True)
+
+ # إنشاء رسم بياني للمقارنة
+ st.markdown("#### مقارنة متوسط الأسعار حسب الفئة")
+
+ fig = px.bar(
+ category_prices,
+ x="الفئة",
+ y="متوسط السعر",
+ title="متوسط أسعار المواد حسب الفئة",
+ color="الفئة",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل توزيع المواد حسب المنشأ
+ st.markdown("#### توزيع المواد حسب المنشأ")
+
+ # حساب عدد المواد حسب المنشأ
+ origin_counts = materials_df["origin"].value_counts().reset_index()
+ origin_counts.columns = ["المنشأ", "عدد المواد"]
+
+ # إنشاء رسم بياني دائري
+ fig = px.pie(
+ origin_counts,
+ values="عدد المواد",
+ names="المنشأ",
+ title="توزيع المواد حسب المنشأ",
+ color="المنشأ"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل مدة التوريد
+ st.markdown("#### تحليل مدة التوريد")
+
+ # حساب متوسط مدة التوريد حسب المنشأ
+ lead_time_by_origin = materials_df.groupby("origin").agg({
+ "lead_time": "mean"
+ }).reset_index()
+
+ # تغيير أسماء الأعمدة
+ lead_time_by_origin.columns = ["المنشأ", "متوسط مدة التوريد (يوم)"]
+
+ # عرض الجدول
+ st.dataframe(lead_time_by_origin, use_container_width=True)
+
+ # إنشاء رسم بياني للمقارنة
+ fig = px.bar(
+ lead_time_by_origin,
+ x="المنشأ",
+ y="متوسط مدة التوريد (يوم)",
+ title="متوسط مدة التوريد حسب المنشأ",
+ color="المنشأ",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # حاسبة تكاليف المشروع
+ st.markdown("#### حاسبة تكاليف المشروع")
+
+ with st.form("project_cost_calculator"):
+ st.markdown("أدخل المواد المطلوبة للمشروع")
+
+ # اختيار المواد
+ selected_materials = st.multiselect(
+ "اختر المواد",
+ options=materials_df["name"].tolist(),
+ format_func=lambda x: f"{x} ({materials_df[materials_df['name'] == x]['id'].iloc[0]})"
+ )
+
+ # إنشاء حقول إدخال الكميات
+ quantities = {}
+
+ if selected_materials:
+ st.markdown("أدخل الكميات المطلوبة")
+
+ for material_name in selected_materials:
+ material = materials_df[materials_df["name"] == material_name].iloc[0]
+ quantities[material_name] = st.number_input(
+ f"{material_name} ({material['unit']})",
+ min_value=0.0,
+ step=0.5,
+ key=f"qty_{material['id']}"
+ )
+
+ # زر الحساب
+ calculate_button = st.form_submit_button("حساب التكاليف")
+
+ if calculate_button:
+ if not selected_materials:
+ st.error("يرجى اختيار مادة واحدة على الأقل")
+ else:
+ # حساب التكاليف
+ project_costs = []
+
+ for material_name in selected_materials:
+ material = materials_df[materials_df["name"] == material_name].iloc[0]
+ quantity = quantities[material_name]
+
+ if quantity > 0:
+ cost = material["price"] * quantity
+
+ project_costs.append({
+ "المادة": material_name,
+ "الكود": material["id"],
+ "الوحدة": material["unit"],
+ "الكمية": quantity,
+ "سعر الوحدة": material["price"],
+ "التكلفة الإجمالية": cost
+ })
+
+ if project_costs:
+ # عرض النتائج
+ project_costs_df = pd.DataFrame(project_costs)
+ st.dataframe(project_costs_df, use_container_width=True)
+
+ # حساب إجمالي التكاليف
+ total_cost = project_costs_df["التكلفة الإجمالية"].sum()
+ st.metric("إجمالي تكاليف المواد للمشروع", f"{total_cost:,.2f} ريال")
+ else:
+ st.warning("يرجى إدخال كميات أكبر من صفر")
+
+ def _render_import_export_tab(self):
+ """عرض تبويب استيراد/تصدير"""
+
+ st.markdown("### استيراد وتصدير بيانات المواد")
+
+ # استيراد البيانات
+ st.markdown("#### استيراد البيانات")
+
+ uploaded_file = st.file_uploader("اختر ملف Excel لاستيراد بيانات المواد", type=["xlsx", "xls"])
+
+ if uploaded_file is not None:
+ try:
+ # قراءة الملف
+ imported_df = pd.read_excel(uploaded_file)
+
+ # عرض البيانات المستوردة
+ st.dataframe(imported_df, use_container_width=True)
+
+ # زر الاستيراد
+ if st.button("استيراد البيانات"):
+ # التحقق من وجود الأعمدة المطلوبة
+ required_columns = ["id", "name", "category", "subcategory", "unit", "price"]
+
+ if all(col in imported_df.columns for col in required_columns):
+ # دمج البيانات المستوردة مع البيانات الحالية
+ st.session_state.materials_catalog = pd.concat([
+ st.session_state.materials_catalog,
+ imported_df
+ ], ignore_index=True).drop_duplicates(subset=["id"])
+
+ st.success(f"تم استيراد {len(imported_df)} مادة بنجاح!")
+ else:
+ st.error("الملف المستورد لا يحتوي على الأعمدة المطلوبة")
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}")
+
+ # تصدير البيانات
+ st.markdown("#### تصدير البيانات")
+
+ # اختيار تنسيق التصدير
+ export_format = st.radio("اختر تنسيق التصدير", ["Excel", "CSV", "JSON"], horizontal=True)
+
+ if st.button("تصدير البيانات"):
+ # استخراج البيانات
+ materials_df = st.session_state.materials_catalog
+
+ # تصدير البيانات حسب التنسيق المختار
+ if export_format == "Excel":
+ # تصدير إلى Excel
+ output = io.BytesIO()
+ with pd.ExcelWriter(output, engine="openpyxl") as writer:
+ materials_df.to_excel(writer, index=False, sheet_name="Materials")
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف Excel",
+ data=output.getvalue(),
+ file_name="materials_catalog.xlsx",
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ )
+
+ elif export_format == "CSV":
+ # تصدير إلى CSV
+ csv_data = materials_df.to_csv(index=False)
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف CSV",
+ data=csv_data,
+ file_name="materials_catalog.csv",
+ mime="text/csv"
+ )
+
+ else: # JSON
+ # تصدير إلى JSON
+ json_data = materials_df.to_json(orient="records", force_ascii=False)
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف JSON",
+ data=json_data,
+ file_name="materials_catalog.json",
+ mime="application/json"
+ )
+
+ def get_material_by_id(self, material_id):
+ """الحصول على مادة بواسطة الكود"""
+
+ materials_df = st.session_state.materials_catalog
+ material = materials_df[materials_df["id"] == material_id]
+
+ if not material.empty:
+ return material.iloc[0].to_dict()
+
+ return None
+
+ def get_materials_by_category(self, category):
+ """الحصول على المواد حسب الفئة"""
+
+ materials_df = st.session_state.materials_catalog
+ materials = materials_df[materials_df["category"] == category]
+
+ if not materials.empty:
+ return materials.to_dict(orient="records")
+
+ return []
+
+ def calculate_material_cost(self, material_id, quantity):
+ """حساب تكلفة المادة بناءً على الكمية"""
+
+ material = self.get_material_by_id(material_id)
+
+ if material:
+ return material["price"] * quantity
+
+ return 0
diff --git a/pricing_system/modules/catalogs/subcontractors_catalog.py b/pricing_system/modules/catalogs/subcontractors_catalog.py
index 9bc99cbb4ef974b6e94709cdfe4fe45a391c0d60..9002ffcdb3ea23c86536d69f63ed313997c17fb9 100644
--- a/pricing_system/modules/catalogs/subcontractors_catalog.py
+++ b/pricing_system/modules/catalogs/subcontractors_catalog.py
@@ -1,1159 +1,1159 @@
-"""
-كتالوج مقاولي الباطن - وحدة إدارة مقاولي الباطن
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import plotly.express as px
-import os
-import json
-from datetime import datetime
-import io
-
-class SubcontractorsCatalog:
- """كتالوج مقاولي الباطن"""
-
- def __init__(self):
- """تهيئة كتالوج مقاولي الباطن"""
-
- # تهيئة حالة الجلسة لكتالوج مقاولي الباطن
- if 'subcontractors_catalog' not in st.session_state:
- # إنشاء بيانات افتراضية لمقاولي الباطن
- self._initialize_subcontractors_catalog()
-
- def _initialize_subcontractors_catalog(self):
- """تهيئة بيانات كتالوج مقاولي الباطن"""
-
- # تعريف فئات مقاولي الباطن
- subcontractor_categories = [
- "أعمال الكهرباء",
- "أعمال ITC",
- "أعمال CCTV",
- "أنظمة التحكم في الوصول",
- "شبكات الري",
- "أعمال الصرف الصحي",
- "أعمال الطرق",
- "أعمال الجسور",
- "أعمال الحفر والردم",
- "أعمال الخرسانة"
- ]
-
- # إنشاء قائمة مقاولي الباطن
- subcontractors_data = []
-
- # 1. أعمال الكهرباء
- subcontractors_data.extend([
- {
- "id": "ELEC-001",
- "name": "شركة الأنظمة الكهربائية المتكاملة",
- "category": "أعمال الكهرباء",
- "subcategory": "تمديدات كهربائية",
- "contact_person": "م. خالد العتيبي",
- "phone": "0555123456",
- "email": "khalid@ies-sa.com",
- "address": "الرياض - حي العليا",
- "classification": "درجة أولى",
- "experience_years": 15,
- "completed_projects": 120,
- "ongoing_projects": 8,
- "min_project_value": 500000,
- "max_project_value": 50000000,
- "specialties": "تمديدات كهربائية، محطات توزيع، لوحات توزيع، أنظمة إنارة",
- "rating": 4.8,
- "description": "شركة متخصصة في تنفيذ أعمال التمديدات الكهربائية ومحطات التوزيع واللوحات الكهربائية وأنظمة الإنارة للمشاريع الكبرى"
- },
- {
- "id": "ELEC-002",
- "name": "مؤسسة النور للمقاولات الكهربائية",
- "category": "أعمال الكهرباء",
- "subcategory": "إنارة طرق",
- "contact_person": "م. سعد القحطاني",
- "phone": "0555234567",
- "email": "saad@alnoor-elec.com",
- "address": "جدة - حي الروضة",
- "classification": "درجة ثانية",
- "experience_years": 10,
- "completed_projects": 75,
- "ongoing_projects": 5,
- "min_project_value": 200000,
- "max_project_value": 20000000,
- "specialties": "إنارة طرق، إنارة ميادين، إنارة حدائق، أنظمة تحكم",
- "rating": 4.5,
- "description": "مؤسسة متخصصة في تنفيذ أعمال إنارة الطرق والميادين والحدائق وأنظمة التحكم في الإنارة"
- },
- {
- "id": "ELEC-003",
- "name": "شركة الطاقة للمقاولات الكهربائية",
- "category": "أعمال الكهرباء",
- "subcategory": "محطات كهربائية",
- "contact_person": "م. فهد الشمري",
- "phone": "0555345678",
- "email": "fahad@energy-sa.com",
- "address": "الدمام - حي الفيصلية",
- "classification": "درجة أولى",
- "experience_years": 18,
- "completed_projects": 150,
- "ongoing_projects": 10,
- "min_project_value": 1000000,
- "max_project_value": 100000000,
- "specialties": "محطات توزيع، محطات تحويل، خطوط نقل، أنظمة حماية",
- "rating": 4.9,
- "description": "شركة متخصصة في تنفيذ أعمال محطات التوزيع والتحويل وخطوط النقل وأنظمة الحماية الكهربائية"
- }
- ])
-
- # 2. أعمال ITC
- subcontractors_data.extend([
- {
- "id": "ITC-001",
- "name": "شركة التقنية المتكاملة للاتصالات",
- "category": "أعمال ITC",
- "subcategory": "شبكات اتصالات",
- "contact_person": "م. محمد العمري",
- "phone": "0555456789",
- "email": "mohammed@itc-sa.com",
- "address": "الرياض - حي الملز",
- "classification": "درجة أولى",
- "experience_years": 12,
- "completed_projects": 90,
- "ongoing_projects": 7,
- "min_project_value": 500000,
- "max_project_value": 40000000,
- "specialties": "شبكات اتصالات، أنظمة مراقبة، أنظمة صوتية، شبكات ألياف ضوئية",
- "rating": 4.7,
- "description": "شركة متخصصة في تنفيذ أعمال شبكات الاتصالات وأنظمة المراقبة والأنظمة الصوتية وشبكات الألياف الضوئية"
- },
- {
- "id": "ITC-002",
- "name": "مؤسسة الاتصالات المتقدمة",
- "category": "أعمال ITC",
- "subcategory": "أنظمة معلومات",
- "contact_person": "م. عبدالله الزهراني",
- "phone": "0555567890",
- "email": "abdullah@advanced-comm.com",
- "address": "جدة - حي السلامة",
- "classification": "درجة ثانية",
- "experience_years": 8,
- "completed_projects": 60,
- "ongoing_projects": 4,
- "min_project_value": 200000,
- "max_project_value": 15000000,
- "specialties": "أنظمة معلومات، شبكات داخلية، أنظمة تخزين، أنظمة حماية",
- "rating": 4.4,
- "description": "مؤسسة متخصصة في تنفيذ أعمال أنظمة المعلومات والشبكات الداخلية وأنظمة التخزين وأنظمة الحماية"
- },
- {
- "id": "ITC-003",
- "name": "شركة البيانات للاتصالات وتقنية المعلومات",
- "category": "أعمال ITC",
- "subcategory": "بنية تحتية للاتصالات",
- "contact_person": "م. سلطان المالكي",
- "phone": "0555678901",
- "email": "sultan@data-sa.com",
- "address": "الدمام - حي الشاطئ",
- "classification": "درجة أولى",
- "experience_years": 15,
- "completed_projects": 110,
- "ongoing_projects": 9,
- "min_project_value": 800000,
- "max_project_value": 60000000,
- "specialties": "بنية تحتية للاتصالات، أبراج اتصالات، محطات إرسال، شبكات ألياف ضوئية",
- "rating": 4.8,
- "description": "شركة متخصصة في تنفيذ أعمال البنية التحتية للاتصالات وأبراج الاتصالات ومحطات الإرسال وشبكات الألياف الضوئية"
- }
- ])
-
- # 3. أعمال CCTV
- subcontractors_data.extend([
- {
- "id": "CCTV-001",
- "name": "شركة الأمان لأنظمة المراقبة",
- "category": "أعمال CCTV",
- "subcategory": "أنظمة مراقبة",
- "contact_person": "م. ناصر الحربي",
- "phone": "0555789012",
- "email": "nasser@security-sa.com",
- "address": "الرياض - حي النزهة",
- "classification": "درجة ثانية",
- "experience_years": 10,
- "completed_projects": 85,
- "ongoing_projects": 6,
- "min_project_value": 100000,
- "max_project_value": 10000000,
- "specialties": "أنظمة مراقبة، كاميرات أمنية، أنظمة تسجيل، أنظمة تحكم",
- "rating": 4.6,
- "description": "شركة متخصصة في تنفيذ أعمال أنظمة المراقبة والكاميرات الأمنية وأنظمة التسجيل وأنظمة التحكم"
- },
- {
- "id": "CCTV-002",
- "name": "مؤسسة الحماية للأنظمة الأمنية",
- "category": "أعمال CCTV",
- "subcategory": "أنظمة أمنية",
- "contact_person": "م. سعيد الغامدي",
- "phone": "0555890123",
- "email": "saeed@protection-sa.com",
- "address": "جدة - حي الصفا",
- "classification": "درجة ثالثة",
- "experience_years": 7,
- "completed_projects": 50,
- "ongoing_projects": 3,
- "min_project_value": 50000,
- "max_project_value": 5000000,
- "specialties": "أنظمة أمنية، كاميرات مراقبة، أنظمة إنذار، أنظمة دخول",
- "rating": 4.3,
- "description": "مؤسسة متخصصة في تنفيذ أعمال الأنظمة الأمنية وكاميرات المراقبة وأنظمة الإنذار وأنظمة الدخول"
- },
- {
- "id": "CCTV-003",
- "name": "شركة المراقبة الذكية",
- "category": "أعمال CCTV",
- "subcategory": "أنظمة مراقبة ذكية",
- "contact_person": "م. عمر السعدي",
- "phone": "0555901234",
- "email": "omar@smart-surveillance.com",
- "address": "الدمام - حي الخليج",
- "classification": "درجة أولى",
- "experience_years": 12,
- "completed_projects": 95,
- "ongoing_projects": 8,
- "min_project_value": 300000,
- "max_project_value": 25000000,
- "specialties": "أنظمة مراقبة ذكية، تحليل فيديو، تعرف على الوجوه، تتبع حركة",
- "rating": 4.8,
- "description": "شركة متخصصة في تنفيذ أعمال أنظمة المراقبة الذكية وتحليل الفيديو والتعرف على الوجوه وتتبع الحركة"
- }
- ])
-
- # 4. أنظمة التحكم في الوصول
- subcontractors_data.extend([
- {
- "id": "ACCESS-001",
- "name": "شركة التحكم الأمني",
- "category": "أنظمة التحكم في الوصول",
- "subcategory": "أنظمة تحكم",
- "contact_person": "م. فيصل العنزي",
- "phone": "0556012345",
- "email": "faisal@security-control.com",
- "address": "الرياض - حي الورود",
- "classification": "درجة ثانية",
- "experience_years": 9,
- "completed_projects": 70,
- "ongoing_projects": 5,
- "min_project_value": 100000,
- "max_project_value": 8000000,
- "specialties": "أنظمة تحكم في الدخول، بصمات، بطاقات ذكية، أقفال إلكترونية",
- "rating": 4.5,
- "description": "شركة متخصصة في تنفيذ أعمال أنظمة التحكم في الدخول والبصمات والبطاقات الذكية والأقفال الإلكترونية"
- },
- {
- "id": "ACCESS-002",
- "name": "مؤسسة الوصول الآمن",
- "category": "أنظمة التحكم في الوصول",
- "subcategory": "أنظمة أمنية",
- "contact_person": "م. ماجد الدوسري",
- "phone": "0556123456",
- "email": "majed@safe-access.com",
- "address": "جدة - حي المروة",
- "classification": "درجة ثالثة",
- "experience_years": 6,
- "completed_projects": 40,
- "ongoing_projects": 3,
- "min_project_value": 50000,
- "max_project_value": 3000000,
- "specialties": "أنظمة أمنية، بوابات إلكترونية، حواجز آلية، كاميرات تعرف",
- "rating": 4.2,
- "description": "مؤسسة متخصصة في تنفيذ أعمال الأنظمة الأمنية والبوابات الإلكترونية والحواجز الآلية وكاميرات التعرف"
- },
- {
- "id": "ACCESS-003",
- "name": "شركة الأمن الذكي",
- "category": "أنظمة التحكم في الوصول",
- "subcategory": "أنظمة متكاملة",
- "contact_person": "م. طارق الشهري",
- "phone": "0556234567",
- "email": "tariq@smart-security.com",
- "address": "الدمام - حي النور",
- "classification": "درجة أولى",
- "experience_years": 14,
- "completed_projects": 100,
- "ongoing_projects": 7,
- "min_project_value": 500000,
- "max_project_value": 20000000,
- "specialties": "أنظمة متكاملة، تحكم مركزي، مراقبة ذكية، تحليل سلوك",
- "rating": 4.7,
- "description": "شركة متخصصة في تنفيذ أعمال الأنظمة المتكاملة والتحكم المركزي والمراقبة الذكية وتحليل السلوك"
- }
- ])
-
- # 5. شبكات الري
- subcontractors_data.extend([
- {
- "id": "IRR-001",
- "name": "شركة الواحة لأنظمة الري",
- "category": "شبكات الري",
- "subcategory": "أنظمة ري",
- "contact_person": "م. سامي المطيري",
- "phone": "0556345678",
- "email": "sami@oasis-irrigation.com",
- "address": "الرياض - حي الياسمين",
- "classification": "درجة ثانية",
- "experience_years": 11,
- "completed_projects": 80,
- "ongoing_projects": 6,
- "min_project_value": 200000,
- "max_project_value": 15000000,
- "specialties": "أنظمة ري، شبكات مياه، مضخات، أنظمة تحكم",
- "rating": 4.6,
- "description": "شركة متخصصة في تنفيذ أعمال أنظمة الري وشبكات المياه والمضخات وأنظمة التحكم"
- },
- {
- "id": "IRR-002",
- "name": "مؤسسة الخضراء للري والزراعة",
- "category": "شبكات الري",
- "subcategory": "ري زراعي",
- "contact_person": "م. عبدالرحمن الحربي",
- "phone": "0556456789",
- "email": "abdulrahman@green-irrigation.com",
- "address": "جدة - حي الفيحاء",
- "classification": "درجة ثالثة",
- "experience_years": 8,
- "completed_projects": 55,
- "ongoing_projects": 4,
- "min_project_value": 100000,
- "max_project_value": 5000000,
- "specialties": "ري زراعي، ري بالتنقيط، ري بالرش، أنظمة تسميد",
- "rating": 4.4,
- "description": "مؤسسة متخصصة في تنفيذ أعمال الري الزراعي والري بالتنقيط والري بالرش وأنظمة التسميد"
- },
- {
- "id": "IRR-003",
- "name": "شركة المياه الذكية",
- "category": "شبكات الري",
- "subcategory": "أنظمة ري ذكية",
- "contact_person": "م. خالد السبيعي",
- "phone": "0556567890",
- "email": "khalid@smart-water.com",
- "address": "الدمام - حي الفردوس",
- "classification": "درجة أولى",
- "experience_years": 13,
- "completed_projects": 90,
- "ongoing_projects": 7,
- "min_project_value": 500000,
- "max_project_value": 25000000,
- "specialties": "أنظمة ري ذكية، تحكم عن بعد، استشعار رطوبة، توفير مياه",
- "rating": 4.8,
- "description": "شركة متخصصة في تنفيذ أعمال أنظمة الري الذكية والتحكم عن بعد واستشعار الرطوبة وتوفير المياه"
- }
- ])
-
- # 6. أعمال الصرف الصحي
- subcontractors_data.extend([
- {
- "id": "SEW-001",
- "name": "شركة البنية التحتية للصرف الصحي",
- "category": "أعمال الصرف الصحي",
- "subcategory": "شبكات صرف",
- "contact_person": "م. عبدالعزيز الشمري",
- "phone": "0556678901",
- "email": "abdulaziz@infrastructure-sewage.com",
- "address": "الرياض - حي الملقا",
- "classification": "درجة أولى",
- "experience_years": 16,
- "completed_projects": 120,
- "ongoing_projects": 9,
- "min_project_value": 1000000,
- "max_project_value": 80000000,
- "specialties": "شبكات صرف، محطات ضخ، محطات معالجة، خطوط رئيسية",
- "rating": 4.9,
- "description": "شركة متخصصة في تنفيذ أعمال شبكات الصرف ومحطات الضخ ومحطات المعالجة والخطوط الرئيسية"
- },
- {
- "id": "SEW-002",
- "name": "مؤسسة الصرف المتكاملة",
- "category": "أعمال الصرف الصحي",
- "subcategory": "صرف داخلي",
- "contact_person": "م. فهد العتيبي",
- "phone": "0556789012",
- "email": "fahad@integrated-sewage.com",
- "address": "جدة - حي الحمراء",
- "classification": "درجة ثانية",
- "experience_years": 9,
- "completed_projects": 65,
- "ongoing_projects": 5,
- "min_project_value": 300000,
- "max_project_value": 20000000,
- "specialties": "صرف داخلي، تمديدات صحية، غرف تفتيش، خزانات تحليل",
- "rating": 4.5,
- "description": "مؤسسة متخصصة في تنفيذ أعمال الصرف الداخلي والتمديدات الصحية وغرف التفتيش وخزانات التحليل"
- },
- {
- "id": "SEW-003",
- "name": "شركة معالجة المياه",
- "category": "أعمال الصرف الصحي",
- "subcategory": "محطات معالجة",
- "contact_person": "م. سلطان القحطاني",
- "phone": "0556890123",
- "email": "sultan@water-treatment.com",
- "address": "الدمام - حي الأنوار",
- "classification": "درجة أولى",
- "experience_years": 18,
- "completed_projects": 130,
- "ongoing_projects": 10,
- "min_project_value": 2000000,
- "max_project_value": 100000000,
- "specialties": "محطات معالجة، تنقية مياه، إعادة تدوير، أنظمة تحكم",
- "rating": 4.9,
- "description": "شركة متخصصة في تنفيذ أعمال محطات المعالجة وتنقية المياه وإعادة التدوير وأنظمة التحكم"
- }
- ])
-
- # 7. أعمال الطرق
- subcontractors_data.extend([
- {
- "id": "ROAD-001",
- "name": "شركة الطرق الحديثة",
- "category": "أعمال الطرق",
- "subcategory": "إنشاء طرق",
- "contact_person": "م. محمد الحارثي",
- "phone": "0556901234",
- "email": "mohammed@modern-roads.com",
- "address": "الرياض - حي النخيل",
- "classification": "درجة أولى",
- "experience_years": 20,
- "completed_projects": 150,
- "ongoing_projects": 12,
- "min_project_value": 5000000,
- "max_project_value": 200000000,
- "specialties": "إنشاء طرق، تقاطعات، أنفاق، جسور صغيرة",
- "rating": 4.9,
- "description": "شركة متخصصة في تنفيذ أعمال إنشاء الطرق والتقاطعات والأنفاق والجسور الصغيرة"
- },
- {
- "id": "ROAD-002",
- "name": "مؤسسة الطرق السريعة",
- "category": "أعمال الطرق",
- "subcategory": "رصف طرق",
- "contact_person": "م. سعد الغامدي",
- "phone": "0557012345",
- "email": "saad@highway-contractors.com",
- "address": "جدة - حي الشرفية",
- "classification": "درجة ثانية",
- "experience_years": 12,
- "completed_projects": 85,
- "ongoing_projects": 7,
- "min_project_value": 1000000,
- "max_project_value": 50000000,
- "specialties": "رصف طرق، سفلتة، خلطات إسفلتية، علامات مرورية",
- "rating": 4.6,
- "description": "مؤسسة متخصصة في تنفيذ أعمال رصف الطرق والسفلتة والخلطات الإسفلتية والعلامات المرورية"
- },
- {
- "id": "ROAD-003",
- "name": "شركة البنية التحتية للطرق",
- "category": "أعمال الطرق",
- "subcategory": "بنية تحتية",
- "contact_person": "م. عبدالله المالكي",
- "phone": "0557123456",
- "email": "abdullah@infrastructure-roads.com",
- "address": "الدمام - حي الفيصلية",
- "classification": "درجة أولى",
- "experience_years": 17,
- "completed_projects": 120,
- "ongoing_projects": 10,
- "min_project_value": 3000000,
- "max_project_value": 150000000,
- "specialties": "بنية تحتية، تصريف مياه، قنوات، عبارات",
- "rating": 4.8,
- "description": "شركة متخصصة في تنفيذ أعمال البنية التحتية للطرق وتصريف المياه والقنوات والعبارات"
- }
- ])
-
- # 8. أعمال الجسور
- subcontractors_data.extend([
- {
- "id": "BRIDGE-001",
- "name": "شركة الجسور العالمية",
- "category": "أعمال الجسور",
- "subcategory": "إنشاء جسور",
- "contact_person": "م. فيصل الدوسري",
- "phone": "0557234567",
- "email": "faisal@global-bridges.com",
- "address": "الرياض - حي الصحافة",
- "classification": "درجة أولى",
- "experience_years": 22,
- "completed_projects": 80,
- "ongoing_projects": 8,
- "min_project_value": 10000000,
- "max_project_value": 500000000,
- "specialties": "إنشاء جسور، جسور معلقة، جسور خرسانية، جسور معدنية",
- "rating": 4.9,
- "description": "شركة متخصصة في تنفيذ أعمال إنشاء الجسور والجسور المعلقة والجسور الخرسانية والجسور المعدنية"
- },
- {
- "id": "BRIDGE-002",
- "name": "مؤسسة الجسور الحديثة",
- "category": "أعمال الجسور",
- "subcategory": "صيانة جسور",
- "contact_person": "م. خالد العمري",
- "phone": "0557345678",
- "email": "khalid@modern-bridges.com",
- "address": "جدة - حي البوادي",
- "classification": "درجة ثانية",
- "experience_years": 14,
- "completed_projects": 60,
- "ongoing_projects": 5,
- "min_project_value": 2000000,
- "max_project_value": 100000000,
- "specialties": "صيانة جسور، ترميم، تقوية، توسعة",
- "rating": 4.7,
- "description": "مؤسسة متخصصة في تنفيذ أعمال صيانة الجسور والترميم والتقوية والتوسعة"
- },
- {
- "id": "BRIDGE-003",
- "name": "شركة الإنشاءات المتخصصة",
- "category": "أعمال الجسور",
- "subcategory": "جسور معقدة",
- "contact_person": "م. سلطان الشهري",
- "phone": "0557456789",
- "email": "sultan@specialized-construction.com",
- "address": "الدمام - حي الروضة",
- "classification": "درجة أولى",
- "experience_years": 25,
- "completed_projects": 90,
- "ongoing_projects": 7,
- "min_project_value": 20000000,
- "max_project_value": 800000000,
- "specialties": "جسور معقدة، تقاطعات متعددة المستويات، أنفاق، منشآت خاصة",
- "rating": 5.0,
- "description": "شركة متخصصة في تنفيذ أعمال الجسور المعقدة والتقاطعات متعددة المستويات والأنفاق والمنشآت الخاصة"
- }
- ])
-
- # 9. أعمال الحفر والردم
- subcontractors_data.extend([
- {
- "id": "EXCAV-001",
- "name": "شركة الحفريات الكبرى",
- "category": "أعمال الحفر والردم",
- "subcategory": "حفر كبير",
- "contact_person": "م. ناصر العتيبي",
- "phone": "0557567890",
- "email": "nasser@major-excavation.com",
- "address": "الرياض - حي العزيزية",
- "classification": "درجة أولى",
- "experience_years": 18,
- "completed_projects": 130,
- "ongoing_projects": 10,
- "min_project_value": 3000000,
- "max_project_value": 150000000,
- "specialties": "حفر كبير، نقل مواد، تسوية، تثبيت تربة",
- "rating": 4.8,
- "description": "شركة متخصصة في تنفيذ أعمال الحفر الكبير ونقل المواد والتسوية وتثبيت التربة"
- },
- {
- "id": "EXCAV-002",
- "name": "مؤسسة الحفر والردم",
- "category": "أعمال الحفر والردم",
- "subcategory": "حفر وردم",
- "contact_person": "م. سعيد القحطاني",
- "phone": "0557678901",
- "email": "saeed@excavation-backfill.com",
- "address": "جدة - حي السلامة",
- "classification": "درجة ثانية",
- "experience_years": 12,
- "completed_projects": 90,
- "ongoing_projects": 6,
- "min_project_value": 500000,
- "max_project_value": 30000000,
- "specialties": "حفر وردم، تسوية مواقع، دك تربة، تصريف مياه",
- "rating": 4.6,
- "description": "مؤسسة متخصصة في تنفيذ أعمال الحفر والردم وتسوية المواقع ودك التربة وتصريف المياه"
- },
- {
- "id": "EXCAV-003",
- "name": "شركة التربة المتخصصة",
- "category": "أعمال الحفر والردم",
- "subcategory": "معالجة تربة",
- "contact_person": "م. فهد الشمري",
- "phone": "0557789012",
- "email": "fahad@specialized-soil.com",
- "address": "الدمام - حي النزهة",
- "classification": "درجة أولى",
- "experience_years": 15,
- "completed_projects": 100,
- "ongoing_projects": 8,
- "min_project_value": 2000000,
- "max_project_value": 100000000,
- "specialties": "معالجة تربة، تثبيت، تحسين خواص، فحوصات",
- "rating": 4.8,
- "description": "شركة متخصصة في تنفيذ أعمال معالجة التربة والتثبيت وتحسين الخواص والفحوصات"
- }
- ])
-
- # 10. أعمال الخرسانة
- subcontractors_data.extend([
- {
- "id": "CONC-001",
- "name": "شركة الخرسانة المتميزة",
- "category": "أعمال الخرسانة",
- "subcategory": "خرسانة جاهزة",
- "contact_person": "م. عبدالرحمن الشهري",
- "phone": "0557890123",
- "email": "abdulrahman@premium-concrete.com",
- "address": "الرياض - حي الربيع",
- "classification": "درجة أولى",
- "experience_years": 20,
- "completed_projects": 200,
- "ongoing_projects": 15,
- "min_project_value": 2000000,
- "max_project_value": 200000000,
- "specialties": "خرسانة جاهزة، خرسانة خاصة، خرسانة مسلحة، خرسانة سابقة الإجهاد",
- "rating": 4.9,
- "description": "شركة متخصصة في تنفيذ أعمال الخرسانة الجاهزة والخرسانة الخاصة والخرسانة المسلحة والخرسانة سابقة الإجهاد"
- },
- {
- "id": "CONC-002",
- "name": "مؤسسة الهياكل الخرسانية",
- "category": "أعمال الخرسانة",
- "subcategory": "هياكل خرسانية",
- "contact_person": "م. ماجد العنزي",
- "phone": "0557901234",
- "email": "majed@concrete-structures.com",
- "address": "جدة - حي الروضة",
- "classification": "درجة ثانية",
- "experience_years": 15,
- "completed_projects": 120,
- "ongoing_projects": 8,
- "min_project_value": 1000000,
- "max_project_value": 80000000,
- "specialties": "هياكل خرسانية، أعمدة، أسقف، جدران",
- "rating": 4.7,
- "description": "مؤسسة متخصصة في تنفيذ أعمال الهياكل الخرسانية والأعمدة والأسقف والجدران"
- },
- {
- "id": "CONC-003",
- "name": "شركة الخرسانة المتخصصة",
- "category": "أعمال الخرسانة",
- "subcategory": "خرسانة خاصة",
- "contact_person": "م. خالد المالكي",
- "phone": "0558012345",
- "email": "khalid@specialized-concrete.com",
- "address": "الدمام - حي الشاطئ",
- "classification": "درجة أولى",
- "experience_years": 18,
- "completed_projects": 150,
- "ongoing_projects": 12,
- "min_project_value": 3000000,
- "max_project_value": 250000000,
- "specialties": "خرسانة خاصة، خرسانة عالية المقاومة، خرسانة مقاومة للكيماويات، خرسانة ذاتية الدمك",
- "rating": 4.9,
- "description": "شركة متخصصة في تنفيذ أعمال الخرسانة الخاصة والخرسانة عالية المقاومة والخرسانة المقاومة للكيماويات والخرسانة ذاتية الدمك"
- }
- ])
-
- # تخزين البيانات في حالة الجلسة
- st.session_state.subcontractors_catalog = pd.DataFrame(subcontractors_data)
-
- def render(self):
- """عرض واجهة كتالوج مقاولي الباطن"""
-
- st.markdown("## كتالوج مقاولي الباطن")
-
- # إنشاء تبويبات لعرض الكتالوج
- tabs = st.tabs([
- "عرض الكتالوج",
- "إضافة مقاول",
- "تحليل المقاولين",
- "استيراد/تصدير"
- ])
-
- with tabs[0]:
- self._render_catalog_view_tab()
-
- with tabs[1]:
- self._render_add_subcontractor_tab()
-
- with tabs[2]:
- self._render_analysis_tab()
-
- with tabs[3]:
- self._render_import_export_tab()
-
- def _render_catalog_view_tab(self):
- """عرض تبويب عرض الكتالوج"""
-
- st.markdown("### عرض كتالوج مقاولي الباطن")
-
- # استخراج البيانات
- subcontractors_df = st.session_state.subcontractors_catalog
-
- # إنشاء فلاتر للعرض
- col1, col2, col3 = st.columns(3)
-
- with col1:
- # فلتر حسب الفئة
- categories = ["الكل"] + sorted(subcontractors_df["category"].unique().tolist())
- selected_category = st.selectbox("اختر فئة المقاول", categories)
-
- with col2:
- # فلتر حسب الفئة الفرعية
- if selected_category != "الكل":
- subcategories = ["الكل"] + sorted(subcontractors_df[subcontractors_df["category"] == selected_category]["subcategory"].unique().tolist())
- else:
- subcategories = ["الكل"] + sorted(subcontractors_df["subcategory"].unique().tolist())
-
- selected_subcategory = st.selectbox("اختر التخصص", subcategories)
-
- with col3:
- # فلتر حسب التصنيف
- classifications = ["الكل"] + sorted(subcontractors_df["classification"].unique().tolist())
- selected_classification = st.selectbox("اختر التصنيف", classifications)
-
- # تطبيق الفلاتر
- filtered_df = subcontractors_df.copy()
-
- if selected_category != "الكل":
- filtered_df = filtered_df[filtered_df["category"] == selected_category]
-
- if selected_subcategory != "الكل":
- filtered_df = filtered_df[filtered_df["subcategory"] == selected_subcategory]
-
- if selected_classification != "الكل":
- filtered_df = filtered_df[filtered_df["classification"] == selected_classification]
-
- # عرض البيانات
- if not filtered_df.empty:
- # عرض عدد النتائج
- st.info(f"تم العثور على {len(filtered_df)} مقاول باطن")
-
- # عرض المقاولين في شكل بطاقات
- for i, (_, subcontractor) in enumerate(filtered_df.iterrows()):
- col1, col2 = st.columns([1, 3])
-
- with col1:
- # عرض صورة المقاول (استخدام صورة افتراضية)
- st.image("https://via.placeholder.com/150", caption=subcontractor["name"])
-
- with col2:
- # عرض معلومات المقاول
- st.markdown(f"**{subcontractor['name']}** (الكود: {subcontractor['id']})")
- st.markdown(f"الفئة: {subcontractor['category']} - {subcontractor['subcategory']}")
- st.markdown(f"التصنيف: {subcontractor['classification']} | الخبرة: {subcontractor['experience_years']} سنة")
- st.markdown(f"التقييم: {'⭐' * int(subcontractor['rating'])} ({subcontractor['rating']})")
- st.markdown(f"المشاريع المنجزة: {subcontractor['completed_projects']} | المشاريع الجارية: {subcontractor['ongoing_projects']}")
-
- # إضافة زر لعرض التفاصيل
- if st.button(f"عرض التفاصيل الكاملة", key=f"details_{subcontractor['id']}"):
- st.session_state.selected_subcontractor = subcontractor['id']
- self._show_subcontractor_details(subcontractor)
-
- st.markdown("---")
- else:
- st.warning("لا يوجد مقاولين يطابقون معايير البحث")
-
- def _show_subcontractor_details(self, subcontractor):
- """عرض تفاصيل المقاول"""
-
- st.markdown(f"## تفاصيل مقاول الباطن: {subcontractor['name']}")
-
- col1, col2 = st.columns([1, 2])
-
- with col1:
- # عرض صورة المقاول (استخدام صورة افتراضية)
- st.image("https://via.placeholder.com/300", caption=subcontractor["name"])
-
- with col2:
- # عرض المعلومات الأساسية
- st.markdown("### المعلومات الأساسية")
- st.markdown(f"**الكود:** {subcontractor['id']}")
- st.markdown(f"**الفئة:** {subcontractor['category']} - {subcontractor['subcategory']}")
- st.markdown(f"**التصنيف:** {subcontractor['classification']}")
- st.markdown(f"**سنوات الخبرة:** {subcontractor['experience_years']} سنة")
- st.markdown(f"**التقييم:** {'⭐' * int(subcontractor['rating'])} ({subcontractor['rating']})")
- st.markdown(f"**التخصصات:** {subcontractor['specialties']}")
- st.markdown(f"**الوصف:** {subcontractor['description']}")
-
- # عرض معلومات الاتصال
- st.markdown("### معلومات الاتصال")
-
- contact_col1, contact_col2 = st.columns(2)
-
- with contact_col1:
- st.markdown(f"**الشخص المسؤول:** {subcontractor['contact_person']}")
- st.markdown(f"**الهاتف:** {subcontractor['phone']}")
-
- with contact_col2:
- st.markdown(f"**البريد الإلكتروني:** {subcontractor['email']}")
- st.markdown(f"**العنوان:** {subcontractor['address']}")
-
- # عرض معلومات المشاريع
- st.markdown("### معلومات المشاريع")
-
- projects_col1, projects_col2 = st.columns(2)
-
- with projects_col1:
- st.markdown(f"**المشاريع المنجزة:** {subcontractor['completed_projects']}")
- st.markdown(f"**المشاريع الجارية:** {subcontractor['ongoing_projects']}")
-
- with projects_col2:
- st.markdown(f"**الحد الأدنى لقيمة المشروع:** {subcontractor['min_project_value']:,} ريال")
- st.markdown(f"**الحد الأقصى لقيمة المشروع:** {subcontractor['max_project_value']:,} ريال")
-
- # إضافة زر للتعديل
- if st.button("تعديل بيانات المقاول"):
- st.session_state.edit_subcontractor = subcontractor['id']
- # هنا يمكن إضافة منطق التعديل
-
- def _render_add_subcontractor_tab(self):
- """عرض تبويب إضافة مقاول"""
-
- st.markdown("### إضافة مقاول باطن جديد")
-
- # استخراج البيانات
- subcontractors_df = st.session_state.subcontractors_catalog
-
- # إنشاء نموذج إضافة مقاول
- with st.form("add_subcontractor_form"):
- st.markdown("#### المعلومات الأساسية")
-
- # الصف الأول
- col1, col2 = st.columns(2)
- with col1:
- subcontractor_id = st.text_input("كود المقاول", value=f"SUB-{len(subcontractors_df) + 1:03d}")
- subcontractor_name = st.text_input("اسم المقاول", placeholder="مثال: شركة الأنظمة الكهربائية المتكاملة")
-
- with col2:
- # استخراج الفئات والفئات الفرعية الموجودة
- categories = sorted(subcontractors_df["category"].unique().tolist())
- subcontractor_category = st.selectbox("فئة المقاول", categories)
-
- # استخراج الفئات الفرعية بناءً على الفئة المختارة
- subcategories = sorted(subcontractors_df[subcontractors_df["category"] == subcontractor_category]["subcategory"].unique().tolist())
- subcontractor_subcategory = st.selectbox("التخصص", subcategories)
-
- # الصف الثاني
- col1, col2 = st.columns(2)
- with col1:
- subcontractor_classification = st.selectbox("التصنيف", ["درجة أولى", "درجة ثانية", "درجة ثالثة", "درجة رابعة", "درجة خامسة"])
- with col2:
- subcontractor_experience_years = st.number_input("سنوات الخبرة", min_value=1, max_value=50, step=1)
-
- # معلومات الاتصال
- st.markdown("#### معلومات الاتصال")
-
- col1, col2 = st.columns(2)
- with col1:
- subcontractor_contact_person = st.text_input("الشخص المسؤول", placeholder="مثال: م. خالد العتيبي")
- subcontractor_phone = st.text_input("الهاتف", placeholder="مثال: 0555123456")
-
- with col2:
- subcontractor_email = st.text_input("البريد الإلكتروني", placeholder="مثال: info@company.com")
- subcontractor_address = st.text_input("العنوان", placeholder="مثال: الرياض - حي العليا")
-
- # معلومات المشاريع
- st.markdown("#### معلومات المشاريع")
-
- col1, col2 = st.columns(2)
- with col1:
- subcontractor_completed_projects = st.number_input("عدد المشاريع المنجزة", min_value=0, step=1)
- subcontractor_ongoing_projects = st.number_input("عدد المشاريع الجارية", min_value=0, step=1)
-
- with col2:
- subcontractor_min_project_value = st.number_input("الحد الأدنى لقيمة المشروع (ريال)", min_value=0, step=100000)
- subcontractor_max_project_value = st.number_input("الحد الأقصى لقيمة المشروع (ريال)", min_value=0, step=1000000)
-
- # معلومات إضافية
- st.markdown("#### معلومات إضافية")
-
- subcontractor_specialties = st.text_area("التخصصات", placeholder="مثال: تمديدات كهربائية، محطات توزيع، لوحات توزيع، أنظمة إنارة")
- subcontractor_rating = st.slider("التقييم", min_value=1.0, max_value=5.0, step=0.1, value=4.0)
- subcontractor_description = st.text_area("وصف المقاول", placeholder="أدخل وصفاً تفصيلياً للمقاول")
-
- # زر الإضافة
- submit_button = st.form_submit_button("إضافة المقاول")
-
- if submit_button:
- # التحقق من البيانات
- if not subcontractor_name or not subcontractor_category or not subcontractor_subcategory:
- st.error("يرجى إدخال المعلومات الأساسية للمقاول")
- else:
- # إنشاء مقاول جديد
- new_subcontractor = {
- "id": subcontractor_id,
- "name": subcontractor_name,
- "category": subcontractor_category,
- "subcategory": subcontractor_subcategory,
- "contact_person": subcontractor_contact_person,
- "phone": subcontractor_phone,
- "email": subcontractor_email,
- "address": subcontractor_address,
- "classification": subcontractor_classification,
- "experience_years": subcontractor_experience_years,
- "completed_projects": subcontractor_completed_projects,
- "ongoing_projects": subcontractor_ongoing_projects,
- "min_project_value": subcontractor_min_project_value,
- "max_project_value": subcontractor_max_project_value,
- "specialties": subcontractor_specialties,
- "rating": subcontractor_rating,
- "description": subcontractor_description
- }
-
- # إضافة المقاول إلى الكتالوج
- st.session_state.subcontractors_catalog = pd.concat([
- st.session_state.subcontractors_catalog,
- pd.DataFrame([new_subcontractor])
- ], ignore_index=True)
-
- st.success(f"تمت إضافة المقاول {subcontractor_name} بنجاح!")
-
- def _render_analysis_tab(self):
- """عرض تبويب تحليل المقاولين"""
-
- st.markdown("### تحليل مقاولي الباطن")
-
- # استخراج البيانات
- subcontractors_df = st.session_state.subcontractors_catalog
-
- # تحليل توزيع المقاولين حسب الفئة
- st.markdown("#### توزيع المقاولين حسب الفئة")
-
- # حساب عدد المقاولين لكل فئة
- category_counts = subcontractors_df["category"].value_counts().reset_index()
- category_counts.columns = ["الفئة", "عدد المقاولين"]
-
- # عرض الجدول
- st.dataframe(category_counts, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة
- fig = px.bar(
- category_counts,
- x="الفئة",
- y="عدد المقاولين",
- title="توزيع مقاولي الباطن حسب الفئة",
- color="الفئة",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل توزيع المقاولين حسب التصنيف
- st.markdown("#### توزيع المقاولين حسب التصنيف")
-
- # حساب عدد المقاولين لكل تصنيف
- classification_counts = subcontractors_df["classification"].value_counts().reset_index()
- classification_counts.columns = ["التصنيف", "عدد المقاولين"]
-
- # إنشاء رسم بياني دائري
- fig = px.pie(
- classification_counts,
- values="عدد المقاولين",
- names="التصنيف",
- title="توزيع مقاولي الباطن حسب التصنيف",
- color="التصنيف"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل متوسط التقييم حسب الفئة
- st.markdown("#### متوسط التقييم حسب الفئة")
-
- # حساب متوسط التقييم لكل فئة
- category_ratings = subcontractors_df.groupby("category").agg({
- "rating": "mean"
- }).reset_index()
-
- # تغيير أسماء الأعمدة
- category_ratings.columns = ["الفئة", "متوسط التقييم"]
-
- # عرض الجدول
- st.dataframe(category_ratings, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة
- fig = px.bar(
- category_ratings,
- x="الفئة",
- y="متوسط التقييم",
- title="متوسط تقييم مقاولي الباطن حسب الفئة",
- color="الفئة",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل متوسط سنوات الخبرة حسب التصنيف
- st.markdown("#### متوسط سنوات الخبرة حسب التصنيف")
-
- # حساب متوسط سنوات الخبرة لكل تصنيف
- classification_experience = subcontractors_df.groupby("classification").agg({
- "experience_years": "mean"
- }).reset_index()
-
- # تغيير أسماء الأعمدة
- classification_experience.columns = ["التصنيف", "متوسط سنوات الخبرة"]
-
- # عرض الجدول
- st.dataframe(classification_experience, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة
- fig = px.bar(
- classification_experience,
- x="التصنيف",
- y="متوسط سنوات الخبرة",
- title="متوسط سنوات خبرة مقاولي الباطن حسب التصنيف",
- color="التصنيف",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل المقاولين الأعلى تقييماً
- st.markdown("#### المقاولين الأعلى تقييماً")
-
- # استخراج المقاولين الأعلى تقييماً
- top_rated = subcontractors_df.sort_values(by="rating", ascending=False).head(10)
-
- # إنشاء جدول للعرض
- top_rated_display = top_rated[["name", "category", "classification", "rating", "experience_years"]].copy()
- top_rated_display.columns = ["اسم المقاول", "الفئة", "التصنيف", "التقييم", "سنوات الخبرة"]
-
- # عرض الجدول
- st.dataframe(top_rated_display, use_container_width=True)
-
- # تحليل المقاولين الأكثر خبرة
- st.markdown("#### المقاولين الأكثر خبرة")
-
- # استخراج المقاولين الأكثر خبرة
- most_experienced = subcontractors_df.sort_values(by="experience_years", ascending=False).head(10)
-
- # إنشاء جدول للعرض
- most_experienced_display = most_experienced[["name", "category", "classification", "experience_years", "rating"]].copy()
- most_experienced_display.columns = ["اسم المقاول", "الفئة", "التصنيف", "سنوات الخبرة", "التقييم"]
-
- # عرض الجدول
- st.dataframe(most_experienced_display, use_container_width=True)
-
- def _render_import_export_tab(self):
- """عرض تبويب استيراد/تصدير"""
-
- st.markdown("### استيراد وتصدير بيانات مقاولي الباطن")
-
- # استيراد البيانات
- st.markdown("#### استيراد البيانات")
-
- uploaded_file = st.file_uploader("اختر ملف Excel لاستيراد بيانات مقاولي الباطن", type=["xlsx", "xls"])
-
- if uploaded_file is not None:
- try:
- # قراءة الملف
- imported_df = pd.read_excel(uploaded_file)
-
- # عرض البيانات المستوردة
- st.dataframe(imported_df, use_container_width=True)
-
- # زر الاستيراد
- if st.button("استيراد البيانات"):
- # التحقق من وجود الأعمدة المطلوبة
- required_columns = ["id", "name", "category", "subcategory", "classification"]
-
- if all(col in imported_df.columns for col in required_columns):
- # دمج البيانات المستوردة مع البيانات الحالية
- st.session_state.subcontractors_catalog = pd.concat([
- st.session_state.subcontractors_catalog,
- imported_df
- ], ignore_index=True).drop_duplicates(subset=["id"])
-
- st.success(f"تم استيراد {len(imported_df)} مقاول باطن بنجاح!")
- else:
- st.error("الملف المستورد لا يحتوي على الأعمدة المطلوبة")
-
- except Exception as e:
- st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}")
-
- # تصدير البيانات
- st.markdown("#### تصدير البيانات")
-
- # اختيار تنسيق التصدير
- export_format = st.radio("اختر تنسيق التصدير", ["Excel", "CSV", "JSON"], horizontal=True)
-
- if st.button("تصدير البيانات"):
- # استخراج البيانات
- subcontractors_df = st.session_state.subcontractors_catalog
-
- # تصدير البيانات حسب التنسيق المختار
- if export_format == "Excel":
- # تصدير إلى Excel
- output = io.BytesIO()
- with pd.ExcelWriter(output, engine="openpyxl") as writer:
- subcontractors_df.to_excel(writer, index=False, sheet_name="Subcontractors")
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف Excel",
- data=output.getvalue(),
- file_name="subcontractors_catalog.xlsx",
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
- )
-
- elif export_format == "CSV":
- # تصدير إلى CSV
- csv_data = subcontractors_df.to_csv(index=False)
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف CSV",
- data=csv_data,
- file_name="subcontractors_catalog.csv",
- mime="text/csv"
- )
-
- else: # JSON
- # تصدير إلى JSON
- json_data = subcontractors_df.to_json(orient="records", force_ascii=False)
-
- # تحميل الملف
- st.download_button(
- label="تنزيل ملف JSON",
- data=json_data,
- file_name="subcontractors_catalog.json",
- mime="application/json"
- )
-
- def get_subcontractor_by_id(self, subcontractor_id):
- """الحصول على مقاول بواسطة الكود"""
-
- subcontractors_df = st.session_state.subcontractors_catalog
- subcontractor = subcontractors_df[subcontractors_df["id"] == subcontractor_id]
-
- if not subcontractor.empty:
- return subcontractor.iloc[0].to_dict()
-
- return None
-
- def get_subcontractors_by_category(self, category):
- """الحصول على المقاولين حسب الفئة"""
-
- subcontractors_df = st.session_state.subcontractors_catalog
- subcontractors = subcontractors_df[subcontractors_df["category"] == category]
-
- if not subcontractors.empty:
- return subcontractors.to_dict(orient="records")
-
- return []
-
- def get_top_rated_subcontractors(self, category=None, limit=5):
- """الحصول على المقاولين الأعلى تقييماً"""
-
- subcontractors_df = st.session_state.subcontractors_catalog
-
- if category:
- filtered_df = subcontractors_df[subcontractors_df["category"] == category]
- else:
- filtered_df = subcontractors_df
-
- top_rated = filtered_df.sort_values(by="rating", ascending=False).head(limit)
-
- if not top_rated.empty:
- return top_rated.to_dict(orient="records")
-
- return []
+"""
+كتالوج مقاولي الباطن - وحدة إدارة مقاولي الباطن
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import plotly.express as px
+import os
+import json
+from datetime import datetime
+import io
+
+class SubcontractorsCatalog:
+ """كتالوج مقاولي الباطن"""
+
+ def __init__(self):
+ """تهيئة كتالوج مقاولي الباطن"""
+
+ # تهيئة حالة الجلسة لكتالوج مقاولي الباطن
+ if 'subcontractors_catalog' not in st.session_state:
+ # إنشاء بيانات افتراضية لمقاولي الباطن
+ self._initialize_subcontractors_catalog()
+
+ def _initialize_subcontractors_catalog(self):
+ """تهيئة بيانات كتالوج مقاولي الباطن"""
+
+ # تعريف فئات مقاولي الباطن
+ subcontractor_categories = [
+ "أعمال الكهرباء",
+ "أعمال ITC",
+ "أعمال CCTV",
+ "أنظمة التحكم في الوصول",
+ "شبكات الري",
+ "أعمال الصرف الصحي",
+ "أعمال الطرق",
+ "أعمال الجسور",
+ "أعمال الحفر والردم",
+ "أعمال الخرسانة"
+ ]
+
+ # إنشاء قائمة مقاولي الباطن
+ subcontractors_data = []
+
+ # 1. أعمال الكهرباء
+ subcontractors_data.extend([
+ {
+ "id": "ELEC-001",
+ "name": "شركة الأنظمة الكهربائية المتكاملة",
+ "category": "أعمال الكهرباء",
+ "subcategory": "تمديدات كهربائية",
+ "contact_person": "م. خالد العتيبي",
+ "phone": "0555123456",
+ "email": "khalid@ies-sa.com",
+ "address": "الرياض - حي العليا",
+ "classification": "درجة أولى",
+ "experience_years": 15,
+ "completed_projects": 120,
+ "ongoing_projects": 8,
+ "min_project_value": 500000,
+ "max_project_value": 50000000,
+ "specialties": "تمديدات كهربائية، محطات توزيع، لوحات توزيع، أنظمة إنارة",
+ "rating": 4.8,
+ "description": "شركة متخصصة في تنفيذ أعمال التمديدات الكهربائية ومحطات التوزيع واللوحات الكهربائية وأنظمة الإنارة للمشاريع الكبرى"
+ },
+ {
+ "id": "ELEC-002",
+ "name": "مؤسسة النور للمقاولات الكهربائية",
+ "category": "أعمال الكهرباء",
+ "subcategory": "إنارة طرق",
+ "contact_person": "م. سعد القحطاني",
+ "phone": "0555234567",
+ "email": "saad@alnoor-elec.com",
+ "address": "جدة - حي الروضة",
+ "classification": "درجة ثانية",
+ "experience_years": 10,
+ "completed_projects": 75,
+ "ongoing_projects": 5,
+ "min_project_value": 200000,
+ "max_project_value": 20000000,
+ "specialties": "إنارة طرق، إنارة ميادين، إنارة حدائق، أنظمة تحكم",
+ "rating": 4.5,
+ "description": "مؤسسة متخصصة في تنفيذ أعمال إنارة الطرق والميادين والحدائق وأنظمة التحكم في الإنارة"
+ },
+ {
+ "id": "ELEC-003",
+ "name": "شركة الطاقة للمقاولات الكهربائية",
+ "category": "أعمال الكهرباء",
+ "subcategory": "محطات كهربائية",
+ "contact_person": "م. فهد الشمري",
+ "phone": "0555345678",
+ "email": "fahad@energy-sa.com",
+ "address": "الدمام - حي الفيصلية",
+ "classification": "درجة أولى",
+ "experience_years": 18,
+ "completed_projects": 150,
+ "ongoing_projects": 10,
+ "min_project_value": 1000000,
+ "max_project_value": 100000000,
+ "specialties": "محطات توزيع، محطات تحويل، خطوط نقل، أنظمة حماية",
+ "rating": 4.9,
+ "description": "شركة متخصصة في تنفيذ أعمال محطات التوزيع والتحويل وخطوط النقل وأنظمة الحماية الكهربائية"
+ }
+ ])
+
+ # 2. أعمال ITC
+ subcontractors_data.extend([
+ {
+ "id": "ITC-001",
+ "name": "شركة التقنية المتكاملة للاتصالات",
+ "category": "أعمال ITC",
+ "subcategory": "شبكات اتصالات",
+ "contact_person": "م. محمد العمري",
+ "phone": "0555456789",
+ "email": "mohammed@itc-sa.com",
+ "address": "الرياض - حي الملز",
+ "classification": "درجة أولى",
+ "experience_years": 12,
+ "completed_projects": 90,
+ "ongoing_projects": 7,
+ "min_project_value": 500000,
+ "max_project_value": 40000000,
+ "specialties": "شبكات اتصالات، أنظمة مراقبة، أنظمة صوتية، شبكات ألياف ضوئية",
+ "rating": 4.7,
+ "description": "شركة متخصصة في تنفيذ أعمال شبكات الاتصالات وأنظمة المراقبة والأنظمة الصوتية وشبكات الألياف الضوئية"
+ },
+ {
+ "id": "ITC-002",
+ "name": "مؤسسة الاتصالات المتقدمة",
+ "category": "أعمال ITC",
+ "subcategory": "أنظمة معلومات",
+ "contact_person": "م. عبدالله الزهراني",
+ "phone": "0555567890",
+ "email": "abdullah@advanced-comm.com",
+ "address": "جدة - حي السلامة",
+ "classification": "درجة ثانية",
+ "experience_years": 8,
+ "completed_projects": 60,
+ "ongoing_projects": 4,
+ "min_project_value": 200000,
+ "max_project_value": 15000000,
+ "specialties": "أنظمة معلومات، شبكات داخلية، أنظمة تخزين، أنظمة حماية",
+ "rating": 4.4,
+ "description": "مؤسسة متخصصة في تنفيذ أعمال أنظمة المعلومات والشبكات الداخلية وأنظمة التخزين وأنظمة الحماية"
+ },
+ {
+ "id": "ITC-003",
+ "name": "شركة البيانات للاتصالات وتقنية المعلومات",
+ "category": "أعمال ITC",
+ "subcategory": "بنية تحتية للاتصالات",
+ "contact_person": "م. سلطان المالكي",
+ "phone": "0555678901",
+ "email": "sultan@data-sa.com",
+ "address": "الدمام - حي الشاطئ",
+ "classification": "درجة أولى",
+ "experience_years": 15,
+ "completed_projects": 110,
+ "ongoing_projects": 9,
+ "min_project_value": 800000,
+ "max_project_value": 60000000,
+ "specialties": "بنية تحتية للاتصالات، أبراج اتصالات، محطات إرسال، شبكات ألياف ضوئية",
+ "rating": 4.8,
+ "description": "شركة متخصصة في تنفيذ أعمال البنية التحتية للاتصالات وأبراج الاتصالات ومحطات الإرسال وشبكات الألياف الضوئية"
+ }
+ ])
+
+ # 3. أعمال CCTV
+ subcontractors_data.extend([
+ {
+ "id": "CCTV-001",
+ "name": "شركة الأمان لأنظمة المراقبة",
+ "category": "أعمال CCTV",
+ "subcategory": "أنظمة مراقبة",
+ "contact_person": "م. ناصر الحربي",
+ "phone": "0555789012",
+ "email": "nasser@security-sa.com",
+ "address": "الرياض - حي النزهة",
+ "classification": "درجة ثانية",
+ "experience_years": 10,
+ "completed_projects": 85,
+ "ongoing_projects": 6,
+ "min_project_value": 100000,
+ "max_project_value": 10000000,
+ "specialties": "أنظمة مراقبة، كاميرات أمنية، أنظمة تسجيل، أنظمة تحكم",
+ "rating": 4.6,
+ "description": "شركة متخصصة في تنفيذ أعمال أنظمة المراقبة والكاميرات الأمنية وأنظمة التسجيل وأنظمة التحكم"
+ },
+ {
+ "id": "CCTV-002",
+ "name": "مؤسسة الحماية للأنظمة الأمنية",
+ "category": "أعمال CCTV",
+ "subcategory": "أنظمة أمنية",
+ "contact_person": "م. سعيد الغامدي",
+ "phone": "0555890123",
+ "email": "saeed@protection-sa.com",
+ "address": "جدة - حي الصفا",
+ "classification": "درجة ثالثة",
+ "experience_years": 7,
+ "completed_projects": 50,
+ "ongoing_projects": 3,
+ "min_project_value": 50000,
+ "max_project_value": 5000000,
+ "specialties": "أنظمة أمنية، كاميرات مراقبة، أنظمة إنذار، أنظمة دخول",
+ "rating": 4.3,
+ "description": "مؤسسة متخصصة في تنفيذ أعمال الأنظمة الأمنية وكاميرات المراقبة وأنظمة الإنذار وأنظمة الدخول"
+ },
+ {
+ "id": "CCTV-003",
+ "name": "شركة المراقبة الذكية",
+ "category": "أعمال CCTV",
+ "subcategory": "أنظمة مراقبة ذكية",
+ "contact_person": "م. عمر السعدي",
+ "phone": "0555901234",
+ "email": "omar@smart-surveillance.com",
+ "address": "الدمام - حي الخليج",
+ "classification": "درجة أولى",
+ "experience_years": 12,
+ "completed_projects": 95,
+ "ongoing_projects": 8,
+ "min_project_value": 300000,
+ "max_project_value": 25000000,
+ "specialties": "أنظمة مراقبة ذكية، تحليل فيديو، تعرف على الوجوه، تتبع حركة",
+ "rating": 4.8,
+ "description": "شركة متخصصة في تنفيذ أعمال أنظمة المراقبة الذكية وتحليل الفيديو والتعرف على الوجوه وتتبع الحركة"
+ }
+ ])
+
+ # 4. أنظمة التحكم في الوصول
+ subcontractors_data.extend([
+ {
+ "id": "ACCESS-001",
+ "name": "شركة التحكم الأمني",
+ "category": "أنظمة التحكم في الوصول",
+ "subcategory": "أنظمة تحكم",
+ "contact_person": "م. فيصل العنزي",
+ "phone": "0556012345",
+ "email": "faisal@security-control.com",
+ "address": "الرياض - حي الورود",
+ "classification": "درجة ثانية",
+ "experience_years": 9,
+ "completed_projects": 70,
+ "ongoing_projects": 5,
+ "min_project_value": 100000,
+ "max_project_value": 8000000,
+ "specialties": "أنظمة تحكم في الدخول، بصمات، بطاقات ذكية، أقفال إلكترونية",
+ "rating": 4.5,
+ "description": "شركة متخصصة في تنفيذ أعمال أنظمة التحكم في الدخول والبصمات والبطاقات الذكية والأقفال الإلكترونية"
+ },
+ {
+ "id": "ACCESS-002",
+ "name": "مؤسسة الوصول الآمن",
+ "category": "أنظمة التحكم في الوصول",
+ "subcategory": "أنظمة أمنية",
+ "contact_person": "م. ماجد الدوسري",
+ "phone": "0556123456",
+ "email": "majed@safe-access.com",
+ "address": "جدة - حي المروة",
+ "classification": "درجة ثالثة",
+ "experience_years": 6,
+ "completed_projects": 40,
+ "ongoing_projects": 3,
+ "min_project_value": 50000,
+ "max_project_value": 3000000,
+ "specialties": "أنظمة أمنية، بوابات إلكترونية، حواجز آلية، كاميرات تعرف",
+ "rating": 4.2,
+ "description": "مؤسسة متخصصة في تنفيذ أعمال الأنظمة الأمنية والبوابات الإلكترونية والحواجز الآلية وكاميرات التعرف"
+ },
+ {
+ "id": "ACCESS-003",
+ "name": "شركة الأمن الذكي",
+ "category": "أنظمة التحكم في الوصول",
+ "subcategory": "أنظمة متكاملة",
+ "contact_person": "م. طارق الشهري",
+ "phone": "0556234567",
+ "email": "tariq@smart-security.com",
+ "address": "الدمام - حي النور",
+ "classification": "درجة أولى",
+ "experience_years": 14,
+ "completed_projects": 100,
+ "ongoing_projects": 7,
+ "min_project_value": 500000,
+ "max_project_value": 20000000,
+ "specialties": "أنظمة متكاملة، تحكم مركزي، مراقبة ذكية، تحليل سلوك",
+ "rating": 4.7,
+ "description": "شركة متخصصة في تنفيذ أعمال الأنظمة المتكاملة والتحكم المركزي والمراقبة الذكية وتحليل السلوك"
+ }
+ ])
+
+ # 5. شبكات الري
+ subcontractors_data.extend([
+ {
+ "id": "IRR-001",
+ "name": "شركة الواحة لأنظمة الري",
+ "category": "شبكات الري",
+ "subcategory": "أنظمة ري",
+ "contact_person": "م. سامي المطيري",
+ "phone": "0556345678",
+ "email": "sami@oasis-irrigation.com",
+ "address": "الرياض - حي الياسمين",
+ "classification": "درجة ثانية",
+ "experience_years": 11,
+ "completed_projects": 80,
+ "ongoing_projects": 6,
+ "min_project_value": 200000,
+ "max_project_value": 15000000,
+ "specialties": "أنظمة ري، شبكات مياه، مضخات، أنظمة تحكم",
+ "rating": 4.6,
+ "description": "شركة متخصصة في تنفيذ أعمال أنظمة الري وشبكات المياه والمضخات وأنظمة التحكم"
+ },
+ {
+ "id": "IRR-002",
+ "name": "مؤسسة الخضراء للري والزراعة",
+ "category": "شبكات الري",
+ "subcategory": "ري زراعي",
+ "contact_person": "م. عبدالرحمن الحربي",
+ "phone": "0556456789",
+ "email": "abdulrahman@green-irrigation.com",
+ "address": "جدة - حي الفيحاء",
+ "classification": "درجة ثالثة",
+ "experience_years": 8,
+ "completed_projects": 55,
+ "ongoing_projects": 4,
+ "min_project_value": 100000,
+ "max_project_value": 5000000,
+ "specialties": "ري زراعي، ري بالتنقيط، ري بالرش، أنظمة تسميد",
+ "rating": 4.4,
+ "description": "مؤسسة متخصصة في تنفيذ أعمال الري الزراعي والري بالتنقيط والري بالرش وأنظمة التسميد"
+ },
+ {
+ "id": "IRR-003",
+ "name": "شركة المياه الذكية",
+ "category": "شبكات الري",
+ "subcategory": "أنظمة ري ذكية",
+ "contact_person": "م. خالد السبيعي",
+ "phone": "0556567890",
+ "email": "khalid@smart-water.com",
+ "address": "الدمام - حي الفردوس",
+ "classification": "درجة أولى",
+ "experience_years": 13,
+ "completed_projects": 90,
+ "ongoing_projects": 7,
+ "min_project_value": 500000,
+ "max_project_value": 25000000,
+ "specialties": "أنظمة ري ذكية، تحكم عن بعد، استشعار رطوبة، توفير مياه",
+ "rating": 4.8,
+ "description": "شركة متخصصة في تنفيذ أعمال أنظمة الري الذكية والتحكم عن بعد واستشعار الرطوبة وتوفير المياه"
+ }
+ ])
+
+ # 6. أعمال الصرف الصحي
+ subcontractors_data.extend([
+ {
+ "id": "SEW-001",
+ "name": "شركة البنية التحتية للصرف الصحي",
+ "category": "أعمال الصرف الصحي",
+ "subcategory": "شبكات صرف",
+ "contact_person": "م. عبدالعزيز الشمري",
+ "phone": "0556678901",
+ "email": "abdulaziz@infrastructure-sewage.com",
+ "address": "الرياض - حي الملقا",
+ "classification": "درجة أولى",
+ "experience_years": 16,
+ "completed_projects": 120,
+ "ongoing_projects": 9,
+ "min_project_value": 1000000,
+ "max_project_value": 80000000,
+ "specialties": "شبكات صرف، محطات ضخ، محطات معالجة، خطوط رئيسية",
+ "rating": 4.9,
+ "description": "شركة متخصصة في تنفيذ أعمال شبكات الصرف ومحطات الضخ ومحطات المعالجة والخطوط الرئيسية"
+ },
+ {
+ "id": "SEW-002",
+ "name": "مؤسسة الصرف المتكاملة",
+ "category": "أعمال الصرف الصحي",
+ "subcategory": "صرف داخلي",
+ "contact_person": "م. فهد العتيبي",
+ "phone": "0556789012",
+ "email": "fahad@integrated-sewage.com",
+ "address": "جدة - حي الحمراء",
+ "classification": "درجة ثانية",
+ "experience_years": 9,
+ "completed_projects": 65,
+ "ongoing_projects": 5,
+ "min_project_value": 300000,
+ "max_project_value": 20000000,
+ "specialties": "صرف داخلي، تمديدات صحية، غرف تفتيش، خزانات تحليل",
+ "rating": 4.5,
+ "description": "مؤسسة متخصصة في تنفيذ أعمال الصرف الداخلي والتمديدات الصحية وغرف التفتيش وخزانات التحليل"
+ },
+ {
+ "id": "SEW-003",
+ "name": "شركة معالجة المياه",
+ "category": "أعمال الصرف الصحي",
+ "subcategory": "محطات معالجة",
+ "contact_person": "م. سلطان القحطاني",
+ "phone": "0556890123",
+ "email": "sultan@water-treatment.com",
+ "address": "الدمام - حي الأنوار",
+ "classification": "درجة أولى",
+ "experience_years": 18,
+ "completed_projects": 130,
+ "ongoing_projects": 10,
+ "min_project_value": 2000000,
+ "max_project_value": 100000000,
+ "specialties": "محطات معالجة، تنقية مياه، إعادة تدوير، أنظمة تحكم",
+ "rating": 4.9,
+ "description": "شركة متخصصة في تنفيذ أعمال محطات المعالجة وتنقية المياه وإعادة التدوير وأنظمة التحكم"
+ }
+ ])
+
+ # 7. أعمال الطرق
+ subcontractors_data.extend([
+ {
+ "id": "ROAD-001",
+ "name": "شركة الطرق الحديثة",
+ "category": "أعمال الطرق",
+ "subcategory": "إنشاء طرق",
+ "contact_person": "م. محمد الحارثي",
+ "phone": "0556901234",
+ "email": "mohammed@modern-roads.com",
+ "address": "الرياض - حي النخيل",
+ "classification": "درجة أولى",
+ "experience_years": 20,
+ "completed_projects": 150,
+ "ongoing_projects": 12,
+ "min_project_value": 5000000,
+ "max_project_value": 200000000,
+ "specialties": "إنشاء طرق، تقاطعات، أنفاق، جسور صغيرة",
+ "rating": 4.9,
+ "description": "شركة متخصصة في تنفيذ أعمال إنشاء الطرق والتقاطعات والأنفاق والجسور الصغيرة"
+ },
+ {
+ "id": "ROAD-002",
+ "name": "مؤسسة الطرق السريعة",
+ "category": "أعمال الطرق",
+ "subcategory": "رصف طرق",
+ "contact_person": "م. سعد الغامدي",
+ "phone": "0557012345",
+ "email": "saad@highway-contractors.com",
+ "address": "جدة - حي الشرفية",
+ "classification": "درجة ثانية",
+ "experience_years": 12,
+ "completed_projects": 85,
+ "ongoing_projects": 7,
+ "min_project_value": 1000000,
+ "max_project_value": 50000000,
+ "specialties": "رصف طرق، سفلتة، خلطات إسفلتية، علامات مرورية",
+ "rating": 4.6,
+ "description": "مؤسسة متخصصة في تنفيذ أعمال رصف الطرق والسفلتة والخلطات الإسفلتية والعلامات المرورية"
+ },
+ {
+ "id": "ROAD-003",
+ "name": "شركة البنية التحتية للطرق",
+ "category": "أعمال الطرق",
+ "subcategory": "بنية تحتية",
+ "contact_person": "م. عبدالله المالكي",
+ "phone": "0557123456",
+ "email": "abdullah@infrastructure-roads.com",
+ "address": "الدمام - حي الفيصلية",
+ "classification": "درجة أولى",
+ "experience_years": 17,
+ "completed_projects": 120,
+ "ongoing_projects": 10,
+ "min_project_value": 3000000,
+ "max_project_value": 150000000,
+ "specialties": "بنية تحتية، تصريف مياه، قنوات، عبارات",
+ "rating": 4.8,
+ "description": "شركة متخصصة في تنفيذ أعمال البنية التحتية للطرق وتصريف المياه والقنوات والعبارات"
+ }
+ ])
+
+ # 8. أعمال الجسور
+ subcontractors_data.extend([
+ {
+ "id": "BRIDGE-001",
+ "name": "شركة الجسور العالمية",
+ "category": "أعمال الجسور",
+ "subcategory": "إنشاء جسور",
+ "contact_person": "م. فيصل الدوسري",
+ "phone": "0557234567",
+ "email": "faisal@global-bridges.com",
+ "address": "الرياض - حي الصحافة",
+ "classification": "درجة أولى",
+ "experience_years": 22,
+ "completed_projects": 80,
+ "ongoing_projects": 8,
+ "min_project_value": 10000000,
+ "max_project_value": 500000000,
+ "specialties": "إنشاء جسور، جسور معلقة، جسور خرسانية، جسور معدنية",
+ "rating": 4.9,
+ "description": "شركة متخصصة في تنفيذ أعمال إنشاء الجسور والجسور المعلقة والجسور الخرسانية والجسور المعدنية"
+ },
+ {
+ "id": "BRIDGE-002",
+ "name": "مؤسسة الجسور الحديثة",
+ "category": "أعمال الجسور",
+ "subcategory": "صيانة جسور",
+ "contact_person": "م. خالد العمري",
+ "phone": "0557345678",
+ "email": "khalid@modern-bridges.com",
+ "address": "جدة - حي البوادي",
+ "classification": "درجة ثانية",
+ "experience_years": 14,
+ "completed_projects": 60,
+ "ongoing_projects": 5,
+ "min_project_value": 2000000,
+ "max_project_value": 100000000,
+ "specialties": "صيانة جسور، ترميم، تقوية، توسعة",
+ "rating": 4.7,
+ "description": "مؤسسة متخصصة في تنفيذ أعمال صيانة الجسور والترميم والتقوية والتوسعة"
+ },
+ {
+ "id": "BRIDGE-003",
+ "name": "شركة الإنشاءات المتخصصة",
+ "category": "أعمال الجسور",
+ "subcategory": "جسور معقدة",
+ "contact_person": "م. سلطان الشهري",
+ "phone": "0557456789",
+ "email": "sultan@specialized-construction.com",
+ "address": "الدمام - حي الروضة",
+ "classification": "درجة أولى",
+ "experience_years": 25,
+ "completed_projects": 90,
+ "ongoing_projects": 7,
+ "min_project_value": 20000000,
+ "max_project_value": 800000000,
+ "specialties": "جسور معقدة، تقاطعات متعددة المستويات، أنفاق، منشآت خاصة",
+ "rating": 5.0,
+ "description": "شركة متخصصة في تنفيذ أعمال الجسور المعقدة والتقاطعات متعددة المستويات والأنفاق والمنشآت الخاصة"
+ }
+ ])
+
+ # 9. أعمال الحفر والردم
+ subcontractors_data.extend([
+ {
+ "id": "EXCAV-001",
+ "name": "شركة الحفريات الكبرى",
+ "category": "أعمال الحفر والردم",
+ "subcategory": "حفر كبير",
+ "contact_person": "م. ناصر العتيبي",
+ "phone": "0557567890",
+ "email": "nasser@major-excavation.com",
+ "address": "الرياض - حي العزيزية",
+ "classification": "درجة أولى",
+ "experience_years": 18,
+ "completed_projects": 130,
+ "ongoing_projects": 10,
+ "min_project_value": 3000000,
+ "max_project_value": 150000000,
+ "specialties": "حفر كبير، نقل مواد، تسوية، تثبيت تربة",
+ "rating": 4.8,
+ "description": "شركة متخصصة في تنفيذ أعمال الحفر الكبير ونقل المواد والتسوية وتثبيت التربة"
+ },
+ {
+ "id": "EXCAV-002",
+ "name": "مؤسسة الحفر والردم",
+ "category": "أعمال الحفر والردم",
+ "subcategory": "حفر وردم",
+ "contact_person": "م. سعيد القحطاني",
+ "phone": "0557678901",
+ "email": "saeed@excavation-backfill.com",
+ "address": "جدة - حي السلامة",
+ "classification": "درجة ثانية",
+ "experience_years": 12,
+ "completed_projects": 90,
+ "ongoing_projects": 6,
+ "min_project_value": 500000,
+ "max_project_value": 30000000,
+ "specialties": "حفر وردم، تسوية مواقع، دك تربة، تصريف مياه",
+ "rating": 4.6,
+ "description": "مؤسسة متخصصة في تنفيذ أعمال الحفر والردم وتسوية المواقع ودك التربة وتصريف المياه"
+ },
+ {
+ "id": "EXCAV-003",
+ "name": "شركة التربة المتخصصة",
+ "category": "أعمال الحفر والردم",
+ "subcategory": "معالجة تربة",
+ "contact_person": "م. فهد الشمري",
+ "phone": "0557789012",
+ "email": "fahad@specialized-soil.com",
+ "address": "الدمام - حي النزهة",
+ "classification": "درجة أولى",
+ "experience_years": 15,
+ "completed_projects": 100,
+ "ongoing_projects": 8,
+ "min_project_value": 2000000,
+ "max_project_value": 100000000,
+ "specialties": "معالجة تربة، تثبيت، تحسين خواص، فحوصات",
+ "rating": 4.8,
+ "description": "شركة متخصصة في تنفيذ أعمال معالجة التربة والتثبيت وتحسين الخواص والفحوصات"
+ }
+ ])
+
+ # 10. أعمال الخرسانة
+ subcontractors_data.extend([
+ {
+ "id": "CONC-001",
+ "name": "شركة الخرسانة المتميزة",
+ "category": "أعمال الخرسانة",
+ "subcategory": "خرسانة جاهزة",
+ "contact_person": "م. عبدالرحمن الشهري",
+ "phone": "0557890123",
+ "email": "abdulrahman@premium-concrete.com",
+ "address": "الرياض - حي الربيع",
+ "classification": "درجة أولى",
+ "experience_years": 20,
+ "completed_projects": 200,
+ "ongoing_projects": 15,
+ "min_project_value": 2000000,
+ "max_project_value": 200000000,
+ "specialties": "خرسانة جاهزة، خرسانة خاصة، خرسانة مسلحة، خرسانة سابقة الإجهاد",
+ "rating": 4.9,
+ "description": "شركة متخصصة في تنفيذ أعمال الخرسانة الجاهزة والخرسانة الخاصة والخرسانة المسلحة والخرسانة سابقة الإجهاد"
+ },
+ {
+ "id": "CONC-002",
+ "name": "مؤسسة الهياكل الخرسانية",
+ "category": "أعمال الخرسانة",
+ "subcategory": "هياكل خرسانية",
+ "contact_person": "م. ماجد العنزي",
+ "phone": "0557901234",
+ "email": "majed@concrete-structures.com",
+ "address": "جدة - حي الروضة",
+ "classification": "درجة ثانية",
+ "experience_years": 15,
+ "completed_projects": 120,
+ "ongoing_projects": 8,
+ "min_project_value": 1000000,
+ "max_project_value": 80000000,
+ "specialties": "هياكل خرسانية، أعمدة، أسقف، جدران",
+ "rating": 4.7,
+ "description": "مؤسسة متخصصة في تنفيذ أعمال الهياكل الخرسانية والأعمدة والأسقف والجدران"
+ },
+ {
+ "id": "CONC-003",
+ "name": "شركة الخرسانة المتخصصة",
+ "category": "أعمال الخرسانة",
+ "subcategory": "خرسانة خاصة",
+ "contact_person": "م. خالد المالكي",
+ "phone": "0558012345",
+ "email": "khalid@specialized-concrete.com",
+ "address": "الدمام - حي الشاطئ",
+ "classification": "درجة أولى",
+ "experience_years": 18,
+ "completed_projects": 150,
+ "ongoing_projects": 12,
+ "min_project_value": 3000000,
+ "max_project_value": 250000000,
+ "specialties": "خرسانة خاصة، خرسانة عالية المقاومة، خرسانة مقاومة للكيماويات، خرسانة ذاتية الدمك",
+ "rating": 4.9,
+ "description": "شركة متخصصة في تنفيذ أعمال الخرسانة الخاصة والخرسانة عالية المقاومة والخرسانة المقاومة للكيماويات والخرسانة ذاتية الدمك"
+ }
+ ])
+
+ # تخزين البيانات في حالة الجلسة
+ st.session_state.subcontractors_catalog = pd.DataFrame(subcontractors_data)
+
+ def render(self):
+ """عرض واجهة كتالوج مقاولي الباطن"""
+
+ st.markdown("## كتالوج مقاولي الباطن")
+
+ # إنشاء تبويبات لعرض الكتالوج
+ tabs = st.tabs([
+ "عرض الكتالوج",
+ "إضافة مقاول",
+ "تحليل المقاولين",
+ "استيراد/تصدير"
+ ])
+
+ with tabs[0]:
+ self._render_catalog_view_tab()
+
+ with tabs[1]:
+ self._render_add_subcontractor_tab()
+
+ with tabs[2]:
+ self._render_analysis_tab()
+
+ with tabs[3]:
+ self._render_import_export_tab()
+
+ def _render_catalog_view_tab(self):
+ """عرض تبويب عرض الكتالوج"""
+
+ st.markdown("### عرض كتالوج مقاولي الباطن")
+
+ # استخراج البيانات
+ subcontractors_df = st.session_state.subcontractors_catalog
+
+ # إنشاء فلاتر للعرض
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ # فلتر حسب الفئة
+ categories = ["الكل"] + sorted(subcontractors_df["category"].unique().tolist())
+ selected_category = st.selectbox("اختر فئة المقاول", categories)
+
+ with col2:
+ # فلتر حسب الفئة الفرعية
+ if selected_category != "الكل":
+ subcategories = ["الكل"] + sorted(subcontractors_df[subcontractors_df["category"] == selected_category]["subcategory"].unique().tolist())
+ else:
+ subcategories = ["الكل"] + sorted(subcontractors_df["subcategory"].unique().tolist())
+
+ selected_subcategory = st.selectbox("اختر التخصص", subcategories)
+
+ with col3:
+ # فلتر حسب التصنيف
+ classifications = ["الكل"] + sorted(subcontractors_df["classification"].unique().tolist())
+ selected_classification = st.selectbox("اختر التصنيف", classifications)
+
+ # تطبيق الفلاتر
+ filtered_df = subcontractors_df.copy()
+
+ if selected_category != "الكل":
+ filtered_df = filtered_df[filtered_df["category"] == selected_category]
+
+ if selected_subcategory != "الكل":
+ filtered_df = filtered_df[filtered_df["subcategory"] == selected_subcategory]
+
+ if selected_classification != "الكل":
+ filtered_df = filtered_df[filtered_df["classification"] == selected_classification]
+
+ # عرض البيانات
+ if not filtered_df.empty:
+ # عرض عدد النتائج
+ st.info(f"تم العثور على {len(filtered_df)} مقاول باطن")
+
+ # عرض المقاولين في شكل بطاقات
+ for i, (_, subcontractor) in enumerate(filtered_df.iterrows()):
+ col1, col2 = st.columns([1, 3])
+
+ with col1:
+ # عرض صورة المقاول (استخدام صورة افتراضية)
+ st.image("https://via.placeholder.com/150", caption=subcontractor["name"])
+
+ with col2:
+ # عرض معلومات المقاول
+ st.markdown(f"**{subcontractor['name']}** (الكود: {subcontractor['id']})")
+ st.markdown(f"الفئة: {subcontractor['category']} - {subcontractor['subcategory']}")
+ st.markdown(f"التصنيف: {subcontractor['classification']} | الخبرة: {subcontractor['experience_years']} سنة")
+ st.markdown(f"التقييم: {'⭐' * int(subcontractor['rating'])} ({subcontractor['rating']})")
+ st.markdown(f"المشاريع المنجزة: {subcontractor['completed_projects']} | المشاريع الجارية: {subcontractor['ongoing_projects']}")
+
+ # إضافة زر لعرض التفاصيل
+ if st.button(f"عرض التفاصيل الكاملة", key=f"details_{subcontractor['id']}"):
+ st.session_state.selected_subcontractor = subcontractor['id']
+ self._show_subcontractor_details(subcontractor)
+
+ st.markdown("---")
+ else:
+ st.warning("لا يوجد مقاولين يطابقون معايير البحث")
+
+ def _show_subcontractor_details(self, subcontractor):
+ """عرض تفاصيل المقاول"""
+
+ st.markdown(f"## تفاصيل مقاول الباطن: {subcontractor['name']}")
+
+ col1, col2 = st.columns([1, 2])
+
+ with col1:
+ # عرض صورة المقاول (استخدام صورة افتراضية)
+ st.image("https://via.placeholder.com/300", caption=subcontractor["name"])
+
+ with col2:
+ # عرض المعلومات الأساسية
+ st.markdown("### المعلومات الأساسية")
+ st.markdown(f"**الكود:** {subcontractor['id']}")
+ st.markdown(f"**الفئة:** {subcontractor['category']} - {subcontractor['subcategory']}")
+ st.markdown(f"**التصنيف:** {subcontractor['classification']}")
+ st.markdown(f"**سنوات الخبرة:** {subcontractor['experience_years']} سنة")
+ st.markdown(f"**التقييم:** {'⭐' * int(subcontractor['rating'])} ({subcontractor['rating']})")
+ st.markdown(f"**التخصصات:** {subcontractor['specialties']}")
+ st.markdown(f"**الوصف:** {subcontractor['description']}")
+
+ # عرض معلومات الاتصال
+ st.markdown("### معلومات الاتصال")
+
+ contact_col1, contact_col2 = st.columns(2)
+
+ with contact_col1:
+ st.markdown(f"**الشخص المسؤول:** {subcontractor['contact_person']}")
+ st.markdown(f"**الهاتف:** {subcontractor['phone']}")
+
+ with contact_col2:
+ st.markdown(f"**البريد الإلكتروني:** {subcontractor['email']}")
+ st.markdown(f"**العنوان:** {subcontractor['address']}")
+
+ # عرض معلومات المشاريع
+ st.markdown("### معلومات المشاريع")
+
+ projects_col1, projects_col2 = st.columns(2)
+
+ with projects_col1:
+ st.markdown(f"**المشاريع المنجزة:** {subcontractor['completed_projects']}")
+ st.markdown(f"**المشاريع الجارية:** {subcontractor['ongoing_projects']}")
+
+ with projects_col2:
+ st.markdown(f"**الحد الأدنى لقيمة المشروع:** {subcontractor['min_project_value']:,} ريال")
+ st.markdown(f"**الحد الأقصى لقيمة المشروع:** {subcontractor['max_project_value']:,} ريال")
+
+ # إضافة زر للتعديل
+ if st.button("تعديل بيانات المقاول"):
+ st.session_state.edit_subcontractor = subcontractor['id']
+ # هنا يمكن إضافة منطق التعديل
+
+ def _render_add_subcontractor_tab(self):
+ """عرض تبويب إضافة مقاول"""
+
+ st.markdown("### إضافة مقاول باطن جديد")
+
+ # استخراج البيانات
+ subcontractors_df = st.session_state.subcontractors_catalog
+
+ # إنشاء نموذج إضافة مقاول
+ with st.form("add_subcontractor_form"):
+ st.markdown("#### المعلومات الأساسية")
+
+ # الصف الأول
+ col1, col2 = st.columns(2)
+ with col1:
+ subcontractor_id = st.text_input("كود المقاول", value=f"SUB-{len(subcontractors_df) + 1:03d}")
+ subcontractor_name = st.text_input("اسم المقاول", placeholder="مثال: شركة الأنظمة الكهربائية المتكاملة")
+
+ with col2:
+ # استخراج الفئات والفئات الفرعية الموجودة
+ categories = sorted(subcontractors_df["category"].unique().tolist())
+ subcontractor_category = st.selectbox("فئة المقاول", categories)
+
+ # استخراج الفئات الفرعية بناءً على الفئة المختارة
+ subcategories = sorted(subcontractors_df[subcontractors_df["category"] == subcontractor_category]["subcategory"].unique().tolist())
+ subcontractor_subcategory = st.selectbox("التخصص", subcategories)
+
+ # الصف الثاني
+ col1, col2 = st.columns(2)
+ with col1:
+ subcontractor_classification = st.selectbox("التصنيف", ["درجة أولى", "درجة ثانية", "درجة ثالثة", "درجة رابعة", "درجة خامسة"])
+ with col2:
+ subcontractor_experience_years = st.number_input("سنوات الخبرة", min_value=1, max_value=50, step=1)
+
+ # معلومات الاتصال
+ st.markdown("#### معلومات الاتصال")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ subcontractor_contact_person = st.text_input("الشخص المسؤول", placeholder="مثال: م. خالد العتيبي")
+ subcontractor_phone = st.text_input("الهاتف", placeholder="مثال: 0555123456")
+
+ with col2:
+ subcontractor_email = st.text_input("البريد الإلكتروني", placeholder="مثال: info@company.com")
+ subcontractor_address = st.text_input("العنوان", placeholder="مثال: الرياض - حي العليا")
+
+ # معلومات المشاريع
+ st.markdown("#### معلومات المشاريع")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ subcontractor_completed_projects = st.number_input("عدد المشاريع المنجزة", min_value=0, step=1)
+ subcontractor_ongoing_projects = st.number_input("عدد المشاريع الجارية", min_value=0, step=1)
+
+ with col2:
+ subcontractor_min_project_value = st.number_input("الحد الأدنى لقيمة المشروع (ريال)", min_value=0, step=100000)
+ subcontractor_max_project_value = st.number_input("الحد الأقصى لقيمة المشروع (ريال)", min_value=0, step=1000000)
+
+ # معلومات إضافية
+ st.markdown("#### معلومات إضافية")
+
+ subcontractor_specialties = st.text_area("التخصصات", placeholder="مثال: تمديدات كهربائية، محطات توزيع، لوحات توزيع، أنظمة إنارة")
+ subcontractor_rating = st.slider("التقييم", min_value=1.0, max_value=5.0, step=0.1, value=4.0)
+ subcontractor_description = st.text_area("وصف المقاول", placeholder="أدخل وصفاً تفصيلياً للمقاول")
+
+ # زر الإضافة
+ submit_button = st.form_submit_button("إضافة المقاول")
+
+ if submit_button:
+ # التحقق من البيانات
+ if not subcontractor_name or not subcontractor_category or not subcontractor_subcategory:
+ st.error("يرجى إدخال المعلومات الأساسية للمقاول")
+ else:
+ # إنشاء مقاول جديد
+ new_subcontractor = {
+ "id": subcontractor_id,
+ "name": subcontractor_name,
+ "category": subcontractor_category,
+ "subcategory": subcontractor_subcategory,
+ "contact_person": subcontractor_contact_person,
+ "phone": subcontractor_phone,
+ "email": subcontractor_email,
+ "address": subcontractor_address,
+ "classification": subcontractor_classification,
+ "experience_years": subcontractor_experience_years,
+ "completed_projects": subcontractor_completed_projects,
+ "ongoing_projects": subcontractor_ongoing_projects,
+ "min_project_value": subcontractor_min_project_value,
+ "max_project_value": subcontractor_max_project_value,
+ "specialties": subcontractor_specialties,
+ "rating": subcontractor_rating,
+ "description": subcontractor_description
+ }
+
+ # إضافة المقاول إلى الكتالوج
+ st.session_state.subcontractors_catalog = pd.concat([
+ st.session_state.subcontractors_catalog,
+ pd.DataFrame([new_subcontractor])
+ ], ignore_index=True)
+
+ st.success(f"تمت إضافة المقاول {subcontractor_name} بنجاح!")
+
+ def _render_analysis_tab(self):
+ """عرض تبويب تحليل المقاولين"""
+
+ st.markdown("### تحليل مقاولي الباطن")
+
+ # استخراج البيانات
+ subcontractors_df = st.session_state.subcontractors_catalog
+
+ # تحليل توزيع المقاولين حسب الفئة
+ st.markdown("#### توزيع المقاولين حسب الفئة")
+
+ # حساب عدد المقاولين لكل فئة
+ category_counts = subcontractors_df["category"].value_counts().reset_index()
+ category_counts.columns = ["الفئة", "عدد المقاولين"]
+
+ # عرض الجدول
+ st.dataframe(category_counts, use_container_width=True)
+
+ # إنشاء رسم بياني للمقارنة
+ fig = px.bar(
+ category_counts,
+ x="الفئة",
+ y="عدد المقاولين",
+ title="توزيع مقاولي الباطن حسب الفئة",
+ color="الفئة",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل توزيع المقاولين حسب التصنيف
+ st.markdown("#### توزيع المقاولين حسب التصنيف")
+
+ # حساب عدد المقاولين لكل تصنيف
+ classification_counts = subcontractors_df["classification"].value_counts().reset_index()
+ classification_counts.columns = ["التصنيف", "عدد المقاولين"]
+
+ # إنشاء رسم بياني دائري
+ fig = px.pie(
+ classification_counts,
+ values="عدد المقاولين",
+ names="التصنيف",
+ title="توزيع مقاولي الباطن حسب التصنيف",
+ color="التصنيف"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل متوسط التقييم حسب الفئة
+ st.markdown("#### متوسط التقييم حسب الفئة")
+
+ # حساب متوسط التقييم لكل فئة
+ category_ratings = subcontractors_df.groupby("category").agg({
+ "rating": "mean"
+ }).reset_index()
+
+ # تغيير أسماء الأعمدة
+ category_ratings.columns = ["الفئة", "متوسط التقييم"]
+
+ # عرض الجدول
+ st.dataframe(category_ratings, use_container_width=True)
+
+ # إنشاء رسم بياني للمقارنة
+ fig = px.bar(
+ category_ratings,
+ x="الفئة",
+ y="متوسط التقييم",
+ title="متوسط تقييم مقاولي الباطن حسب الفئة",
+ color="الفئة",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل متوسط سنوات الخبرة حسب التصنيف
+ st.markdown("#### متوسط سنوات الخبرة حسب التصنيف")
+
+ # حساب متوسط سنوات الخبرة لكل تصنيف
+ classification_experience = subcontractors_df.groupby("classification").agg({
+ "experience_years": "mean"
+ }).reset_index()
+
+ # تغيير أسماء الأعمدة
+ classification_experience.columns = ["التصنيف", "متوسط سنوات الخبرة"]
+
+ # عرض الجدول
+ st.dataframe(classification_experience, use_container_width=True)
+
+ # إنشاء رسم بياني للمقارنة
+ fig = px.bar(
+ classification_experience,
+ x="التصنيف",
+ y="متوسط سنوات الخبرة",
+ title="متوسط سنوات خبرة مقاولي الباطن حسب التصنيف",
+ color="التصنيف",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل المقاولين الأعلى تقييماً
+ st.markdown("#### المقاولين الأعلى تقييماً")
+
+ # استخراج المقاولين الأعلى تقييماً
+ top_rated = subcontractors_df.sort_values(by="rating", ascending=False).head(10)
+
+ # إنشاء جدول للعرض
+ top_rated_display = top_rated[["name", "category", "classification", "rating", "experience_years"]].copy()
+ top_rated_display.columns = ["اسم المقاول", "الفئة", "التصنيف", "التقييم", "سنوات الخبرة"]
+
+ # عرض الجدول
+ st.dataframe(top_rated_display, use_container_width=True)
+
+ # تحليل المقاولين الأكثر خبرة
+ st.markdown("#### المقاولين الأكثر خبرة")
+
+ # استخراج المقاولين الأكثر خبرة
+ most_experienced = subcontractors_df.sort_values(by="experience_years", ascending=False).head(10)
+
+ # إنشاء جدول للعرض
+ most_experienced_display = most_experienced[["name", "category", "classification", "experience_years", "rating"]].copy()
+ most_experienced_display.columns = ["اسم المقاول", "الفئة", "التصنيف", "سنوات الخبرة", "التقييم"]
+
+ # عرض الجدول
+ st.dataframe(most_experienced_display, use_container_width=True)
+
+ def _render_import_export_tab(self):
+ """عرض تبويب استيراد/تصدير"""
+
+ st.markdown("### استيراد وتصدير بيانات مقاولي الباطن")
+
+ # استيراد البيانات
+ st.markdown("#### استيراد البيانات")
+
+ uploaded_file = st.file_uploader("اختر ملف Excel لاستيراد بيانات مقاولي الباطن", type=["xlsx", "xls"])
+
+ if uploaded_file is not None:
+ try:
+ # قراءة الملف
+ imported_df = pd.read_excel(uploaded_file)
+
+ # عرض البيانات المستوردة
+ st.dataframe(imported_df, use_container_width=True)
+
+ # زر الاستيراد
+ if st.button("استيراد البيانات"):
+ # التحقق من وجود الأعمدة المطلوبة
+ required_columns = ["id", "name", "category", "subcategory", "classification"]
+
+ if all(col in imported_df.columns for col in required_columns):
+ # دمج البيانات المستوردة مع البيانات الحالية
+ st.session_state.subcontractors_catalog = pd.concat([
+ st.session_state.subcontractors_catalog,
+ imported_df
+ ], ignore_index=True).drop_duplicates(subset=["id"])
+
+ st.success(f"تم استيراد {len(imported_df)} مقاول باطن بنجاح!")
+ else:
+ st.error("الملف المستورد لا يحتوي على الأعمدة المطلوبة")
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}")
+
+ # تصدير البيانات
+ st.markdown("#### تصدير البيانات")
+
+ # اختيار تنسيق التصدير
+ export_format = st.radio("اختر تنسيق التصدير", ["Excel", "CSV", "JSON"], horizontal=True)
+
+ if st.button("تصدير البيانات"):
+ # استخراج البيانات
+ subcontractors_df = st.session_state.subcontractors_catalog
+
+ # تصدير البيانات حسب التنسيق المختار
+ if export_format == "Excel":
+ # تصدير إلى Excel
+ output = io.BytesIO()
+ with pd.ExcelWriter(output, engine="openpyxl") as writer:
+ subcontractors_df.to_excel(writer, index=False, sheet_name="Subcontractors")
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف Excel",
+ data=output.getvalue(),
+ file_name="subcontractors_catalog.xlsx",
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ )
+
+ elif export_format == "CSV":
+ # تصدير إلى CSV
+ csv_data = subcontractors_df.to_csv(index=False)
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف CSV",
+ data=csv_data,
+ file_name="subcontractors_catalog.csv",
+ mime="text/csv"
+ )
+
+ else: # JSON
+ # تصدير إلى JSON
+ json_data = subcontractors_df.to_json(orient="records", force_ascii=False)
+
+ # تحميل الملف
+ st.download_button(
+ label="تنزيل ملف JSON",
+ data=json_data,
+ file_name="subcontractors_catalog.json",
+ mime="application/json"
+ )
+
+ def get_subcontractor_by_id(self, subcontractor_id):
+ """الحصول على مقاول بواسطة الكود"""
+
+ subcontractors_df = st.session_state.subcontractors_catalog
+ subcontractor = subcontractors_df[subcontractors_df["id"] == subcontractor_id]
+
+ if not subcontractor.empty:
+ return subcontractor.iloc[0].to_dict()
+
+ return None
+
+ def get_subcontractors_by_category(self, category):
+ """الحصول على المقاولين حسب الفئة"""
+
+ subcontractors_df = st.session_state.subcontractors_catalog
+ subcontractors = subcontractors_df[subcontractors_df["category"] == category]
+
+ if not subcontractors.empty:
+ return subcontractors.to_dict(orient="records")
+
+ return []
+
+ def get_top_rated_subcontractors(self, category=None, limit=5):
+ """الحصول على المقاولين الأعلى تقييماً"""
+
+ subcontractors_df = st.session_state.subcontractors_catalog
+
+ if category:
+ filtered_df = subcontractors_df[subcontractors_df["category"] == category]
+ else:
+ filtered_df = subcontractors_df
+
+ top_rated = filtered_df.sort_values(by="rating", ascending=False).head(limit)
+
+ if not top_rated.empty:
+ return top_rated.to_dict(orient="records")
+
+ return []
diff --git a/pricing_system/modules/indirect_support/overheads.py b/pricing_system/modules/indirect_support/overheads.py
index b46d0fb1f2952bfdcaf243819ca0cbbc29cdd8fa..f9b7207156a5a41700c9706cb1eac121b98c24fb 100644
--- a/pricing_system/modules/indirect_support/overheads.py
+++ b/pricing_system/modules/indirect_support/overheads.py
@@ -1,1532 +1,1532 @@
-"""
-وحدة إدارة الإدارات المساندة - إدارة تكاليف الإدارات المساندة
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import plotly.express as px
-import plotly.graph_objects as go
-import os
-import json
-from datetime import datetime
-import io
-
-class IndirectSupportManagement:
- """فئة إدارة الإدارات المساندة"""
-
- def __init__(self):
- """تهيئة وحدة إدارة الإدارات المساندة"""
-
- # تهيئة حالة الجلسة لإدارة الإدارات المساندة
- if 'indirect_support' not in st.session_state:
- self._initialize_indirect_support()
-
- def _initialize_indirect_support(self):
- """تهيئة بيانات إدارة الإدارات المساندة"""
-
- # إنشاء بيانات افتراضية للإدارات المساندة
- departments = [
- {
- "id": "ADM-001",
- "name": "الإدارة العليا",
- "category": "إدارية",
- "annual_cost": 2400000,
- "staff_count": 5,
- "allocation_method": "نسبة من قيمة المشروع",
- "allocation_percentage": 0.02,
- "description": "تشمل الرئيس التنفيذي والمدراء التنفيذيين"
- },
- {
- "id": "ADM-002",
- "name": "إدارة الموارد البشرية",
- "category": "إدارية",
- "annual_cost": 1200000,
- "staff_count": 8,
- "allocation_method": "نسبة من قيمة المشروع",
- "allocation_percentage": 0.01,
- "description": "مسؤولة عن التوظيف والتدريب وشؤون الموظفين"
- },
- {
- "id": "ADM-003",
- "name": "الإدارة المالية",
- "category": "إدارية",
- "annual_cost": 1500000,
- "staff_count": 10,
- "allocation_method": "نسبة من قيمة المشروع",
- "allocation_percentage": 0.015,
- "description": "مسؤولة عن المحاسبة والميزانية والتقارير المالية"
- },
- {
- "id": "ADM-004",
- "name": "إدارة تقنية المعلومات",
- "category": "إدارية",
- "annual_cost": 1800000,
- "staff_count": 12,
- "allocation_method": "تكلفة ثابتة",
- "allocation_percentage": 0,
- "fixed_cost": 50000,
- "description": "مسؤولة عن البنية التحتية التقنية والدعم الفني"
- },
- {
- "id": "TEC-001",
- "name": "إدارة الجودة",
- "category": "فنية",
- "annual_cost": 1600000,
- "staff_count": 15,
- "allocation_method": "نسبة من قيمة المشروع",
- "allocation_percentage": 0.02,
- "description": "مسؤولة عن ضمان الجودة ومراقبة الجودة"
- },
- {
- "id": "TEC-002",
- "name": "إدارة السلامة",
- "category": "فنية",
- "annual_cost": 1400000,
- "staff_count": 12,
- "allocation_method": "نسبة من قيمة المشروع",
- "allocation_percentage": 0.015,
- "description": "مسؤولة عن السلامة المهنية وسلامة الموقع"
- },
- {
- "id": "TEC-003",
- "name": "إدارة المشتريات",
- "category": "فنية",
- "annual_cost": 2000000,
- "staff_count": 18,
- "allocation_method": "نسبة من قيمة المشروع",
- "allocation_percentage": 0.025,
- "description": "مسؤولة عن المشتريات والتعاقدات"
- },
- {
- "id": "TEC-004",
- "name": "إدارة المستودعات",
- "category": "فنية",
- "annual_cost": 1200000,
- "staff_count": 20,
- "allocation_method": "تكلفة ثابتة",
- "allocation_percentage": 0,
- "fixed_cost": 100000,
- "description": "مسؤولة عن إدارة المخزون والمستودعات"
- },
- {
- "id": "SUP-001",
- "name": "إدارة النقل",
- "category": "مساندة",
- "annual_cost": 2500000,
- "staff_count": 25,
- "allocation_method": "تكلفة ثابتة",
- "allocation_percentage": 0,
- "fixed_cost": 200000,
- "description": "مسؤولة عن نقل المواد والمعدات والعمالة"
- },
- {
- "id": "SUP-002",
- "name": "إدارة الصيانة",
- "category": "مساندة",
- "annual_cost": 1800000,
- "staff_count": 22,
- "allocation_method": "تكلفة ثابتة",
- "allocation_percentage": 0,
- "fixed_cost": 150000,
- "description": "مسؤولة عن صيانة المعدات والآليات"
- },
- {
- "id": "SUP-003",
- "name": "إدارة الأمن",
- "category": "مساندة",
- "annual_cost": 1000000,
- "staff_count": 30,
- "allocation_method": "تكلفة ثابتة",
- "allocation_percentage": 0,
- "fixed_cost": 80000,
- "description": "مسؤولة عن أمن المواقع والمنشآت"
- },
- {
- "id": "SUP-004",
- "name": "إدارة الخدمات العامة",
- "category": "مساندة",
- "annual_cost": 800000,
- "staff_count": 15,
- "allocation_method": "تكلفة ثابتة",
- "allocation_percentage": 0,
- "fixed_cost": 50000,
- "description": "مسؤولة عن الخدمات العامة والضيافة"
- }
- ]
-
- # إنشاء بيانات افتراضية للمشاريع
- projects = [
- {
- "id": "PRJ-001",
- "name": "مشروع تطوير طريق الملك عبدالله",
- "value": 50000000,
- "duration": 24,
- "start_date": "2025-01-01",
- "end_date": "2026-12-31",
- "status": "جاري",
- "location": "الرياض",
- "client": "وزارة النقل",
- "description": "مشروع تطوير وتوسعة طريق الملك عبدالله بطول 15 كم"
- },
- {
- "id": "PRJ-002",
- "name": "مشروع إنشاء شبكة صرف صحي",
- "value": 30000000,
- "duration": 18,
- "start_date": "2025-03-01",
- "end_date": "2026-08-31",
- "status": "جاري",
- "location": "جدة",
- "client": "شركة المياه الوطنية",
- "description": "مشروع إنشاء شبكة صرف صحي في حي النزهة بجدة"
- },
- {
- "id": "PRJ-003",
- "name": "مشروع إنشاء جسر تقاطع طريق الملك فهد",
- "value": 80000000,
- "duration": 30,
- "start_date": "2025-02-01",
- "end_date": "2027-07-31",
- "status": "جاري",
- "location": "الدمام",
- "client": "أمانة المنطقة الشرقية",
- "description": "مشروع إنشاء جسر علوي عند تقاطع طريق الملك فهد مع طريق الأمير محمد بن فهد"
- }
- ]
-
- # إنشاء بيانات افتراضية لتخصيص الإدارات المساندة للمشاريع
- allocations = []
-
- for project in projects:
- for department in departments:
- if department["allocation_method"] == "نسبة من قيمة المشروع":
- allocation_amount = project["value"] * department["allocation_percentage"]
- else: # تكلفة ثابتة
- allocation_amount = department.get("fixed_cost", 0)
-
- allocations.append({
- "project_id": project["id"],
- "department_id": department["id"],
- "allocation_amount": allocation_amount,
- "allocation_method": department["allocation_method"],
- "allocation_percentage": department["allocation_percentage"] if department["allocation_method"] == "نسبة من قيمة المشروع" else 0,
- "fixed_cost": department.get("fixed_cost", 0) if department["allocation_method"] == "تكلفة ثابتة" else 0,
- "notes": ""
- })
-
- # تخزين البيانات في حالة الجلسة
- st.session_state.indirect_support = {
- "departments": pd.DataFrame(departments),
- "projects": pd.DataFrame(projects),
- "allocations": pd.DataFrame(allocations)
- }
-
- def render(self):
- """عرض واجهة إدارة الإدارات المساندة"""
-
- st.markdown("## إدارة الإدارات المساندة")
-
- # إنشاء تبويبات لعرض إدارة الإدارات المساندة
- tabs = st.tabs([
- "الإدارات المساندة",
- "المشاريع",
- "تخصيص التكاليف",
- "التقارير"
- ])
-
- with tabs[0]:
- self._render_departments_tab()
-
- with tabs[1]:
- self._render_projects_tab()
-
- with tabs[2]:
- self._render_allocations_tab()
-
- with tabs[3]:
- self._render_reports_tab()
-
- def _render_departments_tab(self):
- """عرض تبويب الإدارات المساندة"""
-
- st.markdown("### الإدارات المساندة")
-
- # استخراج البيانات
- departments = st.session_state.indirect_support["departments"]
-
- # إنشاء فلاتر للعرض
- col1, col2 = st.columns(2)
-
- with col1:
- # فلتر حسب الفئة
- categories = ["الكل"] + sorted(departments["category"].unique().tolist())
- selected_category = st.selectbox("اختر فئة الإدارة", categories, key="departments_category")
-
- with col2:
- # فلتر حسب طريقة التخصيص
- allocation_methods = ["الكل"] + sorted(departments["allocation_method"].unique().tolist())
- selected_allocation_method = st.selectbox("اختر طريقة التخصيص", allocation_methods, key="departments_allocation_method")
-
- # تطبيق الفلاتر
- filtered_df = departments.copy()
-
- if selected_category != "الكل":
- filtered_df = filtered_df[filtered_df["category"] == selected_category]
-
- if selected_allocation_method != "الكل":
- filtered_df = filtered_df[filtered_df["allocation_method"] == selected_allocation_method]
-
- # عرض البيانات
- if not filtered_df.empty:
- # عرض عدد النتائج
- st.info(f"تم العثور على {len(filtered_df)} إدارة")
-
- # إنشاء جدول للعرض
- display_df = filtered_df[["id", "name", "category", "annual_cost", "staff_count", "allocation_method"]].copy()
- display_df.columns = ["الكود", "الاسم", "الفئة", "التكلفة السنوية", "عدد الموظفين", "طريقة التخصيص"]
-
- # عرض الجدول
- st.dataframe(display_df, use_container_width=True)
-
- # إضافة إدارة جديدة
- st.markdown("### إضافة إدارة جديدة")
-
- with st.form("add_department_form"):
- # الصف الأول
- col1, col2 = st.columns(2)
-
- with col1:
- department_id = st.text_input("كود الإدارة", value=f"DEP-{len(departments) + 1:03d}")
- department_name = st.text_input("اسم الإدارة", placeholder="مثال: إدارة الموارد البشرية")
-
- with col2:
- department_category = st.selectbox("فئة الإدارة", ["إدارية", "فنية", "مساندة"])
- department_staff_count = st.number_input("عدد الموظفين", min_value=1, step=1)
-
- # الصف الثاني
- col1, col2 = st.columns(2)
-
- with col1:
- department_annual_cost = st.number_input("التكلفة السنوية (ريال)", min_value=0, step=100000)
-
- with col2:
- department_allocation_method = st.selectbox("طريقة التخصيص", ["نسبة من قيمة المشروع", "تكلفة ثابتة"])
-
- # الصف الثالث
- if department_allocation_method == "نسبة من قيمة المشروع":
- department_allocation_percentage = st.slider("نسبة التخصيص", min_value=0.0, max_value=0.1, value=0.01, step=0.001, format="%g%%") * 100
- department_fixed_cost = 0
- else: # تكلفة ثابتة
- department_fixed_cost = st.number_input("التكلفة الثابتة (ريال)", min_value=0, step=10000)
- department_allocation_percentage = 0
-
- # الصف الرابع
- department_description = st.text_area("وصف الإدارة", placeholder="أدخل وصفاً تفصيلياً للإدارة")
-
- # زر الإضافة
- submit_button = st.form_submit_button("إضافة الإدارة")
-
- if submit_button:
- # التحقق من البيانات
- if not department_name or not department_category:
- st.error("يرجى إدخال المعلومات الأساسية للإدارة")
- else:
- # إنشاء إدارة جديدة
- new_department = {
- "id": department_id,
- "name": department_name,
- "category": department_category,
- "annual_cost": department_annual_cost,
- "staff_count": department_staff_count,
- "allocation_method": department_allocation_method,
- "allocation_percentage": department_allocation_percentage / 100 if department_allocation_method == "نسبة من قيمة المشروع" else 0,
- "fixed_cost": department_fixed_cost if department_allocation_method == "تكلفة ثابتة" else 0,
- "description": department_description
- }
-
- # إضافة الإدارة إلى الجدول
- st.session_state.indirect_support["departments"] = pd.concat([
- st.session_state.indirect_support["departments"],
- pd.DataFrame([new_department])
- ], ignore_index=True)
-
- # إضافة تخصيصات للإدارة الجديدة
- projects = st.session_state.indirect_support["projects"]
- allocations = st.session_state.indirect_support["allocations"]
-
- new_allocations = []
-
- for _, project in projects.iterrows():
- if department_allocation_method == "نسبة من قيمة المشروع":
- allocation_amount = project["value"] * (department_allocation_percentage / 100)
- else: # تكلفة ثابتة
- allocation_amount = department_fixed_cost
-
- new_allocations.append({
- "project_id": project["id"],
- "department_id": department_id,
- "allocation_amount": allocation_amount,
- "allocation_method": department_allocation_method,
- "allocation_percentage": department_allocation_percentage / 100 if department_allocation_method == "نسبة من قيمة المشروع" else 0,
- "fixed_cost": department_fixed_cost if department_allocation_method == "تكلفة ثابتة" else 0,
- "notes": ""
- })
-
- # إضافة التخصيصات الجديدة
- st.session_state.indirect_support["allocations"] = pd.concat([
- allocations,
- pd.DataFrame(new_allocations)
- ], ignore_index=True)
-
- st.success(f"تمت إضافة الإدارة {department_name} بنجاح!")
-
- # إعادة تحميل الصفحة
- st.experimental_rerun()
-
- # عرض تفاصيل الإدارات
- st.markdown("### تفاصيل الإدارات")
-
- selected_department_id = st.selectbox("اختر إدارة لعرض التفاصيل", filtered_df["id"].tolist(), key="department_details")
-
- # استخراج الإدارة المختارة
- selected_department = filtered_df[filtered_df["id"] == selected_department_id].iloc[0]
-
- # عرض تفاصيل الإدارة
- st.markdown(f"**الإدارة:** {selected_department['name']} ({selected_department['id']})")
- st.markdown(f"**الفئة:** {selected_department['category']}")
- st.markdown(f"**التكلفة السنوية:** {selected_department['annual_cost']:,} ريال")
- st.markdown(f"**عدد الموظفين:** {selected_department['staff_count']} موظف")
- st.markdown(f"**طريقة التخصيص:** {selected_department['allocation_method']}")
-
- if selected_department["allocation_method"] == "نسبة من قيمة المشروع":
- st.markdown(f"**نسبة التخصيص:** {selected_department['allocation_percentage'] * 100:.2f}%")
- else: # تكلفة ثابتة
- st.markdown(f"**التكلفة الثابتة:** {selected_department.get('fixed_cost', 0):,} ريال")
-
- if "description" in selected_department and selected_department["description"]:
- st.markdown(f"**الوصف:** {selected_department['description']}")
-
- # عرض تخصيصات الإدارة
- st.markdown("#### تخصيصات الإدارة للمشاريع")
-
- # استخراج تخصيصات الإدارة
- allocations = st.session_state.indirect_support["allocations"]
- projects = st.session_state.indirect_support["projects"]
-
- department_allocations = allocations[allocations["department_id"] == selected_department_id]
-
- if not department_allocations.empty:
- # دمج بيانات المشاريع
- merged_allocations = pd.merge(
- department_allocations,
- projects[["id", "name", "value"]],
- left_on="project_id",
- right_on="id",
- suffixes=("", "_project")
- )
-
- # إنشاء جدول للعرض
- display_allocations = merged_allocations[["id_project", "name", "value", "allocation_amount"]].copy()
- display_allocations.columns = ["كود المشروع", "اسم المشروع", "قيمة المشروع", "مبلغ التخصيص"]
-
- # عرض الجدول
- st.dataframe(display_allocations, use_container_width=True)
-
- # إنشاء رسم بياني للتخصيصات
- fig = px.bar(
- display_allocations,
- x="اسم المشروع",
- y="مبلغ التخصيص",
- title=f"تخصيصات إدارة {selected_department['name']} للمشاريع",
- color="اسم المشروع",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
- else:
- st.info("لا يوجد تخصيصات لهذه الإدارة")
- else:
- st.warning("لا يوجد إدارات تطابق معايير البحث")
-
- def _render_projects_tab(self):
- """عرض تبويب المشاريع"""
-
- st.markdown("### المشاريع")
-
- # استخراج البيانات
- projects = st.session_state.indirect_support["projects"]
-
- # إنشاء فلاتر للعرض
- col1, col2 = st.columns(2)
-
- with col1:
- # فلتر حسب الحالة
- statuses = ["الكل"] + sorted(projects["status"].unique().tolist())
- selected_status = st.selectbox("اختر حالة المشروع", statuses, key="projects_status")
-
- with col2:
- # فلتر حسب الموقع
- locations = ["الكل"] + sorted(projects["location"].unique().tolist())
- selected_location = st.selectbox("اختر موقع المشروع", locations, key="projects_location")
-
- # تطبيق الفلاتر
- filtered_df = projects.copy()
-
- if selected_status != "الكل":
- filtered_df = filtered_df[filtered_df["status"] == selected_status]
-
- if selected_location != "الكل":
- filtered_df = filtered_df[filtered_df["location"] == selected_location]
-
- # عرض البيانات
- if not filtered_df.empty:
- # عرض عدد النتائج
- st.info(f"تم العثور على {len(filtered_df)} مشروع")
-
- # إنشاء جدول للعرض
- display_df = filtered_df[["id", "name", "value", "duration", "status", "location", "client"]].copy()
- display_df.columns = ["الكود", "الاسم", "القيمة", "المدة (شهر)", "الحالة", "الموقع", "العميل"]
-
- # عرض الجدول
- st.dataframe(display_df, use_container_width=True)
-
- # إضافة مشروع جديد
- st.markdown("### إضافة مشروع جديد")
-
- with st.form("add_project_form"):
- # الصف الأول
- col1, col2 = st.columns(2)
-
- with col1:
- project_id = st.text_input("كود المشروع", value=f"PRJ-{len(projects) + 1:03d}")
- project_name = st.text_input("اسم المشروع", placeholder="مثال: مشروع تطوير طريق الملك عبدالله")
-
- with col2:
- project_value = st.number_input("قيمة المشروع (ريال)", min_value=0, step=1000000)
- project_duration = st.number_input("مدة المشروع (شهر)", min_value=1, step=1)
-
- # الصف الثاني
- col1, col2 = st.columns(2)
-
- with col1:
- project_start_date = st.date_input("تاريخ البدء")
- project_status = st.selectbox("حالة المشروع", ["جاري", "مكتمل", "متوقف", "ملغي"])
-
- with col2:
- project_end_date = st.date_input("تاريخ الانتهاء")
- project_location = st.text_input("موقع المشروع", placeholder="مثال: الرياض")
-
- # الصف الثالث
- project_client = st.text_input("العميل", placeholder="مثال: وزارة النقل")
- project_description = st.text_area("وصف المشروع", placeholder="أدخل وصفاً تفصيلياً للمشروع")
-
- # زر الإضافة
- submit_button = st.form_submit_button("إضافة المشروع")
-
- if submit_button:
- # التحقق من البيانات
- if not project_name or not project_value or not project_location or not project_client:
- st.error("يرجى إدخال المعلومات الأساسية للمشروع")
- else:
- # إنشاء مشروع جديد
- new_project = {
- "id": project_id,
- "name": project_name,
- "value": project_value,
- "duration": project_duration,
- "start_date": project_start_date.strftime("%Y-%m-%d"),
- "end_date": project_end_date.strftime("%Y-%m-%d"),
- "status": project_status,
- "location": project_location,
- "client": project_client,
- "description": project_description
- }
-
- # إضافة المشروع إلى الجدول
- st.session_state.indirect_support["projects"] = pd.concat([
- st.session_state.indirect_support["projects"],
- pd.DataFrame([new_project])
- ], ignore_index=True)
-
- # إضافة تخصيصات للمشروع الجديد
- departments = st.session_state.indirect_support["departments"]
- allocations = st.session_state.indirect_support["allocations"]
-
- new_allocations = []
-
- for _, department in departments.iterrows():
- if department["allocation_method"] == "نسبة من قيمة المشروع":
- allocation_amount = project_value * department["allocation_percentage"]
- else: # تكلفة ثابتة
- allocation_amount = department.get("fixed_cost", 0)
-
- new_allocations.append({
- "project_id": project_id,
- "department_id": department["id"],
- "allocation_amount": allocation_amount,
- "allocation_method": department["allocation_method"],
- "allocation_percentage": department["allocation_percentage"] if department["allocation_method"] == "نسبة من قيمة المشروع" else 0,
- "fixed_cost": department.get("fixed_cost", 0) if department["allocation_method"] == "تكلفة ثابتة" else 0,
- "notes": ""
- })
-
- # إضافة التخصيصات الجديدة
- st.session_state.indirect_support["allocations"] = pd.concat([
- allocations,
- pd.DataFrame(new_allocations)
- ], ignore_index=True)
-
- st.success(f"تمت إضافة المشروع {project_name} بنجاح!")
-
- # إعادة تحميل الصفحة
- st.experimental_rerun()
-
- # عرض تفاصيل المشاريع
- st.markdown("### تفاصيل المشاريع")
-
- selected_project_id = st.selectbox("اختر مشروع لعرض التفاصيل", filtered_df["id"].tolist(), key="project_details")
-
- # استخراج المشروع المختار
- selected_project = filtered_df[filtered_df["id"] == selected_project_id].iloc[0]
-
- # عرض تفاصيل المشروع
- st.markdown(f"**المشروع:** {selected_project['name']} ({selected_project['id']})")
- st.markdown(f"**القيمة:** {selected_project['value']:,} ريال")
- st.markdown(f"**المدة:** {selected_project['duration']} شهر")
- st.markdown(f"**تاريخ البدء:** {selected_project['start_date']}")
- st.markdown(f"**تاريخ الانتهاء:** {selected_project['end_date']}")
- st.markdown(f"**الحالة:** {selected_project['status']}")
- st.markdown(f"**الموقع:** {selected_project['location']}")
- st.markdown(f"**العميل:** {selected_project['client']}")
-
- if "description" in selected_project and selected_project["description"]:
- st.markdown(f"**الوصف:** {selected_project['description']}")
-
- # عرض تخصيصات المشروع
- st.markdown("#### تخصيصات الإدارات المساندة للمشروع")
-
- # استخراج تخصيصات المشروع
- allocations = st.session_state.indirect_support["allocations"]
- departments = st.session_state.indirect_support["departments"]
-
- project_allocations = allocations[allocations["project_id"] == selected_project_id]
-
- if not project_allocations.empty:
- # دمج بيانات الإدارات
- merged_allocations = pd.merge(
- project_allocations,
- departments[["id", "name", "category"]],
- left_on="department_id",
- right_on="id",
- suffixes=("", "_department")
- )
-
- # إنشاء جدول للعرض
- display_allocations = merged_allocations[["id_department", "name", "category", "allocation_amount", "allocation_method"]].copy()
- display_allocations.columns = ["كود الإدارة", "اسم الإدارة", "الفئة", "مبلغ التخصيص", "طريقة التخصيص"]
-
- # عرض الجدول
- st.dataframe(display_allocations, use_container_width=True)
-
- # حساب إجمالي التخصيصات
- total_allocation = display_allocations["مبلغ التخصيص"].sum()
-
- st.markdown(f"**إجمالي تخصيصات الإدارات المساندة:** {total_allocation:,} ريال")
- st.markdown(f"**نسبة التخصيصات من قيمة المشروع:** {(total_allocation / selected_project['value']) * 100:.2f}%")
-
- # إنشاء رسم بياني للتخصيصات
- fig = px.bar(
- display_allocations,
- x="اسم الإدارة",
- y="مبلغ التخصيص",
- title=f"تخصيصات الإدارات المساندة لمشروع {selected_project['name']}",
- color="الفئة",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # إنشاء رسم بياني دائري للتخصيصات حسب الفئة
- category_allocations = display_allocations.groupby("الفئة")["مبلغ التخصيص"].sum().reset_index()
-
- fig = px.pie(
- category_allocations,
- values="مبلغ التخصيص",
- names="الفئة",
- title=f"توزيع تخصيصات الإدارات المساندة حسب الفئة لمشروع {selected_project['name']}",
- color="الفئة"
- )
-
- st.plotly_chart(fig, use_container_width=True)
- else:
- st.info("لا يوجد تخصيصات لهذا المشروع")
- else:
- st.warning("لا يوجد مشاريع تطابق معايير البحث")
-
- def _render_allocations_tab(self):
- """عرض تبويب تخصيص التكاليف"""
-
- st.markdown("### تخصيص تكاليف الإدارات المساندة")
-
- # استخراج البيانات
- allocations = st.session_state.indirect_support["allocations"]
- departments = st.session_state.indirect_support["departments"]
- projects = st.session_state.indirect_support["projects"]
-
- # دمج البيانات
- merged_allocations = pd.merge(
- allocations,
- departments[["id", "name", "category"]],
- left_on="department_id",
- right_on="id",
- suffixes=("", "_department")
- )
-
- merged_allocations = pd.merge(
- merged_allocations,
- projects[["id", "name", "value", "status"]],
- left_on="project_id",
- right_on="id",
- suffixes=("_department", "_project")
- )
-
- # إنشاء فلاتر للعرض
- col1, col2, col3 = st.columns(3)
-
- with col1:
- # فلتر حسب المشروع
- project_options = ["الكل"] + sorted(projects["name"].unique().tolist())
- selected_project = st.selectbox("اختر المشروع", project_options, key="allocations_project")
-
- with col2:
- # فلتر حسب فئة الإدارة
- department_categories = ["الكل"] + sorted(departments["category"].unique().tolist())
- selected_department_category = st.selectbox("اختر فئة الإدارة", department_categories, key="allocations_department_category")
-
- with col3:
- # فلتر حسب طريقة التخصيص
- allocation_methods = ["الكل"] + sorted(allocations["allocation_method"].unique().tolist())
- selected_allocation_method = st.selectbox("اختر طريقة التخصيص", allocation_methods, key="allocations_method")
-
- # تطبيق الفلاتر
- filtered_df = merged_allocations.copy()
-
- if selected_project != "الكل":
- filtered_df = filtered_df[filtered_df["name_project"] == selected_project]
-
- if selected_department_category != "الكل":
- filtered_df = filtered_df[filtered_df["category"] == selected_department_category]
-
- if selected_allocation_method != "الكل":
- filtered_df = filtered_df[filtered_df["allocation_method"] == selected_allocation_method]
-
- # عرض البيانات
- if not filtered_df.empty:
- # عرض عدد النتائج
- st.info(f"تم العثور على {len(filtered_df)} تخصيص")
-
- # إنشاء جدول للعرض
- display_df = filtered_df[["id_project", "name_project", "id_department", "name_department", "category", "allocation_method", "allocation_amount"]].copy()
- display_df.columns = ["كود المشروع", "اسم المشروع", "كود الإدارة", "اسم الإدارة", "فئة الإدارة", "طريقة التخصيص", "مبلغ التخصيص"]
-
- # عرض الجدول
- st.dataframe(display_df, use_container_width=True)
-
- # تعديل التخصيصات
- st.markdown("### تعديل التخصيصات")
-
- # اختيار مشروع وإدارة لتعديل التخصيص
- col1, col2 = st.columns(2)
-
- with col1:
- edit_project_id = st.selectbox("اختر المشروع", projects["id"].tolist(), key="edit_allocation_project")
-
- with col2:
- edit_department_id = st.selectbox("اختر الإدارة", departments["id"].tolist(), key="edit_allocation_department")
-
- # استخراج التخصيص المختار
- selected_allocation = allocations[
- (allocations["project_id"] == edit_project_id) &
- (allocations["department_id"] == edit_department_id)
- ]
-
- if not selected_allocation.empty:
- selected_allocation = selected_allocation.iloc[0]
-
- # استخراج بيانات المشروع والإدارة
- selected_project = projects[projects["id"] == edit_project_id].iloc[0]
- selected_department = departments[departments["id"] == edit_department_id].iloc[0]
-
- st.markdown(f"**المشروع:** {selected_project['name']} ({selected_project['id']})")
- st.markdown(f"**الإدارة:** {selected_department['name']} ({selected_department['id']})")
- st.markdown(f"**طريقة التخصيص الحالية:** {selected_allocation['allocation_method']}")
- st.markdown(f"**مبلغ التخصيص الحالي:** {selected_allocation['allocation_amount']:,} ريال")
-
- # نموذج تعديل التخصيص
- with st.form("edit_allocation_form"):
- # اختيار طريقة التخصيص
- allocation_method = st.selectbox(
- "طريقة التخصيص",
- ["نسبة من قيمة المشروع", "تكلفة ثابتة"],
- index=0 if selected_allocation["allocation_method"] == "نسبة من قيمة المشروع" else 1,
- key="edit_allocation_method"
- )
-
- # إدخال قيمة التخصيص
- if allocation_method == "نسبة من قيمة المشروع":
- allocation_percentage = st.slider(
- "نسبة التخصيص",
- min_value=0.0,
- max_value=0.1,
- value=float(selected_allocation["allocation_percentage"]),
- step=0.001,
- format="%g%%",
- key="edit_allocation_percentage"
- ) * 100
-
- # حساب مبلغ التخصيص
- allocation_amount = selected_project["value"] * (allocation_percentage / 100)
-
- st.markdown(f"**مبلغ التخصيص المحسوب:** {allocation_amount:,} ريال")
-
- fixed_cost = 0
- else: # تكلفة ثابتة
- fixed_cost = st.number_input(
- "التكلفة الثابتة (ريال)",
- min_value=0,
- value=int(selected_allocation["fixed_cost"]),
- step=10000,
- key="edit_allocation_fixed_cost"
- )
-
- allocation_amount = fixed_cost
- allocation_percentage = 0
-
- # ملاحظات
- notes = st.text_area(
- "ملاحظات",
- value=selected_allocation["notes"] if "notes" in selected_allocation else "",
- key="edit_allocation_notes"
- )
-
- # زر الحفظ
- submit_button = st.form_submit_button("حفظ التعديلات")
-
- if submit_button:
- # تحديث التخصيص
- allocation_index = allocations[
- (allocations["project_id"] == edit_project_id) &
- (allocations["department_id"] == edit_department_id)
- ].index[0]
-
- allocations.at[allocation_index, "allocation_method"] = allocation_method
- allocations.at[allocation_index, "allocation_percentage"] = allocation_percentage / 100 if allocation_method == "نسبة من قيمة المشروع" else 0
- allocations.at[allocation_index, "fixed_cost"] = fixed_cost if allocation_method == "تكلفة ثابتة" else 0
- allocations.at[allocation_index, "allocation_amount"] = allocation_amount
- allocations.at[allocation_index, "notes"] = notes
-
- # تحديث حالة الجلسة
- st.session_state.indirect_support["allocations"] = allocations
-
- st.success("تم تحديث التخصيص بنجاح!")
-
- # إعادة تحميل الصفحة
- st.experimental_rerun()
- else:
- st.warning("لم يتم العثور على تخصيص للمشروع والإدارة المختارين")
-
- # عرض ملخص التخصيصات
- st.markdown("### ملخص التخصيصات")
-
- # حساب إجمالي التخصيصات لكل مشروع
- project_allocations = filtered_df.groupby("name_project")["allocation_amount"].sum().reset_index()
- project_allocations.columns = ["المشروع", "إجمالي التخصيصات"]
-
- # عرض الجدول
- st.dataframe(project_allocations, use_container_width=True)
-
- # إنشاء رسم بياني للتخصيصات حسب المشروع
- fig = px.bar(
- project_allocations,
- x="المشروع",
- y="إجمالي التخصيصات",
- title="إجمالي تخصيصات الإدارات المساندة حسب المشروع",
- color="المشروع",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # حساب إجمالي التخصيصات لكل فئة إدارة
- category_allocations = filtered_df.groupby("category")["allocation_amount"].sum().reset_index()
- category_allocations.columns = ["فئة الإدارة", "إجمالي التخصيصات"]
-
- # عرض الجدول
- st.dataframe(category_allocations, use_container_width=True)
-
- # إنشاء رسم بياني للتخصيصات حسب فئة الإدارة
- fig = px.pie(
- category_allocations,
- values="إجمالي التخصيصات",
- names="فئة الإدارة",
- title="توزيع تخصيصات الإدارات المساندة حسب الفئة",
- color="فئة الإدارة"
- )
-
- st.plotly_chart(fig, use_container_width=True)
- else:
- st.warning("لا يوجد تخصيصات تطابق معايير البحث")
-
- def _render_reports_tab(self):
- """عرض تبويب التقارير"""
-
- st.markdown("### تقارير الإدارات المساندة")
-
- # استخراج البيانات
- allocations = st.session_state.indirect_support["allocations"]
- departments = st.session_state.indirect_support["departments"]
- projects = st.session_state.indirect_support["projects"]
-
- # دمج البيانات
- merged_allocations = pd.merge(
- allocations,
- departments[["id", "name", "category", "annual_cost", "staff_count"]],
- left_on="department_id",
- right_on="id",
- suffixes=("", "_department")
- )
-
- merged_allocations = pd.merge(
- merged_allocations,
- projects[["id", "name", "value", "status", "duration"]],
- left_on="project_id",
- right_on="id",
- suffixes=("_department", "_project")
- )
-
- # اختيار نوع التقرير
- report_type = st.selectbox(
- "اختر نوع التقرير",
- [
- "تقرير تكاليف الإدارات المساندة",
- "تقرير تخصيصات المشاريع",
- "تقرير مقارنة التكاليف",
- "تقرير تحليل الكفاءة"
- ],
- key="report_type"
- )
-
- if report_type == "تقرير تكاليف الإدارات المساندة":
- self._render_departments_cost_report(departments, merged_allocations)
- elif report_type == "تقرير تخصيصات المشاريع":
- self._render_projects_allocation_report(projects, merged_allocations)
- elif report_type == "تقرير مقارنة التكاليف":
- self._render_cost_comparison_report(departments, projects, merged_allocations)
- elif report_type == "تقرير تحليل الكفاءة":
- self._render_efficiency_analysis_report(departments, projects, merged_allocations)
-
- def _render_departments_cost_report(self, departments, merged_allocations):
- """عرض تقرير تكاليف الإدارات المساندة"""
-
- st.markdown("#### تقرير تكاليف الإدارات المساندة")
-
- # حساب إجمالي التكاليف السنوية
- total_annual_cost = departments["annual_cost"].sum()
- total_staff_count = departments["staff_count"].sum()
-
- # عرض ملخص التكاليف
- st.markdown(f"**إجمالي التكاليف السنوية للإدارات المساندة:** {total_annual_cost:,} ريال")
- st.markdown(f"**إجمالي عدد الموظفين:** {total_staff_count} موظف")
- st.markdown(f"**متوسط تكلفة الموظف السنوية:** {total_annual_cost / total_staff_count:,.2f} ريال")
-
- # حساب التكاليف حسب الفئة
- category_costs = departments.groupby("category").agg({
- "annual_cost": "sum",
- "staff_count": "sum"
- }).reset_index()
-
- category_costs["نسبة التكلفة"] = category_costs["annual_cost"] / total_annual_cost * 100
- category_costs["متوسط تكلفة الموظف"] = category_costs["annual_cost"] / category_costs["staff_count"]
-
- # تغيير أسماء الأعمدة
- category_costs.columns = ["الفئة", "التكلفة السنوية", "عدد الموظفين", "نسبة التكلفة", "متوسط تكلفة الموظف"]
-
- # عرض الجدول
- st.dataframe(category_costs, use_container_width=True)
-
- # إنشاء رسم بياني للتكاليف حسب الفئة
- fig = px.pie(
- category_costs,
- values="التكلفة السنوية",
- names="الفئة",
- title="توزيع تكاليف الإدارات المساندة حسب الفئة",
- color="الفئة"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # إنشاء رسم بياني لمتوسط تكلفة الموظف حسب الفئة
- fig = px.bar(
- category_costs,
- x="الفئة",
- y="متوسط تكلفة الموظف",
- title="متوسط تكلفة الموظف السنوية حسب فئة الإدارة",
- color="الفئة",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # حساب إجمالي التخصيصات لكل إدارة
- department_allocations = merged_allocations.groupby("name_department").agg({
- "allocation_amount": "sum",
- "annual_cost": "first",
- "category": "first"
- }).reset_index()
-
- department_allocations["نسبة التغطية"] = department_allocations["allocation_amount"] / department_allocations["annual_cost"] * 100
-
- # تغيير أسماء الأعمدة
- department_allocations.columns = ["الإدارة", "إجمالي التخصيصات", "التكلفة السنوية", "الفئة", "نسبة التغطية"]
-
- # ترتيب البيانات حسب نسبة التغطية
- department_allocations = department_allocations.sort_values(by="نسبة التغطية", ascending=False)
-
- # عرض الجدول
- st.dataframe(department_allocations, use_container_width=True)
-
- # إنشاء رسم بياني لنسبة التغطية
- fig = px.bar(
- department_allocations,
- x="الإدارة",
- y="نسبة التغطية",
- title="نسبة تغطية تكاليف الإدارات المساندة من خلال التخصيصات",
- color="الفئة",
- text_auto=True
- )
-
- # إضافة خط أفقي عند 100%
- fig.add_shape(
- type="line",
- x0=-0.5,
- y0=100,
- x1=len(department_allocations) - 0.5,
- y1=100,
- line=dict(
- color="red",
- width=2,
- dash="dash"
- )
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # حساب إجمالي التغطية
- total_allocations = department_allocations["إجمالي التخصيصات"].sum()
- total_coverage = total_allocations / total_annual_cost * 100
-
- st.markdown(f"**إجمالي التخصيصات:** {total_allocations:,} ريال")
- st.markdown(f"**نسبة التغطية الإجمالية:** {total_coverage:.2f}%")
-
- if total_coverage < 100:
- st.warning(f"هناك عجز في تغطية تكاليف الإدارات المساندة بنسبة {100 - total_coverage:.2f}%")
- elif total_coverage > 100:
- st.success(f"هناك فائض في تغطية تكاليف الإدارات المساندة بنسبة {total_coverage - 100:.2f}%")
- else:
- st.success("تمت تغطية تكاليف الإدارات المساندة بالكامل")
-
- def _render_projects_allocation_report(self, projects, merged_allocations):
- """عرض تقرير تخصيصات المشاريع"""
-
- st.markdown("#### تقرير تخصيصات المشاريع")
-
- # حساب إجمالي قيم المشاريع
- total_projects_value = projects["value"].sum()
-
- # عرض ملخص المشاريع
- st.markdown(f"**عدد المشاريع:** {len(projects)}")
- st.markdown(f"**إجمالي قيم المشاريع:** {total_projects_value:,} ريال")
-
- # حساب إجمالي التخصيصات لكل مشروع
- project_allocations = merged_allocations.groupby("name_project").agg({
- "allocation_amount": "sum",
- "value": "first",
- "status": "first",
- "duration": "first"
- }).reset_index()
-
- project_allocations["نسبة التخصيص"] = project_allocations["allocation_amount"] / project_allocations["value"] * 100
- project_allocations["تكلفة التخصيص الشهرية"] = project_allocations["allocation_amount"] / project_allocations["duration"]
-
- # تغيير أسماء الأعمدة
- project_allocations.columns = ["المشروع", "إجمالي التخصيصات", "قيمة المشروع", "الحالة", "المدة (شهر)", "نسبة التخصيص", "تكلفة التخصيص الشهرية"]
-
- # ترتيب البيانات حسب نسبة التخصيص
- project_allocations = project_allocations.sort_values(by="نسبة التخصيص", ascending=False)
-
- # عرض الجدول
- st.dataframe(project_allocations, use_container_width=True)
-
- # إنشاء رسم بياني لنسبة التخصيص
- fig = px.bar(
- project_allocations,
- x="المشروع",
- y="نسبة التخصيص",
- title="نسبة تخصيص الإدارات المساندة من قيمة المشروع",
- color="الحالة",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # إنشاء رسم بياني للتكلفة الشهرية
- fig = px.bar(
- project_allocations,
- x="المشروع",
- y="تكلفة التخصيص الشهرية",
- title="تكلفة تخصيص الإدارات المساندة الشهرية لكل مشروع",
- color="الحالة",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # حساب توزيع التخصيصات حسب فئة الإدارة لكل مشروع
- category_allocations = merged_allocations.groupby(["name_project", "category"]).agg({
- "allocation_amount": "sum"
- }).reset_index()
-
- # تغيير أسماء الأعمدة
- category_allocations.columns = ["المشروع", "فئة الإدارة", "مبلغ التخصيص"]
-
- # إنشاء رسم بياني للتخصيصات حسب فئة الإدارة لكل مشروع
- fig = px.bar(
- category_allocations,
- x="المشروع",
- y="مبلغ التخصيص",
- color="فئة الإدارة",
- title="توزيع تخصيصات الإدارات المساندة حسب الفئة لكل مشروع",
- barmode="stack",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # حساب متوسط نسبة التخصيص
- avg_allocation_percentage = project_allocations["نسبة التخصيص"].mean()
-
- st.markdown(f"**متوسط نسبة تخصيص الإدارات المساندة من قيمة المشروع:** {avg_allocation_percentage:.2f}%")
-
- # تحليل العلاقة بين قيمة المشروع ونسبة التخصيص
- fig = px.scatter(
- project_allocations,
- x="قيمة المشروع",
- y="نسبة التخصيص",
- title="العلاقة بين قيمة المشروع ونسبة تخصيص الإدارات المساندة",
- color="الحالة",
- size="المدة (شهر)",
- hover_data=["المشروع"],
- trendline="ols"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل العلاقة بين مدة المشروع وتكلفة التخصيص الشهرية
- fig = px.scatter(
- project_allocations,
- x="المدة (شهر)",
- y="تكلفة التخصيص الشهرية",
- title="العلاقة بين مدة المشروع وتكلفة تخصيص الإدارات المساندة الشهرية",
- color="الحالة",
- size="قيمة المشروع",
- hover_data=["المشروع"],
- trendline="ols"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_cost_comparison_report(self, departments, projects, merged_allocations):
- """عرض تقرير مقارنة التكاليف"""
-
- st.markdown("#### تقرير مقارنة التكاليف")
-
- # حساب إجمالي التكاليف السنوية
- total_annual_cost = departments["annual_cost"].sum()
-
- # حساب إجمالي قيم المشاريع
- total_projects_value = projects["value"].sum()
-
- # حساب إجمالي التخصيصات
- total_allocations = merged_allocations["allocation_amount"].sum()
-
- # عرض ملخص التكاليف
- st.markdown(f"**إجمالي التكاليف السنوية للإدارات المساندة:** {total_annual_cost:,} ريال")
- st.markdown(f"**إجمالي قيم المشاريع:** {total_projects_value:,} ريال")
- st.markdown(f"**إجمالي التخصيصات:** {total_allocations:,} ريال")
- st.markdown(f"**نسبة التكاليف السنوية من إجمالي قيم المشاريع:** {(total_annual_cost / total_projects_value) * 100:.2f}%")
- st.markdown(f"**نسبة التخصيصات من إجمالي قيم المشاريع:** {(total_allocations / total_projects_value) * 100:.2f}%")
- st.markdown(f"**نسبة تغطية التكاليف السنوية من خلال التخصيصات:** {(total_allocations / total_annual_cost) * 100:.2f}%")
-
- # إنشاء بيانات للرسم البياني
- comparison_data = pd.DataFrame({
- "البند": ["التكاليف السنوية", "التخصيصات"],
- "القيمة": [total_annual_cost, total_allocations]
- })
-
- # إنشاء رسم بياني للمقارنة
- fig = px.bar(
- comparison_data,
- x="البند",
- y="القيمة",
- title="مقارنة بين التكاليف السنوية والتخصيصات",
- color="البند",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # حساب التكاليف والتخصيصات حسب فئة الإدارة
- category_costs = departments.groupby("category").agg({
- "annual_cost": "sum"
- }).reset_index()
-
- category_allocations = merged_allocations.groupby("category").agg({
- "allocation_amount": "sum"
- }).reset_index()
-
- # دمج البيانات
- category_comparison = pd.merge(
- category_costs,
- category_allocations,
- on="category",
- how="left"
- )
-
- category_comparison["نسبة التغطية"] = category_comparison["allocation_amount"] / category_comparison["annual_cost"] * 100
-
- # تغيير أسماء الأعمدة
- category_comparison.columns = ["الفئة", "التكلفة السنوية", "التخصيصات", "نسبة التغطية"]
-
- # عرض الجدول
- st.dataframe(category_comparison, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة حسب الفئة
- category_comparison_data = []
-
- for _, row in category_comparison.iterrows():
- category_comparison_data.extend([
- {"الفئة": row["الفئة"], "البند": "التكلفة السنوية", "القيمة": row["التكلفة السنوية"]},
- {"الفئة": row["الفئة"], "البند": "التخصيصات", "القيمة": row["التخصيصات"]}
- ])
-
- category_comparison_df = pd.DataFrame(category_comparison_data)
-
- fig = px.bar(
- category_comparison_df,
- x="الفئة",
- y="القيمة",
- color="البند",
- title="مقارنة بين التكاليف السنوية والتخصيصات حسب فئة الإدارة",
- barmode="group",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # إنشاء رسم بياني لنسبة التغطية
- fig = px.bar(
- category_comparison,
- x="الفئة",
- y="نسبة التغطية",
- title="نسبة تغطية التكاليف السنوية من خلال التخصيصات حسب فئة الإدارة",
- color="الفئة",
- text_auto=True
- )
-
- # إضافة خط أفقي عند 100%
- fig.add_shape(
- type="line",
- x0=-0.5,
- y0=100,
- x1=len(category_comparison) - 0.5,
- y1=100,
- line=dict(
- color="red",
- width=2,
- dash="dash"
- )
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل تأثير التخصيصات على تكلفة المشاريع
- project_allocations = merged_allocations.groupby("name_project").agg({
- "allocation_amount": "sum",
- "value": "first"
- }).reset_index()
-
- project_allocations["نسبة التخصيص"] = project_allocations["allocation_amount"] / project_allocations["value"] * 100
-
- # تغيير أسماء الأعمدة
- project_allocations.columns = ["المشروع", "التخصيصات", "قيمة المشروع", "نسبة التخصيص"]
-
- # ترتيب البيانات حسب نسبة التخصيص
- project_allocations = project_allocations.sort_values(by="نسبة التخصيص", ascending=False)
-
- # عرض الجدول
- st.dataframe(project_allocations, use_container_width=True)
-
- # إنشاء رسم بياني لتأثير التخصيصات على تكلفة المشاريع
- fig = px.bar(
- project_allocations,
- x="المشروع",
- y=["قيمة المشروع", "التخصيصات"],
- title="تأثير تخصيصات الإدارات المساندة على تكلفة المشاريع",
- barmode="stack",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_efficiency_analysis_report(self, departments, projects, merged_allocations):
- """عرض تقرير تحليل الكفاءة"""
-
- st.markdown("#### تقرير تحليل الكفاءة")
-
- # حساب مؤشرات الكفاءة للإدارات
- department_efficiency = departments.copy()
-
- # حساب إجمالي التخصيصات لكل إدارة
- department_allocations = merged_allocations.groupby("department_id").agg({
- "allocation_amount": "sum"
- }).reset_index()
-
- # دمج البيانات
- department_efficiency = pd.merge(
- department_efficiency,
- department_allocations,
- left_on="id",
- right_on="department_id",
- how="left"
- )
-
- # حساب مؤشرات الكفاءة
- department_efficiency["allocation_amount"] = department_efficiency["allocation_amount"].fillna(0)
- department_efficiency["نسبة التغطية"] = department_efficiency["allocation_amount"] / department_efficiency["annual_cost"] * 100
- department_efficiency["تكلفة الموظف السنوية"] = department_efficiency["annual_cost"] / department_efficiency["staff_count"]
- department_efficiency["تخصيص الموظف"] = department_efficiency["allocation_amount"] / department_efficiency["staff_count"]
- department_efficiency["مؤشر الكفاءة"] = department_efficiency["تخصيص الموظف"] / department_efficiency["تكلفة الموظف السنوية"] * 100
-
- # تغيير أسماء الأعمدة
- efficiency_display = department_efficiency[["name", "category", "annual_cost", "staff_count", "allocation_amount", "نسبة التغطية", "تكلفة الموظف السنوية", "تخصيص الموظف", "مؤشر الكفاءة"]].copy()
- efficiency_display.columns = ["الإدارة", "الفئة", "التكلفة السنوية", "عدد الموظفين", "التخصيصات", "نسبة التغطية", "تكلفة الموظف السنوية", "تخصيص الموظف", "مؤشر الكفاءة"]
-
- # ترتيب البيانات حسب مؤشر الكفاءة
- efficiency_display = efficiency_display.sort_values(by="مؤشر الكفاءة", ascending=False)
-
- # عرض الجدول
- st.dataframe(efficiency_display, use_container_width=True)
-
- # إنشاء رسم بياني لمؤشر الكفاءة
- fig = px.bar(
- efficiency_display,
- x="الإدارة",
- y="مؤشر الكفاءة",
- title="مؤشر كفاءة الإدارات المساندة",
- color="الفئة",
- text_auto=True
- )
-
- # إضافة خط أفقي عند 100%
- fig.add_shape(
- type="line",
- x0=-0.5,
- y0=100,
- x1=len(efficiency_display) - 0.5,
- y1=100,
- line=dict(
- color="red",
- width=2,
- dash="dash"
- )
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل العلاقة بين عدد الموظفين ومؤشر الكفاءة
- fig = px.scatter(
- efficiency_display,
- x="عدد الموظفين",
- y="مؤشر الكفاءة",
- title="العلاقة بين عدد الموظفين ومؤشر الكفاءة",
- color="الفئة",
- size="التكلفة السنوية",
- hover_data=["الإدارة"],
- trendline="ols"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل العلاقة بين تكلفة الموظف السنوية ومؤشر الكفاءة
- fig = px.scatter(
- efficiency_display,
- x="تكلفة الموظف السنوية",
- y="مؤشر الكفاءة",
- title="العلاقة بين تكلفة الموظف السنوية ومؤشر الكفاءة",
- color="الفئة",
- size="عدد الموظفين",
- hover_data=["الإدارة"],
- trendline="ols"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل كفاءة الإدارات حسب الفئة
- category_efficiency = efficiency_display.groupby("الفئة").agg({
- "التكلفة السنوية": "sum",
- "عدد الموظفين": "sum",
- "التخصيصات": "sum",
- "مؤشر الكفاءة": "mean"
- }).reset_index()
-
- category_efficiency["نسبة التغطية"] = category_efficiency["التخصيصات"] / category_efficiency["التكلفة السنوية"] * 100
- category_efficiency["تكلفة الموظف السنوية"] = category_efficiency["التكلفة السنوية"] / category_efficiency["عدد الموظفين"]
-
- # عرض الجدول
- st.dataframe(category_efficiency, use_container_width=True)
-
- # إنشاء رسم بياني لمؤشر الكفاءة حسب الفئة
- fig = px.bar(
- category_efficiency,
- x="الفئة",
- y="مؤشر الكفاءة",
- title="متوسط مؤشر كفاءة الإدارات المساندة حسب الفئة",
- color="الفئة",
- text_auto=True
- )
-
- # إضافة خط أفقي عند 100%
- fig.add_shape(
- type="line",
- x0=-0.5,
- y0=100,
- x1=len(category_efficiency) - 0.5,
- y1=100,
- line=dict(
- color="red",
- width=2,
- dash="dash"
- )
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # توصيات لتحسين الكفاءة
- st.markdown("#### توصيات لتحسين الكفاءة")
-
- # تحديد الإدارات ذات الكفاءة المنخفضة
- low_efficiency_departments = efficiency_display[efficiency_display["مؤشر الكفاءة"] < 80].sort_values(by="مؤشر الكفاءة")
-
- if not low_efficiency_departments.empty:
- st.markdown("##### الإدارات ذات الكفاءة المنخفضة")
-
- for _, row in low_efficiency_departments.iterrows():
- st.markdown(f"**{row['الإدارة']} (مؤشر الكفاءة: {row['مؤشر الكفاءة']:.2f}%):**")
-
- if row["نسبة التغطية"] < 80:
- st.markdown("- زيادة التخصيصات للإدارة من خلال مراجعة طريقة التخصيص")
-
- if row["تكلفة الموظف السنوية"] > category_efficiency[category_efficiency["الفئة"] == row["الفئة"]]["تكلفة الموظف السنوية"].values[0]:
- st.markdown("- مراجعة تكلفة الموظفين في الإدارة")
-
- st.markdown("- تحسين إنتاجية الإدارة من خلال تطوير العمليات وأتمتة الأعمال")
- st.markdown("- مراجعة عدد الموظفين في الإدارة")
-
- st.markdown("---")
- else:
- st.success("جميع الإدارات تتمتع بمستوى كفاءة جيد (أكثر من 80%)")
-
- # تحديد الإدارات ذات الكفاءة العالية
- high_efficiency_departments = efficiency_display[efficiency_display["مؤشر الكفاءة"] > 120].sort_values(by="مؤشر الكفاءة", ascending=False)
-
- if not high_efficiency_departments.empty:
- st.markdown("##### الإدارات ذات الكفاءة العالية")
-
- for _, row in high_efficiency_departments.iterrows():
- st.markdown(f"**{row['الإدارة']} (مؤشر الكفاءة: {row['مؤشر الكفاءة']:.2f}%):**")
-
- if row["نسبة التغطية"] > 120:
- st.markdown("- مراجعة طريقة التخصيص للتأكد من عدم المبالغة في التخصيصات")
-
- st.markdown("- دراسة أسباب ارتفاع الكفاءة والاستفادة منها في تطوير الإدارات الأخرى")
- st.markdown("- تقييم جودة الخدمات المقدمة للتأكد من عدم تأثرها بارتفاع الكفاءة")
-
- st.markdown("---")
-
- # توصيات عامة
- st.markdown("##### توصيات عامة لتحسين كفاءة الإدارات المساندة")
-
- st.markdown("1. مراجعة طرق تخصيص تكاليف الإدارات المساندة للمشاريع")
- st.markdown("2. تطوير نظام لقياس أداء الإدارات المساندة")
- st.markdown("3. تحسين عمليات الإدارات المساندة من خلال أتمتة الأعمال")
- st.markdown("4. تطوير برامج تدريبية لرفع كفاءة الموظفين")
- st.markdown("5. مراجعة الهيكل التنظيمي للإدارات المساندة")
- st.markdown("6. تطبيق مبادئ الإدارة الرشيقة (Lean Management) في الإدارات المساندة")
- st.markdown("7. تحسين التنسيق بين الإدارات المساندة والمشاريع")
-
- def calculate_project_indirect_cost(self, project_id):
- """حساب تكلفة الإدارات المساندة لمشروع معين"""
-
- # استخراج البيانات
- allocations = st.session_state.indirect_support["allocations"]
-
- # حساب إجمالي التخصيصات للمشروع
- project_allocations = allocations[allocations["project_id"] == project_id]
-
- if not project_allocations.empty:
- return project_allocations["allocation_amount"].sum()
-
- return 0
-
- def calculate_department_allocations(self, department_id):
- """حساب تخصيصات إدارة معينة"""
-
- # استخراج البيانات
- allocations = st.session_state.indirect_support["allocations"]
-
- # حساب إجمالي التخصيصات للإدارة
- department_allocations = allocations[allocations["department_id"] == department_id]
-
- if not department_allocations.empty:
- return department_allocations["allocation_amount"].sum()
-
- return 0
-
- def get_department_by_id(self, department_id):
- """الحصول على إدارة بواسطة الكود"""
-
- # استخراج البيانات
- departments = st.session_state.indirect_support["departments"]
-
- # البحث عن الإدارة
- department = departments[departments["id"] == department_id]
-
- if not department.empty:
- return department.iloc[0].to_dict()
-
- return None
-
- def get_project_by_id(self, project_id):
- """الحصول على مشروع بواسطة الكود"""
-
- # استخراج البيانات
- projects = st.session_state.indirect_support["projects"]
-
- # البحث عن المشروع
- project = projects[projects["id"] == project_id]
-
- if not project.empty:
- return project.iloc[0].to_dict()
-
- return None
+"""
+وحدة إدارة الإدارات المساندة - إدارة تكاليف الإدارات المساندة
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import plotly.express as px
+import plotly.graph_objects as go
+import os
+import json
+from datetime import datetime
+import io
+
+class IndirectSupportManagement:
+ """فئة إدارة الإدارات المساندة"""
+
+ def __init__(self):
+ """تهيئة وحدة إدارة الإدارات المساندة"""
+
+ # تهيئة حالة الجلسة لإدارة الإدارات المساندة
+ if 'indirect_support' not in st.session_state:
+ self._initialize_indirect_support()
+
+ def _initialize_indirect_support(self):
+ """تهيئة بيانات إدارة الإدارات المساندة"""
+
+ # إنشاء بيانات افتراضية للإدارات المساندة
+ departments = [
+ {
+ "id": "ADM-001",
+ "name": "الإدارة العليا",
+ "category": "إدارية",
+ "annual_cost": 2400000,
+ "staff_count": 5,
+ "allocation_method": "نسبة من قيمة المشروع",
+ "allocation_percentage": 0.02,
+ "description": "تشمل الرئيس التنفيذي والمدراء التنفيذيين"
+ },
+ {
+ "id": "ADM-002",
+ "name": "إدارة الموارد البشرية",
+ "category": "إدارية",
+ "annual_cost": 1200000,
+ "staff_count": 8,
+ "allocation_method": "نسبة من قيمة المشروع",
+ "allocation_percentage": 0.01,
+ "description": "مسؤولة عن التوظيف والتدريب وشؤون الموظفين"
+ },
+ {
+ "id": "ADM-003",
+ "name": "الإدارة المالية",
+ "category": "إدارية",
+ "annual_cost": 1500000,
+ "staff_count": 10,
+ "allocation_method": "نسبة من قيمة المشروع",
+ "allocation_percentage": 0.015,
+ "description": "مسؤولة عن المحاسبة والميزانية والتقارير المالية"
+ },
+ {
+ "id": "ADM-004",
+ "name": "إدارة تقنية المعلومات",
+ "category": "إدارية",
+ "annual_cost": 1800000,
+ "staff_count": 12,
+ "allocation_method": "تكلفة ثابتة",
+ "allocation_percentage": 0,
+ "fixed_cost": 50000,
+ "description": "مسؤولة عن البنية التحتية التقنية والدعم الفني"
+ },
+ {
+ "id": "TEC-001",
+ "name": "إدارة الجودة",
+ "category": "فنية",
+ "annual_cost": 1600000,
+ "staff_count": 15,
+ "allocation_method": "نسبة من قيمة المشروع",
+ "allocation_percentage": 0.02,
+ "description": "مسؤولة عن ضمان الجودة ومراقبة الجودة"
+ },
+ {
+ "id": "TEC-002",
+ "name": "إدارة السلامة",
+ "category": "فنية",
+ "annual_cost": 1400000,
+ "staff_count": 12,
+ "allocation_method": "نسبة من قيمة المشروع",
+ "allocation_percentage": 0.015,
+ "description": "مسؤولة عن السلامة المهنية وسلامة الموقع"
+ },
+ {
+ "id": "TEC-003",
+ "name": "إدارة المشتريات",
+ "category": "فنية",
+ "annual_cost": 2000000,
+ "staff_count": 18,
+ "allocation_method": "نسبة من قيمة المشروع",
+ "allocation_percentage": 0.025,
+ "description": "مسؤولة عن المشتريات والتعاقدات"
+ },
+ {
+ "id": "TEC-004",
+ "name": "إدارة المستودعات",
+ "category": "فنية",
+ "annual_cost": 1200000,
+ "staff_count": 20,
+ "allocation_method": "تكلفة ثابتة",
+ "allocation_percentage": 0,
+ "fixed_cost": 100000,
+ "description": "مسؤولة عن إدارة المخزون والمستودعات"
+ },
+ {
+ "id": "SUP-001",
+ "name": "إدارة النقل",
+ "category": "مساندة",
+ "annual_cost": 2500000,
+ "staff_count": 25,
+ "allocation_method": "تكلفة ثابتة",
+ "allocation_percentage": 0,
+ "fixed_cost": 200000,
+ "description": "مسؤولة عن نقل المواد والمعدات والعمالة"
+ },
+ {
+ "id": "SUP-002",
+ "name": "إدارة الصيانة",
+ "category": "مساندة",
+ "annual_cost": 1800000,
+ "staff_count": 22,
+ "allocation_method": "تكلفة ثابتة",
+ "allocation_percentage": 0,
+ "fixed_cost": 150000,
+ "description": "مسؤولة عن صيانة المعدات والآليات"
+ },
+ {
+ "id": "SUP-003",
+ "name": "إدارة الأمن",
+ "category": "مساندة",
+ "annual_cost": 1000000,
+ "staff_count": 30,
+ "allocation_method": "تكلفة ثابتة",
+ "allocation_percentage": 0,
+ "fixed_cost": 80000,
+ "description": "مسؤولة عن أمن المواقع والمنشآت"
+ },
+ {
+ "id": "SUP-004",
+ "name": "إدارة الخدمات العامة",
+ "category": "مساندة",
+ "annual_cost": 800000,
+ "staff_count": 15,
+ "allocation_method": "تكلفة ثابتة",
+ "allocation_percentage": 0,
+ "fixed_cost": 50000,
+ "description": "مسؤولة عن الخدمات العامة والضيافة"
+ }
+ ]
+
+ # إنشاء بيانات افتراضية للمشاريع
+ projects = [
+ {
+ "id": "PRJ-001",
+ "name": "مشروع تطوير طريق الملك عبدالله",
+ "value": 50000000,
+ "duration": 24,
+ "start_date": "2025-01-01",
+ "end_date": "2026-12-31",
+ "status": "جاري",
+ "location": "الرياض",
+ "client": "وزارة النقل",
+ "description": "مشروع تطوير وتوسعة طريق الملك عبدالله بطول 15 كم"
+ },
+ {
+ "id": "PRJ-002",
+ "name": "مشروع إنشاء شبكة صرف صحي",
+ "value": 30000000,
+ "duration": 18,
+ "start_date": "2025-03-01",
+ "end_date": "2026-08-31",
+ "status": "جاري",
+ "location": "جدة",
+ "client": "شركة المياه الوطنية",
+ "description": "مشروع إنشاء شبكة صرف صحي في حي النزهة بجدة"
+ },
+ {
+ "id": "PRJ-003",
+ "name": "مشروع إنشاء جسر تقاطع طريق الملك فهد",
+ "value": 80000000,
+ "duration": 30,
+ "start_date": "2025-02-01",
+ "end_date": "2027-07-31",
+ "status": "جاري",
+ "location": "الدمام",
+ "client": "أمانة المنطقة الشرقية",
+ "description": "مشروع إنشاء جسر علوي عند تقاطع طريق الملك فهد مع طريق الأمير محمد بن فهد"
+ }
+ ]
+
+ # إنشاء بيانات افتراضية لتخصيص الإدارات المساندة للمشاريع
+ allocations = []
+
+ for project in projects:
+ for department in departments:
+ if department["allocation_method"] == "نسبة من قيمة المشروع":
+ allocation_amount = project["value"] * department["allocation_percentage"]
+ else: # تكلفة ثابتة
+ allocation_amount = department.get("fixed_cost", 0)
+
+ allocations.append({
+ "project_id": project["id"],
+ "department_id": department["id"],
+ "allocation_amount": allocation_amount,
+ "allocation_method": department["allocation_method"],
+ "allocation_percentage": department["allocation_percentage"] if department["allocation_method"] == "نسبة من قيمة المشروع" else 0,
+ "fixed_cost": department.get("fixed_cost", 0) if department["allocation_method"] == "تكلفة ثابتة" else 0,
+ "notes": ""
+ })
+
+ # تخزين البيانات في حالة الجلسة
+ st.session_state.indirect_support = {
+ "departments": pd.DataFrame(departments),
+ "projects": pd.DataFrame(projects),
+ "allocations": pd.DataFrame(allocations)
+ }
+
+ def render(self):
+ """عرض واجهة إدارة الإدارات المساندة"""
+
+ st.markdown("## إدارة الإدارات المساندة")
+
+ # إنشاء تبويبات لعرض إدارة الإدارات المساندة
+ tabs = st.tabs([
+ "الإدارات المساندة",
+ "المشاريع",
+ "تخصيص التكاليف",
+ "التقارير"
+ ])
+
+ with tabs[0]:
+ self._render_departments_tab()
+
+ with tabs[1]:
+ self._render_projects_tab()
+
+ with tabs[2]:
+ self._render_allocations_tab()
+
+ with tabs[3]:
+ self._render_reports_tab()
+
+ def _render_departments_tab(self):
+ """عرض تبويب الإدارات المساندة"""
+
+ st.markdown("### الإدارات المساندة")
+
+ # استخراج البيانات
+ departments = st.session_state.indirect_support["departments"]
+
+ # إنشاء فلاتر للعرض
+ col1, col2 = st.columns(2)
+
+ with col1:
+ # فلتر حسب الفئة
+ categories = ["الكل"] + sorted(departments["category"].unique().tolist())
+ selected_category = st.selectbox("اختر فئة الإدارة", categories, key="departments_category")
+
+ with col2:
+ # فلتر حسب طريقة التخصيص
+ allocation_methods = ["الكل"] + sorted(departments["allocation_method"].unique().tolist())
+ selected_allocation_method = st.selectbox("اختر طريقة التخصيص", allocation_methods, key="departments_allocation_method")
+
+ # تطبيق الفلاتر
+ filtered_df = departments.copy()
+
+ if selected_category != "الكل":
+ filtered_df = filtered_df[filtered_df["category"] == selected_category]
+
+ if selected_allocation_method != "الكل":
+ filtered_df = filtered_df[filtered_df["allocation_method"] == selected_allocation_method]
+
+ # عرض البيانات
+ if not filtered_df.empty:
+ # عرض عدد النتائج
+ st.info(f"تم العثور على {len(filtered_df)} إدارة")
+
+ # إنشاء جدول للعرض
+ display_df = filtered_df[["id", "name", "category", "annual_cost", "staff_count", "allocation_method"]].copy()
+ display_df.columns = ["الكود", "الاسم", "الفئة", "التكلفة السنوية", "عدد الموظفين", "طريقة التخصيص"]
+
+ # عرض الجدول
+ st.dataframe(display_df, use_container_width=True)
+
+ # إضافة إدارة جديدة
+ st.markdown("### إضافة إدارة جديدة")
+
+ with st.form("add_department_form"):
+ # الصف الأول
+ col1, col2 = st.columns(2)
+
+ with col1:
+ department_id = st.text_input("كود الإدارة", value=f"DEP-{len(departments) + 1:03d}")
+ department_name = st.text_input("اسم الإدارة", placeholder="مثال: إدارة الموارد البشرية")
+
+ with col2:
+ department_category = st.selectbox("فئة الإدارة", ["إدارية", "فنية", "مساندة"])
+ department_staff_count = st.number_input("عدد الموظفين", min_value=1, step=1)
+
+ # الصف الثاني
+ col1, col2 = st.columns(2)
+
+ with col1:
+ department_annual_cost = st.number_input("التكلفة السنوية (ريال)", min_value=0, step=100000)
+
+ with col2:
+ department_allocation_method = st.selectbox("طريقة التخصيص", ["نسبة من قيمة المشروع", "تكلفة ثابتة"])
+
+ # الصف الثالث
+ if department_allocation_method == "نسبة من قيمة المشروع":
+ department_allocation_percentage = st.slider("نسبة التخصيص", min_value=0.0, max_value=0.1, value=0.01, step=0.001, format="%g%%") * 100
+ department_fixed_cost = 0
+ else: # تكلفة ثابتة
+ department_fixed_cost = st.number_input("التكلفة الثابتة (ريال)", min_value=0, step=10000)
+ department_allocation_percentage = 0
+
+ # الصف الرابع
+ department_description = st.text_area("وصف الإدارة", placeholder="أدخل وصفاً تفصيلياً للإدارة")
+
+ # زر الإضافة
+ submit_button = st.form_submit_button("إضافة الإدارة")
+
+ if submit_button:
+ # التحقق من البيانات
+ if not department_name or not department_category:
+ st.error("يرجى إدخال المعلومات الأساسية للإدارة")
+ else:
+ # إنشاء إدارة جديدة
+ new_department = {
+ "id": department_id,
+ "name": department_name,
+ "category": department_category,
+ "annual_cost": department_annual_cost,
+ "staff_count": department_staff_count,
+ "allocation_method": department_allocation_method,
+ "allocation_percentage": department_allocation_percentage / 100 if department_allocation_method == "نسبة من قيمة المشروع" else 0,
+ "fixed_cost": department_fixed_cost if department_allocation_method == "تكلفة ثابتة" else 0,
+ "description": department_description
+ }
+
+ # إضافة الإدارة إلى الجدول
+ st.session_state.indirect_support["departments"] = pd.concat([
+ st.session_state.indirect_support["departments"],
+ pd.DataFrame([new_department])
+ ], ignore_index=True)
+
+ # إضافة تخصيصات للإدارة الجديدة
+ projects = st.session_state.indirect_support["projects"]
+ allocations = st.session_state.indirect_support["allocations"]
+
+ new_allocations = []
+
+ for _, project in projects.iterrows():
+ if department_allocation_method == "نسبة من قيمة المشروع":
+ allocation_amount = project["value"] * (department_allocation_percentage / 100)
+ else: # تكلفة ثابتة
+ allocation_amount = department_fixed_cost
+
+ new_allocations.append({
+ "project_id": project["id"],
+ "department_id": department_id,
+ "allocation_amount": allocation_amount,
+ "allocation_method": department_allocation_method,
+ "allocation_percentage": department_allocation_percentage / 100 if department_allocation_method == "نسبة من قيمة المشروع" else 0,
+ "fixed_cost": department_fixed_cost if department_allocation_method == "تكلفة ثابتة" else 0,
+ "notes": ""
+ })
+
+ # إضافة التخصيصات الجديدة
+ st.session_state.indirect_support["allocations"] = pd.concat([
+ allocations,
+ pd.DataFrame(new_allocations)
+ ], ignore_index=True)
+
+ st.success(f"تمت إضافة الإدارة {department_name} بنجاح!")
+
+ # إعادة تحميل الصفحة
+ st.experimental_rerun()
+
+ # عرض تفاصيل الإدارات
+ st.markdown("### تفاصيل الإدارات")
+
+ selected_department_id = st.selectbox("اختر إدارة لعرض التفاصيل", filtered_df["id"].tolist(), key="department_details")
+
+ # استخراج الإدارة المختارة
+ selected_department = filtered_df[filtered_df["id"] == selected_department_id].iloc[0]
+
+ # عرض تفاصيل الإدارة
+ st.markdown(f"**الإدارة:** {selected_department['name']} ({selected_department['id']})")
+ st.markdown(f"**الفئة:** {selected_department['category']}")
+ st.markdown(f"**التكلفة السنوية:** {selected_department['annual_cost']:,} ريال")
+ st.markdown(f"**عدد الموظفين:** {selected_department['staff_count']} موظف")
+ st.markdown(f"**طريقة التخصيص:** {selected_department['allocation_method']}")
+
+ if selected_department["allocation_method"] == "نسبة من قيمة المشروع":
+ st.markdown(f"**نسبة التخصيص:** {selected_department['allocation_percentage'] * 100:.2f}%")
+ else: # تكلفة ثابتة
+ st.markdown(f"**التكلفة الثابتة:** {selected_department.get('fixed_cost', 0):,} ريال")
+
+ if "description" in selected_department and selected_department["description"]:
+ st.markdown(f"**الوصف:** {selected_department['description']}")
+
+ # عرض تخصيصات الإدارة
+ st.markdown("#### تخصيصات الإدارة للمشاريع")
+
+ # استخراج تخصيصات الإدارة
+ allocations = st.session_state.indirect_support["allocations"]
+ projects = st.session_state.indirect_support["projects"]
+
+ department_allocations = allocations[allocations["department_id"] == selected_department_id]
+
+ if not department_allocations.empty:
+ # دمج بيانات المشاريع
+ merged_allocations = pd.merge(
+ department_allocations,
+ projects[["id", "name", "value"]],
+ left_on="project_id",
+ right_on="id",
+ suffixes=("", "_project")
+ )
+
+ # إنشاء جدول للعرض
+ display_allocations = merged_allocations[["id_project", "name", "value", "allocation_amount"]].copy()
+ display_allocations.columns = ["كود المشروع", "اسم المشروع", "قيمة المشروع", "مبلغ التخصيص"]
+
+ # عرض الجدول
+ st.dataframe(display_allocations, use_container_width=True)
+
+ # إنشاء رسم بياني للتخصيصات
+ fig = px.bar(
+ display_allocations,
+ x="اسم المشروع",
+ y="مبلغ التخصيص",
+ title=f"تخصيصات إدارة {selected_department['name']} للمشاريع",
+ color="اسم المشروع",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.info("لا يوجد تخصيصات لهذه الإدارة")
+ else:
+ st.warning("لا يوجد إدارات تطابق معايير البحث")
+
+ def _render_projects_tab(self):
+ """عرض تبويب المشاريع"""
+
+ st.markdown("### المشاريع")
+
+ # استخراج البيانات
+ projects = st.session_state.indirect_support["projects"]
+
+ # إنشاء فلاتر للعرض
+ col1, col2 = st.columns(2)
+
+ with col1:
+ # فلتر حسب الحالة
+ statuses = ["الكل"] + sorted(projects["status"].unique().tolist())
+ selected_status = st.selectbox("اختر حالة المشروع", statuses, key="projects_status")
+
+ with col2:
+ # فلتر حسب الموقع
+ locations = ["الكل"] + sorted(projects["location"].unique().tolist())
+ selected_location = st.selectbox("اختر موقع المشروع", locations, key="projects_location")
+
+ # تطبيق الفلاتر
+ filtered_df = projects.copy()
+
+ if selected_status != "الكل":
+ filtered_df = filtered_df[filtered_df["status"] == selected_status]
+
+ if selected_location != "الكل":
+ filtered_df = filtered_df[filtered_df["location"] == selected_location]
+
+ # عرض البيانات
+ if not filtered_df.empty:
+ # عرض عدد النتائج
+ st.info(f"تم العثور على {len(filtered_df)} مشروع")
+
+ # إنشاء جدول للعرض
+ display_df = filtered_df[["id", "name", "value", "duration", "status", "location", "client"]].copy()
+ display_df.columns = ["الكود", "الاسم", "القيمة", "المدة (شهر)", "الحالة", "الموقع", "العميل"]
+
+ # عرض الجدول
+ st.dataframe(display_df, use_container_width=True)
+
+ # إضافة مشروع جديد
+ st.markdown("### إضافة مشروع جديد")
+
+ with st.form("add_project_form"):
+ # الصف الأول
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_id = st.text_input("كود المشروع", value=f"PRJ-{len(projects) + 1:03d}")
+ project_name = st.text_input("اسم المشروع", placeholder="مثال: مشروع تطوير طريق الملك عبدالله")
+
+ with col2:
+ project_value = st.number_input("قيمة المشروع (ريال)", min_value=0, step=1000000)
+ project_duration = st.number_input("مدة المشروع (شهر)", min_value=1, step=1)
+
+ # الصف الثاني
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_start_date = st.date_input("تاريخ البدء")
+ project_status = st.selectbox("حالة المشروع", ["جاري", "مكتمل", "متوقف", "ملغي"])
+
+ with col2:
+ project_end_date = st.date_input("تاريخ الانتهاء")
+ project_location = st.text_input("موقع المشروع", placeholder="مثال: الرياض")
+
+ # الصف الثالث
+ project_client = st.text_input("العميل", placeholder="مثال: وزارة النقل")
+ project_description = st.text_area("وصف المشروع", placeholder="أدخل وصفاً تفصيلياً للمشروع")
+
+ # زر الإضافة
+ submit_button = st.form_submit_button("إضافة المشروع")
+
+ if submit_button:
+ # التحقق من البيانات
+ if not project_name or not project_value or not project_location or not project_client:
+ st.error("يرجى إدخال المعلومات الأساسية للمشروع")
+ else:
+ # إنشاء مشروع جديد
+ new_project = {
+ "id": project_id,
+ "name": project_name,
+ "value": project_value,
+ "duration": project_duration,
+ "start_date": project_start_date.strftime("%Y-%m-%d"),
+ "end_date": project_end_date.strftime("%Y-%m-%d"),
+ "status": project_status,
+ "location": project_location,
+ "client": project_client,
+ "description": project_description
+ }
+
+ # إضافة المشروع إلى الجدول
+ st.session_state.indirect_support["projects"] = pd.concat([
+ st.session_state.indirect_support["projects"],
+ pd.DataFrame([new_project])
+ ], ignore_index=True)
+
+ # إضافة تخصيصات للمشروع الجديد
+ departments = st.session_state.indirect_support["departments"]
+ allocations = st.session_state.indirect_support["allocations"]
+
+ new_allocations = []
+
+ for _, department in departments.iterrows():
+ if department["allocation_method"] == "نسبة من قيمة المشروع":
+ allocation_amount = project_value * department["allocation_percentage"]
+ else: # تكلفة ثابتة
+ allocation_amount = department.get("fixed_cost", 0)
+
+ new_allocations.append({
+ "project_id": project_id,
+ "department_id": department["id"],
+ "allocation_amount": allocation_amount,
+ "allocation_method": department["allocation_method"],
+ "allocation_percentage": department["allocation_percentage"] if department["allocation_method"] == "نسبة من قيمة المشروع" else 0,
+ "fixed_cost": department.get("fixed_cost", 0) if department["allocation_method"] == "تكلفة ثابتة" else 0,
+ "notes": ""
+ })
+
+ # إضافة التخصيصات الجديدة
+ st.session_state.indirect_support["allocations"] = pd.concat([
+ allocations,
+ pd.DataFrame(new_allocations)
+ ], ignore_index=True)
+
+ st.success(f"تمت إضافة المشروع {project_name} بنجاح!")
+
+ # إعادة تحميل الصفحة
+ st.experimental_rerun()
+
+ # عرض تفاصيل المشاريع
+ st.markdown("### تفاصيل المشاريع")
+
+ selected_project_id = st.selectbox("اختر مشروع لعرض التفاصيل", filtered_df["id"].tolist(), key="project_details")
+
+ # استخراج المشروع المختار
+ selected_project = filtered_df[filtered_df["id"] == selected_project_id].iloc[0]
+
+ # عرض تفاصيل المشروع
+ st.markdown(f"**المشروع:** {selected_project['name']} ({selected_project['id']})")
+ st.markdown(f"**القيمة:** {selected_project['value']:,} ريال")
+ st.markdown(f"**المدة:** {selected_project['duration']} شهر")
+ st.markdown(f"**تاريخ البدء:** {selected_project['start_date']}")
+ st.markdown(f"**تاريخ الانتهاء:** {selected_project['end_date']}")
+ st.markdown(f"**الحالة:** {selected_project['status']}")
+ st.markdown(f"**الموقع:** {selected_project['location']}")
+ st.markdown(f"**العميل:** {selected_project['client']}")
+
+ if "description" in selected_project and selected_project["description"]:
+ st.markdown(f"**الوصف:** {selected_project['description']}")
+
+ # عرض تخصيصات المشروع
+ st.markdown("#### تخصيصات الإدارات المساندة للمشروع")
+
+ # استخراج تخصيصات المشروع
+ allocations = st.session_state.indirect_support["allocations"]
+ departments = st.session_state.indirect_support["departments"]
+
+ project_allocations = allocations[allocations["project_id"] == selected_project_id]
+
+ if not project_allocations.empty:
+ # دمج بيانات الإدارات
+ merged_allocations = pd.merge(
+ project_allocations,
+ departments[["id", "name", "category"]],
+ left_on="department_id",
+ right_on="id",
+ suffixes=("", "_department")
+ )
+
+ # إنشاء جدول للعرض
+ display_allocations = merged_allocations[["id_department", "name", "category", "allocation_amount", "allocation_method"]].copy()
+ display_allocations.columns = ["كود الإدارة", "اسم الإدارة", "الفئة", "مبلغ التخصيص", "طريقة التخصيص"]
+
+ # عرض الجدول
+ st.dataframe(display_allocations, use_container_width=True)
+
+ # حساب إجمالي التخصيصات
+ total_allocation = display_allocations["مبلغ التخصيص"].sum()
+
+ st.markdown(f"**إجمالي تخصيصات الإدارات المساندة:** {total_allocation:,} ريال")
+ st.markdown(f"**نسبة التخصيصات من قيمة المشروع:** {(total_allocation / selected_project['value']) * 100:.2f}%")
+
+ # إنشاء رسم بياني للتخصيصات
+ fig = px.bar(
+ display_allocations,
+ x="اسم الإدارة",
+ y="مبلغ التخصيص",
+ title=f"تخصيصات الإدارات المساندة لمشروع {selected_project['name']}",
+ color="الفئة",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # إنشاء رسم بياني دائري للتخصيصات حسب الفئة
+ category_allocations = display_allocations.groupby("الفئة")["مبلغ التخصيص"].sum().reset_index()
+
+ fig = px.pie(
+ category_allocations,
+ values="مبلغ التخصيص",
+ names="الفئة",
+ title=f"توزيع تخصيصات الإدارات المساندة حسب الفئة لمشروع {selected_project['name']}",
+ color="الفئة"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.info("لا يوجد تخصيصات لهذا المشروع")
+ else:
+ st.warning("لا يوجد مشاريع تطابق معايير البحث")
+
+ def _render_allocations_tab(self):
+ """عرض تبويب تخصيص التكاليف"""
+
+ st.markdown("### تخصيص تكاليف الإدارات المساندة")
+
+ # استخراج البيانات
+ allocations = st.session_state.indirect_support["allocations"]
+ departments = st.session_state.indirect_support["departments"]
+ projects = st.session_state.indirect_support["projects"]
+
+ # دمج البيانات
+ merged_allocations = pd.merge(
+ allocations,
+ departments[["id", "name", "category"]],
+ left_on="department_id",
+ right_on="id",
+ suffixes=("", "_department")
+ )
+
+ merged_allocations = pd.merge(
+ merged_allocations,
+ projects[["id", "name", "value", "status"]],
+ left_on="project_id",
+ right_on="id",
+ suffixes=("_department", "_project")
+ )
+
+ # إنشاء فلاتر للعرض
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ # فلتر حسب المشروع
+ project_options = ["الكل"] + sorted(projects["name"].unique().tolist())
+ selected_project = st.selectbox("اختر المشروع", project_options, key="allocations_project")
+
+ with col2:
+ # فلتر حسب فئة الإدارة
+ department_categories = ["الكل"] + sorted(departments["category"].unique().tolist())
+ selected_department_category = st.selectbox("اختر فئة الإدارة", department_categories, key="allocations_department_category")
+
+ with col3:
+ # فلتر حسب طريقة التخصيص
+ allocation_methods = ["الكل"] + sorted(allocations["allocation_method"].unique().tolist())
+ selected_allocation_method = st.selectbox("اختر طريقة التخصيص", allocation_methods, key="allocations_method")
+
+ # تطبيق الفلاتر
+ filtered_df = merged_allocations.copy()
+
+ if selected_project != "الكل":
+ filtered_df = filtered_df[filtered_df["name_project"] == selected_project]
+
+ if selected_department_category != "الكل":
+ filtered_df = filtered_df[filtered_df["category"] == selected_department_category]
+
+ if selected_allocation_method != "الكل":
+ filtered_df = filtered_df[filtered_df["allocation_method"] == selected_allocation_method]
+
+ # عرض البيانات
+ if not filtered_df.empty:
+ # عرض عدد النتائج
+ st.info(f"تم العثور على {len(filtered_df)} تخصيص")
+
+ # إنشاء جدول للعرض
+ display_df = filtered_df[["id_project", "name_project", "id_department", "name_department", "category", "allocation_method", "allocation_amount"]].copy()
+ display_df.columns = ["كود المشروع", "اسم المشروع", "كود الإدارة", "اسم الإدارة", "فئة الإدارة", "طريقة التخصيص", "مبلغ التخصيص"]
+
+ # عرض الجدول
+ st.dataframe(display_df, use_container_width=True)
+
+ # تعديل التخصيصات
+ st.markdown("### تعديل التخصيصات")
+
+ # اختيار مشروع وإدارة لتعديل التخصيص
+ col1, col2 = st.columns(2)
+
+ with col1:
+ edit_project_id = st.selectbox("اختر المشروع", projects["id"].tolist(), key="edit_allocation_project")
+
+ with col2:
+ edit_department_id = st.selectbox("اختر الإدارة", departments["id"].tolist(), key="edit_allocation_department")
+
+ # استخراج التخصيص المختار
+ selected_allocation = allocations[
+ (allocations["project_id"] == edit_project_id) &
+ (allocations["department_id"] == edit_department_id)
+ ]
+
+ if not selected_allocation.empty:
+ selected_allocation = selected_allocation.iloc[0]
+
+ # استخراج بيانات المشروع والإدارة
+ selected_project = projects[projects["id"] == edit_project_id].iloc[0]
+ selected_department = departments[departments["id"] == edit_department_id].iloc[0]
+
+ st.markdown(f"**المشروع:** {selected_project['name']} ({selected_project['id']})")
+ st.markdown(f"**الإدارة:** {selected_department['name']} ({selected_department['id']})")
+ st.markdown(f"**طريقة التخصيص الحالية:** {selected_allocation['allocation_method']}")
+ st.markdown(f"**مبلغ التخصيص الحالي:** {selected_allocation['allocation_amount']:,} ريال")
+
+ # نموذج تعديل التخصيص
+ with st.form("edit_allocation_form"):
+ # اختيار طريقة التخصيص
+ allocation_method = st.selectbox(
+ "طريقة التخصيص",
+ ["نسبة من قيمة المشروع", "تكلفة ثابتة"],
+ index=0 if selected_allocation["allocation_method"] == "نسبة من قيمة المشروع" else 1,
+ key="edit_allocation_method"
+ )
+
+ # إدخال قيمة التخصيص
+ if allocation_method == "نسبة من قيمة المشروع":
+ allocation_percentage = st.slider(
+ "نسبة التخصيص",
+ min_value=0.0,
+ max_value=0.1,
+ value=float(selected_allocation["allocation_percentage"]),
+ step=0.001,
+ format="%g%%",
+ key="edit_allocation_percentage"
+ ) * 100
+
+ # حساب مبلغ التخصيص
+ allocation_amount = selected_project["value"] * (allocation_percentage / 100)
+
+ st.markdown(f"**مبلغ التخصيص المحسوب:** {allocation_amount:,} ريال")
+
+ fixed_cost = 0
+ else: # تكلفة ثابتة
+ fixed_cost = st.number_input(
+ "التكلفة الثابتة (ريال)",
+ min_value=0,
+ value=int(selected_allocation["fixed_cost"]),
+ step=10000,
+ key="edit_allocation_fixed_cost"
+ )
+
+ allocation_amount = fixed_cost
+ allocation_percentage = 0
+
+ # ملاحظات
+ notes = st.text_area(
+ "ملاحظات",
+ value=selected_allocation["notes"] if "notes" in selected_allocation else "",
+ key="edit_allocation_notes"
+ )
+
+ # زر الحفظ
+ submit_button = st.form_submit_button("حفظ التعديلات")
+
+ if submit_button:
+ # تحديث التخصيص
+ allocation_index = allocations[
+ (allocations["project_id"] == edit_project_id) &
+ (allocations["department_id"] == edit_department_id)
+ ].index[0]
+
+ allocations.at[allocation_index, "allocation_method"] = allocation_method
+ allocations.at[allocation_index, "allocation_percentage"] = allocation_percentage / 100 if allocation_method == "نسبة من قيمة المشروع" else 0
+ allocations.at[allocation_index, "fixed_cost"] = fixed_cost if allocation_method == "تكلفة ثابتة" else 0
+ allocations.at[allocation_index, "allocation_amount"] = allocation_amount
+ allocations.at[allocation_index, "notes"] = notes
+
+ # تحديث حالة الجلسة
+ st.session_state.indirect_support["allocations"] = allocations
+
+ st.success("تم تحديث التخصيص بنجاح!")
+
+ # إعادة تحميل الصفحة
+ st.experimental_rerun()
+ else:
+ st.warning("لم يتم العثور على تخصيص للمشروع والإدارة المختارين")
+
+ # عرض ملخص التخصيصات
+ st.markdown("### ملخص التخصيصات")
+
+ # حساب إجمالي التخصيصات لكل مشروع
+ project_allocations = filtered_df.groupby("name_project")["allocation_amount"].sum().reset_index()
+ project_allocations.columns = ["المشروع", "إجمالي التخصيصات"]
+
+ # عرض الجدول
+ st.dataframe(project_allocations, use_container_width=True)
+
+ # إنشاء رسم بياني للتخصيصات حسب المشروع
+ fig = px.bar(
+ project_allocations,
+ x="المشروع",
+ y="إجمالي التخصيصات",
+ title="إجمالي تخصيصات الإدارات المساندة حسب المشروع",
+ color="المشروع",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # حساب إجمالي التخصيصات لكل فئة إدارة
+ category_allocations = filtered_df.groupby("category")["allocation_amount"].sum().reset_index()
+ category_allocations.columns = ["فئة الإدارة", "إجمالي التخصيصات"]
+
+ # عرض الجدول
+ st.dataframe(category_allocations, use_container_width=True)
+
+ # إنشاء رسم بياني للتخصيصات حسب فئة الإدارة
+ fig = px.pie(
+ category_allocations,
+ values="إجمالي التخصيصات",
+ names="فئة الإدارة",
+ title="توزيع تخصيصات الإدارات المساندة حسب الفئة",
+ color="فئة الإدارة"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.warning("لا يوجد تخصيصات تطابق معايير البحث")
+
+ def _render_reports_tab(self):
+ """عرض تبويب التقارير"""
+
+ st.markdown("### تقارير الإدارات المساندة")
+
+ # استخراج البيانات
+ allocations = st.session_state.indirect_support["allocations"]
+ departments = st.session_state.indirect_support["departments"]
+ projects = st.session_state.indirect_support["projects"]
+
+ # دمج البيانات
+ merged_allocations = pd.merge(
+ allocations,
+ departments[["id", "name", "category", "annual_cost", "staff_count"]],
+ left_on="department_id",
+ right_on="id",
+ suffixes=("", "_department")
+ )
+
+ merged_allocations = pd.merge(
+ merged_allocations,
+ projects[["id", "name", "value", "status", "duration"]],
+ left_on="project_id",
+ right_on="id",
+ suffixes=("_department", "_project")
+ )
+
+ # اختيار نوع التقرير
+ report_type = st.selectbox(
+ "اختر نوع التقرير",
+ [
+ "تقرير تكاليف الإدارات المساندة",
+ "تقرير تخصيصات المشاريع",
+ "تقرير مقارنة التكاليف",
+ "تقرير تحليل الكفاءة"
+ ],
+ key="report_type"
+ )
+
+ if report_type == "تقرير تكاليف الإدارات المساندة":
+ self._render_departments_cost_report(departments, merged_allocations)
+ elif report_type == "تقرير تخصيصات المشاريع":
+ self._render_projects_allocation_report(projects, merged_allocations)
+ elif report_type == "تقرير مقارنة التكاليف":
+ self._render_cost_comparison_report(departments, projects, merged_allocations)
+ elif report_type == "تقرير تحليل الكفاءة":
+ self._render_efficiency_analysis_report(departments, projects, merged_allocations)
+
+ def _render_departments_cost_report(self, departments, merged_allocations):
+ """عرض تقرير تكاليف الإدارات المساندة"""
+
+ st.markdown("#### تقرير تكاليف الإدارات المساندة")
+
+ # حساب إجمالي التكاليف السنوية
+ total_annual_cost = departments["annual_cost"].sum()
+ total_staff_count = departments["staff_count"].sum()
+
+ # عرض ملخص التكاليف
+ st.markdown(f"**إجمالي التكاليف السنوية للإدارات المساندة:** {total_annual_cost:,} ريال")
+ st.markdown(f"**إجمالي عدد الموظفين:** {total_staff_count} موظف")
+ st.markdown(f"**متوسط تكلفة الموظف السنوية:** {total_annual_cost / total_staff_count:,.2f} ريال")
+
+ # حساب التكاليف حسب الفئة
+ category_costs = departments.groupby("category").agg({
+ "annual_cost": "sum",
+ "staff_count": "sum"
+ }).reset_index()
+
+ category_costs["نسبة التكلفة"] = category_costs["annual_cost"] / total_annual_cost * 100
+ category_costs["متوسط تكلفة الموظف"] = category_costs["annual_cost"] / category_costs["staff_count"]
+
+ # تغيير أسماء الأعمدة
+ category_costs.columns = ["الفئة", "التكلفة السنوية", "عدد الموظفين", "نسبة التكلفة", "متوسط تكلفة الموظف"]
+
+ # عرض الجدول
+ st.dataframe(category_costs, use_container_width=True)
+
+ # إنشاء رسم بياني للتكاليف حسب الفئة
+ fig = px.pie(
+ category_costs,
+ values="التكلفة السنوية",
+ names="الفئة",
+ title="توزيع تكاليف الإدارات المساندة حسب الفئة",
+ color="الفئة"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # إنشاء رسم بياني لمتوسط تكلفة الموظف حسب الفئة
+ fig = px.bar(
+ category_costs,
+ x="الفئة",
+ y="متوسط تكلفة الموظف",
+ title="متوسط تكلفة الموظف السنوية حسب فئة الإدارة",
+ color="الفئة",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # حساب إجمالي التخصيصات لكل إدارة
+ department_allocations = merged_allocations.groupby("name_department").agg({
+ "allocation_amount": "sum",
+ "annual_cost": "first",
+ "category": "first"
+ }).reset_index()
+
+ department_allocations["نسبة التغطية"] = department_allocations["allocation_amount"] / department_allocations["annual_cost"] * 100
+
+ # تغيير أسماء الأعمدة
+ department_allocations.columns = ["الإدارة", "إجمالي التخصيصات", "التكلفة السنوية", "الفئة", "نسبة التغطية"]
+
+ # ترتيب البيانات حسب نسبة التغطية
+ department_allocations = department_allocations.sort_values(by="نسبة التغطية", ascending=False)
+
+ # عرض الجدول
+ st.dataframe(department_allocations, use_container_width=True)
+
+ # إنشاء رسم بياني لنسبة التغطية
+ fig = px.bar(
+ department_allocations,
+ x="الإدارة",
+ y="نسبة التغطية",
+ title="نسبة تغطية تكاليف الإدارات المساندة من خلال التخصيصات",
+ color="الفئة",
+ text_auto=True
+ )
+
+ # إضافة خط أفقي عند 100%
+ fig.add_shape(
+ type="line",
+ x0=-0.5,
+ y0=100,
+ x1=len(department_allocations) - 0.5,
+ y1=100,
+ line=dict(
+ color="red",
+ width=2,
+ dash="dash"
+ )
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # حساب إجمالي التغطية
+ total_allocations = department_allocations["إجمالي التخصيصات"].sum()
+ total_coverage = total_allocations / total_annual_cost * 100
+
+ st.markdown(f"**إجمالي التخصيصات:** {total_allocations:,} ريال")
+ st.markdown(f"**نسبة التغطية الإجمالية:** {total_coverage:.2f}%")
+
+ if total_coverage < 100:
+ st.warning(f"هناك عجز في تغطية تكاليف الإدارات المساندة بنسبة {100 - total_coverage:.2f}%")
+ elif total_coverage > 100:
+ st.success(f"هناك فائض في تغطية تكاليف الإدارات المساندة بنسبة {total_coverage - 100:.2f}%")
+ else:
+ st.success("تمت تغطية تكاليف الإدارات المساندة بالكامل")
+
+ def _render_projects_allocation_report(self, projects, merged_allocations):
+ """عرض تقرير تخصيصات المشاريع"""
+
+ st.markdown("#### تقرير تخصيصات المشاريع")
+
+ # حساب إجمالي قيم المشاريع
+ total_projects_value = projects["value"].sum()
+
+ # عرض ملخص المشاريع
+ st.markdown(f"**عدد المشاريع:** {len(projects)}")
+ st.markdown(f"**إجمالي قيم المشاريع:** {total_projects_value:,} ريال")
+
+ # حساب إجمالي التخصيصات لكل مشروع
+ project_allocations = merged_allocations.groupby("name_project").agg({
+ "allocation_amount": "sum",
+ "value": "first",
+ "status": "first",
+ "duration": "first"
+ }).reset_index()
+
+ project_allocations["نسبة التخصيص"] = project_allocations["allocation_amount"] / project_allocations["value"] * 100
+ project_allocations["تكلفة التخصيص الشهرية"] = project_allocations["allocation_amount"] / project_allocations["duration"]
+
+ # تغيير أسماء الأعمدة
+ project_allocations.columns = ["المشروع", "إجمالي التخصيصات", "قيمة المشروع", "الحالة", "المدة (شهر)", "نسبة التخصيص", "تكلفة التخصيص الشهرية"]
+
+ # ترتيب البيانات حسب نسبة التخصيص
+ project_allocations = project_allocations.sort_values(by="نسبة التخصيص", ascending=False)
+
+ # عرض الجدول
+ st.dataframe(project_allocations, use_container_width=True)
+
+ # إنشاء رسم بياني لنسبة التخصيص
+ fig = px.bar(
+ project_allocations,
+ x="المشروع",
+ y="نسبة التخصيص",
+ title="نسبة تخصيص الإدارات المساندة من قيمة المشروع",
+ color="الحالة",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # إنشاء رسم بياني للتكلفة الشهرية
+ fig = px.bar(
+ project_allocations,
+ x="المشروع",
+ y="تكلفة التخصيص الشهرية",
+ title="تكلفة تخصيص الإدارات المساندة الشهرية لكل مشروع",
+ color="الحالة",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # حساب توزيع التخصيصات حسب فئة الإدارة لكل مشروع
+ category_allocations = merged_allocations.groupby(["name_project", "category"]).agg({
+ "allocation_amount": "sum"
+ }).reset_index()
+
+ # تغيير أسماء الأعمدة
+ category_allocations.columns = ["المشروع", "فئة الإدارة", "مبلغ التخصيص"]
+
+ # إنشاء رسم بياني للتخصيصات حسب فئة الإدارة لكل مشروع
+ fig = px.bar(
+ category_allocations,
+ x="المشروع",
+ y="مبلغ التخصيص",
+ color="فئة الإدارة",
+ title="توزيع تخصيصات الإدارات المساندة حسب الفئة لكل مشروع",
+ barmode="stack",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # حساب متوسط نسبة التخصيص
+ avg_allocation_percentage = project_allocations["نسبة التخصيص"].mean()
+
+ st.markdown(f"**متوسط نسبة تخصيص الإدارات المساندة من قيمة المشروع:** {avg_allocation_percentage:.2f}%")
+
+ # تحليل العلاقة بين قيمة المشروع ونسبة التخصيص
+ fig = px.scatter(
+ project_allocations,
+ x="قيمة المشروع",
+ y="نسبة التخصيص",
+ title="العلاقة بين قيمة المشروع ونسبة تخصيص الإدارات المساندة",
+ color="الحالة",
+ size="المدة (شهر)",
+ hover_data=["المشروع"],
+ trendline="ols"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل العلاقة بين مدة المشروع وتكلفة التخصيص الشهرية
+ fig = px.scatter(
+ project_allocations,
+ x="المدة (شهر)",
+ y="تكلفة التخصيص الشهرية",
+ title="العلاقة بين مدة المشروع وتكلفة تخصيص الإدارات المساندة الشهرية",
+ color="الحالة",
+ size="قيمة المشروع",
+ hover_data=["المشروع"],
+ trendline="ols"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _render_cost_comparison_report(self, departments, projects, merged_allocations):
+ """عرض تقرير مقارنة التكاليف"""
+
+ st.markdown("#### تقرير مقارنة التكاليف")
+
+ # حساب إجمالي التكاليف السنوية
+ total_annual_cost = departments["annual_cost"].sum()
+
+ # حساب إجمالي قيم المشاريع
+ total_projects_value = projects["value"].sum()
+
+ # حساب إجمالي التخصيصات
+ total_allocations = merged_allocations["allocation_amount"].sum()
+
+ # عرض ملخص التكاليف
+ st.markdown(f"**إجمالي التكاليف السنوية للإدارات المساندة:** {total_annual_cost:,} ريال")
+ st.markdown(f"**إجمالي قيم المشاريع:** {total_projects_value:,} ريال")
+ st.markdown(f"**إجمالي التخصيصات:** {total_allocations:,} ريال")
+ st.markdown(f"**نسبة التكاليف السنوية من إجمالي قيم المشاريع:** {(total_annual_cost / total_projects_value) * 100:.2f}%")
+ st.markdown(f"**نسبة التخصيصات من إجمالي قيم المشاريع:** {(total_allocations / total_projects_value) * 100:.2f}%")
+ st.markdown(f"**نسبة تغطية التكاليف السنوية من خلال التخصيصات:** {(total_allocations / total_annual_cost) * 100:.2f}%")
+
+ # إنشاء بيانات للرسم البياني
+ comparison_data = pd.DataFrame({
+ "البند": ["التكاليف السنوية", "التخصيصات"],
+ "القيمة": [total_annual_cost, total_allocations]
+ })
+
+ # إنشاء رسم بياني للمقارنة
+ fig = px.bar(
+ comparison_data,
+ x="البند",
+ y="القيمة",
+ title="مقارنة بين التكاليف السنوية والتخصيصات",
+ color="البند",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # حساب التكاليف والتخصيصات حسب فئة الإدارة
+ category_costs = departments.groupby("category").agg({
+ "annual_cost": "sum"
+ }).reset_index()
+
+ category_allocations = merged_allocations.groupby("category").agg({
+ "allocation_amount": "sum"
+ }).reset_index()
+
+ # دمج البيانات
+ category_comparison = pd.merge(
+ category_costs,
+ category_allocations,
+ on="category",
+ how="left"
+ )
+
+ category_comparison["نسبة التغطية"] = category_comparison["allocation_amount"] / category_comparison["annual_cost"] * 100
+
+ # تغيير أسماء الأعمدة
+ category_comparison.columns = ["الفئة", "التكلفة السنوية", "التخصيصات", "نسبة التغطية"]
+
+ # عرض الجدول
+ st.dataframe(category_comparison, use_container_width=True)
+
+ # إنشاء رسم بياني للمقارنة حسب الفئة
+ category_comparison_data = []
+
+ for _, row in category_comparison.iterrows():
+ category_comparison_data.extend([
+ {"الفئة": row["الفئة"], "البند": "التكلفة السنوية", "القيمة": row["التكلفة السنوية"]},
+ {"الفئة": row["الفئة"], "البند": "التخصيصات", "القيمة": row["التخصيصات"]}
+ ])
+
+ category_comparison_df = pd.DataFrame(category_comparison_data)
+
+ fig = px.bar(
+ category_comparison_df,
+ x="الفئة",
+ y="القيمة",
+ color="البند",
+ title="مقارنة بين التكاليف السنوية والتخصيصات حسب فئة الإدارة",
+ barmode="group",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # إنشاء رسم بياني لنسبة التغطية
+ fig = px.bar(
+ category_comparison,
+ x="الفئة",
+ y="نسبة التغطية",
+ title="نسبة تغطية التكاليف السنوية من خلال التخصيصات حسب فئة الإدارة",
+ color="الفئة",
+ text_auto=True
+ )
+
+ # إضافة خط أفقي عند 100%
+ fig.add_shape(
+ type="line",
+ x0=-0.5,
+ y0=100,
+ x1=len(category_comparison) - 0.5,
+ y1=100,
+ line=dict(
+ color="red",
+ width=2,
+ dash="dash"
+ )
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل تأثير التخصيصات على تكلفة المشاريع
+ project_allocations = merged_allocations.groupby("name_project").agg({
+ "allocation_amount": "sum",
+ "value": "first"
+ }).reset_index()
+
+ project_allocations["نسبة التخصيص"] = project_allocations["allocation_amount"] / project_allocations["value"] * 100
+
+ # تغيير أسماء الأعمدة
+ project_allocations.columns = ["المشروع", "التخصيصات", "قيمة المشروع", "نسبة التخصيص"]
+
+ # ترتيب البيانات حسب نسبة التخصيص
+ project_allocations = project_allocations.sort_values(by="نسبة التخصيص", ascending=False)
+
+ # عرض الجدول
+ st.dataframe(project_allocations, use_container_width=True)
+
+ # إنشاء رسم بياني لتأثير التخصيصات على تكلفة المشاريع
+ fig = px.bar(
+ project_allocations,
+ x="المشروع",
+ y=["قيمة المشروع", "التخصيصات"],
+ title="تأثير تخصيصات الإدارات المساندة على تكلفة المشاريع",
+ barmode="stack",
+ text_auto=True
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _render_efficiency_analysis_report(self, departments, projects, merged_allocations):
+ """عرض تقرير تحليل الكفاءة"""
+
+ st.markdown("#### تقرير تحليل الكفاءة")
+
+ # حساب مؤشرات الكفاءة للإدارات
+ department_efficiency = departments.copy()
+
+ # حساب إجمالي التخصيصات لكل إدارة
+ department_allocations = merged_allocations.groupby("department_id").agg({
+ "allocation_amount": "sum"
+ }).reset_index()
+
+ # دمج البيانات
+ department_efficiency = pd.merge(
+ department_efficiency,
+ department_allocations,
+ left_on="id",
+ right_on="department_id",
+ how="left"
+ )
+
+ # حساب مؤشرات الكفاءة
+ department_efficiency["allocation_amount"] = department_efficiency["allocation_amount"].fillna(0)
+ department_efficiency["نسبة التغطية"] = department_efficiency["allocation_amount"] / department_efficiency["annual_cost"] * 100
+ department_efficiency["تكلفة الموظف السنوية"] = department_efficiency["annual_cost"] / department_efficiency["staff_count"]
+ department_efficiency["تخصيص الموظف"] = department_efficiency["allocation_amount"] / department_efficiency["staff_count"]
+ department_efficiency["مؤشر الكفاءة"] = department_efficiency["تخصيص الموظف"] / department_efficiency["تكلفة الموظف السنوية"] * 100
+
+ # تغيير أسماء الأعمدة
+ efficiency_display = department_efficiency[["name", "category", "annual_cost", "staff_count", "allocation_amount", "نسبة التغطية", "تكلفة الموظف السنوية", "تخصيص الموظف", "مؤشر الكفاءة"]].copy()
+ efficiency_display.columns = ["الإدارة", "الفئة", "التكلفة السنوية", "عدد الموظفين", "التخصيصات", "نسبة التغطية", "تكلفة الموظف السنوية", "تخصيص الموظف", "مؤشر الكفاءة"]
+
+ # ترتيب البيانات حسب مؤشر الكفاءة
+ efficiency_display = efficiency_display.sort_values(by="مؤشر الكفاءة", ascending=False)
+
+ # عرض الجدول
+ st.dataframe(efficiency_display, use_container_width=True)
+
+ # إنشاء رسم بياني لمؤشر الكفاءة
+ fig = px.bar(
+ efficiency_display,
+ x="الإدارة",
+ y="مؤشر الكفاءة",
+ title="مؤشر كفاءة الإدارات المساندة",
+ color="الفئة",
+ text_auto=True
+ )
+
+ # إضافة خط أفقي عند 100%
+ fig.add_shape(
+ type="line",
+ x0=-0.5,
+ y0=100,
+ x1=len(efficiency_display) - 0.5,
+ y1=100,
+ line=dict(
+ color="red",
+ width=2,
+ dash="dash"
+ )
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل العلاقة بين عدد الموظفين ومؤشر الكفاءة
+ fig = px.scatter(
+ efficiency_display,
+ x="عدد الموظفين",
+ y="مؤشر الكفاءة",
+ title="العلاقة بين عدد الموظفين ومؤشر الكفاءة",
+ color="الفئة",
+ size="التكلفة السنوية",
+ hover_data=["الإدارة"],
+ trendline="ols"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل العلاقة بين تكلفة الموظف السنوية ومؤشر الكفاءة
+ fig = px.scatter(
+ efficiency_display,
+ x="تكلفة الموظف السنوية",
+ y="مؤشر الكفاءة",
+ title="العلاقة بين تكلفة الموظف السنوية ومؤشر الكفاءة",
+ color="الفئة",
+ size="عدد الموظفين",
+ hover_data=["الإدارة"],
+ trendline="ols"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل كفاءة الإدارات حسب الفئة
+ category_efficiency = efficiency_display.groupby("الفئة").agg({
+ "التكلفة السنوية": "sum",
+ "عدد الموظفين": "sum",
+ "التخصيصات": "sum",
+ "مؤشر الكفاءة": "mean"
+ }).reset_index()
+
+ category_efficiency["نسبة التغطية"] = category_efficiency["التخصيصات"] / category_efficiency["التكلفة السنوية"] * 100
+ category_efficiency["تكلفة الموظف السنوية"] = category_efficiency["التكلفة السنوية"] / category_efficiency["عدد الموظفين"]
+
+ # عرض الجدول
+ st.dataframe(category_efficiency, use_container_width=True)
+
+ # إنشاء رسم بياني لمؤشر الكفاءة حسب الفئة
+ fig = px.bar(
+ category_efficiency,
+ x="الفئة",
+ y="مؤشر الكفاءة",
+ title="متوسط مؤشر كفاءة الإدارات المساندة حسب الفئة",
+ color="الفئة",
+ text_auto=True
+ )
+
+ # إضافة خط أفقي عند 100%
+ fig.add_shape(
+ type="line",
+ x0=-0.5,
+ y0=100,
+ x1=len(category_efficiency) - 0.5,
+ y1=100,
+ line=dict(
+ color="red",
+ width=2,
+ dash="dash"
+ )
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # توصيات لتحسين الكفاءة
+ st.markdown("#### توصيات لتحسين الكفاءة")
+
+ # تحديد الإدارات ذات الكفاءة المنخفضة
+ low_efficiency_departments = efficiency_display[efficiency_display["مؤشر الكفاءة"] < 80].sort_values(by="مؤشر الكفاءة")
+
+ if not low_efficiency_departments.empty:
+ st.markdown("##### الإدارات ذات الكفاءة المنخفضة")
+
+ for _, row in low_efficiency_departments.iterrows():
+ st.markdown(f"**{row['الإدارة']} (مؤشر الكفاءة: {row['مؤشر الكفاءة']:.2f}%):**")
+
+ if row["نسبة التغطية"] < 80:
+ st.markdown("- زيادة التخصيصات للإدارة من خلال مراجعة طريقة التخصيص")
+
+ if row["تكلفة الموظف السنوية"] > category_efficiency[category_efficiency["الفئة"] == row["الفئة"]]["تكلفة الموظف السنوية"].values[0]:
+ st.markdown("- مراجعة تكلفة الموظفين في الإدارة")
+
+ st.markdown("- تحسين إنتاجية الإدارة من خلال تطوير العمليات وأتمتة الأعمال")
+ st.markdown("- مراجعة عدد الموظفين في الإدارة")
+
+ st.markdown("---")
+ else:
+ st.success("جميع الإدارات تتمتع بمستوى كفاءة جيد (أكثر من 80%)")
+
+ # تحديد الإدارات ذات الكفاءة العالية
+ high_efficiency_departments = efficiency_display[efficiency_display["مؤشر الكفاءة"] > 120].sort_values(by="مؤشر الكفاءة", ascending=False)
+
+ if not high_efficiency_departments.empty:
+ st.markdown("##### الإدارات ذات الكفاءة العالية")
+
+ for _, row in high_efficiency_departments.iterrows():
+ st.markdown(f"**{row['الإدارة']} (مؤشر الكفاءة: {row['مؤشر الكفاءة']:.2f}%):**")
+
+ if row["نسبة التغطية"] > 120:
+ st.markdown("- مراجعة طريقة التخصيص للتأكد من عدم المبالغة في التخصيصات")
+
+ st.markdown("- دراسة أسباب ارتفاع الكفاءة والاستفادة منها في تطوير الإدارات الأخرى")
+ st.markdown("- تقييم جودة الخدمات المقدمة للتأكد من عدم تأثرها بارتفاع الكفاءة")
+
+ st.markdown("---")
+
+ # توصيات عامة
+ st.markdown("##### توصيات عامة لتحسين كفاءة الإدارات المساندة")
+
+ st.markdown("1. مراجعة طرق تخصيص تكاليف الإدارات المساندة للمشاريع")
+ st.markdown("2. تطوير نظام لقياس أداء الإدارات المساندة")
+ st.markdown("3. تحسين عمليات الإدارات المساندة من خلال أتمتة الأعمال")
+ st.markdown("4. تطوير برامج تدريبية لرفع كفاءة الموظفين")
+ st.markdown("5. مراجعة الهيكل التنظيمي للإدارات المساندة")
+ st.markdown("6. تطبيق مبادئ الإدارة الرشيقة (Lean Management) في الإدارات المساندة")
+ st.markdown("7. تحسين التنسيق بين الإدارات المساندة والمشاريع")
+
+ def calculate_project_indirect_cost(self, project_id):
+ """حساب تكلفة الإدارات المساندة لمشروع معين"""
+
+ # استخراج البيانات
+ allocations = st.session_state.indirect_support["allocations"]
+
+ # حساب إجمالي التخصيصات للمشروع
+ project_allocations = allocations[allocations["project_id"] == project_id]
+
+ if not project_allocations.empty:
+ return project_allocations["allocation_amount"].sum()
+
+ return 0
+
+ def calculate_department_allocations(self, department_id):
+ """حساب تخصيصات إدارة معينة"""
+
+ # استخراج البيانات
+ allocations = st.session_state.indirect_support["allocations"]
+
+ # حساب إجمالي التخصيصات للإدارة
+ department_allocations = allocations[allocations["department_id"] == department_id]
+
+ if not department_allocations.empty:
+ return department_allocations["allocation_amount"].sum()
+
+ return 0
+
+ def get_department_by_id(self, department_id):
+ """الحصول على إدارة بواسطة الكود"""
+
+ # استخراج البيانات
+ departments = st.session_state.indirect_support["departments"]
+
+ # البحث عن الإدارة
+ department = departments[departments["id"] == department_id]
+
+ if not department.empty:
+ return department.iloc[0].to_dict()
+
+ return None
+
+ def get_project_by_id(self, project_id):
+ """الحصول على مشروع بواسطة الكود"""
+
+ # استخراج البيانات
+ projects = st.session_state.indirect_support["projects"]
+
+ # البحث عن المشروع
+ project = projects[projects["id"] == project_id]
+
+ if not project.empty:
+ return project.iloc[0].to_dict()
+
+ return None
diff --git a/pricing_system/modules/pricing_strategies/__init__.py b/pricing_system/modules/pricing_strategies/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..096ba0186154ec49cc588f99e95012ec046eab7e
--- /dev/null
+++ b/pricing_system/modules/pricing_strategies/__init__.py
@@ -0,0 +1,10 @@
+
+from .balanced_pricing import render_balanced_strategy, calculate_balanced_price
+from .profit_oriented import render_profit_driven_strategy, calculate_profit_oriented_price
+
+__all__ = [
+ 'render_balanced_strategy',
+ 'calculate_balanced_price',
+ 'render_profit_driven_strategy',
+ 'calculate_profit_oriented_price'
+]
diff --git a/pricing_system/modules/pricing_strategies/balanced_pricing.py b/pricing_system/modules/pricing_strategies/balanced_pricing.py
new file mode 100644
index 0000000000000000000000000000000000000000..d5aea9e6bc7472cbdafe67ab47290dc456161269
--- /dev/null
+++ b/pricing_system/modules/pricing_strategies/balanced_pricing.py
@@ -0,0 +1,92 @@
+
+"""
+استراتيجية التسعير المتوازن
+"""
+import streamlit as st
+import pandas as pd
+
+def render_balanced_strategy():
+ st.markdown("### التسعير المتوازن")
+
+ strategies = [
+ "التسعير القياسي",
+ "التسعير المتزن",
+ "التسعير غير المتزن",
+ "التسعير الموجه للربحية",
+ "التسعير بالتجميع",
+ "التسعير بالمحتوى المحلي"
+ ]
+
+ selected_strategy = st.selectbox(
+ "اختر استراتيجية التسعير",
+ strategies
+ )
+
+ # قراءة البنود من المشروع الحالي
+ if 'current_project' not in st.session_state:
+ st.warning("يرجى اختيار مشروع أولاً")
+ return
+
+ project = st.session_state.current_project
+ if not project:
+ st.info("لم يتم اختيار مشروع بعد. يرجى إدخال بيانات المشروع أولاً.")
+ return
+
+ boq_items = project.get('boq_items', [])
+
+ if not boq_items:
+ st.info("لا توجد بنود مضافة للمشروع بعد. يرجى إضافة البنود أولاً.")
+ return
+
+ # عرض تحليل البنود
+ st.markdown("#### تحليل بنود المشروع")
+
+ for i, item in enumerate(boq_items):
+ with st.expander(f"البند {i+1}: {item['description']}"):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.write("معلومات البند:")
+ st.write(f"- الكود: {item['code']}")
+ st.write(f"- الوحدة: {item['unit']}")
+ st.write(f"- الكمية: {item['quantity']}")
+ st.write(f"- سعر الوحدة: {item['unit_price']} ريال")
+
+ with col2:
+ st.write("تحليل التكاليف:")
+
+ # تعديل سعر الوحدة
+ new_unit_price = st.number_input(
+ "سعر الوحدة الجديد",
+ min_value=0.0,
+ value=float(item['unit_price']),
+ key=f"unit_price_{selected_strategy}_{i}"
+ )
+
+ # تعديل الكمية
+ new_quantity = st.number_input(
+ "الكمية الجديدة",
+ min_value=0.0,
+ value=float(item['quantity']),
+ key=f"quantity_{selected_strategy}_{i}"
+ )
+
+ # زر تحديث البند
+ if st.button("تحديث البند", key=f"update_{selected_strategy}_{i}"):
+ item['unit_price'] = new_unit_price
+ item['quantity'] = new_quantity
+ item['total_price'] = new_unit_price * new_quantity
+ st.success("تم تحديث البند بنجاح")
+ st.rerun()
+
+ # زر حذف البند
+ if st.button("حذف البند", key=f"delete_{selected_strategy}_{i}"):
+ st.session_state.current_project['boq_items'].pop(i)
+ st.success("تم حذف البند بنجاح")
+ st.rerun()
+
+def calculate_balanced_price(base_cost, overhead_ratio=0.15, profit_ratio=0.10):
+ """حساب السعر المتوازن"""
+ overhead = base_cost * overhead_ratio
+ profit = base_cost * profit_ratio
+ return base_cost + overhead + profit
diff --git a/pricing_system/modules/pricing_strategies/pricing_strategies.py b/pricing_system/modules/pricing_strategies/pricing_strategies.py
index 3d0f062edfbc04753689587b8717e06c97886252..ea4fa0b1cdd23b48e75996affac7776773d0546d 100644
--- a/pricing_system/modules/pricing_strategies/pricing_strategies.py
+++ b/pricing_system/modules/pricing_strategies/pricing_strategies.py
@@ -1,1505 +1,461 @@
"""
-وحدة استراتيجيات التسعير المتقدمة - تنفيذ استراتيجيات متعددة للتسعير
+وحدة استراتيجيات التسعير المتقدمة
"""
-
import streamlit as st
import pandas as pd
import numpy as np
-import plotly.express as px
-import plotly.graph_objects as go
-import os
-import json
from datetime import datetime
-import io
class PricingStrategies:
- """فئة استراتيجيات التسعير المتقدمة"""
-
def __init__(self):
- """تهيئة وحدة استراتيجيات التسعير"""
-
- # تهيئة حالة الجلسة لاستراتيجيات التسعير
if 'pricing_strategies' not in st.session_state:
self._initialize_pricing_strategies()
-
+ if 'items' not in st.session_state:
+ st.session_state.items = []
+
def _initialize_pricing_strategies(self):
- """تهيئة بيانات استراتيجيات التسعير"""
-
- # إنشاء بيانات افتراضية لاستراتيجيات التسعير
- strategies = [
- {
- "id": "STD-001",
- "name": "التسعير القياسي",
- "description": "استراتيجية التسعير القياسية تعتمد على تحديد سعر كل بند بناءً على تكلفته الفعلية مضافاً إليها نسبة ربح ثابتة",
- "profit_margin": 15,
- "risk_factor": 5,
- "overhead_percentage": 10,
- "is_active": True,
- "parameters": {
- "apply_uniform_margin": True,
- "margin_variation": 0
- }
- },
- {
- "id": "BAL-001",
- "name": "التسعير المتزن",
- "description": "استراتيجية التسعير المتزن تعتمد على توزيع هامش الربح بشكل متوازن على جميع بنود المشروع مع مراعاة المخاطر",
- "profit_margin": 18,
- "risk_factor": 8,
- "overhead_percentage": 12,
- "is_active": True,
- "parameters": {
- "balance_factor": 0.8,
- "risk_weight": 1.2
- }
- },
- {
- "id": "UNB-001",
- "name": "التسعير غير المتزن",
- "description": "استراتيجية التسعير غير المتزن تعتمد على زيادة أسعار البنود المبكرة في المشروع وتخفيض أسعار البنود المتأخرة",
- "profit_margin": 20,
- "risk_factor": 10,
- "overhead_percentage": 15,
- "is_active": True,
- "parameters": {
- "front_loading_factor": 1.3,
- "back_loading_factor": 0.7,
- "threshold_percentage": 30
- }
- },
- {
- "id": "PRF-001",
- "name": "التسعير الموجه للربحية",
- "description": "استراتيجية التسعير الموجه للربحية تعتمد على زيادة أسعار البنود ذات التكلفة المنخفضة والكميات الكبيرة",
- "profit_margin": 25,
- "risk_factor": 12,
- "overhead_percentage": 18,
- "is_active": True,
- "parameters": {
- "high_volume_factor": 1.4,
- "low_cost_factor": 1.5,
- "volume_threshold": 70,
- "cost_threshold": 30
- }
- },
- {
- "id": "BDL-001",
- "name": "التسعير بالتجميع",
- "description": "استراتيجية التسعير بالتجميع تعتمد على تجميع البنود المتشابهة وتسعيرها كمجموعة واحدة",
- "profit_margin": 17,
- "risk_factor": 7,
- "overhead_percentage": 12,
- "is_active": True,
- "parameters": {
- "bundling_threshold": 0.8,
- "similarity_measure": "category",
- "min_bundle_size": 3
- }
- },
- {
- "id": "LOC-001",
- "name": "التسعير بالمحتوى المحلي",
- "description": "استراتيجية التسعير بالمحتوى المحلي تعتمد على زيادة نسبة المحتوى المحلي في المشروع لتحقيق متطلبات الجهات المالكة",
- "profit_margin": 15,
- "risk_factor": 6,
- "overhead_percentage": 10,
- "is_active": True,
- "parameters": {
- "local_content_target": 70,
- "local_content_premium": 1.2,
- "imported_content_discount": 0.9
- }
- }
- ]
-
- # إنشاء بيانات افتراضية للمشاريع
- projects = [
- {
- "id": "PRJ-001",
- "name": "مشروع تطوير طريق الملك عبدالله",
- "value": 50000000,
- "duration": 24,
- "start_date": "2025-01-01",
- "end_date": "2026-12-31",
- "status": "جاري",
- "location": "الرياض",
- "client": "وزارة النقل",
- "description": "مشروع تطوير وتوسعة طريق الملك عبدالله بطول 15 كم"
- },
- {
- "id": "PRJ-002",
- "name": "مشروع إنشاء شبكة صرف صحي",
- "value": 30000000,
- "duration": 18,
- "start_date": "2025-03-01",
- "end_date": "2026-08-31",
- "status": "جاري",
- "location": "جدة",
- "client": "شركة المياه الوطنية",
- "description": "مشروع إنشاء شبكة صرف صحي في حي النزهة بجدة"
- },
- {
- "id": "PRJ-003",
- "name": "مشروع إنشاء جسر تقاطع طريق الملك فهد",
- "value": 80000000,
- "duration": 30,
- "start_date": "2025-02-01",
- "end_date": "2027-07-31",
- "status": "جاري",
- "location": "الدمام",
- "client": "أمانة المنطقة الشرقية",
- "description": "مشروع إنشاء جسر علوي عند تقاطع طريق الملك فهد مع طريق الأمير محمد بن فهد"
- }
- ]
-
- # إنشاء بيانات افتراضية لتطبيق الاستراتيجيات على المشاريع
- strategy_applications = []
-
- for project in projects:
- # اختيار استراتيجية افتراضية لكل مشروع
- if project["id"] == "PRJ-001":
- strategy_id = "BAL-001" # التسعير المتزن
- elif project["id"] == "PRJ-002":
- strategy_id = "UNB-001" # التسعير غير المتزن
- else:
- strategy_id = "LOC-001" # التسعير بالمحتوى المحلي
-
- strategy_applications.append({
- "project_id": project["id"],
- "strategy_id": strategy_id,
- "application_date": datetime.now().strftime("%Y-%m-%d"),
- "applied_by": "مدير التسعير",
- "status": "مطبق",
- "notes": "تم تطبيق الاستراتيجية بنجاح",
- "results": {
- "original_value": project["value"],
- "new_value": project["value"] * 1.1, # زيادة افتراضية بنسبة 10%
- "profit_margin": 18,
- "local_content_percentage": 65,
- "risk_assessment": "متوسط"
- }
- })
-
- # إنشاء بيانات افتراضية لمقارنة الاستراتيجيات
- strategy_comparisons = []
-
- for project in projects:
- comparison = {
- "project_id": project["id"],
- "comparison_date": datetime.now().strftime("%Y-%m-%d"),
- "compared_by": "مدير التسعير",
- "strategies": {}
- }
-
- # إضافة نتائج لكل استراتيجية
- base_value = project["value"]
-
- for strategy in strategies:
- # حساب قيمة افتراضية مختلفة لكل استراتيجية
- if strategy["id"] == "STD-001": # التسعير القياسي
- value_factor = 1.0
- profit_margin = 15
- local_content = 60
- elif strategy["id"] == "BAL-001": # التسعير المتزن
- value_factor = 1.05
- profit_margin = 18
- local_content = 62
- elif strategy["id"] == "UNB-001": # التسعير غير المتزن
- value_factor = 1.08
- profit_margin = 20
- local_content = 58
- elif strategy["id"] == "PRF-001": # التسعير الموجه للربحية
- value_factor = 1.15
- profit_margin = 25
- local_content = 55
- elif strategy["id"] == "BDL-001": # التسعير بالتجميع
- value_factor = 1.03
- profit_margin = 17
- local_content = 63
- else: # التسعير بالمحتوى المحلي
- value_factor = 1.1
- profit_margin = 15
- local_content = 70
-
- comparison["strategies"][strategy["id"]] = {
- "value": base_value * value_factor,
- "profit_margin": profit_margin,
- "local_content_percentage": local_content,
- "risk_assessment": "متوسط" if profit_margin < 20 else "مرتفع",
- "cash_flow_impact": "إيجابي" if strategy["id"] in ["UNB-001", "PRF-001"] else "محايد"
- }
-
- strategy_comparisons.append(comparison)
-
- # تخزين البيانات في حالة الجلسة
+ """تهيئة استراتيجيات التسعير"""
st.session_state.pricing_strategies = {
- "strategies": pd.DataFrame(strategies),
- "projects": pd.DataFrame(projects),
- "strategy_applications": pd.DataFrame(strategy_applications),
- "strategy_comparisons": strategy_comparisons
- }
-
- def render(self):
- """عرض واجهة استراتيجيات التسعير"""
-
- st.markdown("## استراتيجيات التسعير المتقدمة")
-
- # إنشاء تبويبات لعرض استراتيجيات التسعير
- tabs = st.tabs([
- "الاستراتيجيات",
- "تطبيق الاستراتيجيات",
- "مقارنة الاستراتيجيات",
- "تحليل المحتوى المحلي"
- ])
-
- with tabs[0]:
- self._render_strategies_tab()
-
- with tabs[1]:
- self._render_apply_strategies_tab()
-
- with tabs[2]:
- self._render_compare_strategies_tab()
-
- with tabs[3]:
- self._render_local_content_tab()
-
- def _render_strategies_tab(self):
- """عرض تبويب الاستراتيجيات"""
-
- st.markdown("### استراتيجيات التسعير")
-
- # استخراج البيانات
- strategies = st.session_state.pricing_strategies["strategies"]
-
- # عرض الاستراتيجيات
- if not strategies.empty:
- # إنشاء جدول للعرض
- display_df = strategies[["id", "name", "profit_margin", "risk_factor", "overhead_percentage", "is_active"]].copy()
- display_df.columns = ["الكود", "الاسم", "هامش الربح (%)", "عامل المخاطرة (%)", "نسبة المصاريف العمومية (%)", "نشط"]
-
- # عرض الجدول
- st.dataframe(display_df, use_container_width=True)
-
- # إضافة استراتيجية جديدة
- st.markdown("### إضافة استراتيجية جديدة")
-
- with st.form("add_strategy_form"):
- # الصف الأول
- col1, col2 = st.columns(2)
-
- with col1:
- strategy_id = st.text_input("كود الاستراتيجية", value=f"STR-{len(strategies) + 1:03d}")
- strategy_name = st.text_input("اسم الاستراتيجية", placeholder="مثال: استراتيجية التسعير التنافسي")
-
- with col2:
- strategy_profit_margin = st.slider("هامش الربح (%)", min_value=5, max_value=40, value=15, step=1)
- strategy_risk_factor = st.slider("عامل المخاطرة (%)", min_value=0, max_value=20, value=5, step=1)
-
- # الصف الثاني
- col1, col2 = st.columns(2)
-
- with col1:
- strategy_overhead = st.slider("نسبة المصاريف العمومية (%)", min_value=5, max_value=30, value=10, step=1)
- strategy_is_active = st.checkbox("نشط", value=True)
-
- with col2:
- strategy_type = st.selectbox(
- "نوع الاستراتيجية",
- [
- "التسعير القياسي",
- "التسعير المتزن",
- "التسعير غير المتزن",
- "التسعير الموجه للربحية",
- "التسعير بالتجميع",
- "التسعير بالمحتوى المحلي",
- "أخرى"
- ]
- )
-
- # الصف الثالث
- strategy_description = st.text_area("وصف الاستراتيجية", placeholder="أدخل وصفاً تفصيلياً للاستراتيجية")
-
- # معلمات الاستراتيجية
- st.markdown("#### معلمات الاستراتيجية")
-
- # إظهار معلمات مختلفة حسب نوع الاستراتيجية
- parameters = {}
-
- if strategy_type == "التسعير القياسي":
- col1, col2 = st.columns(2)
-
- with col1:
- apply_uniform_margin = st.checkbox("تطبيق هامش ربح موحد", value=True)
-
- with col2:
- margin_variation = st.slider("تباين الهامش (%)", min_value=0, max_value=20, value=0, step=1)
-
- parameters = {
- "apply_uniform_margin": apply_uniform_margin,
- "margin_variation": margin_variation
- }
-
- elif strategy_type == "التسعير المتزن":
- col1, col2 = st.columns(2)
-
- with col1:
- balance_factor = st.slider("معامل التوازن", min_value=0.1, max_value=1.0, value=0.8, step=0.1)
-
- with col2:
- risk_weight = st.slider("وزن المخاطرة", min_value=0.5, max_value=2.0, value=1.2, step=0.1)
-
- parameters = {
- "balance_factor": balance_factor,
- "risk_weight": risk_weight
- }
-
- elif strategy_type == "التسعير غير المتزن":
- col1, col2 = st.columns(2)
-
- with col1:
- front_loading_factor = st.slider("معامل التحميل الأمامي", min_value=1.0, max_value=2.0, value=1.3, step=0.1)
- threshold_percentage = st.slider("نسبة الحد الفاصل (%)", min_value=10, max_value=50, value=30, step=5)
-
- with col2:
- back_loading_factor = st.slider("معامل التحميل الخلفي", min_value=0.5, max_value=1.0, value=0.7, step=0.1)
-
- parameters = {
- "front_loading_factor": front_loading_factor,
- "back_loading_factor": back_loading_factor,
- "threshold_percentage": threshold_percentage
- }
-
- elif strategy_type == "التسعير الموجه للربحية":
- col1, col2 = st.columns(2)
-
- with col1:
- high_volume_factor = st.slider("معامل الكميات الكبيرة", min_value=1.0, max_value=2.0, value=1.4, step=0.1)
- volume_threshold = st.slider("حد الكميات الكبيرة (%)", min_value=50, max_value=90, value=70, step=5)
-
- with col2:
- low_cost_factor = st.slider("معامل التكلفة المنخفضة", min_value=1.0, max_value=2.0, value=1.5, step=0.1)
- cost_threshold = st.slider("حد التكلفة المنخفضة (%)", min_value=10, max_value=50, value=30, step=5)
-
- parameters = {
- "high_volume_factor": high_volume_factor,
- "low_cost_factor": low_cost_factor,
- "volume_threshold": volume_threshold,
- "cost_threshold": cost_threshold
- }
-
- elif strategy_type == "التسعير بالتجميع":
- col1, col2 = st.columns(2)
-
- with col1:
- bundling_threshold = st.slider("حد التجميع", min_value=0.5, max_value=1.0, value=0.8, step=0.1)
- min_bundle_size = st.slider("الحد الأدنى لحجم المجموعة", min_value=2, max_value=10, value=3, step=1)
-
- with col2:
- similarity_measure = st.selectbox(
- "مقياس التشابه",
- ["category", "subcategory", "custom"],
- format_func=lambda x: "الفئة" if x == "category" else "الفئة الفرعية" if x == "subcategory" else "مخصص"
- )
-
- parameters = {
- "bundling_threshold": bundling_threshold,
- "similarity_measure": similarity_measure,
- "min_bundle_size": min_bundle_size
- }
-
- elif strategy_type == "التسعير بالمحتوى المحلي":
- col1, col2 = st.columns(2)
-
- with col1:
- local_content_target = st.slider("نسبة المحتوى المحلي المستهدفة (%)", min_value=30, max_value=100, value=70, step=5)
- local_content_premium = st.slider("علاوة المحتوى المحلي", min_value=1.0, max_value=1.5, value=1.2, step=0.1)
-
- with col2:
- imported_content_discount = st.slider("خصم المحتوى المستورد", min_value=0.5, max_value=1.0, value=0.9, step=0.1)
-
- parameters = {
- "local_content_target": local_content_target,
- "local_content_premium": local_content_premium,
- "imported_content_discount": imported_content_discount
- }
-
- # زر الإضافة
- submit_button = st.form_submit_button("إضافة الاستراتيجية")
-
- if submit_button:
- # التحقق من البيانات
- if not strategy_name or not strategy_description:
- st.error("يرجى إدخال المعلومات الأساسية للاستراتيجية")
- else:
- # إنشاء استراتيجية جديدة
- new_strategy = {
- "id": strategy_id,
- "name": strategy_name,
- "description": strategy_description,
- "profit_margin": strategy_profit_margin,
- "risk_factor": strategy_risk_factor,
- "overhead_percentage": strategy_overhead,
- "is_active": strategy_is_active,
- "parameters": parameters
- }
-
- # إضافة الاستراتيجية إلى الجدول
- st.session_state.pricing_strategies["strategies"] = pd.concat([
- st.session_state.pricing_strategies["strategies"],
- pd.DataFrame([new_strategy])
- ], ignore_index=True)
-
- st.success(f"تمت إضافة الاستراتيجية {strategy_name} بنجاح!")
-
- # إعادة تحميل الصفحة
- st.experimental_rerun()
-
- # عرض تفاصيل الاستراتيجيات
- st.markdown("### تفاصيل الاستراتيجيات")
-
- selected_strategy_id = st.selectbox("اختر استراتيجية لعرض التفاصيل", strategies["id"].tolist(), key="strategy_details")
-
- # استخراج الاستراتيجية المختارة
- selected_strategy = strategies[strategies["id"] == selected_strategy_id].iloc[0]
-
- # عرض تفاصيل الاستراتيجية
- st.markdown(f"**الاستراتيجية:** {selected_strategy['name']} ({selected_strategy['id']})")
- st.markdown(f"**الوصف:** {selected_strategy['description']}")
- st.markdown(f"**هامش الربح:** {selected_strategy['profit_margin']}%")
- st.markdown(f"**عامل المخاطرة:** {selected_strategy['risk_factor']}%")
- st.markdown(f"**نسبة المصاريف العمومية:** {selected_strategy['overhead_percentage']}%")
- st.markdown(f"**الحالة:** {'نشط' if selected_strategy['is_active'] else 'غير نشط'}")
-
- # عرض معلمات الاستراتيجية
- st.markdown("#### معلمات الاستراتيجية")
-
- parameters = selected_strategy["parameters"]
-
- if isinstance(parameters, dict):
- for key, value in parameters.items():
- # تنسيق اسم المعلمة
- formatted_key = key.replace("_", " ").title()
-
- # عرض المعلمة
- st.markdown(f"**{formatted_key}:** {value}")
-
- # تعديل الاستراتيجية
- st.markdown("#### تعديل الاستراتيجية")
-
- with st.form("edit_strategy_form"):
- # الصف الأول
- col1, col2 = st.columns(2)
-
- with col1:
- edit_strategy_name = st.text_input("اسم الاستراتيجية", value=selected_strategy["name"], key="edit_name")
-
- with col2:
- edit_strategy_profit_margin = st.slider("هامش الربح (%)", min_value=5, max_value=40, value=int(selected_strategy["profit_margin"]), step=1, key="edit_profit")
- edit_strategy_risk_factor = st.slider("عامل المخاطرة (%)", min_value=0, max_value=20, value=int(selected_strategy["risk_factor"]), step=1, key="edit_risk")
-
- # الصف الثاني
- col1, col2 = st.columns(2)
-
- with col1:
- edit_strategy_overhead = st.slider("نسبة المصاريف العمومية (%)", min_value=5, max_value=30, value=int(selected_strategy["overhead_percentage"]), step=1, key="edit_overhead")
- edit_strategy_is_active = st.checkbox("نشط", value=bool(selected_strategy["is_active"]), key="edit_active")
-
- # الصف الثالث
- edit_strategy_description = st.text_area("وصف الاستراتيجية", value=selected_strategy["description"], key="edit_description")
-
- # زر الحفظ
- submit_button = st.form_submit_button("حفظ التعديلات")
-
- if submit_button:
- # تحديث الاستراتيجية
- strategy_index = strategies[strategies["id"] == selected_strategy_id].index[0]
-
- strategies.at[strategy_index, "name"] = edit_strategy_name
- strategies.at[strategy_index, "description"] = edit_strategy_description
- strategies.at[strategy_index, "profit_margin"] = edit_strategy_profit_margin
- strategies.at[strategy_index, "risk_factor"] = edit_strategy_risk_factor
- strategies.at[strategy_index, "overhead_percentage"] = edit_strategy_overhead
- strategies.at[strategy_index, "is_active"] = edit_strategy_is_active
-
- # تحديث حالة الجلسة
- st.session_state.pricing_strategies["strategies"] = strategies
-
- st.success("تم تحديث الاستراتيجية بنجاح!")
-
- # إعادة تحميل الصفحة
- st.experimental_rerun()
- else:
- st.warning("لا يوجد استراتيجيات تسعير")
-
- def _render_apply_strategies_tab(self):
- """عرض تبويب تطبيق الاستراتيجيات"""
-
- st.markdown("### تطبيق استراتيجيات التسعير")
-
- # استخراج البيانات
- strategies = st.session_state.pricing_strategies["strategies"]
- projects = st.session_state.pricing_strategies["projects"]
- strategy_applications = st.session_state.pricing_strategies["strategy_applications"]
-
- # عرض التطبيقات الحالية
- if not strategy_applications.empty:
- # دمج البيانات
- merged_applications = pd.merge(
- strategy_applications,
- projects[["id", "name", "value"]],
- left_on="project_id",
- right_on="id",
- suffixes=("", "_project")
- )
-
- merged_applications = pd.merge(
- merged_applications,
- strategies[["id", "name"]],
- left_on="strategy_id",
- right_on="id",
- suffixes=("_project", "_strategy")
- )
-
- # إنشاء جدول للعرض
- display_df = merged_applications[["project_id", "name_project", "strategy_id", "name_strategy", "application_date", "status"]].copy()
- display_df.columns = ["كود المشروع", "اسم المشروع", "كود الاستراتيجية", "اسم الاستراتيجية", "تاريخ التطبيق", "الحالة"]
-
- # عرض الجدول
- st.dataframe(display_df, use_container_width=True)
-
- # تطبيق استراتيجية جديدة
- st.markdown("### تطبيق استراتيجية جديدة")
-
- with st.form("apply_strategy_form"):
- # الصف الأول
- col1, col2 = st.columns(2)
-
- with col1:
- project_id = st.selectbox("اختر المشروع", projects["id"].tolist(), format_func=lambda x: f"{x} - {projects[projects['id'] == x]['name'].values[0]}")
-
- with col2:
- active_strategies = strategies[strategies["is_active"] == True]
- strategy_id = st.selectbox("اختر الاستراتيجية", active_strategies["id"].tolist(), format_func=lambda x: f"{x} - {strategies[strategies['id'] == x]['name'].values[0]}")
-
- # الصف الثاني
- col1, col2 = st.columns(2)
-
- with col1:
- application_date = st.date_input("تاريخ التطبيق", value=datetime.now())
- applied_by = st.text_input("تم التطبيق بواسطة", value="مدير التسعير")
-
- with col2:
- status = st.selectbox("الحالة", ["مطبق", "قيد التطبيق", "معلق"])
-
- # الصف الثالث
- notes = st.text_area("ملاحظات", placeholder="أدخل ملاحظات حول تطبيق الاستراتيجية")
-
- # زر التطبيق
- submit_button = st.form_submit_button("تطبيق الاستراتيجية")
-
- if submit_button:
- # التحقق من البيانات
- if not project_id or not strategy_id:
- st.error("يرجى اختيار المشروع والاستراتيجية")
- else:
- # التحقق من عدم وجود تطبيق سابق للاستراتيجية على المشروع
- existing_application = strategy_applications[
- (strategy_applications["project_id"] == project_id) &
- (strategy_applications["strategy_id"] == strategy_id)
- ]
-
- if not existing_application.empty:
- st.warning("تم تطبيق هذه الاستراتيجية على هذا المشروع مسبقاً")
- else:
- # استخراج بيانات المشروع والاستراتيجية
- project = projects[projects["id"] == project_id].iloc[0]
- strategy = strategies[strategies["id"] == strategy_id].iloc[0]
-
- # حساب نتائج تطبيق الاستراتيجية
- original_value = project["value"]
-
- # حساب القيمة الجديدة بناءً على نوع الاستراتيجية
- if "STD" in strategy_id: # التسعير القياسي
- new_value = original_value * (1 + strategy["profit_margin"] / 100)
- profit_margin = strategy["profit_margin"]
- local_content = 60
- elif "BAL" in strategy_id: # التسعير المتزن
- new_value = original_value * (1 + (strategy["profit_margin"] + strategy["risk_factor"]) / 100)
- profit_margin = strategy["profit_margin"]
- local_content = 62
- elif "UNB" in strategy_id: # التسعير غير المتزن
- new_value = original_value * (1 + (strategy["profit_margin"] + strategy["risk_factor"]) / 100)
- profit_margin = strategy["profit_margin"]
- local_content = 58
- elif "PRF" in strategy_id: # التسعير الموجه للربحية
- new_value = original_value * (1 + (strategy["profit_margin"] + strategy["risk_factor"] / 2) / 100)
- profit_margin = strategy["profit_margin"]
- local_content = 55
- elif "BDL" in strategy_id: # التسعير بالتجميع
- new_value = original_value * (1 + strategy["profit_margin"] / 100)
- profit_margin = strategy["profit_margin"]
- local_content = 63
- else: # التسعير بالمحتوى المحلي
- new_value = original_value * (1 + (strategy["profit_margin"] - 2) / 100)
- profit_margin = strategy["profit_margin"] - 2
- local_content = 70
-
- # تحديد تقييم المخاطر
- risk_assessment = "منخفض" if strategy["risk_factor"] < 8 else "متوسط" if strategy["risk_factor"] < 15 else "مرتفع"
-
- # إنشاء تطبيق جديد
- new_application = {
- "project_id": project_id,
- "strategy_id": strategy_id,
- "application_date": application_date.strftime("%Y-%m-%d"),
- "applied_by": applied_by,
- "status": status,
- "notes": notes,
- "results": {
- "original_value": original_value,
- "new_value": new_value,
- "profit_margin": profit_margin,
- "local_content_percentage": local_content,
- "risk_assessment": risk_assessment
- }
- }
-
- # إضافة التطبيق إلى الجدول
- st.session_state.pricing_strategies["strategy_applications"] = pd.concat([
- st.session_state.pricing_strategies["strategy_applications"],
- pd.DataFrame([new_application])
- ], ignore_index=True)
-
- st.success(f"تم تطبيق استراتيجية {strategy['name']} على مشروع {project['name']} بنجاح!")
-
- # إعادة تحميل الصفحة
- st.experimental_rerun()
-
- # عرض نتائج التطبيق
- if not strategy_applications.empty:
- st.markdown("### نتائج تطبيق الاستراتيجيات")
-
- # اختيار مشروع لعرض النتائج
- selected_project_id = st.selectbox("اختر المشروع", strategy_applications["project_id"].unique().tolist(), key="results_project", format_func=lambda x: f"{x} - {projects[projects['id'] == x]['name'].values[0]}")
-
- # استخراج تطبيقات المشروع المختار
- project_applications = strategy_applications[strategy_applications["project_id"] == selected_project_id]
-
- if not project_applications.empty:
- # استخراج المشروع
- project = projects[projects["id"] == selected_project_id].iloc[0]
-
- st.markdown(f"**المشروع:** {project['name']} ({project['id']})")
- st.markdown(f"**القيمة الأصلية:** {project['value']:,} ريال")
-
- # عرض نتائج كل تطبيق
- for _, application in project_applications.iterrows():
- # استخراج الاستراتيجية
- strategy = strategies[strategies["id"] == application["strategy_id"]].iloc[0]
-
- st.markdown(f"#### استراتيجية {strategy['name']}")
-
- # استخراج النتائج
- results = application["results"]
-
- if isinstance(results, dict):
- col1, col2, col3 = st.columns(3)
-
- with col1:
- st.metric("القيمة الجديدة", f"{results['new_value']:,} ريال", f"{((results['new_value'] / results['original_value']) - 1) * 100:.1f}%")
-
- with col2:
- st.metric("هامش الربح", f"{results['profit_margin']}%")
-
- with col3:
- st.metric("نسبة المحتوى المحلي", f"{results['local_content_percentage']}%")
-
- st.markdown(f"**تقييم المخاطر:** {results['risk_assessment']}")
-
- if "notes" in application and application["notes"]:
- st.markdown(f"**ملاحظات:** {application['notes']}")
-
- st.markdown("---")
-
- # مقارنة نتائج التطبيقات
- if len(project_applications) > 1:
- st.markdown("#### مقارنة نتائج تطبيق الاستراتيجيات")
-
- # إنشاء بيانات للمقارنة
- comparison_data = []
-
- for _, application in project_applications.iterrows():
- # استخراج الاستراتيجية
- strategy = strategies[strategies["id"] == application["strategy_id"]].iloc[0]
-
- # استخراج النتائج
- results = application["results"]
-
- if isinstance(results, dict):
- comparison_data.append({
- "الاستراتيجية": strategy["name"],
- "القيمة الجديدة": results["new_value"],
- "هامش الربح": results["profit_margin"],
- "نسبة المحتوى المحلي": results["local_content_percentage"],
- "تقييم المخاطر": results["risk_assessment"]
- })
-
- comparison_df = pd.DataFrame(comparison_data)
-
- # عرض الجدول
- st.dataframe(comparison_df, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة
- fig = px.bar(
- comparison_df,
- x="الاستراتيجية",
- y="القيمة الجديدة",
- title=f"مقارنة القيمة الجديدة للمشروع حسب الاستراتيجية",
- color="الاستراتيجية",
- text_auto=True
- )
-
- # إضافة خط أفقي عند القيمة الأصلية
- fig.add_shape(
- type="line",
- x0=-0.5,
- y0=project["value"],
- x1=len(comparison_df) - 0.5,
- y1=project["value"],
- line=dict(
- color="red",
- width=2,
- dash="dash"
- )
- )
-
- # إضافة تسمية للخط الأفقي
- fig.add_annotation(
- x=len(comparison_df) - 0.5,
- y=project["value"],
- text="القيمة الأصلية",
- showarrow=False,
- yshift=10
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة بين هامش الربح ونسبة المحتوى المحلي
- fig = px.scatter(
- comparison_df,
- x="هامش الربح",
- y="نسبة المحتوى المحلي",
- title=f"مقارنة هامش الربح ونسبة المحتوى المحلي حسب الاستراتيجية",
- color="الاستراتيجية",
- size="القيمة الجديدة",
- hover_data=["تقييم المخاطر"]
- )
-
- st.plotly_chart(fig, use_container_width=True)
- else:
- st.info("لا يوجد تطبيقات لهذا المشروع")
-
- def _render_compare_strategies_tab(self):
- """عرض تبويب مقارنة الاستراتيجيات"""
-
- st.markdown("### مقارنة استراتيجيات التسعير")
-
- # استخراج البيانات
- strategies = st.session_state.pricing_strategies["strategies"]
- projects = st.session_state.pricing_strategies["projects"]
- strategy_comparisons = st.session_state.pricing_strategies["strategy_comparisons"]
-
- # اختيار مشروع للمقارنة
- selected_project_id = st.selectbox("اختر المشروع", projects["id"].tolist(), format_func=lambda x: f"{x} - {projects[projects['id'] == x]['name'].values[0]}")
-
- # استخراج المشروع
- project = projects[projects["id"] == selected_project_id].iloc[0]
-
- # البحث عن مقارنة موجودة
- existing_comparison = None
-
- for comparison in strategy_comparisons:
- if comparison["project_id"] == selected_project_id:
- existing_comparison = comparison
- break
-
- if existing_comparison:
- st.markdown(f"**المشروع:** {project['name']} ({project['id']})")
- st.markdown(f"**القيمة:** {project['value']:,} ريال")
- st.markdown(f"**تاريخ المقارنة:** {existing_comparison['comparison_date']}")
-
- # إنشاء بيانات للمقارنة
- comparison_data = []
-
- for strategy_id, results in existing_comparison["strategies"].items():
- # استخراج الاستراتيجية
- strategy = strategies[strategies["id"] == strategy_id].iloc[0]
-
- comparison_data.append({
- "الاستراتيجية": strategy["name"],
- "القيمة": results["value"],
- "هامش الربح": results["profit_margin"],
- "نسبة المحتوى المحلي": results["local_content_percentage"],
- "تقييم المخاطر": results["risk_assessment"],
- "تأثير التدفق النقدي": results["cash_flow_impact"]
- })
-
- comparison_df = pd.DataFrame(comparison_data)
-
- # عرض الجدول
- st.dataframe(comparison_df, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة
- fig = px.bar(
- comparison_df,
- x="الاستراتيجية",
- y="القيمة",
- title=f"مقارنة قيمة المشروع حسب الاستراتيجية",
- color="الاستراتيجية",
- text_auto=True
- )
-
- # إضافة خط أفقي عند القيمة الأصلية
- fig.add_shape(
- type="line",
- x0=-0.5,
- y0=project["value"],
- x1=len(comparison_df) - 0.5,
- y1=project["value"],
- line=dict(
- color="red",
- width=2,
- dash="dash"
- )
- )
-
- # إضافة تسمية للخط الأفقي
- fig.add_annotation(
- x=len(comparison_df) - 0.5,
- y=project["value"],
- text="القيمة الأصلية",
- showarrow=False,
- yshift=10
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة بين هامش الربح ونسبة المحتوى المحلي
- fig = px.scatter(
- comparison_df,
- x="هامش الربح",
- y="نسبة المحتوى المحلي",
- title=f"مقارنة هامش الربح ونسبة المحتوى المحلي حسب الاستراتيجية",
- color="الاستراتيجية",
- size="القيمة",
- hover_data=["تقييم المخاطر", "تأثير التدفق النقدي"]
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة بين تأثير التدفق النقدي وتقييم المخاطر
- cash_flow_impact_map = {"إيجابي": 3, "محايد": 2, "سلبي": 1}
- risk_assessment_map = {"منخفض": 1, "متوسط": 2, "مرتفع": 3}
-
- comparison_df["تأثير التدفق النقدي (رقمي)"] = comparison_df["تأثير التدفق النقدي"].map(cash_flow_impact_map)
- comparison_df["تقييم المخاطر (رقمي)"] = comparison_df["تقييم المخاطر"].map(risk_assessment_map)
-
- fig = px.scatter(
- comparison_df,
- x="تقييم المخاطر (رقمي)",
- y="تأثير التدفق النقدي (رقمي)",
- title=f"مقارنة تأثير التدفق النقدي وتقييم المخاطر حسب الاستراتيجية",
- color="الاستراتيجية",
- size="القيمة",
- hover_data=["هامش الربح", "نسبة المحتوى المحلي"],
- labels={
- "تقييم المخاطر (رقمي)": "تقييم المخاطر",
- "تأثير التدفق النقدي (رقمي)": "تأثير التدفق النقدي"
- }
- )
-
- # تعديل محاور الرسم البياني
- fig.update_xaxes(
- tickvals=[1, 2, 3],
- ticktext=["منخفض", "متوسط", "مرتفع"]
- )
-
- fig.update_yaxes(
- tickvals=[1, 2, 3],
- ticktext=["سلبي", "محايد", "إيجابي"]
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل وتوصيات
- st.markdown("### تحليل وتوصيات")
-
- # تحديد أفضل استراتيجية حسب المعايير المختلفة
- best_value_strategy = comparison_df.loc[comparison_df["القيمة"].idxmax()]
- best_profit_strategy = comparison_df.loc[comparison_df["هامش الربح"].idxmax()]
- best_local_content_strategy = comparison_df.loc[comparison_df["نسبة المحتوى المحلي"].idxmax()]
- best_cash_flow_strategy = comparison_df.loc[comparison_df["تأثير التدفق النقدي (رقمي)"].idxmax()]
-
- # تحديد الاستراتيجية المتوازنة
- comparison_df["النقاط"] = (
- comparison_df["القيمة"] / project["value"] * 25 +
- comparison_df["هامش الربح"] * 2 +
- comparison_df["نسبة المحتوى المحلي"] * 0.5 +
- comparison_df["تأثير التدفق النقدي (رقمي)"] * 5 -
- comparison_df["تقييم المخاطر (رقمي)"] * 5
- )
-
- balanced_strategy = comparison_df.loc[comparison_df["النقاط"].idxmax()]
-
- st.markdown("#### أفضل الاستراتيجيات حسب المعايير المختلفة")
-
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown(f"**أعلى قيمة:** {best_value_strategy['الاستراتيجية']} ({best_value_strategy['القيمة']:,} ريال)")
- st.markdown(f"**أعلى هامش ربح:** {best_profit_strategy['الاستراتيجية']} ({best_profit_strategy['هامش الربح']}%)")
-
- with col2:
- st.markdown(f"**أعلى محتوى محلي:** {best_local_content_strategy['الاستراتيجية']} ({best_local_content_strategy['نسبة المحتوى المحلي']}%)")
- st.markdown(f"**أفضل تدفق نقدي:** {best_cash_flow_strategy['الاستراتيجية']} ({best_cash_flow_strategy['تأثير التدفق النقدي']})")
-
- st.markdown(f"**الاستراتيجية المتوازنة:** {balanced_strategy['الاستراتيجية']}")
-
- # توصيات
- st.markdown("#### التوصيات")
-
- # تحديد التوصية بناءً على خصائص المشروع
- if project["client"] == "وزارة النقل" or project["client"] == "أمانة المنطقة الشرقية":
- # المشاريع الحكومية تتطلب محتوى محلي عالي
- if best_local_content_strategy["نسبة المحتوى المحلي"] >= 65:
- recommended_strategy = best_local_content_strategy["الاستراتيجية"]
- recommendation_reason = "لتلبية متطلبات المحتوى المحلي للمشاريع الحكومية"
- else:
- recommended_strategy = balanced_strategy["الاستراتيجية"]
- recommendation_reason = "لتحقيق توازن بين المعايير المختلفة مع مراعاة متطلبات المشاريع الحكومية"
- elif "صرف صحي" in project["name"]:
- # مشاريع الصرف الصحي تتطلب تدفق نقدي جيد
- if best_cash_flow_strategy["تأثير التدفق النقدي"] == "إيجابي":
- recommended_strategy = best_cash_flow_strategy["الاستراتيجية"]
- recommendation_reason = "لضمان تدفق نقدي إيجابي في مشاريع الصرف الصحي التي تتطلب استثمارات أولية كبيرة"
- else:
- recommended_strategy = balanced_strategy["الاستراتيجية"]
- recommendation_reason = "لتحقيق توازن بين المعايير المختلفة مع مراعاة متطلبات مشاريع الصرف الصحي"
- elif project["value"] > 50000000:
- # المشاريع الكبيرة تتطلب إدارة مخاطر جيدة
- low_risk_strategies = comparison_df[comparison_df["تقييم المخاطر"] == "منخفض"]
-
- if not low_risk_strategies.empty:
- low_risk_balanced = low_risk_strategies.loc[low_risk_strategies["النقاط"].idxmax()]
- recommended_strategy = low_risk_balanced["الاستراتيجية"]
- recommendation_reason = "لتقليل المخاطر في المشاريع الكبيرة مع تحقيق توازن بين المعايير الأخرى"
- else:
- recommended_strategy = balanced_strategy["الاستراتيجية"]
- recommendation_reason = "لتحقيق توازن بين المعايير المختلفة مع مراعاة حجم المشروع الكبير"
- else:
- # المشاريع الأخرى
- recommended_strategy = balanced_strategy["الاستراتيجية"]
- recommendation_reason = "لتحقيق توازن بين المعايير المختلفة"
-
- st.markdown(f"**الاستراتيجية الموصى بها:** {recommended_strategy}")
- st.markdown(f"**سبب التوصية:** {recommendation_reason}")
-
- # نصائح إضافية
- st.markdown("#### نصائح إضافية")
-
- if "جسر" in project["name"] or "طريق" in project["name"]:
- st.markdown("- مراعاة تكاليف المواد الإنشائية وتقلبات أسعارها في السوق السعودي")
- st.markdown("- التأكد من توفر المعدات الثقيلة اللازمة للمشروع")
- st.markdown("- مراعاة تكاليف النقل للمواد الإنشائية")
-
- if "صرف صحي" in project["name"]:
- st.markdown("- مراعاة تكاليف الحفر والردم")
- st.markdown("- التأكد من توفر المواسير والملحقات بالمواصفات المطلوبة")
- st.markdown("- مراعاة تكاليف الاختبارات والفحوصات")
-
- if project["value"] > 50000000:
- st.markdown("- تقسيم المشروع إلى مراحل لتحسين التدفق النقدي")
- st.markdown("- مراعاة تكاليف الضمانات البنكية")
- st.markdown("- التأكد من توفر السيولة اللازمة للمشروع")
-
- # إعادة المقارنة
- st.markdown("#### إعادة المقارنة")
-
- if st.button("إعادة المقارنة"):
- # إنشاء مقارنة جديدة
- new_comparison = {
- "project_id": selected_project_id,
- "comparison_date": datetime.now().strftime("%Y-%m-%d"),
- "compared_by": "مدير التسعير",
- "strategies": {}
- }
-
- # إضافة نتائج لكل استراتيجية
- base_value = project["value"]
-
- for _, strategy in strategies.iterrows():
- # حساب قيمة افتراضية مختلفة لكل استراتيجية
- if strategy["id"] == "STD-001": # التسعير القياسي
- value_factor = 1.0
- profit_margin = 15
- local_content = 60
- cash_flow_impact = "محايد"
- elif strategy["id"] == "BAL-001": # التسعير المتزن
- value_factor = 1.05
- profit_margin = 18
- local_content = 62
- cash_flow_impact = "محايد"
- elif strategy["id"] == "UNB-001": # التسعير غير المتزن
- value_factor = 1.08
- profit_margin = 20
- local_content = 58
- cash_flow_impact = "إيجابي"
- elif strategy["id"] == "PRF-001": # التسعير الموجه للربحية
- value_factor = 1.15
- profit_margin = 25
- local_content = 55
- cash_flow_impact = "إيجابي"
- elif strategy["id"] == "BDL-001": # التسعير بالتجميع
- value_factor = 1.03
- profit_margin = 17
- local_content = 63
- cash_flow_impact = "محايد"
- else: # التسعير بالمحتوى المحلي
- value_factor = 1.1
- profit_margin = 15
- local_content = 70
- cash_flow_impact = "محايد"
-
- # تحديد تقييم المخاطر
- risk_assessment = "منخفض" if strategy["risk_factor"] < 8 else "متوسط" if strategy["risk_factor"] < 15 else "مرتفع"
-
- new_comparison["strategies"][strategy["id"]] = {
- "value": base_value * value_factor,
- "profit_margin": profit_margin,
- "local_content_percentage": local_content,
- "risk_assessment": risk_assessment,
- "cash_flow_impact": cash_flow_impact
- }
-
- # تحديث المقارنة الموجودة
- for i, comparison in enumerate(strategy_comparisons):
- if comparison["project_id"] == selected_project_id:
- strategy_comparisons[i] = new_comparison
- break
-
- st.success("تم إعادة المقارنة بنجاح!")
-
- # إعادة تحميل الصفحة
- st.experimental_rerun()
- else:
- st.info("لا يوجد مقارنة لهذا المشروع")
-
- # إنشاء مقارنة جديدة
- if st.button("إنشاء مقارنة جديدة"):
- # إنشاء مقارنة جديدة
- new_comparison = {
- "project_id": selected_project_id,
- "comparison_date": datetime.now().strftime("%Y-%m-%d"),
- "compared_by": "مدير التسعير",
- "strategies": {}
+ "strategies": [
+ {
+ "id": "MTR-001",
+ "name": "تسعير المواد",
+ "description": "استراتيجية خاصة بتسعير المواد تأخذ في الاعتبار تقلبات الأسعار وتكاليف النقل والتخزين",
+ "profit_margin": 0.12,
+ "risk_factor": 0.05,
+ "storage_cost": 0.03,
+ "transport_cost": 0.04,
+ "market_volatility": 0.02,
+ "applicable_to": "materials"
+ },
+ {
+ "id": "LBR-001",
+ "name": "تسعير العمالة",
+ "description": "استراتيجية مخصصة للعمالة تراعي المهارات والخبرات ومعدلات الإنتاجية",
+ "profit_margin": 0.15,
+ "risk_factor": 0.03,
+ "productivity_factor": 1.0,
+ "overtime_factor": 1.5,
+ "skill_premium": 0.1,
+ "applicable_to": "labor"
+ },
+ {
+ "id": "EQP-001",
+ "name": "تسعير المعدات",
+ "description": "استراتيجية لتسعير المعدات تشمل تكاليف التشغيل والصيانة والاستهلاك",
+ "profit_margin": 0.18,
+ "risk_factor": 0.07,
+ "maintenance_factor": 0.1,
+ "depreciation_rate": 0.15,
+ "utilization_rate": 0.8,
+ "applicable_to": "equipment"
+ },
+ {
+ "id": "SUB-001",
+ "name": "تسعير المقاولين",
+ "description": "استراتيجية لتسعير أعمال المقاولين من الباطن مع مراعاة جودة العمل والالتزام",
+ "profit_margin": 0.10,
+ "risk_factor": 0.08,
+ "quality_factor": 1.0,
+ "reliability_factor": 1.0,
+ "market_competition": 0.05,
+ "applicable_to": "subcontractors"
}
-
- # إضافة نتائج لكل استراتيجية
- base_value = project["value"]
-
- for _, strategy in strategies.iterrows():
- # حساب قيمة افتراضية مختلفة لكل استراتيجية
- if strategy["id"] == "STD-001": # التسعير القياسي
- value_factor = 1.0
- profit_margin = 15
- local_content = 60
- cash_flow_impact = "محايد"
- elif strategy["id"] == "BAL-001": # التسعير المتزن
- value_factor = 1.05
- profit_margin = 18
- local_content = 62
- cash_flow_impact = "محايد"
- elif strategy["id"] == "UNB-001": # التسعير غير المتزن
- value_factor = 1.08
- profit_margin = 20
- local_content = 58
- cash_flow_impact = "إيجابي"
- elif strategy["id"] == "PRF-001": # التسعير الموجه للربحية
- value_factor = 1.15
- profit_margin = 25
- local_content = 55
- cash_flow_impact = "إيجابي"
- elif strategy["id"] == "BDL-001": # التسعير بالتجميع
- value_factor = 1.03
- profit_margin = 17
- local_content = 63
- cash_flow_impact = "محايد"
- else: # التسعير بالمحتوى المحلي
- value_factor = 1.1
- profit_margin = 15
- local_content = 70
- cash_flow_impact = "محايد"
-
- # تحديد تقييم المخاطر
- risk_assessment = "منخفض" if strategy["risk_factor"] < 8 else "متوسط" if strategy["risk_factor"] < 15 else "مرتفع"
-
- new_comparison["strategies"][strategy["id"]] = {
- "value": base_value * value_factor,
- "profit_margin": profit_margin,
- "local_content_percentage": local_content,
- "risk_assessment": risk_assessment,
- "cash_flow_impact": cash_flow_impact
- }
-
- # إضافة المقارنة الجديدة
- st.session_state.pricing_strategies["strategy_comparisons"].append(new_comparison)
-
- st.success("تم إنشاء المقارنة بنجاح!")
-
- # إعادة تحميل الصفحة
- st.experimental_rerun()
-
- def _render_local_content_tab(self):
- """عرض تبويب تحليل المحتوى المحلي"""
-
- st.markdown("### تحليل المحتوى المحلي")
-
- # استخراج البيانات
- strategies = st.session_state.pricing_strategies["strategies"]
- projects = st.session_state.pricing_strategies["projects"]
- strategy_applications = st.session_state.pricing_strategies["strategy_applications"]
-
- # عرض معلومات عن المحتوى المحلي
- st.markdown("""
- المحتوى المحلي هو نسبة المواد والخدمات والعمالة المحلية المستخدمة في المشروع. يعتبر المحتوى المحلي من المتطلبات الهامة في المشاريع الحكومية في المملكة العربية السعودية، وذلك تماشياً مع رؤية المملكة 2030 وبرنامج تعزيز القيمة المضافة المحلية (اكتفاء).
-
- يتم حساب نسبة المحتوى المحلي بناءً على:
- - نسبة المواد المحلية المستخدمة في المشروع
- - نسبة المعدات المحلية المستخدمة في المشروع
- - نسبة العمالة المحلية المستخدمة في المشروع
- - نسبة المقاولين من الباطن المحليين المشاركين في المشروع
- """)
-
- # إنشاء نموذج لحساب المحتوى المحلي
- st.markdown("#### حساب المحتوى المحلي")
-
- with st.form("calculate_local_content_form"):
- # الصف الأول
- col1, col2 = st.columns(2)
-
- with col1:
- project_id = st.selectbox("اختر المشروع", projects["id"].tolist(), format_func=lambda x: f"{x} - {projects[projects['id'] == x]['name'].values[0]}")
- materials_percentage = st.slider("نسبة المواد من إجمالي التكلفة (%)", min_value=0, max_value=100, value=50, step=5)
-
- with col2:
- local_materials_percentage = st.slider("نسبة المواد المحلية (%)", min_value=0, max_value=100, value=60, step=5)
- equipment_percentage = st.slider("نسبة المعدات من إجمالي التكلفة (%)", min_value=0, max_value=100, value=20, step=5)
-
- # الصف الثاني
- col1, col2 = st.columns(2)
-
- with col1:
- local_equipment_percentage = st.slider("نسبة المعدات المحلية (%)", min_value=0, max_value=100, value=40, step=5)
- labor_percentage = st.slider("نسبة العمالة من إجمالي التكلفة (%)", min_value=0, max_value=100, value=20, step=5)
-
- with col2:
- local_labor_percentage = st.slider("نسبة العمالة المحلية (%)", min_value=0, max_value=100, value=30, step=5)
- subcontractors_percentage = st.slider("نسبة المقاولين من الباطن من إجمالي التكلفة (%)", min_value=0, max_value=100, value=10, step=5)
-
- # الصف الثالث
- local_subcontractors_percentage = st.slider("نسبة المقاولين من الباطن المحليين (%)", min_value=0, max_value=100, value=70, step=5)
-
- # التحقق من إجمالي النسب
- total_percentage = materials_percentage + equipment_percentage + labor_percentage + subcontractors_percentage
-
- if total_percentage != 100:
- st.warning(f"إجمالي النسب يجب أن يكون 100%، الإجمالي الحالي: {total_percentage}%")
-
- # زر الحساب
- submit_button = st.form_submit_button("حساب المحتوى المحلي")
-
- if submit_button:
- # حساب المحتوى المحلي
- local_content = (
- materials_percentage * local_materials_percentage / 100 +
- equipment_percentage * local_equipment_percentage / 100 +
- labor_percentage * local_labor_percentage / 100 +
- subcontractors_percentage * local_subcontractors_percentage / 100
- )
-
- # عرض النتيجة
- st.success(f"نسبة المحتوى المحلي: {local_content:.2f}%")
-
- # تحليل النتيجة
- if local_content >= 70:
- st.success("تحقق هذه النسبة متطلبات المحتوى المحلي للمشاريع الحكومية")
- elif local_content >= 50:
- st.warning("تحقق هذه النسبة الحد الأدنى من متطلبات المحتوى المحلي، ولكن يفضل زيادتها")
- else:
- st.error("لا تحقق هذه النسبة متطلبات المحتوى المحلي للمشاريع الحكومية")
-
- # توصيات لزيادة المحتوى المحلي
- st.markdown("#### توصيات لزيادة المحتوى المحلي")
-
- recommendations = []
-
- if local_materials_percentage < 70:
- recommendations.append("- زيادة نسبة المواد المحلية المستخدمة في المشروع")
-
- if local_equipment_percentage < 50:
- recommendations.append("- زيادة نسبة المعدات المحلية المستخدمة في المشروع")
-
- if local_labor_percentage < 40:
- recommendations.append("- زيادة نسبة العمالة المحلية المستخدمة في المشروع")
-
- if local_subcontractors_percentage < 80:
- recommendations.append("- زيادة نسبة المقاولين من الباطن المحليين المشاركين في المشروع")
-
- if recommendations:
- for recommendation in recommendations:
- st.markdown(recommendation)
- else:
- st.success("لا توجد توصيات إضافية، نسبة المحتوى المحلي جيدة")
-
- # إنشاء رسم بياني للمحتوى المحلي
- local_content_data = pd.DataFrame({
- "المكون": ["المواد", "المعدات", "العمالة", "المقاولين من الباطن"],
- "النسبة من إجمالي التكلفة": [materials_percentage, equipment_percentage, labor_percentage, subcontractors_percentage],
- "نسبة المحتوى المحلي": [local_materials_percentage, local_equipment_percentage, local_labor_percentage, local_subcontractors_percentage],
- "المساهمة في المحتوى المحلي": [
- materials_percentage * local_materials_percentage / 100,
- equipment_percentage * local_equipment_percentage / 100,
- labor_percentage * local_labor_percentage / 100,
- subcontractors_percentage * local_subcontractors_percentage / 100
- ]
+ ]
+ }
+
+ def apply_strategy(self, strategy_id, item_data):
+ """تطبيق استراتيجية التسعير حسب نوع البند"""
+ strategy = next((s for s in st.session_state.pricing_strategies["strategies"]
+ if s["id"] == strategy_id), None)
+
+ if not strategy:
+ return None
+
+ base_cost = self._calculate_base_cost(item_data, strategy)
+ adjustments = self._apply_strategy_adjustments(base_cost, strategy, item_data)
+
+ return {
+ "base_cost": base_cost,
+ "adjustments": adjustments,
+ "total_price": base_cost + sum(adjustments.values())
+ }
+
+ def _calculate_base_cost(self, item_data, strategy):
+ """حساب التكلفة الأساسية حسب نوع البند"""
+ if strategy["applicable_to"] == "materials":
+ return self._calculate_materials_cost(item_data, strategy)
+ elif strategy["applicable_to"] == "labor":
+ return self._calculate_labor_cost(item_data, strategy)
+ elif strategy["applicable_to"] == "equipment":
+ return self._calculate_equipment_cost(item_data, strategy)
+ elif strategy["applicable_to"] == "subcontractors":
+ return self._calculate_subcontractor_cost(item_data, strategy)
+ return 0
+
+ def _calculate_materials_cost(self, item_data, strategy):
+ """حساب تكلفة المواد مع العوامل المؤثرة"""
+ base_cost = item_data.get("unit_price", 0) * item_data.get("quantity", 0)
+ storage_cost = base_cost * strategy["storage_cost"]
+ transport_cost = base_cost * strategy["transport_cost"]
+ market_adjustment = base_cost * strategy["market_volatility"]
+ return base_cost + storage_cost + transport_cost + market_adjustment
+
+ def _calculate_labor_cost(self, item_data, strategy):
+ """حساب تكلفة العمالة مع عوامل الإنتاجية والمهارة"""
+ daily_rate = item_data.get("daily_rate", 0)
+ productivity = item_data.get("productivity", 1.0) * strategy["productivity_factor"]
+ skill_level = item_data.get("skill_level", 1.0)
+ skill_premium = daily_rate * strategy["skill_premium"] * (skill_level - 1)
+ return (daily_rate + skill_premium) * productivity
+
+ def _calculate_equipment_cost(self, item_data, strategy):
+ """حساب تكلفة المعدات مع الصيانة والاستهلاك"""
+ daily_rate = item_data.get("daily_rate", 0)
+ utilization = strategy["utilization_rate"]
+ maintenance = daily_rate * strategy["maintenance_factor"]
+ depreciation = daily_rate * strategy["depreciation_rate"]
+ return (daily_rate + maintenance + depreciation) / utilization
+
+ def _calculate_subcontractor_cost(self, item_data, strategy):
+ """حساب تكلفة المقاولين من الباطن مع عوامل الجودة"""
+ base_cost = item_data.get("quoted_price", 0)
+ quality_adjustment = base_cost * (strategy["quality_factor"] - 1)
+ reliability_adjustment = base_cost * (strategy["reliability_factor"] - 1)
+ market_adjustment = base_cost * strategy["market_competition"]
+ return base_cost + quality_adjustment + reliability_adjustment + market_adjustment
+
+ def _apply_strategy_adjustments(self, base_cost, strategy, item_data):
+ """تطبيق التعديلات حسب الاستراتيجية"""
+ return {
+ "profit": base_cost * strategy["profit_margin"],
+ "risk": base_cost * strategy["risk_factor"]
+ }
+
+ def render_strategy_selection(self):
+ """عرض واجهة اختيار الاستراتيجية"""
+ st.subheader("اختيار استراتيجية التسعير")
+ item_type = st.selectbox(
+ "نوع البند",
+ ["المواد", "العمالة", "المعدات", "المقاولين من الباطن"]
+ )
+
+ strategies = [s for s in st.session_state.pricing_strategies["strategies"]
+ if s["applicable_to"] == self._get_type_key(item_type)]
+
+ selected_strategy = st.selectbox(
+ "اختر الاستراتيجية المناسبة",
+ options=[s["id"] for s in strategies],
+ format_func=lambda x: next(s["name"] for s in strategies if s["id"] == x)
+ )
+
+ if selected_strategy:
+ strategy = next(s for s in strategies if s["id"] == selected_strategy)
+ st.info(strategy["description"])
+ self._render_strategy_params(strategy)
+
+ return selected_strategy
+
+ def _get_type_key(self, display_type):
+ """تحويل النوع المعروض إلى المفتاح المناسب"""
+ type_map = {
+ "المواد": "materials",
+ "العمالة": "labor",
+ "المعدات": "equipment",
+ "المقاولين من الباطن": "subcontractors"
+ }
+ return type_map.get(display_type, "")
+
+ def _render_strategy_params(self, strategy):
+ """عرض وتعديل معاملات الاستراتيجية"""
+ st.write("معاملات الاستراتيجية:")
+ cols = st.columns(2)
+
+ with cols[0]:
+ st.metric("هامش الربح", f"{strategy['profit_margin']*100:.1f}%")
+ st.metric("عامل المخاطرة", f"{strategy['risk_factor']*100:.1f}%")
+
+ with cols[1]:
+ if strategy["applicable_to"] == "materials":
+ st.metric("تكلفة التخزين", f"{strategy['storage_cost']*100:.1f}%")
+ st.metric("تكلفة النقل", f"{strategy['transport_cost']*100:.1f}%")
+ elif strategy["applicable_to"] == "labor":
+ st.metric("معامل الإنتاجية", f"{strategy['productivity_factor']:.2f}")
+ st.metric("علاوة المهارة", f"{strategy['skill_premium']*100:.1f}%")
+ elif strategy["applicable_to"] == "equipment":
+ st.metric("معدل الاستهلاك", f"{strategy['depreciation_rate']*100:.1f}%")
+ st.metric("معدل الاستغلال", f"{strategy['utilization_rate']*100:.1f}%")
+ elif strategy["applicable_to"] == "subcontractors":
+ st.metric("معامل الجودة", f"{strategy['quality_factor']:.2f}")
+ st.metric("معامل الموثوقية", f"{strategy['reliability_factor']:.2f}")
+
+
+ def render_strategy_results(self, strategy_id, items_data):
+ """عرض نتائج تطبيق الاستراتيجية"""
+ if not items_data:
+ st.warning("لا توجد بنود لعرض النتائج")
+ return
+
+ results = []
+ for item in items_data:
+ result = self.apply_strategy(strategy_id, item)
+ if result:
+ results.append({
+ **item,
+ **result
})
-
- # عرض الجدول
- st.dataframe(local_content_data, use_container_width=True)
-
- # إنشاء رسم بياني للمساهمة في المحتوى المحلي
- fig = px.bar(
- local_content_data,
- x="المكون",
- y="المساهمة في المحتوى المحلي",
- title="مساهمة كل مكون في المحتوى المحلي",
- color="المكون",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # إنشاء رسم بياني للمقارنة بين النسبة من إجمالي التكلفة ونسبة المحتوى المحلي
- fig = px.bar(
- local_content_data,
- x="المكون",
- y=["النسبة من إجمالي التكلفة", "نسبة المحتوى المحلي"],
- title="مقارنة بين النسبة من إجمالي التكلفة ونسبة المحتوى المحلي",
- barmode="group",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض معلومات عن استراتيجية التسعير بالمحتوى المحلي
- st.markdown("#### استراتيجية التسعير بالمحتوى المحلي")
-
- st.markdown("""
- استراتيجية التسعير بالمحتوى المحلي تهدف إلى زيادة نسبة المحتوى المحلي في المشروع لتحقيق متطلبات الجهات المالكة. تعتمد هذه الاستراتيجية على:
-
- 1. **تحديد نسبة المحتوى المحلي المستهدفة**: تحديد النسبة المستهدفة للمحتوى المحلي في المشروع بناءً على متطلبات الجهة المالكة.
-
- 2. **تحديد علاوة المحتوى المحلي**: زيادة أسعار البنود التي تستخدم مواد ومعدات وعمالة محلية لتعويض التكلفة الإضافية.
-
- 3. **تحديد خصم المحتوى المستورد**: تخفيض أسعار البنود التي تستخدم مواد ومعدات وعمالة مستوردة لتحقيق التوازن في السعر الإجمالي.
-
- 4. **تحليل تأثير المحتوى المحلي على التكلفة**: دراسة تأثير زيادة المحتوى المحلي على التكلفة الإجمالية للمشروع.
-
- 5. **تحليل تأثير المحتوى المحلي على الجدول الزمني**: دراسة تأثير زيادة المحتوى المحلي على الجدول الزمني للمشروع.
- """)
-
- # عرض مزايا وعيوب استراتيجية التسعير بالمحتوى المحلي
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown("##### مزايا استراتيجية التسعير بالمحتوى المحلي")
-
- st.markdown("""
- - تلبية متطلبات الجهات المالكة للمحتوى المحلي
- - زيادة فرص الفوز بالمناقصات الحكومية
- - المساهمة في تحقيق رؤية المملكة 2030
- - تقليل مخاطر تقلبات أسعار الصرف
- - تقليل مخاطر التأخير في التوريد
- """)
-
- with col2:
- st.markdown("##### عيوب استراتيجية التسعير بالمحتوى المحلي")
-
- st.markdown("""
- - زيادة التكلفة الإجمالية للمشروع
- - صعوبة توفير بعض المواد والمعدات محلياً
- - صعوبة توفير العمالة المحلية المؤهلة
- - تأثير محتمل على جودة المشروع
- - تأثير محتمل على الجدول الزمني للمشروع
- """)
-
- # عرض متطلبات المحتوى المحلي في المشاريع الحكومية
- st.markdown("#### متطلبات المحتوى المحلي في المشاريع الحكومية")
-
- st.markdown("""
- تختلف متطلبات المحتوى المحلي في المشاريع الحكومية حسب نوع المشروع والجهة المالكة. بشكل عام، تتراوح نسبة المحتوى المحلي المطلوبة بين 30% و 70% حسب نوع المشروع.
-
- بعض الجهات الحكومية تطبق نظام النقاط في تقييم العروض، حيث يتم منح نقاط إضافية للعروض التي تحقق نسبة محتوى محلي أعلى.
-
- يمكن الاطلاع على متطلبات المحتوى المحلي لكل جهة حكومية من خلال موقع هيئة المحتوى المحلي والمشتريات الحكومية.
- """)
-
- # عرض مصادر المواد والمعدات المحلية
- st.markdown("#### مصادر المواد والمعدات المحلية")
-
- st.markdown("""
- يمكن الحصول على المواد والمعدات المحلية من خلال:
-
- 1. **المصانع المحلية**: مثل مصانع الحديد والإسمنت والخرسانة الجاهزة والأنابيب وغيرها.
-
- 2. **الموردين المحليين**: مثل موردي المعدات والآليات والأدوات وغيرها.
-
- 3. **الوكلاء المحليين**: مثل وكلاء الشركات العالمية في المملكة.
-
- 4. **المقاولين من الباطن المحليين**: مثل مقاولي الحفر والردم والتشطيبات وغيرها.
-
- يمكن الاطلاع على قائمة المصانع والموردين المحليين من خلال موقع هيئة المحتوى المحلي والمشتريات الحكومية.
- """)
-
+
+ if results:
+ df = pd.DataFrame(results)
+ st.dataframe(df)
+
+ # عرض الرسم البياني
+ st.bar_chart(df[["base_cost", "profit", "risk_cost", "local_content_adjustment"]])
+
+ def calculate_local_content(self, items_data):
+ """حساب نسبة المحتوى المحلي"""
+ total_cost = 0
+ local_content_cost = 0
+
+ for item in items_data:
+ total_cost += item.get("total_cost", 0)
+ if item.get("is_local", False):
+ local_content_cost += item.get("total_cost", 0)
+
+ if total_cost > 0:
+ return (local_content_cost / total_cost) * 100
+ return 0
+
+ def get_strategy_by_id(self, strategy_id):
+ """الحصول على استراتيجية بواسطة المعرف"""
+ strategies = st.session_state.pricing_strategies.get("strategies", [])
+ strategy = next((s for s in strategies if s["id"] == strategy_id), None)
+ return strategy
+
def apply_strategy_to_project(self, project_id, strategy_id):
"""تطبيق استراتيجية على مشروع"""
-
- # استخراج البيانات
- strategies = st.session_state.pricing_strategies["strategies"]
- projects = st.session_state.pricing_strategies["projects"]
- strategy_applications = st.session_state.pricing_strategies["strategy_applications"]
-
- # التحقق من وجود المشروع والاستراتيجية
- project = projects[projects["id"] == project_id]
- strategy = strategies[strategies["id"] == strategy_id]
-
- if project.empty or strategy.empty:
- return False, "المشروع أو الاستراتيجية غير موجودة"
-
- # التحقق من عدم وجود تطبيق سابق للاستراتيجية على المشروع
- existing_application = strategy_applications[
- (strategy_applications["project_id"] == project_id) &
- (strategy_applications["strategy_id"] == strategy_id)
- ]
-
- if not existing_application.empty:
- return False, "تم تطبيق هذه الاستراتيجية على هذا المشروع مسبقاً"
-
- # استخراج بيانات المشروع والاستراتيجية
- project = project.iloc[0]
- strategy = strategy.iloc[0]
-
- # حساب نتائج تطبيق الاستراتيجية
- original_value = project["value"]
-
- # حساب القيمة الجديدة بناءً على نوع الاستراتيجية
- if "STD" in strategy_id: # التسعير القياسي
- new_value = original_value * (1 + strategy["profit_margin"] / 100)
- profit_margin = strategy["profit_margin"]
- local_content = 60
- elif "BAL" in strategy_id: # التسعير المتزن
- new_value = original_value * (1 + (strategy["profit_margin"] + strategy["risk_factor"]) / 100)
- profit_margin = strategy["profit_margin"]
- local_content = 62
- elif "UNB" in strategy_id: # التسعير غير المتزن
- new_value = original_value * (1 + (strategy["profit_margin"] + strategy["risk_factor"]) / 100)
- profit_margin = strategy["profit_margin"]
- local_content = 58
- elif "PRF" in strategy_id: # التسعير الموجه للربحية
- new_value = original_value * (1 + (strategy["profit_margin"] + strategy["risk_factor"] / 2) / 100)
- profit_margin = strategy["profit_margin"]
- local_content = 55
- elif "BDL" in strategy_id: # التسعير بالتجميع
- new_value = original_value * (1 + strategy["profit_margin"] / 100)
- profit_margin = strategy["profit_margin"]
- local_content = 63
- else: # التسعير بالمحتوى المحلي
- new_value = original_value * (1 + (strategy["profit_margin"] - 2) / 100)
- profit_margin = strategy["profit_margin"] - 2
- local_content = 70
-
- # تحديد تقييم المخاطر
- risk_assessment = "منخفض" if strategy["risk_factor"] < 8 else "متوسط" if strategy["risk_factor"] < 15 else "مرتفع"
-
- # إنشاء تطبيق جديد
- new_application = {
- "project_id": project_id,
- "strategy_id": strategy_id,
- "application_date": datetime.now().strftime("%Y-%m-%d"),
- "applied_by": "مدير التسعير",
- "status": "مطبق",
- "notes": "تم تطبيق الاستراتيجية بنجاح",
- "results": {
- "original_value": original_value,
- "new_value": new_value,
- "profit_margin": profit_margin,
- "local_content_percentage": local_content,
- "risk_assessment": risk_assessment
- }
- }
-
- # إضافة التطبيق إلى الجدول
- st.session_state.pricing_strategies["strategy_applications"] = pd.concat([
- st.session_state.pricing_strategies["strategy_applications"],
- pd.DataFrame([new_application])
- ], ignore_index=True)
-
- return True, f"تم تطبيق استراتيجية {strategy['name']} على مشروع {project['name']} بنجاح!"
-
- def get_strategy_by_id(self, strategy_id):
- """الحصول على استراتيجية بواسطة الكود"""
-
- # استخراج البيانات
- strategies = st.session_state.pricing_strategies["strategies"]
-
- # البحث عن الاستراتيجية
- strategy = strategies[strategies["id"] == strategy_id]
-
- if not strategy.empty:
- return strategy.iloc[0].to_dict()
-
- return None
-
+ pass
+
def get_project_by_id(self, project_id):
"""الحصول على مشروع بواسطة الكود"""
-
- # استخراج البيانات
projects = st.session_state.pricing_strategies["projects"]
-
- # البحث عن المشروع
project = projects[projects["id"] == project_id]
-
- if not project.empty:
- return project.iloc[0].to_dict()
-
- return None
-
+ return project.iloc[0].to_dict() if not project.empty else None
+
def calculate_local_content(self, materials_percentage, local_materials_percentage, equipment_percentage, local_equipment_percentage, labor_percentage, local_labor_percentage, subcontractors_percentage, local_subcontractors_percentage):
"""حساب نسبة المحتوى المحلي"""
-
- # التحقق من إجمالي النسب
total_percentage = materials_percentage + equipment_percentage + labor_percentage + subcontractors_percentage
-
+
if total_percentage != 100:
return False, f"إجمالي النسب يجب أن يكون 100%، الإجمالي الحالي: {total_percentage}%"
-
- # حساب المحتوى المحلي
+
local_content = (
materials_percentage * local_materials_percentage / 100 +
equipment_percentage * local_equipment_percentage / 100 +
labor_percentage * local_labor_percentage / 100 +
subcontractors_percentage * local_subcontractors_percentage / 100
)
-
+
return True, local_content
+
+ def compare_strategies(self, project_data, price_analysis):
+ """مقارنة استراتيجيات التسعير المختلفة"""
+ strategies = self.get_strategies_list()
+ if not strategies:
+ return {"success": False, "message": "لا توجد استراتيجيات للمقارنة"}
+
+ try:
+ strategies_result = []
+ for strategy in strategies:
+ result = self.apply_strategy(strategy['id'], project_data) #modified to handle item data
+ if result['success']:
+ strategies_result.append(result)
+ return {
+ "success": True,
+ "strategies_result": strategies_result
+ }
+ except Exception as e:
+ return {"success": False, "message": str(e)}
+
+ def get_strategies_list(self):
+ """الحصول على قائمة الاستراتيجيات النشطة"""
+ strategies = st.session_state.pricing_strategies.get("strategies", []) # Handle case where strategies might not exist yet.
+ return strategies
+
+
+ def render(self):
+ """عرض واجهة استراتيجيات التسعير"""
+ st.markdown("## استراتيجيات التسعير المتقدمة")
+
+ # إنشاء تبويبات
+ tabs = st.tabs([
+ "الاستراتيجيات المتاحة",
+ "تطبيق الاستراتيجيات",
+ "إعدادات التسعير"
+ ])
+
+ with tabs[0]:
+ self._render_strategies_list()
+
+ with tabs[1]:
+ self._render_strategy_application()
+
+ with tabs[2]:
+ self._render_pricing_settings()
+
+ def _render_strategies_list(self):
+ """عرض قائمة الاستراتيجيات"""
+ st.markdown("### الاستراتيجيات المتاحة")
+ strategies = st.session_state.pricing_strategies.get("strategies", []) # Handle missing strategies
+
+ # تحويل البيانات للعرض
+ if strategies: #Added check for empty list
+ display_df = pd.DataFrame(strategies)
+ display_df = display_df[["name", "description", "profit_margin", "risk_factor", "local_content_factor"]].copy()
+ display_df.columns = ["الاستراتيجية", "الوصف", "هامش الربح", "عامل المخاطرة", "عامل المحتوى المحلي"]
+ st.dataframe(display_df, use_container_width=True)
+ else:
+ st.write("لا توجد استراتيجيات متاحة حاليًا.")
+
+
+ def _render_strategy_application(self):
+ """عرض واجهة تطبيق الاستراتيجيات"""
+ st.markdown("### تطبيق استراتيجية التسعير")
+ if 'current_project' not in st.session_state:
+ st.warning("يرجى اختيار مشروع أولاً")
+ return
+
+ project = st.session_state.current_project
+ strategies = st.session_state.pricing_strategies["strategies"]
+
+ render_items_management()
+
+ selected_strategy = self.render_strategy_selection()
+
+ if selected_strategy:
+ if st.button("تطبيق الاستراتيجية المختارة"):
+ if project and "items" in project:
+ strategy = next(s for s in strategies if s["id"] == selected_strategy)
+ st.subheader(f"نتائج تطبيق {strategy['name']}")
+ for item in project["items"]:
+ result = self.apply_strategy(selected_strategy, item)
+ if result:
+ st.write(f"نتائج تطبيق الاستراتيجية على البند: {item.get('code', 'غير محدد')}")
+ st.write(result)
+
+ else:
+ st.error("لا توجد بيانات للبنود في المشروع الحالي.")
+
+ def _render_pricing_settings(self):
+ """عرض إعدادات التسعير"""
+ st.markdown("### إعدادات التسعير")
+
+ with st.form("pricing_settings"):
+ st.number_input("الحد الأدنى لهامش الربح", 0.0, 1.0, 0.15)
+ st.number_input("الحد الأقصى لهامش الربح", 0.0, 1.0, 0.30)
+ st.number_input("نسبة المحتوى المحلي المستهدفة", 0.0, 1.0, 0.40)
+
+ if st.form_submit_button("حفظ الإعدادات"):
+ st.success("تم حفظ الإعدادات بنجاح")
+
+ def _calculate_base_costs(self, project_data):
+ """حساب التكاليف الأساسية"""
+ if not project_data:
+ return {}
+
+ materials_cost = sum(item.get("materials_cost", 0) for item in project_data.get("items", []))
+ equipment_cost = sum(item.get("equipment_cost", 0) for item in project_data.get("items", []))
+ labor_cost = sum(item.get("labor_cost", 0) for item in project_data.get("items", []))
+ subcontractors_cost = sum(item.get("subcontractors_cost", 0) for item in project_data.get("items", []))
+
+ return {
+ "materials": materials_cost,
+ "equipment": equipment_cost,
+ "labor": labor_cost,
+ "subcontractors": subcontractors_cost,
+ "total": materials_cost + equipment_cost + labor_cost + subcontractors_cost
+ }
+
+ def _calculate_final_price(self, base_costs, strategy_factors):
+ """حساب السعر النهائي"""
+ total_base_cost = base_costs["total"]
+ profit = total_base_cost * strategy_factors["profit_margin"]
+ risk_cost = total_base_cost * strategy_factors["risk_factor"]
+ local_content_adjustment = total_base_cost * (strategy_factors["local_content_factor"] - 1)
+
+ return {
+ "base_cost": total_base_cost,
+ "profit": profit,
+ "risk_cost": risk_cost,
+ "local_content_adjustment": local_content_adjustment,
+ "total": total_base_cost + profit + risk_cost + local_content_adjustment
+ }
+
+
+
+def render_items_management():
+ st.header("نظام إدارة البنود")
+
+ tab1, tab2, tab3 = st.tabs(["إضافة بنود يدوياً", "استيراد من Excel", "البنود الجاهزة"])
+
+ with tab1:
+ with st.form("manual_item_entry"):
+ item_code = st.text_input("رمز البند")
+ item_desc = st.text_area("وصف البند")
+ unit = st.selectbox("الوحدة", ["متر مربع", "متر مكعب", "متر طولي", "عدد", "طن", "كجم"])
+ quantity = st.number_input("الكمية", min_value=0.0)
+
+ item_type = st.selectbox("نوع البند", ["المواد", "العمالة", "المعدات", "المقاولين من الباطن"])
+
+ col1, col2 = st.columns(2)
+ with col1:
+ material_cost = st.number_input("تكلفة المواد", min_value=0.0)
+ labor_cost = st.number_input("تكلفة العمالة", min_value=0.0)
+ with col2:
+ equipment_cost = st.number_input("تكلفة المعدات", min_value=0.0)
+ overhead_cost = st.number_input("التكاليف غير المباشرة", min_value=0.0)
+
+ if st.form_submit_button("إضافة البند"):
+ if item_code and item_desc:
+ if 'items' not in st.session_state:
+ st.session_state.items = []
+
+ item_data = {
+ 'code': item_code,
+ 'description': item_desc,
+ 'unit': unit,
+ 'quantity': quantity,
+ 'item_type': item_type,
+ 'materials_cost': material_cost,
+ 'labor_cost': labor_cost,
+ 'equipment_cost': equipment_cost,
+ 'subcontractors_cost': overhead_cost,
+ 'total_cost': material_cost + labor_cost + equipment_cost + overhead_cost
+ }
+
+ if item_type == "المواد":
+ item_data['unit_price'] = material_cost / quantity if quantity > 0 else 0
+ elif item_type == "العمالة":
+ item_data['daily_rate'] = labor_cost / quantity if quantity > 0 else 0
+ elif item_type == "المعدات":
+ item_data['daily_rate'] = equipment_cost / quantity if quantity > 0 else 0
+ elif item_type == "المقاولين من الباطن":
+ item_data['quoted_price'] = overhead_cost
+
+ st.session_state.items.append(item_data)
+ st.success("تم إضافة البند بنجاح")
+
+ with tab2:
+ uploaded_file = st.file_uploader("اختر ملف Excel", type=['xlsx', 'xls'])
+ if uploaded_file:
+ df = pd.read_excel(uploaded_file)
+ st.dataframe(df)
+ if st.button("استيراد البنود"):
+ # معالجة وتخزين البنود من Excel
+ st.success("تم استيراد البنود بنجاح")
+
+ with tab3:
+ # عرض البنود المخزنة مسبقاً
+ if 'items' in st.session_state:
+ df = pd.DataFrame(st.session_state.items)
+ st.dataframe(df)
\ No newline at end of file
diff --git a/pricing_system/modules/pricing_strategies/profit_oriented.py b/pricing_system/modules/pricing_strategies/profit_oriented.py
new file mode 100644
index 0000000000000000000000000000000000000000..c1697280e6fdf7fae02af8010ab7ee5f6f32ddfb
--- /dev/null
+++ b/pricing_system/modules/pricing_strategies/profit_oriented.py
@@ -0,0 +1,34 @@
+
+"""
+وحدة التسعير الموجه للربحية
+"""
+import streamlit as st
+import pandas as pd
+
+def calculate_profit_oriented_price(costs, target_profit=0.25):
+ """حساب السعر الموجه للربحية"""
+ base_cost = sum(costs.values())
+ required_price = base_cost / (1 - target_profit)
+
+ return {
+ "base_cost": base_cost,
+ "target_profit_margin": target_profit,
+ "required_price": required_price,
+ "expected_profit": required_price - base_cost
+ }
+
+def render_profit_driven_strategy():
+ """عرض واجهة التسعير الموجه للربحية"""
+ st.header("التسعير الموجه للربحية")
+
+ target_profit = st.slider(
+ "هامش الربح المستهدف",
+ min_value=0.10,
+ max_value=0.40,
+ value=0.25,
+ format="%d%%"
+ )
+
+ st.info("هذه الاستراتيجية تركز على تحقيق هامش ربح مستهدف مع الأخذ في الاعتبار تكاليف المشروع الأساسية")
+
+ return target_profit
diff --git a/pricing_system/modules/reference_guides/pricing_guidelines.py b/pricing_system/modules/reference_guides/pricing_guidelines.py
new file mode 100644
index 0000000000000000000000000000000000000000..abbdc0e4dc9ef1eaef90c66e698b7459f7983185
--- /dev/null
+++ b/pricing_system/modules/reference_guides/pricing_guidelines.py
@@ -0,0 +1,90 @@
+
+import streamlit as st
+import pandas as pd
+from pathlib import Path
+
+class PricingGuidelines:
+ def __init__(self):
+ self.guides_path = Path("attached_assets")
+
+ def render(self):
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ st.markdown('الدليل المرجعي للتسعير ', unsafe_allow_html=True)
+
+ # القسم الأول: دليل تحليل الأسعار
+ with st.expander("دليل تحليل أسعار بنود الإنشاءات", expanded=True):
+ st.markdown("""
+ ### محتويات الدليل:
+ 1. المبادئ الأساسية لتحليل الأسعار
+ 2. طرق حساب تكاليف المواد
+ 3. معايير تقدير تكاليف العمالة
+ 4. حساب تكاليف المعدات
+ 5. المصروفات غير المباشرة
+ 6. هوامش الربح المقترحة
+ """)
+
+ # زر تحميل الدليل
+ try:
+ with open(self.guides_path / "دليل تحليل أسعار بنود الإنشاءات.pdf", "rb") as pdf_file:
+ st.download_button(
+ label="تحميل الدليل الكامل (PDF)",
+ data=pdf_file,
+ file_name="pricing_analysis_guide.pdf",
+ mime="application/pdf"
+ )
+ except FileNotFoundError:
+ st.warning("ملف الدليل غير متوفر حالياً")
+
+ # القسم الثاني: معدلات الأداء
+ with st.expander("معدلات الأداء القياسية"):
+ try:
+ rates_df = pd.read_excel(self.guides_path / "معدلات استهلاك الخامات واداء العمالة والمعدات.xlsx")
+ st.dataframe(rates_df)
+ except Exception:
+ st.error("لم يتم العثور على ملف المعدلات")
+
+ # القسم الثالث: النصائح والإرشادات
+ with st.expander("نصائح وإرشادات التسعير"):
+ st.markdown("""
+ ### أفضل الممارسات في التسعير:
+
+ #### 1. تحليل السوق
+ - دراسة أسعار السوق الحالية
+ - تحليل المنافسين
+ - مراقبة اتجاهات الأسعار
+
+ #### 2. تقييم المخاطر
+ - تحديد المخاطر المحتملة
+ - تقدير تأثيرها على التكلفة
+ - وضع احتياطيات مناسبة
+
+ #### 3. حساب التكاليف
+ - تحديد التكاليف المباشرة بدقة
+ - تضمين جميع التكاليف غير المباشرة
+ - مراعاة التضخم والتغيرات المحتملة
+
+ #### 4. تحديد هامش الربح
+ - تحليل هوامش الربح التاريخية
+ - مراعاة ظروف السوق
+ - تحديد الحد الأدنى المقبول
+ """)
diff --git a/pricing_system/modules/reference_guides/references.py b/pricing_system/modules/reference_guides/references.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4088ab4e0ae7b58e58fa43925c2f0210b0e17c3
--- /dev/null
+++ b/pricing_system/modules/reference_guides/references.py
@@ -0,0 +1,98 @@
+
+import streamlit as st
+import docx
+import pandas as pd
+from pathlib import Path
+import os
+from datetime import datetime
+
+class ReferenceGuides:
+ def __init__(self):
+ self.guides_path = Path("attached_assets")
+
+ def render(self):
+ st.title("المراجع والأدلة الإرشادية")
+
+ # Add completion button at the top
+ col1, col2, col3 = st.columns([1, 2, 1])
+ with col2:
+ if st.button("✅ إنهاء التسعير وحفظ البيانات", key="complete_pricing_btn", type="primary"):
+ try:
+ if 'current_project' in st.session_state and 'boq_items' in st.session_state.current_project:
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ total_price = sum(item['total_price'] for item in st.session_state.current_project['boq_items'])
+
+ # Calculate local content if available
+ local_content = 0
+ if hasattr(st.session_state, 'local_content'):
+ local_content = (
+ st.session_state.local_content.get('materials_local', 0.4) * 40 +
+ st.session_state.local_content.get('equipment_local', 0.3) * 20 +
+ st.session_state.local_content.get('labor_local', 0.8) * 30 +
+ st.session_state.local_content.get('subcontractors_local', 0.5) * 10
+ )
+
+ pricing_data = {
+ 'timestamp': timestamp,
+ 'project_name': st.session_state.current_project.get('name', 'مشروع جديد'),
+ 'total_price': total_price,
+ 'items': st.session_state.current_project['boq_items'],
+ 'local_content': local_content
+ }
+
+ if 'saved_pricing' not in st.session_state:
+ st.session_state.saved_pricing = []
+
+ st.session_state.saved_pricing.append(pricing_data)
+ st.success("✅ تم حفظ وإنهاء التسعير بنجاح!")
+
+ # Export to Excel
+ try:
+ export_path = "data/exports"
+ os.makedirs(export_path, exist_ok=True)
+ excel_file = f"{export_path}/final_pricing_{timestamp}.xlsx"
+
+ df = pd.DataFrame(st.session_state.current_project['boq_items'])
+ with pd.ExcelWriter(excel_file, engine='openpyxl') as writer:
+ df.to_excel(writer, index=False, sheet_name='التسعير النهائي')
+ worksheet = writer.sheets['التسعير النهائي']
+ worksheet['A1'] = f"اسم المشروع: {pricing_data['project_name']}"
+ worksheet['A2'] = f"التاريخ: {timestamp}"
+ worksheet['A3'] = f"إجمالي السعر: {total_price:,.2f} ريال"
+ worksheet['A4'] = f"نسبة المحتوى المحلي: {local_content:.1f}%"
+
+ st.success("📊 تم تصدير ملف Excel للتسعير النهائي")
+ except Exception as e:
+ st.warning(f"لم يتم تصدير ملف Excel: {str(e)}")
+ else:
+ st.warning("لا توجد بيانات تسعير لحفظها")
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء حفظ التسعير: {str(e)}")
+
+ st.markdown("---")
+
+ # عرض الدليل
+ with st.expander("دليل تحليل أسعار بنود الإنشاءات", expanded=True):
+ try:
+ doc = docx.Document(self.guides_path / "دليل تحليل أسعار بنود الإنشاءات.docx")
+ for paragraph in doc.paragraphs:
+ st.write(paragraph.text)
+ except Exception:
+ st.error("لم يتم العثور على ملف الدليل")
+
+ # إضافة رابط لتحميل الدليل
+ with open(self.guides_path / "دليل تحليل أسعار بنود الإنشاءات.pdf", "rb") as pdf_file:
+ st.download_button(
+ label="تحميل الدليل (PDF)",
+ data=pdf_file,
+ file_name="دليل_تحليل_أسعار_بنود_الإنشاءات.pdf",
+ mime="application/pdf"
+ )
+
+ # عرض جداول المعدلات
+ with st.expander("معدلات الأداء والاستهلاك"):
+ try:
+ rates_df = pd.read_excel(self.guides_path / "معدلات استهلاك الخامات واداء العمالة والمعدات.xlsx")
+ st.dataframe(rates_df)
+ except Exception:
+ st.error("لم يتم العثور على ملف المعدلات")
diff --git a/pricing_system/modules/risk_analysis/__init__.py b/pricing_system/modules/risk_analysis/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d9b34bb4cc1576dc3676b3bbfc833389e099a2f
--- /dev/null
+++ b/pricing_system/modules/risk_analysis/__init__.py
@@ -0,0 +1,2 @@
+
+# Risk Analysis Module
diff --git a/pricing_system/modules/risk_analysis/risk_analyzer.py b/pricing_system/modules/risk_analysis/risk_analyzer.py
new file mode 100644
index 0000000000000000000000000000000000000000..13769072d07f384a3b89dde6310a287fbe08fd31
--- /dev/null
+++ b/pricing_system/modules/risk_analysis/risk_analyzer.py
@@ -0,0 +1,105 @@
+import streamlit as st
+import pandas as pd
+import numpy as np
+import plotly.express as px
+
+class RiskAnalyzer:
+ def __init__(self):
+ self.risk_categories = [
+ "مخاطر السوق",
+ "مخاطر التنفيذ",
+ "مخاطر العقود",
+ "مخاطر التمويل",
+ "مخاطر الموارد"
+ ]
+
+ self.impact_levels = {
+ "منخفض": 1,
+ "متوسط": 2,
+ "عالي": 3,
+ "حرج": 4
+ }
+
+ self.probability_levels = {
+ "نادر": 1,
+ "محتمل": 2,
+ "مرجح": 3,
+ "شبه مؤكد": 4
+ }
+
+ def render(self):
+ """عرض واجهة تحليل المخاطر"""
+ st.markdown("### تحليل المخاطر")
+
+ # إضافة مخاطر جديدة
+ with st.form("add_risk_form"):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ category = st.selectbox("فئة المخاطر", self.risk_categories)
+ description = st.text_area("وصف المخاطر")
+ probability = st.selectbox("احتمالية الحدوث", list(self.probability_levels.keys()))
+
+ with col2:
+ impact = st.selectbox("مستوى التأثير", list(self.impact_levels.keys()))
+ response = st.text_area("استراتيجية الاستجابة")
+ responsible = st.text_input("المسؤول")
+
+ if st.form_submit_button("إضافة المخاطر"):
+ if not description or not response:
+ st.error("يرجى إدخال جميع البيانات المطلوبة")
+ else:
+ if 'project_risks' not in st.session_state:
+ st.session_state.project_risks = []
+
+ risk = {
+ 'category': category,
+ 'description': description,
+ 'probability': probability,
+ 'impact': impact,
+ 'response': response,
+ 'responsible': responsible,
+ 'risk_score': self.probability_levels[probability] * self.impact_levels[impact]
+ }
+
+ st.session_state.project_risks.append(risk)
+ st.success("تم إضافة المخاطر بنجاح")
+ st.rerun()
+
+ # عرض المخاطر المضافة
+ if 'project_risks' in st.session_state and st.session_state.project_risks:
+ st.markdown("### المخاطر المحددة")
+ risks_df = pd.DataFrame(st.session_state.project_risks)
+ # تعيين أسماء الأعمدة بالعربية
+ column_names = {
+ 'category': 'فئة المخاطر',
+ 'description': 'وصف المخاطر',
+ 'probability': 'احتمالية الحدوث',
+ 'impact': 'مستوى التأثير',
+ 'response': 'استراتيجية الاستجابة',
+ 'responsible': 'المسؤول',
+ 'risk_score': 'درجة الخطورة'
+ }
+ risks_df = risks_df.rename(columns=column_names)
+ st.dataframe(risks_df)
+
+ # عرض مصفوفة المخاطر
+ self._render_risk_matrix(risks_df)
+
+ def _render_risk_matrix(self, risks_df):
+ """عرض مصفوفة المخاطر"""
+ st.markdown("### مصفوفة المخاطر")
+
+ matrix_data = np.zeros((4, 4))
+ for _, risk in risks_df.iterrows():
+ prob_idx = self.probability_levels[risk['احتمالية الحدوث']] - 1
+ impact_idx = self.impact_levels[risk['مستوى التأثير']] - 1
+ matrix_data[prob_idx, impact_idx] += 1
+
+ fig = px.imshow(
+ matrix_data,
+ labels=dict(x="التأثير", y="الاحتمالية"),
+ x=list(self.impact_levels.keys()),
+ y=list(self.probability_levels.keys())
+ )
+ st.plotly_chart(fig)
\ No newline at end of file
diff --git a/pricing_system/modules/stages/project_entry.py b/pricing_system/modules/stages/project_entry.py
new file mode 100644
index 0000000000000000000000000000000000000000..c5aeb63543a29af2f8519e85ebd6f36ba7cc8c93
--- /dev/null
+++ b/pricing_system/modules/stages/project_entry.py
@@ -0,0 +1,87 @@
+
+import streamlit as st
+from datetime import datetime
+
+def render_project_entry():
+ """عرض نموذج إدخال بيانات المشروع"""
+ st.header("إدخال تفاصيل المشروع")
+ project_data = {}
+
+ # التأكد من وجود حالة المشروع في session_state
+ if 'current_project' not in st.session_state:
+ st.session_state.current_project = {}
+
+ # نموذج إدخال البيانات
+ with st.form("project_entry_form", clear_on_submit=False):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_name = st.text_input(
+ "اسم المشروع",
+ value=st.session_state.current_project.get('name', ''),
+ key="project_name"
+ )
+ project_code = st.text_input(
+ "رقم المشروع/المناقصة",
+ value=st.session_state.current_project.get('code', ''),
+ key="project_code"
+ )
+ location = st.text_input(
+ "الموقع",
+ value=st.session_state.current_project.get('location', ''),
+ key="location"
+ )
+
+ with col2:
+ start_date = st.date_input(
+ "تاريخ البدء",
+ value=st.session_state.current_project.get('start_date', datetime.now().date())
+ )
+ duration = st.number_input(
+ "مدة المشروع (بالأيام)",
+ min_value=1,
+ value=st.session_state.current_project.get('duration', 180)
+ )
+ estimated_budget = st.number_input(
+ "الميزانية التقديرية",
+ min_value=0.0,
+ value=st.session_state.current_project.get('budget', 0.0),
+ format="%f"
+ )
+
+ project_description = st.text_area(
+ "وصف المشروع",
+ value=st.session_state.current_project.get('description', ''),
+ height=100
+ )
+
+ submitted = st.form_submit_button("حفظ البيانات")
+
+ if submitted:
+ if not project_name or not project_code:
+ st.error("يجب إدخال اسم المشروع ورقمه")
+ else:
+ # تحديث بيانات المشروع في session_state
+ st.session_state.current_project.update({
+ 'name': project_name,
+ 'code': project_code,
+ 'location': location,
+ 'start_date': start_date,
+ 'duration': duration,
+ 'budget': estimated_budget,
+ 'description': project_description,
+ 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ })
+ project_data = {
+ 'name': project_name,
+ 'code': project_code,
+ 'location': location,
+ 'start_date': start_date,
+ 'duration': duration,
+ 'budget': estimated_budget,
+ 'description': project_description
+ }
+ st.success("تم حفظ بيانات المشروع بنجاح")
+ return project_data
+
+ return False
diff --git a/pricing_system/static/css/unified_style.css b/pricing_system/static/css/unified_style.css
index c3f23c4ae4de4385de5a054f2443541353b4b9e6..47a231d588678f4083097ac40c585ad075ae7616 100644
--- a/pricing_system/static/css/unified_style.css
+++ b/pricing_system/static/css/unified_style.css
@@ -1,423 +1,423 @@
-/*
- * نظام تحليل المناقصات - الأنماط الموحدة
- * Tender Analysis System - Unified Styles
- */
-
-/* الألوان الأساسية */
-:root {
- --primary-color: #1e88e5;
- --secondary-color: #26a69a;
- --accent-color: #ff9800;
- --background-color: #f5f5f5;
- --card-color: #ffffff;
- --text-color: #333333;
- --text-light-color: #757575;
- --border-color: #e0e0e0;
- --success-color: #4caf50;
- --warning-color: #ff9800;
- --error-color: #f44336;
- --info-color: #2196f3;
-}
-
-/* تنسيق الخط والاتجاه */
-body {
- font-family: 'Cairo', 'Tajawal', sans-serif;
- direction: rtl;
- text-align: right;
- background-color: var(--background-color);
- color: var(--text-color);
-}
-
-/* العناوين */
-h1, h2, h3, h4, h5, h6 {
- font-family: 'Cairo', 'Tajawal', sans-serif;
- font-weight: 700;
- color: var(--primary-color);
- margin-bottom: 1rem;
-}
-
-h1 {
- font-size: 2.2rem;
- border-bottom: 2px solid var(--primary-color);
- padding-bottom: 0.5rem;
-}
-
-h2 {
- font-size: 1.8rem;
- color: var(--secondary-color);
-}
-
-h3 {
- font-size: 1.5rem;
-}
-
-/* البطاقات */
-.card {
- background-color: var(--card-color);
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
- padding: 1.5rem;
- margin-bottom: 1.5rem;
-}
-
-.card-header {
- border-bottom: 1px solid var(--border-color);
- padding-bottom: 1rem;
- margin-bottom: 1rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.card-title {
- font-size: 1.3rem;
- font-weight: 600;
- color: var(--primary-color);
- margin: 0;
-}
-
-/* الأزرار */
-.stButton > button {
- background-color: var(--primary-color);
- color: white;
- border-radius: 4px;
- border: none;
- padding: 0.5rem 1rem;
- font-family: 'Cairo', 'Tajawal', sans-serif;
- font-weight: 600;
- transition: all 0.3s ease;
-}
-
-.stButton > button:hover {
- background-color: #1976d2;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
-}
-
-.secondary-button > button {
- background-color: var(--secondary-color);
-}
-
-.secondary-button > button:hover {
- background-color: #00897b;
-}
-
-.accent-button > button {
- background-color: var(--accent-color);
-}
-
-.accent-button > button:hover {
- background-color: #fb8c00;
-}
-
-.danger-button > button {
- background-color: var(--error-color);
-}
-
-.danger-button > button:hover {
- background-color: #e53935;
-}
-
-/* حقول الإدخال */
-.stTextInput > div > div > input {
- border-radius: 4px;
- border: 1px solid var(--border-color);
- padding: 0.5rem;
- font-family: 'Cairo', 'Tajawal', sans-serif;
-}
-
-.stTextInput > div > div > input:focus {
- border-color: var(--primary-color);
- box-shadow: 0 0 0 2px rgba(30, 136, 229, 0.2);
-}
-
-/* القوائم المنسدلة */
-.stSelectbox > div > div > div {
- border-radius: 4px;
- border: 1px solid var(--border-color);
-}
-
-.stSelectbox > div > div > div:focus {
- border-color: var(--primary-color);
- box-shadow: 0 0 0 2px rgba(30, 136, 229, 0.2);
-}
-
-/* الجداول */
-.stDataFrame {
- border-radius: 8px;
- overflow: hidden;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
-}
-
-.stDataFrame > div {
- border: none !important;
-}
-
-.stDataFrame th {
- background-color: var(--primary-color);
- color: white;
- font-weight: 600;
- padding: 0.75rem 1rem;
-}
-
-.stDataFrame td {
- padding: 0.75rem 1rem;
- border-bottom: 1px solid var(--border-color);
-}
-
-.stDataFrame tr:nth-child(even) {
- background-color: rgba(0, 0, 0, 0.02);
-}
-
-/* الشريط الجانبي */
-.sidebar .sidebar-content {
- background-color: #263238;
- color: white;
-}
-
-.sidebar .sidebar-content h1,
-.sidebar .sidebar-content h2,
-.sidebar .sidebar-content h3 {
- color: white;
-}
-
-.sidebar .sidebar-content .stRadio > div {
- background-color: #37474f;
- border-radius: 8px;
- padding: 0.5rem;
-}
-
-.sidebar .sidebar-content .stRadio > div > div > label {
- color: white;
-}
-
-/* علامات التبويب */
-.stTabs {
- background-color: var(--card-color);
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
- padding: 1rem;
-}
-
-.stTabs > div > div > div > div {
- font-family: 'Cairo', 'Tajawal', sans-serif;
- font-weight: 600;
-}
-
-.stTabs > div > div > div > div[data-baseweb="tab-highlight"] {
- background-color: var(--primary-color);
-}
-
-/* الرسائل */
-.stSuccess, .stInfo, .stWarning, .stError {
- border-radius: 8px;
- padding: 1rem;
- margin-bottom: 1rem;
-}
-
-.stSuccess {
- background-color: rgba(76, 175, 80, 0.1);
- border-left: 4px solid var(--success-color);
-}
-
-.stInfo {
- background-color: rgba(33, 150, 243, 0.1);
- border-left: 4px solid var(--info-color);
-}
-
-.stWarning {
- background-color: rgba(255, 152, 0, 0.1);
- border-left: 4px solid var(--warning-color);
-}
-
-.stError {
- background-color: rgba(244, 67, 54, 0.1);
- border-left: 4px solid var(--error-color);
-}
-
-/* الرسوم البيانية */
-.stPlotlyChart {
- background-color: var(--card-color);
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
- padding: 1rem;
- margin-bottom: 1.5rem;
-}
-
-/* تخصيص للغة العربية */
-[dir="rtl"] .stPlotlyChart {
- direction: ltr;
-}
-
-/* الشعار */
-.logo-container {
- display: flex;
- justify-content: center;
- margin-bottom: 1.5rem;
-}
-
-.logo-container img {
- max-width: 200px;
- height: auto;
-}
-
-/* تنسيق الصفوف والأعمدة */
-.row {
- display: flex;
- flex-wrap: wrap;
- margin-right: -0.75rem;
- margin-left: -0.75rem;
-}
-
-.col {
- flex: 1 0 0%;
- padding-right: 0.75rem;
- padding-left: 0.75rem;
-}
-
-/* تنسيق البطاقات الإحصائية */
-.stat-card {
- background-color: var(--card-color);
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
- padding: 1.5rem;
- margin-bottom: 1.5rem;
- text-align: center;
-}
-
-.stat-card-primary {
- border-top: 4px solid var(--primary-color);
-}
-
-.stat-card-secondary {
- border-top: 4px solid var(--secondary-color);
-}
-
-.stat-card-accent {
- border-top: 4px solid var(--accent-color);
-}
-
-.stat-card-success {
- border-top: 4px solid var(--success-color);
-}
-
-.stat-value {
- font-size: 2.5rem;
- font-weight: 700;
- color: var(--text-color);
- margin-bottom: 0.5rem;
-}
-
-.stat-label {
- font-size: 1rem;
- color: var(--text-light-color);
-}
-
-/* تنسيق النماذج */
-.form-group {
- margin-bottom: 1.5rem;
-}
-
-.form-label {
- display: block;
- margin-bottom: 0.5rem;
- font-weight: 600;
-}
-
-/* تنسيق الشاشة الرئيسية */
-.welcome-section {
- text-align: center;
- padding: 2rem 0;
-}
-
-.welcome-title {
- font-size: 2.5rem;
- color: var(--primary-color);
- margin-bottom: 1rem;
-}
-
-.welcome-subtitle {
- font-size: 1.2rem;
- color: var(--text-light-color);
- margin-bottom: 2rem;
-}
-
-/* تنسيق القائمة الرئيسية */
-.main-menu {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- gap: 1.5rem;
- margin-bottom: 2rem;
-}
-
-.menu-item {
- background-color: var(--card-color);
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
- padding: 1.5rem;
- text-align: center;
- width: 200px;
- transition: all 0.3s ease;
-}
-
-.menu-item:hover {
- transform: translateY(-5px);
- box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
-}
-
-.menu-icon {
- font-size: 2.5rem;
- color: var(--primary-color);
- margin-bottom: 1rem;
-}
-
-.menu-title {
- font-size: 1.2rem;
- font-weight: 600;
- color: var(--text-color);
-}
-
-/* تنسيق الشاشة الترحيبية */
-.features-section {
- margin-top: 3rem;
-}
-
-.feature-card {
- background-color: var(--card-color);
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
- padding: 1.5rem;
- margin-bottom: 1.5rem;
- display: flex;
- align-items: flex-start;
-}
-
-.feature-icon {
- font-size: 2rem;
- color: var(--primary-color);
- margin-left: 1.5rem;
-}
-
-.feature-content {
- flex: 1;
-}
-
-.feature-title {
- font-size: 1.3rem;
- font-weight: 600;
- color: var(--primary-color);
- margin-bottom: 0.5rem;
-}
-
-.feature-description {
- color: var(--text-light-color);
-}
-
-/* تنسيق الشاشة الترحيبية */
-.footer {
- text-align: center;
- padding: 2rem 0;
- margin-top: 3rem;
- border-top: 1px solid var(--border-color);
- color: var(--text-light-color);
-}
+/*
+ * نظام تحليل المناقصات - الأنماط الموحدة
+ * Tender Analysis System - Unified Styles
+ */
+
+/* الألوان الأساسية */
+:root {
+ --primary-color: #1e88e5;
+ --secondary-color: #26a69a;
+ --accent-color: #ff9800;
+ --background-color: #f5f5f5;
+ --card-color: #ffffff;
+ --text-color: #333333;
+ --text-light-color: #757575;
+ --border-color: #e0e0e0;
+ --success-color: #4caf50;
+ --warning-color: #ff9800;
+ --error-color: #f44336;
+ --info-color: #2196f3;
+}
+
+/* تنسيق الخط والاتجاه */
+body {
+ font-family: 'Cairo', 'Tajawal', sans-serif;
+ direction: rtl;
+ text-align: right;
+ background-color: var(--background-color);
+ color: var(--text-color);
+}
+
+/* العناوين */
+h1, h2, h3, h4, h5, h6 {
+ font-family: 'Cairo', 'Tajawal', sans-serif;
+ font-weight: 700;
+ color: var(--primary-color);
+ margin-bottom: 1rem;
+}
+
+h1 {
+ font-size: 2.2rem;
+ border-bottom: 2px solid var(--primary-color);
+ padding-bottom: 0.5rem;
+}
+
+h2 {
+ font-size: 1.8rem;
+ color: var(--secondary-color);
+}
+
+h3 {
+ font-size: 1.5rem;
+}
+
+/* البطاقات */
+.card {
+ background-color: var(--card-color);
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+ margin-bottom: 1.5rem;
+}
+
+.card-header {
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: 1rem;
+ margin-bottom: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.card-title {
+ font-size: 1.3rem;
+ font-weight: 600;
+ color: var(--primary-color);
+ margin: 0;
+}
+
+/* الأزرار */
+.stButton > button {
+ background-color: var(--primary-color);
+ color: white;
+ border-radius: 4px;
+ border: none;
+ padding: 0.5rem 1rem;
+ font-family: 'Cairo', 'Tajawal', sans-serif;
+ font-weight: 600;
+ transition: all 0.3s ease;
+}
+
+.stButton > button:hover {
+ background-color: #1976d2;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.secondary-button > button {
+ background-color: var(--secondary-color);
+}
+
+.secondary-button > button:hover {
+ background-color: #00897b;
+}
+
+.accent-button > button {
+ background-color: var(--accent-color);
+}
+
+.accent-button > button:hover {
+ background-color: #fb8c00;
+}
+
+.danger-button > button {
+ background-color: var(--error-color);
+}
+
+.danger-button > button:hover {
+ background-color: #e53935;
+}
+
+/* حقول الإدخال */
+.stTextInput > div > div > input {
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ padding: 0.5rem;
+ font-family: 'Cairo', 'Tajawal', sans-serif;
+}
+
+.stTextInput > div > div > input:focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(30, 136, 229, 0.2);
+}
+
+/* القوائم المنسدلة */
+.stSelectbox > div > div > div {
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+}
+
+.stSelectbox > div > div > div:focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(30, 136, 229, 0.2);
+}
+
+/* الجداول */
+.stDataFrame {
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+.stDataFrame > div {
+ border: none !important;
+}
+
+.stDataFrame th {
+ background-color: var(--primary-color);
+ color: white;
+ font-weight: 600;
+ padding: 0.75rem 1rem;
+}
+
+.stDataFrame td {
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.stDataFrame tr:nth-child(even) {
+ background-color: rgba(0, 0, 0, 0.02);
+}
+
+/* الشريط الجانبي */
+.sidebar .sidebar-content {
+ background-color: #263238;
+ color: white;
+}
+
+.sidebar .sidebar-content h1,
+.sidebar .sidebar-content h2,
+.sidebar .sidebar-content h3 {
+ color: white;
+}
+
+.sidebar .sidebar-content .stRadio > div {
+ background-color: #37474f;
+ border-radius: 8px;
+ padding: 0.5rem;
+}
+
+.sidebar .sidebar-content .stRadio > div > div > label {
+ color: white;
+}
+
+/* علامات التبويب */
+.stTabs {
+ background-color: var(--card-color);
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1rem;
+}
+
+.stTabs > div > div > div > div {
+ font-family: 'Cairo', 'Tajawal', sans-serif;
+ font-weight: 600;
+}
+
+.stTabs > div > div > div > div[data-baseweb="tab-highlight"] {
+ background-color: var(--primary-color);
+}
+
+/* الرسائل */
+.stSuccess, .stInfo, .stWarning, .stError {
+ border-radius: 8px;
+ padding: 1rem;
+ margin-bottom: 1rem;
+}
+
+.stSuccess {
+ background-color: rgba(76, 175, 80, 0.1);
+ border-left: 4px solid var(--success-color);
+}
+
+.stInfo {
+ background-color: rgba(33, 150, 243, 0.1);
+ border-left: 4px solid var(--info-color);
+}
+
+.stWarning {
+ background-color: rgba(255, 152, 0, 0.1);
+ border-left: 4px solid var(--warning-color);
+}
+
+.stError {
+ background-color: rgba(244, 67, 54, 0.1);
+ border-left: 4px solid var(--error-color);
+}
+
+/* الرسوم البيانية */
+.stPlotlyChart {
+ background-color: var(--card-color);
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1rem;
+ margin-bottom: 1.5rem;
+}
+
+/* تخصيص للغة العربية */
+[dir="rtl"] .stPlotlyChart {
+ direction: ltr;
+}
+
+/* الشعار */
+.logo-container {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 1.5rem;
+}
+
+.logo-container img {
+ max-width: 200px;
+ height: auto;
+}
+
+/* تنسيق الصفوف والأعمدة */
+.row {
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: -0.75rem;
+ margin-left: -0.75rem;
+}
+
+.col {
+ flex: 1 0 0%;
+ padding-right: 0.75rem;
+ padding-left: 0.75rem;
+}
+
+/* تنسيق البطاقات الإحصائية */
+.stat-card {
+ background-color: var(--card-color);
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+ margin-bottom: 1.5rem;
+ text-align: center;
+}
+
+.stat-card-primary {
+ border-top: 4px solid var(--primary-color);
+}
+
+.stat-card-secondary {
+ border-top: 4px solid var(--secondary-color);
+}
+
+.stat-card-accent {
+ border-top: 4px solid var(--accent-color);
+}
+
+.stat-card-success {
+ border-top: 4px solid var(--success-color);
+}
+
+.stat-value {
+ font-size: 2.5rem;
+ font-weight: 700;
+ color: var(--text-color);
+ margin-bottom: 0.5rem;
+}
+
+.stat-label {
+ font-size: 1rem;
+ color: var(--text-light-color);
+}
+
+/* تنسيق النماذج */
+.form-group {
+ margin-bottom: 1.5rem;
+}
+
+.form-label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+}
+
+/* تنسيق الشاشة الرئيسية */
+.welcome-section {
+ text-align: center;
+ padding: 2rem 0;
+}
+
+.welcome-title {
+ font-size: 2.5rem;
+ color: var(--primary-color);
+ margin-bottom: 1rem;
+}
+
+.welcome-subtitle {
+ font-size: 1.2rem;
+ color: var(--text-light-color);
+ margin-bottom: 2rem;
+}
+
+/* تنسيق القائمة الرئيسية */
+.main-menu {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+.menu-item {
+ background-color: var(--card-color);
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+ text-align: center;
+ width: 200px;
+ transition: all 0.3s ease;
+}
+
+.menu-item:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
+}
+
+.menu-icon {
+ font-size: 2.5rem;
+ color: var(--primary-color);
+ margin-bottom: 1rem;
+}
+
+.menu-title {
+ font-size: 1.2rem;
+ font-weight: 600;
+ color: var(--text-color);
+}
+
+/* تنسيق الشاشة الترحيبية */
+.features-section {
+ margin-top: 3rem;
+}
+
+.feature-card {
+ background-color: var(--card-color);
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+ margin-bottom: 1.5rem;
+ display: flex;
+ align-items: flex-start;
+}
+
+.feature-icon {
+ font-size: 2rem;
+ color: var(--primary-color);
+ margin-left: 1.5rem;
+}
+
+.feature-content {
+ flex: 1;
+}
+
+.feature-title {
+ font-size: 1.3rem;
+ font-weight: 600;
+ color: var(--primary-color);
+ margin-bottom: 0.5rem;
+}
+
+.feature-description {
+ color: var(--text-light-color);
+}
+
+/* تنسيق الشاشة الترحيبية */
+.footer {
+ text-align: center;
+ padding: 2rem 0;
+ margin-top: 3rem;
+ border-top: 1px solid var(--border-color);
+ color: var(--text-light-color);
+}
diff --git a/pricing_system/tests/test_integrated_system.py b/pricing_system/tests/test_integrated_system.py
index 6eff203562414e39a640a3e8076b9739fd329c9f..f6ee916fc1b67848a68cf80c7cf25039bbc7dbb9 100644
--- a/pricing_system/tests/test_integrated_system.py
+++ b/pricing_system/tests/test_integrated_system.py
@@ -1,531 +1,531 @@
-import unittest
-import sys
-import os
-import pandas as pd
-import numpy as np
-import streamlit as st
-from unittest.mock import patch, MagicMock
-
-# إضافة مسار الوحدات
-sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-# استيراد الوحدات للاختبار
-from pricing_system.integration_framework import IntegrationFramework
-from pricing_system.modules.catalogs.equipment_catalog import EquipmentCatalog
-from pricing_system.modules.catalogs.materials_catalog import MaterialsCatalog
-from pricing_system.modules.catalogs.labor_catalog import LaborCatalog
-from pricing_system.modules.catalogs.subcontractors_catalog import SubcontractorsCatalog
-from pricing_system.modules.analysis.smart_price_analysis import SmartPriceAnalysis
-from pricing_system.modules.indirect_support.indirect_support_management import IndirectSupportManagement
-from pricing_system.modules.pricing_strategies.pricing_strategies import PricingStrategies
-
-class TestIntegratedSystem(unittest.TestCase):
- """
- اختبارات النظام المتكامل للتأكد من عمل جميع المكونات معًا بشكل صحيح
- """
-
- def setUp(self):
- """
- إعداد بيئة الاختبار
- """
- # تهيئة حالة الجلسة الوهمية
- st.session_state = {}
-
- # إنشاء مثيلات من الوحدات للاختبار
- self.equipment_catalog = EquipmentCatalog()
- self.materials_catalog = MaterialsCatalog()
- self.labor_catalog = LaborCatalog()
- self.subcontractors_catalog = SubcontractorsCatalog()
- self.smart_price_analysis = SmartPriceAnalysis()
- self.indirect_support = IndirectSupportManagement()
- self.pricing_strategies = PricingStrategies()
-
- # إنشاء مثيل من إطار التكامل
- self.integration_framework = IntegrationFramework()
-
- # إعداد بيانات اختبار المشروع
- self.project_data = {
- 'name': 'مشروع اختبار',
- 'location': 'الرياض',
- 'client': 'وزارة الإسكان',
- 'start_date': '2025-01-01',
- 'end_date': '2025-12-31',
- 'budget': 10000000.0,
- 'boq_items': [
- {
- 'item_code': 'B001',
- 'description': 'حفر وردم',
- 'unit': 'م3',
- 'quantity': 1000.0,
- 'unit_price': 50.0,
- 'total_price': 50000.0
- },
- {
- 'item_code': 'B002',
- 'description': 'خرسانة مسلحة',
- 'unit': 'م3',
- 'quantity': 500.0,
- 'unit_price': 1200.0,
- 'total_price': 600000.0
- },
- {
- 'item_code': 'B003',
- 'description': 'أعمال تشطيبات',
- 'unit': 'م2',
- 'quantity': 2000.0,
- 'unit_price': 300.0,
- 'total_price': 600000.0
- }
- ],
- 'resources': [],
- 'pricing_strategy': 'standard',
- 'indirect_costs': {},
- 'profit_margin': 0.15,
- 'local_content_target': 0.40
- }
-
- # إضافة بيانات اختبار للكتالوجات
- self._add_test_data_to_catalogs()
-
- def _add_test_data_to_catalogs(self):
- """
- إضافة بيانات اختبار إلى الكتالوجات
- """
- # إضافة معدات للاختبار
- equipment_data = [
- {
- 'name': 'حفار',
- 'type': 'حفار',
- 'description': 'حفار هيدروليكي',
- 'price_per_hour': 200.0,
- 'price_per_day': 1600.0,
- 'price_per_week': 8000.0,
- 'price_per_month': 30000.0,
- 'is_local': True
- },
- {
- 'name': 'لودر',
- 'type': 'لودر',
- 'description': 'لودر متوسط الحجم',
- 'price_per_hour': 150.0,
- 'price_per_day': 1200.0,
- 'price_per_week': 6000.0,
- 'price_per_month': 22000.0,
- 'is_local': False
- },
- {
- 'name': 'شاحنة نقل',
- 'type': 'شاحنة',
- 'description': 'شاحنة نقل ثقيلة',
- 'price_per_hour': 120.0,
- 'price_per_day': 960.0,
- 'price_per_week': 4800.0,
- 'price_per_month': 18000.0,
- 'is_local': True
- }
- ]
-
- for equipment in equipment_data:
- self.equipment_catalog.add_equipment(equipment['name'], equipment)
-
- # إضافة مواد للاختبار
- materials_data = [
- {
- 'name': 'اسمنت',
- 'description': 'اسمنت بورتلاندي',
- 'unit': 'طن',
- 'price': 600.0,
- 'is_local': True
- },
- {
- 'name': 'حديد تسليح',
- 'description': 'حديد تسليح قطر 16 مم',
- 'unit': 'طن',
- 'price': 3500.0,
- 'is_local': True
- },
- {
- 'name': 'رمل',
- 'description': 'رمل ناعم للخرسانة',
- 'unit': 'م3',
- 'price': 80.0,
- 'is_local': True
- },
- {
- 'name': 'بلاط سيراميك',
- 'description': 'بلاط سيراميك للأرضيات',
- 'unit': 'م2',
- 'price': 120.0,
- 'is_local': False
- }
- ]
-
- for material in materials_data:
- self.materials_catalog.add_material(material['name'], material)
-
- # إضافة عمالة للاختبار
- labor_data = [
- {
- 'name': 'مهندس مدني',
- 'type': 'مهندس مدني',
- 'description': 'مهندس مدني خبرة 5 سنوات',
- 'price_per_hour': 100.0,
- 'price_per_day': 800.0,
- 'price_per_week': 4000.0,
- 'price_per_month': 15000.0,
- 'is_local': True
- },
- {
- 'name': 'عامل بناء',
- 'type': 'عامل بناء',
- 'description': 'عامل بناء ماهر',
- 'price_per_hour': 20.0,
- 'price_per_day': 160.0,
- 'price_per_week': 800.0,
- 'price_per_month': 3000.0,
- 'is_local': False
- },
- {
- 'name': 'فني كهرباء',
- 'type': 'كهربائي',
- 'description': 'فني كهرباء خبرة 3 سنوات',
- 'price_per_hour': 30.0,
- 'price_per_day': 240.0,
- 'price_per_week': 1200.0,
- 'price_per_month': 4500.0,
- 'is_local': True
- }
- ]
-
- for labor in labor_data:
- self.labor_catalog.add_labor(labor['name'], labor)
-
- # إضافة مقاولي باطن للاختبار
- subcontractors_data = [
- {
- 'name': 'شركة الأعمال الكهربائية',
- 'specialization': 'أعمال كهربائية',
- 'description': 'شركة متخصصة في الأعمال الكهربائية',
- 'contact_info': 'info@electrical-works.com',
- 'is_local': True
- },
- {
- 'name': 'مؤسسة أنظمة التكييف',
- 'specialization': 'أعمال تكييف',
- 'description': 'مؤسسة متخصصة في أنظمة التكييف',
- 'contact_info': 'info@hvac-systems.com',
- 'is_local': True
- },
- {
- 'name': 'شركة أنظمة المراقبة',
- 'specialization': 'أعمال CCTV',
- 'description': 'شركة متخصصة في أنظمة المراقبة والكاميرات',
- 'contact_info': 'info@cctv-systems.com',
- 'is_local': False
- }
- ]
-
- for subcontractor in subcontractors_data:
- self.subcontractors_catalog.add_subcontractor(subcontractor['name'], subcontractor)
-
- # إضافة إدارات مساندة للاختبار
- departments_data = [
- {
- 'name': 'إدارة المشاريع',
- 'description': 'إدارة متابعة وتنفيذ المشاريع',
- 'monthly_cost': 100000.0,
- 'employees_count': 10,
- 'allocation_percentage': 20.0
- },
- {
- 'name': 'إدارة المشتريات',
- 'description': 'إدارة المشتريات والتوريدات',
- 'monthly_cost': 80000.0,
- 'employees_count': 8,
- 'allocation_percentage': 15.0
- },
- {
- 'name': 'الإدارة المالية',
- 'description': 'الإدارة المالية والمحاسبة',
- 'monthly_cost': 70000.0,
- 'employees_count': 7,
- 'allocation_percentage': 10.0
- }
- ]
-
- for department in departments_data:
- self.indirect_support.add_department(department['name'], department)
-
- def test_equipment_catalog_integration(self):
- """
- اختبار تكامل كتالوج المعدات
- """
- # التحقق من وجود المعدات في الكتالوج
- equipment_list = self.equipment_catalog.get_equipment_list()
- self.assertEqual(len(equipment_list), 3)
- self.assertIn('حفار', equipment_list)
- self.assertIn('لودر', equipment_list)
- self.assertIn('شاحنة نقل', equipment_list)
-
- # التحقق من تفاصيل المعدات
- حفار_details = self.equipment_catalog.get_equipment_details('حفار')
- self.assertEqual(حفار_details['price_per_day'], 1600.0)
- self.assertTrue(حفار_details['is_local'])
-
- لودر_details = self.equipment_catalog.get_equipment_details('لودر')
- self.assertEqual(لودر_details['price_per_day'], 1200.0)
- self.assertFalse(لودر_details['is_local'])
-
- def test_materials_catalog_integration(self):
- """
- اختبار تكامل كتالوج المواد
- """
- # التحقق من وجود المواد في الكتالوج
- materials_list = self.materials_catalog.get_materials_list()
- self.assertEqual(len(materials_list), 4)
- self.assertIn('اسمنت', materials_list)
- self.assertIn('حديد تسليح', materials_list)
- self.assertIn('رمل', materials_list)
- self.assertIn('بلاط سيراميك', materials_list)
-
- # التحقق من تفاصيل المواد
- اسمنت_details = self.materials_catalog.get_material_details('اسمنت')
- self.assertEqual(اسمنت_details['price'], 600.0)
- self.assertEqual(اسمنت_details['unit'], 'طن')
- self.assertTrue(اسمنت_details['is_local'])
-
- بلاط_details = self.materials_catalog.get_material_details('بلاط سيراميك')
- self.assertEqual(بلاط_details['price'], 120.0)
- self.assertEqual(بلاط_details['unit'], 'م2')
- self.assertFalse(بلاط_details['is_local'])
-
- def test_labor_catalog_integration(self):
- """
- اختبار تكامل كتالوج العمالة
- """
- # التحقق من وجود العمالة في الكتالوج
- labor_list = self.labor_catalog.get_labor_list()
- self.assertEqual(len(labor_list), 3)
- self.assertIn('مهندس مدني', labor_list)
- self.assertIn('عامل بناء', labor_list)
- self.assertIn('فني كهرباء', labor_list)
-
- # التحقق من تفاصيل العمالة
- مهندس_details = self.labor_catalog.get_labor_details('مهندس مدني')
- self.assertEqual(مهندس_details['price_per_month'], 15000.0)
- self.assertTrue(مهندس_details['is_local'])
-
- عامل_details = self.labor_catalog.get_labor_details('عامل بناء')
- self.assertEqual(عامل_details['price_per_day'], 160.0)
- self.assertFalse(عامل_details['is_local'])
-
- def test_subcontractors_catalog_integration(self):
- """
- اختبار تكامل كتالوج مقاولي الباطن
- """
- # التحقق من وجود مقاولي الباطن في الكتالوج
- subcontractors_list = self.subcontractors_catalog.get_subcontractors_list()
- self.assertEqual(len(subcontractors_list), 3)
- self.assertIn('شركة الأعمال الكهربائية', subcontractors_list)
- self.assertIn('مؤسسة أنظمة التكييف', subcontractors_list)
- self.assertIn('شركة أنظمة المراقبة', subcontractors_list)
-
- # التحقق من تفاصيل مقاولي الباطن
- كهرباء_details = self.subcontractors_catalog.get_subcontractor_details('شركة الأعمال الكهربائية')
- self.assertEqual(كهرباء_details['specialization'], 'أعمال كهربائية')
- self.assertTrue(كهرباء_details['is_local'])
-
- مراقبة_details = self.subcontractors_catalog.get_subcontractor_details('شركة أنظمة المراقبة')
- self.assertEqual(مراقبة_details['specialization'], 'أعمال CCTV')
- self.assertFalse(مراقبة_details['is_local'])
-
- def test_indirect_support_integration(self):
- """
- اختبار تكامل إدارة الإدارات المساندة
- """
- # التحقق من وجود الإدارات المساندة
- departments = self.indirect_support.get_departments()
- self.assertEqual(len(departments), 3)
- self.assertIn('إدارة المشاريع', departments)
- self.assertIn('إدارة المشتريات', departments)
- self.assertIn('الإدارة المالية', departments)
-
- # التحقق من تفاصيل الإدارات المساندة
- مشاريع_details = self.indirect_support.get_department_details('إدارة المشاريع')
- self.assertEqual(مشاريع_details['monthly_cost'], 100000.0)
- self.assertEqual(مشاريع_details['allocation_percentage'], 20.0)
-
- # التحقق من حساب التكاليف الإجمالية
- total_monthly_cost = self.indirect_support.get_total_monthly_cost()
- self.assertEqual(total_monthly_cost, 250000.0)
-
- total_allocated_cost = self.indirect_support.get_total_allocated_cost()
- self.assertEqual(total_allocated_cost, 45000.0) # (100000*0.2 + 80000*0.15 + 70000*0.1)
-
- def test_smart_price_analysis_integration(self):
- """
- اختبار تكامل التحليل الذكي للأسعار
- """
- # إضافة تحليل لبنود المشروع
- for i, item in enumerate(self.project_data['boq_items']):
- analysis = {
- 'direct_cost': item['total_price'] * 0.7, # 70% من السعر الإجمالي
- 'indirect_cost': item['total_price'] * 0.15, # 15% من السعر الإجمالي
- 'profit_margin': item['total_price'] * 0.15, # 15% من السعر الإجمالي
- 'total_price': item['total_price'],
- 'materials': [
- {
- 'name': 'اسمنت',
- 'quantity': 10,
- 'unit': 'طن',
- 'price': 600.0,
- 'total': 6000.0,
- 'is_local': True
- },
- {
- 'name': 'حديد تسليح',
- 'quantity': 5,
- 'unit': 'طن',
- 'price': 3500.0,
- 'total': 17500.0,
- 'is_local': True
- }
- ],
- 'equipment': [
- {
- 'name': 'حفار',
- 'duration': 5,
- 'duration_unit': 'يوم',
- 'price': 1600.0,
- 'total': 8000.0,
- 'is_local': True
- }
- ],
- 'labor': [
- {
- 'name': 'عامل بناء',
- 'duration': 20,
- 'duration_unit': 'يوم',
- 'price': 160.0,
- 'total': 3200.0,
- 'is_local': False
- }
- ],
- 'materials_cost': 23500.0,
- 'equipment_cost': 8000.0,
- 'labor_cost': 3200.0,
- 'subcontractors_cost': 0.0
- }
-
- self.smart_price_analysis.add_item_analysis(i, analysis)
-
- # التحقق من تحليل البنود
- all_analyses = self.smart_price_analysis.get_all_items_analysis()
- self.assertEqual(len(all_analyses), 3)
-
- # التحقق من تحليل التكاليف الإجمالية للمشروع
- cost_analysis = self.smart_price_analysis.get_project_cost_analysis()
- self.assertEqual(cost_analysis['total_materials_cost'], 23500.0 * 3)
- self.assertEqual(cost_analysis['total_equipment_cost'], 8000.0 * 3)
- self.assertEqual(cost_analysis['total_labor_cost'], 3200.0 * 3)
-
- # التحقق من المواد الأكثر تكلفة
- top_materials = self.smart_price_analysis.get_top_materials(limit=2)
- self.assertEqual(len(top_materials), 2)
- self.assertEqual(top_materials[0]['name'], 'حديد تسليح')
- self.assertEqual(top_materials[0]['total_cost'], 17500.0 * 3)
-
- def test_pricing_strategies_integration(self):
- """
- اختبار تكامل استراتيجيات التسعير
- """
- # تطبيق استراتيجية التسعير القياسي
- result = self.pricing_strategies.apply_strategy(
- 'standard',
- self.project_data,
- self.smart_price_analysis
- )
-
- self.assertTrue(result['success'])
- self.assertEqual(len(result['items_result']), 3)
-
- # التحقق من نتائج تطبيق الاستراتيجية
- total_cost = sum(item['cost'] for item in result['items_result'])
- total_price = sum(item['price'] for item in result['items_result'])
- profit_margin = total_price - total_cost
-
- self.assertEqual(result['total_cost'], total_cost)
- self.assertEqual(result['total_price'], total_price)
- self.assertEqual(result['profit_margin'], profit_margin)
-
- # مقارنة استراتيجيات التسعير
- comparison_result = self.pricing_strategies.compare_strategies(
- self.project_data,
- self.smart_price_analysis
- )
-
- self.assertTrue(comparison_result['success'])
- self.assertEqual(len(comparison_result['strategies_result']), 6) # 6 استراتيجيات
-
- def test_local_content_analysis_integration(self):
- """
- اختبار تكامل تحليل المحتوى المحلي
- """
- # تحليل المحتوى المحلي
- local_content_analysis = self.pricing_strategies.analyze_local_content(
- self.project_data,
- self.smart_price_analysis
- )
-
- self.assertTrue(local_content_analysis['success'])
-
- # التحقق من نتائج تحليل المحتوى المحلي
- self.assertIn('local_content_percentage', local_content_analysis)
- self.assertIn('resources_local_content', local_content_analysis)
-
- # التحقق من تحليل المحتوى المحلي حسب نوع الموارد
- resources_local_content = local_content_analysis['resources_local_content']
- self.assertIn('materials', resources_local_content)
- self.assertIn('equipment', resources_local_content)
- self.assertIn('labor', resources_local_content)
-
- # التحقق من التوصيات
- if local_content_analysis['local_content_percentage'] < self.project_data['local_content_target']:
- self.assertIn('recommendations', local_content_analysis)
- self.assertTrue(len(local_content_analysis['recommendations']) > 0)
-
- def test_integration_framework(self):
- """
- اختبار إطار التكامل
- """
- # تهيئة حالة الجلسة
- st.session_state = {}
-
- # إنشاء مثيلات وهمية من PricingApp و ResourcesApp
- pricing_app_mock = MagicMock()
- pricing_app_mock.tabs = ["جدول الكميات", "تحليل التكاليف", "سيناريوهات التسعير", "التحليل التنافسي", "التقارير"]
- pricing_app_mock._render_bill_of_quantities_tab = MagicMock()
- pricing_app_mock._render_cost_analysis_tab = MagicMock()
- pricing_app_mock._render_pricing_scenarios_tab = MagicMock()
- pricing_app_mock._render_competitive_analysis_tab = MagicMock()
- pricing_app_mock._render_reports_tab = MagicMock()
-
- resources_app_mock = MagicMock()
-
- # ربط إطار التكامل مع التطبيقات الوهمية
- self.integration_framework.connect_pricing_app(pricing_app_mock)
- self.integration_framework.connect_resources_app(resources_app_mock)
-
- # التحقق من إضافة علامات التبويب الجديدة
- self.assertEqual(len(pricing_app_mock.tabs), 8) # 5 علامات أصلية + 3 جديدة
- self.assertIn("كتالوجات الموارد", pricing_app_mock.tabs)
- self.assertIn("الإدارات المساندة", pricing_app_mock.tabs)
- self.assertIn("المحتوى المحلي", pricing_app_mock.tabs)
-
- # التحقق من إضافة الدوال الجديدة
- self.assertTrue(hasattr(pricing_app_mock, '_render_resource_catalogs_tab'))
- self.assertTrue(hasattr(pricing_app_mock, '_render_indirect_support_tab'))
- self.assertTrue(hasattr(pricing_app_mock, '_render_local_content_tab'))
-
-if __name__ == '__main__':
- unittest.main()
+import unittest
+import sys
+import os
+import pandas as pd
+import numpy as np
+import streamlit as st
+from unittest.mock import patch, MagicMock
+
+# إضافة مسار الوحدات
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+# استيراد الوحدات للاختبار
+from pricing_system.integration_framework import IntegrationFramework
+from pricing_system.modules.catalogs.equipment_catalog import EquipmentCatalog
+from pricing_system.modules.catalogs.materials_catalog import MaterialsCatalog
+from pricing_system.modules.catalogs.labor_catalog import LaborCatalog
+from pricing_system.modules.catalogs.subcontractors_catalog import SubcontractorsCatalog
+from pricing_system.modules.analysis.smart_price_analysis import SmartPriceAnalysis
+from pricing_system.modules.indirect_support.indirect_support_management import IndirectSupportManagement
+from pricing_system.modules.pricing_strategies.pricing_strategies import PricingStrategies
+
+class TestIntegratedSystem(unittest.TestCase):
+ """
+ اختبارات النظام المتكامل للتأكد من عمل جميع المكونات معًا بشكل صحيح
+ """
+
+ def setUp(self):
+ """
+ إعداد بيئة الاختبار
+ """
+ # تهيئة حالة الجلسة الوهمية
+ st.session_state = {}
+
+ # إنشاء مثيلات من الوحدات للاختبار
+ self.equipment_catalog = EquipmentCatalog()
+ self.materials_catalog = MaterialsCatalog()
+ self.labor_catalog = LaborCatalog()
+ self.subcontractors_catalog = SubcontractorsCatalog()
+ self.smart_price_analysis = SmartPriceAnalysis()
+ self.indirect_support = IndirectSupportManagement()
+ self.pricing_strategies = PricingStrategies()
+
+ # إنشاء مثيل من إطار التكامل
+ self.integration_framework = IntegrationFramework()
+
+ # إعداد بيانات اختبار المشروع
+ self.project_data = {
+ 'name': 'مشروع اختبار',
+ 'location': 'الرياض',
+ 'client': 'وزارة الإسكان',
+ 'start_date': '2025-01-01',
+ 'end_date': '2025-12-31',
+ 'budget': 10000000.0,
+ 'boq_items': [
+ {
+ 'item_code': 'B001',
+ 'description': 'حفر وردم',
+ 'unit': 'م3',
+ 'quantity': 1000.0,
+ 'unit_price': 50.0,
+ 'total_price': 50000.0
+ },
+ {
+ 'item_code': 'B002',
+ 'description': 'خرسانة مسلحة',
+ 'unit': 'م3',
+ 'quantity': 500.0,
+ 'unit_price': 1200.0,
+ 'total_price': 600000.0
+ },
+ {
+ 'item_code': 'B003',
+ 'description': 'أعمال تشطيبات',
+ 'unit': 'م2',
+ 'quantity': 2000.0,
+ 'unit_price': 300.0,
+ 'total_price': 600000.0
+ }
+ ],
+ 'resources': [],
+ 'pricing_strategy': 'standard',
+ 'indirect_costs': {},
+ 'profit_margin': 0.15,
+ 'local_content_target': 0.40
+ }
+
+ # إضافة بيانات اختبار للكتالوجات
+ self._add_test_data_to_catalogs()
+
+ def _add_test_data_to_catalogs(self):
+ """
+ إضافة بيانات اختبار إلى الكتالوجات
+ """
+ # إضافة معدات للاختبار
+ equipment_data = [
+ {
+ 'name': 'حفار',
+ 'type': 'حفار',
+ 'description': 'حفار هيدروليكي',
+ 'price_per_hour': 200.0,
+ 'price_per_day': 1600.0,
+ 'price_per_week': 8000.0,
+ 'price_per_month': 30000.0,
+ 'is_local': True
+ },
+ {
+ 'name': 'لودر',
+ 'type': 'لودر',
+ 'description': 'لودر متوسط الحجم',
+ 'price_per_hour': 150.0,
+ 'price_per_day': 1200.0,
+ 'price_per_week': 6000.0,
+ 'price_per_month': 22000.0,
+ 'is_local': False
+ },
+ {
+ 'name': 'شاحنة نقل',
+ 'type': 'شاحنة',
+ 'description': 'شاحنة نقل ثقيلة',
+ 'price_per_hour': 120.0,
+ 'price_per_day': 960.0,
+ 'price_per_week': 4800.0,
+ 'price_per_month': 18000.0,
+ 'is_local': True
+ }
+ ]
+
+ for equipment in equipment_data:
+ self.equipment_catalog.add_equipment(equipment['name'], equipment)
+
+ # إضافة مواد للاختبار
+ materials_data = [
+ {
+ 'name': 'اسمنت',
+ 'description': 'اسمنت بورتلاندي',
+ 'unit': 'طن',
+ 'price': 600.0,
+ 'is_local': True
+ },
+ {
+ 'name': 'حديد تسليح',
+ 'description': 'حديد تسليح قطر 16 مم',
+ 'unit': 'طن',
+ 'price': 3500.0,
+ 'is_local': True
+ },
+ {
+ 'name': 'رمل',
+ 'description': 'رمل ناعم للخرسانة',
+ 'unit': 'م3',
+ 'price': 80.0,
+ 'is_local': True
+ },
+ {
+ 'name': 'بلاط سيراميك',
+ 'description': 'بلاط سيراميك للأرضيات',
+ 'unit': 'م2',
+ 'price': 120.0,
+ 'is_local': False
+ }
+ ]
+
+ for material in materials_data:
+ self.materials_catalog.add_material(material['name'], material)
+
+ # إضافة عمالة للاختبار
+ labor_data = [
+ {
+ 'name': 'مهندس مدني',
+ 'type': 'مهندس مدني',
+ 'description': 'مهندس مدني خبرة 5 سنوات',
+ 'price_per_hour': 100.0,
+ 'price_per_day': 800.0,
+ 'price_per_week': 4000.0,
+ 'price_per_month': 15000.0,
+ 'is_local': True
+ },
+ {
+ 'name': 'عامل بناء',
+ 'type': 'عامل بناء',
+ 'description': 'عامل بناء ماهر',
+ 'price_per_hour': 20.0,
+ 'price_per_day': 160.0,
+ 'price_per_week': 800.0,
+ 'price_per_month': 3000.0,
+ 'is_local': False
+ },
+ {
+ 'name': 'فني كهرباء',
+ 'type': 'كهربائي',
+ 'description': 'فني كهرباء خبرة 3 سنوات',
+ 'price_per_hour': 30.0,
+ 'price_per_day': 240.0,
+ 'price_per_week': 1200.0,
+ 'price_per_month': 4500.0,
+ 'is_local': True
+ }
+ ]
+
+ for labor in labor_data:
+ self.labor_catalog.add_labor(labor['name'], labor)
+
+ # إضافة مقاولي باطن للاختبار
+ subcontractors_data = [
+ {
+ 'name': 'شركة الأعمال الكهربائية',
+ 'specialization': 'أعمال كهربائية',
+ 'description': 'شركة متخصصة في الأعمال الكهربائية',
+ 'contact_info': 'info@electrical-works.com',
+ 'is_local': True
+ },
+ {
+ 'name': 'مؤسسة أنظمة التكييف',
+ 'specialization': 'أعمال تكييف',
+ 'description': 'مؤسسة متخصصة في أنظمة التكييف',
+ 'contact_info': 'info@hvac-systems.com',
+ 'is_local': True
+ },
+ {
+ 'name': 'شركة أنظمة المراقبة',
+ 'specialization': 'أعمال CCTV',
+ 'description': 'شركة متخصصة في أنظمة المراقبة والكاميرات',
+ 'contact_info': 'info@cctv-systems.com',
+ 'is_local': False
+ }
+ ]
+
+ for subcontractor in subcontractors_data:
+ self.subcontractors_catalog.add_subcontractor(subcontractor['name'], subcontractor)
+
+ # إضافة إدارات مساندة للاختبار
+ departments_data = [
+ {
+ 'name': 'إدارة المشاريع',
+ 'description': 'إدارة متابعة وتنفيذ المشاريع',
+ 'monthly_cost': 100000.0,
+ 'employees_count': 10,
+ 'allocation_percentage': 20.0
+ },
+ {
+ 'name': 'إدارة المشتريات',
+ 'description': 'إدارة المشتريات والتوريدات',
+ 'monthly_cost': 80000.0,
+ 'employees_count': 8,
+ 'allocation_percentage': 15.0
+ },
+ {
+ 'name': 'الإدارة المالية',
+ 'description': 'الإدارة المالية والمحاسبة',
+ 'monthly_cost': 70000.0,
+ 'employees_count': 7,
+ 'allocation_percentage': 10.0
+ }
+ ]
+
+ for department in departments_data:
+ self.indirect_support.add_department(department['name'], department)
+
+ def test_equipment_catalog_integration(self):
+ """
+ اختبار تكامل كتالوج المعدات
+ """
+ # التحقق من وجود المعدات في الكتالوج
+ equipment_list = self.equipment_catalog.get_equipment_list()
+ self.assertEqual(len(equipment_list), 3)
+ self.assertIn('حفار', equipment_list)
+ self.assertIn('لودر', equipment_list)
+ self.assertIn('شاحنة نقل', equipment_list)
+
+ # التحقق من تفاصيل المعدات
+ حفار_details = self.equipment_catalog.get_equipment_details('حفار')
+ self.assertEqual(حفار_details['price_per_day'], 1600.0)
+ self.assertTrue(حفار_details['is_local'])
+
+ لودر_details = self.equipment_catalog.get_equipment_details('لودر')
+ self.assertEqual(لودر_details['price_per_day'], 1200.0)
+ self.assertFalse(لودر_details['is_local'])
+
+ def test_materials_catalog_integration(self):
+ """
+ اختبار تكامل كتالوج المواد
+ """
+ # التحقق من وجود المواد في الكتالوج
+ materials_list = self.materials_catalog.get_materials_list()
+ self.assertEqual(len(materials_list), 4)
+ self.assertIn('اسمنت', materials_list)
+ self.assertIn('حديد تسليح', materials_list)
+ self.assertIn('رمل', materials_list)
+ self.assertIn('بلاط سيراميك', materials_list)
+
+ # التحقق من تفاصيل المواد
+ اسمنت_details = self.materials_catalog.get_material_details('اسمنت')
+ self.assertEqual(اسمنت_details['price'], 600.0)
+ self.assertEqual(اسمنت_details['unit'], 'طن')
+ self.assertTrue(اسمنت_details['is_local'])
+
+ بلاط_details = self.materials_catalog.get_material_details('بلاط سيراميك')
+ self.assertEqual(بلاط_details['price'], 120.0)
+ self.assertEqual(بلاط_details['unit'], 'م2')
+ self.assertFalse(بلاط_details['is_local'])
+
+ def test_labor_catalog_integration(self):
+ """
+ اختبار تكامل كتالوج العمالة
+ """
+ # التحقق من وجود العمالة في الكتالوج
+ labor_list = self.labor_catalog.get_labor_list()
+ self.assertEqual(len(labor_list), 3)
+ self.assertIn('مهندس مدني', labor_list)
+ self.assertIn('عامل بناء', labor_list)
+ self.assertIn('فني كهرباء', labor_list)
+
+ # التحقق من تفاصيل العمالة
+ مهندس_details = self.labor_catalog.get_labor_details('مهندس مدني')
+ self.assertEqual(مهندس_details['price_per_month'], 15000.0)
+ self.assertTrue(مهندس_details['is_local'])
+
+ عامل_details = self.labor_catalog.get_labor_details('عامل بناء')
+ self.assertEqual(عامل_details['price_per_day'], 160.0)
+ self.assertFalse(عامل_details['is_local'])
+
+ def test_subcontractors_catalog_integration(self):
+ """
+ اختبار تكامل كتالوج مقاولي الباطن
+ """
+ # التحقق من وجود مقاولي الباطن في الكتالوج
+ subcontractors_list = self.subcontractors_catalog.get_subcontractors_list()
+ self.assertEqual(len(subcontractors_list), 3)
+ self.assertIn('شركة الأعمال الكهربائية', subcontractors_list)
+ self.assertIn('مؤسسة أنظمة التكييف', subcontractors_list)
+ self.assertIn('شركة أنظمة المراقبة', subcontractors_list)
+
+ # التحقق من تفاصيل مقاولي الباطن
+ كهرباء_details = self.subcontractors_catalog.get_subcontractor_details('شركة الأعمال الكهربائية')
+ self.assertEqual(كهرباء_details['specialization'], 'أعمال كهربائية')
+ self.assertTrue(كهرباء_details['is_local'])
+
+ مراقبة_details = self.subcontractors_catalog.get_subcontractor_details('شركة أنظمة المراقبة')
+ self.assertEqual(مراقبة_details['specialization'], 'أعمال CCTV')
+ self.assertFalse(مراقبة_details['is_local'])
+
+ def test_indirect_support_integration(self):
+ """
+ اختبار تكامل إدارة الإدارات المساندة
+ """
+ # التحقق من وجود الإدارات المساندة
+ departments = self.indirect_support.get_departments()
+ self.assertEqual(len(departments), 3)
+ self.assertIn('إدارة المشاريع', departments)
+ self.assertIn('إدارة المشتريات', departments)
+ self.assertIn('الإدارة المالية', departments)
+
+ # التحقق من تفاصيل الإدارات المساندة
+ مشاريع_details = self.indirect_support.get_department_details('إدارة المشاريع')
+ self.assertEqual(مشاريع_details['monthly_cost'], 100000.0)
+ self.assertEqual(مشاريع_details['allocation_percentage'], 20.0)
+
+ # التحقق من حساب التكاليف الإجمالية
+ total_monthly_cost = self.indirect_support.get_total_monthly_cost()
+ self.assertEqual(total_monthly_cost, 250000.0)
+
+ total_allocated_cost = self.indirect_support.get_total_allocated_cost()
+ self.assertEqual(total_allocated_cost, 45000.0) # (100000*0.2 + 80000*0.15 + 70000*0.1)
+
+ def test_smart_price_analysis_integration(self):
+ """
+ اختبار تكامل التحليل الذكي للأسعار
+ """
+ # إضافة تحليل لبنود المشروع
+ for i, item in enumerate(self.project_data['boq_items']):
+ analysis = {
+ 'direct_cost': item['total_price'] * 0.7, # 70% من السعر الإجمالي
+ 'indirect_cost': item['total_price'] * 0.15, # 15% من السعر الإجمالي
+ 'profit_margin': item['total_price'] * 0.15, # 15% من السعر الإجمالي
+ 'total_price': item['total_price'],
+ 'materials': [
+ {
+ 'name': 'اسمنت',
+ 'quantity': 10,
+ 'unit': 'طن',
+ 'price': 600.0,
+ 'total': 6000.0,
+ 'is_local': True
+ },
+ {
+ 'name': 'حديد تسليح',
+ 'quantity': 5,
+ 'unit': 'طن',
+ 'price': 3500.0,
+ 'total': 17500.0,
+ 'is_local': True
+ }
+ ],
+ 'equipment': [
+ {
+ 'name': 'حفار',
+ 'duration': 5,
+ 'duration_unit': 'يوم',
+ 'price': 1600.0,
+ 'total': 8000.0,
+ 'is_local': True
+ }
+ ],
+ 'labor': [
+ {
+ 'name': 'عامل بناء',
+ 'duration': 20,
+ 'duration_unit': 'يوم',
+ 'price': 160.0,
+ 'total': 3200.0,
+ 'is_local': False
+ }
+ ],
+ 'materials_cost': 23500.0,
+ 'equipment_cost': 8000.0,
+ 'labor_cost': 3200.0,
+ 'subcontractors_cost': 0.0
+ }
+
+ self.smart_price_analysis.add_item_analysis(i, analysis)
+
+ # التحقق من تحليل البنود
+ all_analyses = self.smart_price_analysis.get_all_items_analysis()
+ self.assertEqual(len(all_analyses), 3)
+
+ # التحقق من تحليل التكاليف الإجمالية للمشروع
+ cost_analysis = self.smart_price_analysis.get_project_cost_analysis()
+ self.assertEqual(cost_analysis['total_materials_cost'], 23500.0 * 3)
+ self.assertEqual(cost_analysis['total_equipment_cost'], 8000.0 * 3)
+ self.assertEqual(cost_analysis['total_labor_cost'], 3200.0 * 3)
+
+ # التحقق من المواد الأكثر تكلفة
+ top_materials = self.smart_price_analysis.get_top_materials(limit=2)
+ self.assertEqual(len(top_materials), 2)
+ self.assertEqual(top_materials[0]['name'], 'حديد تسليح')
+ self.assertEqual(top_materials[0]['total_cost'], 17500.0 * 3)
+
+ def test_pricing_strategies_integration(self):
+ """
+ اختبار تكامل استراتيجيات التسعير
+ """
+ # تطبيق استراتيجية التسعير القياسي
+ result = self.pricing_strategies.apply_strategy(
+ 'standard',
+ self.project_data,
+ self.smart_price_analysis
+ )
+
+ self.assertTrue(result['success'])
+ self.assertEqual(len(result['items_result']), 3)
+
+ # التحقق من نتائج تطبيق الاستراتيجية
+ total_cost = sum(item['cost'] for item in result['items_result'])
+ total_price = sum(item['price'] for item in result['items_result'])
+ profit_margin = total_price - total_cost
+
+ self.assertEqual(result['total_cost'], total_cost)
+ self.assertEqual(result['total_price'], total_price)
+ self.assertEqual(result['profit_margin'], profit_margin)
+
+ # مقارنة استراتيجيات التسعير
+ comparison_result = self.pricing_strategies.compare_strategies(
+ self.project_data,
+ self.smart_price_analysis
+ )
+
+ self.assertTrue(comparison_result['success'])
+ self.assertEqual(len(comparison_result['strategies_result']), 6) # 6 استراتيجيات
+
+ def test_local_content_analysis_integration(self):
+ """
+ اختبار تكامل تحليل المحتوى المحلي
+ """
+ # تحليل المحتوى المحلي
+ local_content_analysis = self.pricing_strategies.analyze_local_content(
+ self.project_data,
+ self.smart_price_analysis
+ )
+
+ self.assertTrue(local_content_analysis['success'])
+
+ # التحقق من نتائج تحليل المحتوى المحلي
+ self.assertIn('local_content_percentage', local_content_analysis)
+ self.assertIn('resources_local_content', local_content_analysis)
+
+ # التحقق من تحليل المحتوى المحلي حسب نوع الموارد
+ resources_local_content = local_content_analysis['resources_local_content']
+ self.assertIn('materials', resources_local_content)
+ self.assertIn('equipment', resources_local_content)
+ self.assertIn('labor', resources_local_content)
+
+ # التحقق من التوصيات
+ if local_content_analysis['local_content_percentage'] < self.project_data['local_content_target']:
+ self.assertIn('recommendations', local_content_analysis)
+ self.assertTrue(len(local_content_analysis['recommendations']) > 0)
+
+ def test_integration_framework(self):
+ """
+ اختبار إطار التكامل
+ """
+ # تهيئة حالة الجلسة
+ st.session_state = {}
+
+ # إنشاء مثيلات وهمية من PricingApp و ResourcesApp
+ pricing_app_mock = MagicMock()
+ pricing_app_mock.tabs = ["جدول الكميات", "تحليل التكاليف", "سيناريوهات التسعير", "التحليل التنافسي", "التقارير"]
+ pricing_app_mock._render_bill_of_quantities_tab = MagicMock()
+ pricing_app_mock._render_cost_analysis_tab = MagicMock()
+ pricing_app_mock._render_pricing_scenarios_tab = MagicMock()
+ pricing_app_mock._render_competitive_analysis_tab = MagicMock()
+ pricing_app_mock._render_reports_tab = MagicMock()
+
+ resources_app_mock = MagicMock()
+
+ # ربط إطار التكامل مع التطبيقات الوهمية
+ self.integration_framework.connect_pricing_app(pricing_app_mock)
+ self.integration_framework.connect_resources_app(resources_app_mock)
+
+ # التحقق من إضافة علامات التبويب الجديدة
+ self.assertEqual(len(pricing_app_mock.tabs), 8) # 5 علامات أصلية + 3 جديدة
+ self.assertIn("كتالوجات الموارد", pricing_app_mock.tabs)
+ self.assertIn("الإدارات المساندة", pricing_app_mock.tabs)
+ self.assertIn("المحتوى المحلي", pricing_app_mock.tabs)
+
+ # التحقق من إضافة الدوال الجديدة
+ self.assertTrue(hasattr(pricing_app_mock, '_render_resource_catalogs_tab'))
+ self.assertTrue(hasattr(pricing_app_mock, '_render_indirect_support_tab'))
+ self.assertTrue(hasattr(pricing_app_mock, '_render_local_content_tab'))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pricing_system/utils.py b/pricing_system/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..8af399928e2d92f4e65cfa9927f10be540566088
--- /dev/null
+++ b/pricing_system/utils.py
@@ -0,0 +1,20 @@
+
+import os
+import requests
+
+def download_image(url, save_path):
+ """تحميل صورة من URL وحفظها في المسار المحدد"""
+ try:
+ response = requests.get(url)
+ response.raise_for_status()
+
+ # التأكد من وجود المجلد
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
+
+ # حفظ الصورة
+ with open(save_path, 'wb') as f:
+ f.write(response.content)
+ return True
+ except Exception as e:
+ print(f"خطأ في تحميل الصورة: {e}")
+ return False
diff --git a/requirements.txt b/requirements.txt
index 2acc2facf01e93f5fb00afc49a1a9ae98331375c..35f477f6514afec6dfc82e1e52b2aafceebdd36e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,72 +1,23 @@
-# الاعتماديات الأساسية
-streamlit==1.32.0
-pandas==2.2.0
-numpy==1.26.3
-matplotlib==3.8.2
-seaborn==0.13.1
-plotly==5.18.0
-
-# معالجة البيانات
-openpyxl==3.1.2
-xlrd==2.0.1
-xlsxwriter==3.1.9
-pyarrow==14.0.1
-
-# تحليل المستندات
-PyPDF2==3.0.1
-python-docx==1.1.0
-pdf2image==1.17.0
-pytesseract==0.3.10
-pymupdf==1.23.7
-pdfplumber==0.10.3
-opencv-python-headless==4.8.1.78
-# poppler-utils ← يُثبت من apt
-
-# معالجة اللغة العربية
-arabic-reshaper==3.0.0
-python-bidi==0.4.2
-langdetect==1.0.9
-farasapy==0.0.14
-# cameltools==1.1.0
-
-# الذكاء الاصطناعي والتعلم الآلي
-scikit-learn==1.4.0
-transformers==4.39.3
-torch==2.1.2
-nltk==3.8.1
-gensim==4.3.2
-openai==1.69.0
-anthropic==0.5.0
-pydantic>=1.9.0,<2.0.0
-joblib==1.3.2
-
-# قواعد البيانات
-SQLAlchemy==2.0.25
-SQLAlchemy-Utils==0.41.1
-alembic==1.13.1
-sqlite-utils==3.35.1
-
-# مكونات واجهة المستخدم
-streamlit-option-menu==0.3.2
-streamlit-elements==0.1.0
-streamlit-aggrid==0.3.4.post3
-streamlit-authenticator==0.2.3
-streamlit-extras==0.3.5
-streamlit-echarts==0.4.0
-streamlit-image-coordinates==0.1.6
-
-# أدوات وتبعيات إضافية
-pycountry==23.12.11
-watchdog==3.0
-reportlab>=3.6.0
-pycountry==23.12.11
-watchdog==3.0
-folium==0.16.0
-streamlit-folium==0.18.0
-python-dotenv==1.0.0
-jsonschema==4.19.0
-pytest==7.4.0
-pytest-cov==4.1.0
-reportlab==4.0.8
-rouge-score==0.1.2
-python-Levenshtein==0.25.1
+streamlit>=1.32.0
+pandas>=2.2.0
+numpy>=1.26.3
+matplotlib>=3.8.2
+seaborn>=0.13.1
+plotly>=5.18.0
+PyPDF2>=3.0.1
+python-docx>=0.8.11
+anthropic>=0.5.0
+openai>=1.69.0
+requests>=2.31.0
+pillow>=10.0.0
+python-dotenv>=1.0.0
+matplotlib
+numpy
+pandas
+plotly
+seaborn
+streamlit
+openpyxl
+python-docx
+openpyxl
+pdfkit==1.0.0
\ No newline at end of file
diff --git a/styling/charts.py b/styling/charts.py
index 670998ca5acdc09de9d7b9d3f4f70602c3cb418a..241694f7fc748f87c868d77d88a68fbb15b207c8 100644
--- a/styling/charts.py
+++ b/styling/charts.py
@@ -1,282 +1,282 @@
-"""
-مولد الرسوم البيانية لنظام إدارة المناقصات
-"""
-
-import os
-import numpy as np
-import matplotlib.pyplot as plt
-import matplotlib as mpl
-from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
-import tkinter as tk
-import customtkinter as ctk
-
-class ChartGenerator:
- """فئة مولد الرسوم البيانية"""
-
- def __init__(self, theme):
- """تهيئة مولد الرسوم البيانية"""
- self.theme = theme
-
- # تحديد مسار مجلد الرسوم البيانية
- self.charts_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "charts")
-
- # إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
- os.makedirs(self.charts_dir, exist_ok=True)
-
- # تهيئة نمط الرسوم البيانية
- self._setup_chart_style()
-
- def _setup_chart_style(self):
- """إعداد نمط الرسوم البيانية"""
- # تعيين نمط الرسوم البيانية
- plt.style.use('ggplot')
-
- # تعيين الخط
- plt.rcParams['font.family'] = 'sans-serif'
- plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans', 'Liberation Sans', 'Bitstream Vera Sans', 'sans-serif']
-
- # تعيين حجم الخط
- plt.rcParams['font.size'] = 10
- plt.rcParams['axes.titlesize'] = 14
- plt.rcParams['axes.labelsize'] = 12
- plt.rcParams['xtick.labelsize'] = 10
- plt.rcParams['ytick.labelsize'] = 10
- plt.rcParams['legend.fontsize'] = 10
-
- # تعيين الألوان
- if self.theme.current_theme == "light":
- plt.rcParams['figure.facecolor'] = self.theme.LIGHT_CARD_BG_COLOR
- plt.rcParams['axes.facecolor'] = self.theme.LIGHT_BG_COLOR
- plt.rcParams['axes.edgecolor'] = self.theme.LIGHT_BORDER_COLOR
- plt.rcParams['axes.labelcolor'] = self.theme.LIGHT_FG_COLOR
- plt.rcParams['xtick.color'] = self.theme.LIGHT_FG_COLOR
- plt.rcParams['ytick.color'] = self.theme.LIGHT_FG_COLOR
- plt.rcParams['text.color'] = self.theme.LIGHT_FG_COLOR
- plt.rcParams['grid.color'] = self.theme.LIGHT_BORDER_COLOR
- else:
- plt.rcParams['figure.facecolor'] = self.theme.DARK_CARD_BG_COLOR
- plt.rcParams['axes.facecolor'] = self.theme.DARK_BG_COLOR
- plt.rcParams['axes.edgecolor'] = self.theme.DARK_BORDER_COLOR
- plt.rcParams['axes.labelcolor'] = self.theme.DARK_FG_COLOR
- plt.rcParams['xtick.color'] = self.theme.DARK_FG_COLOR
- plt.rcParams['ytick.color'] = self.theme.DARK_FG_COLOR
- plt.rcParams['text.color'] = self.theme.DARK_FG_COLOR
- plt.rcParams['grid.color'] = self.theme.DARK_BORDER_COLOR
-
- def create_bar_chart(self, data, title, xlabel, ylabel):
- """إنشاء رسم بياني شريطي"""
- # إنشاء الشكل والمحاور
- fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
-
- # رسم الرسم البياني الشريطي
- bars = ax.bar(data['labels'], data['values'], color=self.theme.PRIMARY_COLOR[self.theme.current_theme])
-
- # إضافة القيم فوق الأشرطة
- for bar in bars:
- height = bar.get_height()
- ax.text(bar.get_x() + bar.get_width() / 2., height + 0.1 * max(data['values']),
- f'{height:,.0f}', ha='center', va='bottom')
-
- # تعيين العنوان والتسميات
- ax.set_title(title)
- ax.set_xlabel(xlabel)
- ax.set_ylabel(ylabel)
-
- # تعيين حدود المحور y
- ax.set_ylim(0, max(data['values']) * 1.2)
-
- # إضافة الشبكة
- ax.grid(True, linestyle='--', alpha=0.7)
-
- # تضييق الشكل
- fig.tight_layout()
-
- return fig
-
- def create_line_chart(self, data, title, xlabel, ylabel):
- """إنشاء رسم بياني خطي"""
- # إنشاء الشكل والمحاور
- fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
-
- # رسم الرسم البياني الخطي
- line = ax.plot(data['labels'], data['values'], marker='o', linestyle='-', linewidth=2,
- color=self.theme.PRIMARY_COLOR[self.theme.current_theme],
- markersize=8, markerfacecolor=self.theme.SECONDARY_COLOR[self.theme.current_theme])
-
- # إضافة القيم فوق النقاط
- for i, value in enumerate(data['values']):
- ax.text(i, value + 0.05 * max(data['values']), f'{value:,.0f}', ha='center', va='bottom')
-
- # تعيين العنوان والتسميات
- ax.set_title(title)
- ax.set_xlabel(xlabel)
- ax.set_ylabel(ylabel)
-
- # تعيين حدود المحور y
- ax.set_ylim(0, max(data['values']) * 1.2)
-
- # إضافة الشبكة
- ax.grid(True, linestyle='--', alpha=0.7)
-
- # تضييق الشكل
- fig.tight_layout()
-
- return fig
-
- def create_pie_chart(self, data, title):
- """إنشاء رسم بياني دائري"""
- # إنشاء الشكل والمحاور
- fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
-
- # تعيين الألوان
- colors = [
- self.theme.PRIMARY_COLOR[self.theme.current_theme],
- self.theme.SECONDARY_COLOR[self.theme.current_theme],
- self.theme.ACCENT_COLOR[self.theme.current_theme],
- self.theme.WARNING_COLOR[self.theme.current_theme],
- self.theme.SUCCESS_COLOR[self.theme.current_theme]
- ]
-
- # رسم الرسم البياني الدائري
- wedges, texts, autotexts = ax.pie(
- data['values'],
- labels=data['labels'],
- autopct='%1.1f%%',
- startangle=90,
- colors=colors,
- wedgeprops={'edgecolor': 'white', 'linewidth': 1},
- textprops={'color': self.theme.get_color('fg_color')}
- )
-
- # تعيين خصائص النص
- for autotext in autotexts:
- autotext.set_color('white')
- autotext.set_fontweight('bold')
-
- # تعيين العنوان
- ax.set_title(title)
-
- # جعل الرسم البياني دائريًا
- ax.axis('equal')
-
- # تضييق الشكل
- fig.tight_layout()
-
- return fig
-
- def create_stacked_bar_chart(self, data, title, xlabel, ylabel):
- """إنشاء رسم بياني شريطي متراكم"""
- # إنشاء الشكل والمحاور
- fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
-
- # تعيين الألوان
- colors = [
- self.theme.PRIMARY_COLOR[self.theme.current_theme],
- self.theme.SECONDARY_COLOR[self.theme.current_theme],
- self.theme.ACCENT_COLOR[self.theme.current_theme],
- self.theme.WARNING_COLOR[self.theme.current_theme],
- self.theme.SUCCESS_COLOR[self.theme.current_theme]
- ]
-
- # رسم الرسم البياني الشريطي المتراكم
- bottom = np.zeros(len(data['labels']))
- for i, category in enumerate(data['categories']):
- values = data['values'][i]
- bars = ax.bar(data['labels'], values, bottom=bottom, label=category, color=colors[i % len(colors)])
- bottom += values
-
- # تعيين العنوان والتسميات
- ax.set_title(title)
- ax.set_xlabel(xlabel)
- ax.set_ylabel(ylabel)
-
- # إضافة وسيلة إيضاح
- ax.legend()
-
- # إضافة الشبكة
- ax.grid(True, linestyle='--', alpha=0.7)
-
- # تضييق الشكل
- fig.tight_layout()
-
- return fig
-
- def create_risk_matrix(self, data, title):
- """إنشاء مصفوفة المخاطر"""
- # إنشاء الشكل والمحاور
- fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
-
- # تعيين الألوان
- colors = {
- 'منخفض': self.theme.SUCCESS_COLOR[self.theme.current_theme],
- 'متوسط': self.theme.WARNING_COLOR[self.theme.current_theme],
- 'عالي': self.theme.ERROR_COLOR[self.theme.current_theme]
- }
-
- # تعيين قيم المحاور
- probability_values = {'منخفض': 1, 'متوسط': 2, 'عالي': 3}
- impact_values = {'منخفض': 1, 'متوسط': 2, 'عالي': 3}
-
- # رسم المصفوفة
- for risk in data['risks']:
- prob = probability_values[risk['probability']]
- impact = impact_values[risk['impact']]
- color = colors[risk['probability']] if prob > impact else colors[risk['impact']]
- ax.scatter(impact, prob, color=color, s=100, alpha=0.7)
- ax.annotate(risk['name'], (impact, prob), xytext=(5, 5), textcoords='offset points')
-
- # تعيين حدود المحاور
- ax.set_xlim(0.5, 3.5)
- ax.set_ylim(0.5, 3.5)
-
- # تعيين تسميات المحاور
- ax.set_xticks([1, 2, 3])
- ax.set_xticklabels(['منخفض', 'متوسط', 'عالي'])
- ax.set_yticks([1, 2, 3])
- ax.set_yticklabels(['منخفض', 'متوسط', 'عالي'])
-
- # تعيين العنوان والتسميات
- ax.set_title(title)
- ax.set_xlabel('التأثير')
- ax.set_ylabel('الاحتمالية')
-
- # إضافة الشبكة
- ax.grid(True, linestyle='--', alpha=0.7)
-
- # إضافة مناطق المخاطر
- # منطقة المخاطر المنخفضة (أخضر)
- ax.add_patch(plt.Rectangle((0.5, 0.5), 1, 1, fill=True, color=self.theme.SUCCESS_COLOR[self.theme.current_theme], alpha=0.1))
- # منطقة المخاطر المتوسطة (أصفر)
- ax.add_patch(plt.Rectangle((1.5, 0.5), 1, 1, fill=True, color=self.theme.WARNING_COLOR[self.theme.current_theme], alpha=0.1))
- ax.add_patch(plt.Rectangle((0.5, 1.5), 1, 1, fill=True, color=self.theme.WARNING_COLOR[self.theme.current_theme], alpha=0.1))
- # منطقة المخاطر العالية (أحمر)
- ax.add_patch(plt.Rectangle((2.5, 0.5), 1, 3, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
- ax.add_patch(plt.Rectangle((0.5, 2.5), 2, 1, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
- ax.add_patch(plt.Rectangle((1.5, 1.5), 1, 1, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
-
- # تضييق الشكل
- fig.tight_layout()
-
- return fig
-
- def embed_chart_in_frame(self, parent, fig):
- """تضمين الرسم البياني في إطار"""
- # إنشاء إطار للرسم البياني
- chart_frame = ctk.CTkFrame(parent, fg_color="transparent")
-
- # تضمين الرسم البياني في الإطار
- canvas = FigureCanvasTkAgg(fig, master=chart_frame)
- canvas.draw()
- canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
-
- return chart_frame
-
- def save_chart(self, fig, name):
- """حفظ الرسم البياني"""
- # تحديد مسار الملف
- file_path = os.path.join(self.charts_dir, f"{name}.png")
-
- # حفظ الرسم البياني
- fig.savefig(file_path, dpi=100, bbox_inches='tight')
-
- return file_path
+"""
+مولد الرسوم البيانية لنظام إدارة المناقصات
+"""
+
+import os
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib as mpl
+from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
+import tkinter as tk
+import customtkinter as ctk
+
+class ChartGenerator:
+ """فئة مولد الرسوم البيانية"""
+
+ def __init__(self, theme):
+ """تهيئة مولد الرسوم البيانية"""
+ self.theme = theme
+
+ # تحديد مسار مجلد الرسوم البيانية
+ self.charts_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "charts")
+
+ # إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
+ os.makedirs(self.charts_dir, exist_ok=True)
+
+ # تهيئة نمط الرسوم البيانية
+ self._setup_chart_style()
+
+ def _setup_chart_style(self):
+ """إعداد نمط الرسوم البيانية"""
+ # تعيين نمط الرسوم البيانية
+ plt.style.use('ggplot')
+
+ # تعيين الخط
+ plt.rcParams['font.family'] = 'sans-serif'
+ plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans', 'Liberation Sans', 'Bitstream Vera Sans', 'sans-serif']
+
+ # تعيين حجم الخط
+ plt.rcParams['font.size'] = 10
+ plt.rcParams['axes.titlesize'] = 14
+ plt.rcParams['axes.labelsize'] = 12
+ plt.rcParams['xtick.labelsize'] = 10
+ plt.rcParams['ytick.labelsize'] = 10
+ plt.rcParams['legend.fontsize'] = 10
+
+ # تعيين الألوان
+ if self.theme.current_theme == "light":
+ plt.rcParams['figure.facecolor'] = self.theme.LIGHT_CARD_BG_COLOR
+ plt.rcParams['axes.facecolor'] = self.theme.LIGHT_BG_COLOR
+ plt.rcParams['axes.edgecolor'] = self.theme.LIGHT_BORDER_COLOR
+ plt.rcParams['axes.labelcolor'] = self.theme.LIGHT_FG_COLOR
+ plt.rcParams['xtick.color'] = self.theme.LIGHT_FG_COLOR
+ plt.rcParams['ytick.color'] = self.theme.LIGHT_FG_COLOR
+ plt.rcParams['text.color'] = self.theme.LIGHT_FG_COLOR
+ plt.rcParams['grid.color'] = self.theme.LIGHT_BORDER_COLOR
+ else:
+ plt.rcParams['figure.facecolor'] = self.theme.DARK_CARD_BG_COLOR
+ plt.rcParams['axes.facecolor'] = self.theme.DARK_BG_COLOR
+ plt.rcParams['axes.edgecolor'] = self.theme.DARK_BORDER_COLOR
+ plt.rcParams['axes.labelcolor'] = self.theme.DARK_FG_COLOR
+ plt.rcParams['xtick.color'] = self.theme.DARK_FG_COLOR
+ plt.rcParams['ytick.color'] = self.theme.DARK_FG_COLOR
+ plt.rcParams['text.color'] = self.theme.DARK_FG_COLOR
+ plt.rcParams['grid.color'] = self.theme.DARK_BORDER_COLOR
+
+ def create_bar_chart(self, data, title, xlabel, ylabel):
+ """إنشاء رسم بياني شريطي"""
+ # إنشاء الشكل والمحاور
+ fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
+
+ # رسم الرسم البياني الشريطي
+ bars = ax.bar(data['labels'], data['values'], color=self.theme.PRIMARY_COLOR[self.theme.current_theme])
+
+ # إضافة القيم فوق الأشرطة
+ for bar in bars:
+ height = bar.get_height()
+ ax.text(bar.get_x() + bar.get_width() / 2., height + 0.1 * max(data['values']),
+ f'{height:,.0f}', ha='center', va='bottom')
+
+ # تعيين العنوان والتسميات
+ ax.set_title(title)
+ ax.set_xlabel(xlabel)
+ ax.set_ylabel(ylabel)
+
+ # تعيين حدود المحور y
+ ax.set_ylim(0, max(data['values']) * 1.2)
+
+ # إضافة الشبكة
+ ax.grid(True, linestyle='--', alpha=0.7)
+
+ # تضييق الشكل
+ fig.tight_layout()
+
+ return fig
+
+ def create_line_chart(self, data, title, xlabel, ylabel):
+ """إنشاء رسم بياني خطي"""
+ # إنشاء الشكل والمحاور
+ fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
+
+ # رسم الرسم البياني الخطي
+ line = ax.plot(data['labels'], data['values'], marker='o', linestyle='-', linewidth=2,
+ color=self.theme.PRIMARY_COLOR[self.theme.current_theme],
+ markersize=8, markerfacecolor=self.theme.SECONDARY_COLOR[self.theme.current_theme])
+
+ # إضافة القيم فوق النقاط
+ for i, value in enumerate(data['values']):
+ ax.text(i, value + 0.05 * max(data['values']), f'{value:,.0f}', ha='center', va='bottom')
+
+ # تعيين العنوان والتسميات
+ ax.set_title(title)
+ ax.set_xlabel(xlabel)
+ ax.set_ylabel(ylabel)
+
+ # تعيين حدود المحور y
+ ax.set_ylim(0, max(data['values']) * 1.2)
+
+ # إضافة الشبكة
+ ax.grid(True, linestyle='--', alpha=0.7)
+
+ # تضييق الشكل
+ fig.tight_layout()
+
+ return fig
+
+ def create_pie_chart(self, data, title):
+ """إنشاء رسم بياني دائري"""
+ # إنشاء الشكل والمحاور
+ fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
+
+ # تعيين الألوان
+ colors = [
+ self.theme.PRIMARY_COLOR[self.theme.current_theme],
+ self.theme.SECONDARY_COLOR[self.theme.current_theme],
+ self.theme.ACCENT_COLOR[self.theme.current_theme],
+ self.theme.WARNING_COLOR[self.theme.current_theme],
+ self.theme.SUCCESS_COLOR[self.theme.current_theme]
+ ]
+
+ # رسم الرسم البياني الدائري
+ wedges, texts, autotexts = ax.pie(
+ data['values'],
+ labels=data['labels'],
+ autopct='%1.1f%%',
+ startangle=90,
+ colors=colors,
+ wedgeprops={'edgecolor': 'white', 'linewidth': 1},
+ textprops={'color': self.theme.get_color('fg_color')}
+ )
+
+ # تعيين خصائص النص
+ for autotext in autotexts:
+ autotext.set_color('white')
+ autotext.set_fontweight('bold')
+
+ # تعيين العنوان
+ ax.set_title(title)
+
+ # جعل الرسم البياني دائريًا
+ ax.axis('equal')
+
+ # تضييق الشكل
+ fig.tight_layout()
+
+ return fig
+
+ def create_stacked_bar_chart(self, data, title, xlabel, ylabel):
+ """إنشاء رسم بياني شريطي متراكم"""
+ # إنشاء الشكل والمحاور
+ fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
+
+ # تعيين الألوان
+ colors = [
+ self.theme.PRIMARY_COLOR[self.theme.current_theme],
+ self.theme.SECONDARY_COLOR[self.theme.current_theme],
+ self.theme.ACCENT_COLOR[self.theme.current_theme],
+ self.theme.WARNING_COLOR[self.theme.current_theme],
+ self.theme.SUCCESS_COLOR[self.theme.current_theme]
+ ]
+
+ # رسم الرسم البياني الشريطي المتراكم
+ bottom = np.zeros(len(data['labels']))
+ for i, category in enumerate(data['categories']):
+ values = data['values'][i]
+ bars = ax.bar(data['labels'], values, bottom=bottom, label=category, color=colors[i % len(colors)])
+ bottom += values
+
+ # تعيين العنوان والتسميات
+ ax.set_title(title)
+ ax.set_xlabel(xlabel)
+ ax.set_ylabel(ylabel)
+
+ # إضافة وسيلة إيضاح
+ ax.legend()
+
+ # إضافة الشبكة
+ ax.grid(True, linestyle='--', alpha=0.7)
+
+ # تضييق الشكل
+ fig.tight_layout()
+
+ return fig
+
+ def create_risk_matrix(self, data, title):
+ """إنشاء مصفوفة المخاطر"""
+ # إنشاء الشكل والمحاور
+ fig, ax = plt.subplots(figsize=(8, 5), dpi=100)
+
+ # تعيين الألوان
+ colors = {
+ 'منخفض': self.theme.SUCCESS_COLOR[self.theme.current_theme],
+ 'متوسط': self.theme.WARNING_COLOR[self.theme.current_theme],
+ 'عالي': self.theme.ERROR_COLOR[self.theme.current_theme]
+ }
+
+ # تعيين قيم المحاور
+ probability_values = {'منخفض': 1, 'متوسط': 2, 'عالي': 3}
+ impact_values = {'منخفض': 1, 'متوسط': 2, 'عالي': 3}
+
+ # رسم المصفوفة
+ for risk in data['risks']:
+ prob = probability_values[risk['probability']]
+ impact = impact_values[risk['impact']]
+ color = colors[risk['probability']] if prob > impact else colors[risk['impact']]
+ ax.scatter(impact, prob, color=color, s=100, alpha=0.7)
+ ax.annotate(risk['name'], (impact, prob), xytext=(5, 5), textcoords='offset points')
+
+ # تعيين حدود المحاور
+ ax.set_xlim(0.5, 3.5)
+ ax.set_ylim(0.5, 3.5)
+
+ # تعيين تسميات المحاور
+ ax.set_xticks([1, 2, 3])
+ ax.set_xticklabels(['منخفض', 'متوسط', 'عالي'])
+ ax.set_yticks([1, 2, 3])
+ ax.set_yticklabels(['منخفض', 'متوسط', 'عالي'])
+
+ # تعيين العنوان والتسميات
+ ax.set_title(title)
+ ax.set_xlabel('التأثير')
+ ax.set_ylabel('الاحتمالية')
+
+ # إضافة الشبكة
+ ax.grid(True, linestyle='--', alpha=0.7)
+
+ # إضافة مناطق المخاطر
+ # منطقة المخاطر المنخفضة (أخضر)
+ ax.add_patch(plt.Rectangle((0.5, 0.5), 1, 1, fill=True, color=self.theme.SUCCESS_COLOR[self.theme.current_theme], alpha=0.1))
+ # منطقة المخاطر المتوسطة (أصفر)
+ ax.add_patch(plt.Rectangle((1.5, 0.5), 1, 1, fill=True, color=self.theme.WARNING_COLOR[self.theme.current_theme], alpha=0.1))
+ ax.add_patch(plt.Rectangle((0.5, 1.5), 1, 1, fill=True, color=self.theme.WARNING_COLOR[self.theme.current_theme], alpha=0.1))
+ # منطقة المخاطر العالية (أحمر)
+ ax.add_patch(plt.Rectangle((2.5, 0.5), 1, 3, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
+ ax.add_patch(plt.Rectangle((0.5, 2.5), 2, 1, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
+ ax.add_patch(plt.Rectangle((1.5, 1.5), 1, 1, fill=True, color=self.theme.ERROR_COLOR[self.theme.current_theme], alpha=0.1))
+
+ # تضييق الشكل
+ fig.tight_layout()
+
+ return fig
+
+ def embed_chart_in_frame(self, parent, fig):
+ """تضمين الرسم البياني في إطار"""
+ # إنشاء إطار للرسم البياني
+ chart_frame = ctk.CTkFrame(parent, fg_color="transparent")
+
+ # تضمين الرسم البياني في الإطار
+ canvas = FigureCanvasTkAgg(fig, master=chart_frame)
+ canvas.draw()
+ canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
+
+ return chart_frame
+
+ def save_chart(self, fig, name):
+ """حفظ الرسم البياني"""
+ # تحديد مسار الملف
+ file_path = os.path.join(self.charts_dir, f"{name}.png")
+
+ # حفظ الرسم البياني
+ fig.savefig(file_path, dpi=100, bbox_inches='tight')
+
+ return file_path
diff --git a/styling/enhanced_ui.py b/styling/enhanced_ui.py
index 1a19e53b49aedaab8e13cbb04ac77faa1b8162b4..6bae0b23a0fae35e7635c3b42e212478bdc3cb0f 100644
--- a/styling/enhanced_ui.py
+++ b/styling/enhanced_ui.py
@@ -1,582 +1,582 @@
-"""
-محسن واجهة المستخدم - نظام تحليل المناقصات
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import base64
-from pathlib import Path
-import os
-
-class UIEnhancer:
- """فئة لتحسين واجهة المستخدم وتوحيد التصميم عبر النظام"""
-
- # ألوان النظام
- COLORS = {
- 'primary': '#1E88E5', # أزرق
- 'secondary': '#5E35B1', # بنفسجي
- 'success': '#43A047', # أخضر
- 'warning': '#FB8C00', # برتقالي
- 'danger': '#E53935', # أحمر
- 'info': '#00ACC1', # سماوي
- 'light': '#F5F5F5', # رمادي فاتح
- 'dark': '#212121', # رمادي داكن
- 'accent': '#FF4081', # وردي
- 'background': '#FFFFFF', # أبيض
- 'text': '#212121', # أسود
- 'border': '#E0E0E0' # رمادي حدود
- }
-
- # أحجام الخطوط
- FONT_SIZES = {
- 'xs': '0.75rem',
- 'sm': '0.875rem',
- 'md': '1rem',
- 'lg': '1.125rem',
- 'xl': '1.25rem',
- '2xl': '1.5rem',
- '3xl': '1.875rem',
- '4xl': '2.25rem',
- '5xl': '3rem'
- }
-
- def __init__(self, page_title="نظام تحليل المناقصات", page_icon="📊"):
- """تهيئة محسن واجهة المستخدم"""
- self.page_title = page_title
- self.page_icon = page_icon
- self.theme_mode = "light" # الوضع الافتراضي هو الوضع الفاتح
-
- # تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
- if 'theme' not in st.session_state:
- st.session_state.theme = 'light'
-
- def apply_global_styles(self):
- """تطبيق التنسيقات العامة على الصفحة"""
- # تعريف CSS العام
- css = f"""
- @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700&display=swap');
-
- * {{
- font-family: 'Tajawal', sans-serif;
- direction: rtl;
- }}
-
- h1, h2, h3, h4, h5, h6 {{
- font-family: 'Tajawal', sans-serif;
- font-weight: 700;
- color: {self.COLORS['dark']};
- }}
-
- .module-title {{
- color: {self.COLORS['primary']};
- font-size: {self.FONT_SIZES['3xl']};
- margin-bottom: 1rem;
- border-bottom: 2px solid {self.COLORS['primary']};
- padding-bottom: 0.5rem;
- }}
-
- .stTabs [data-baseweb="tab-list"] {{
- gap: 2px;
- }}
-
- .stTabs [data-baseweb="tab"] {{
- height: 50px;
- white-space: pre-wrap;
- background-color: {self.COLORS['light']};
- border-radius: 4px 4px 0 0;
- gap: 1px;
- padding-top: 10px;
- padding-bottom: 10px;
- }}
-
- .stTabs [aria-selected="true"] {{
- background-color: {self.COLORS['primary']};
- color: white;
- }}
-
- div[data-testid="stSidebarNav"] li div a span {{
- direction: rtl;
- text-align: right;
- font-family: 'Tajawal', sans-serif;
- }}
-
- div[data-testid="stSidebarNav"] {{
- background-color: {self.COLORS['light']};
- }}
-
- div[data-testid="stSidebarNav"] li div {{
- margin-right: 0;
- margin-left: auto;
- }}
-
- div[data-testid="stSidebarNav"] li div a {{
- padding-right: 10px;
- padding-left: 0;
- }}
-
- div[data-testid="stSidebarNav"] li div a:hover {{
- background-color: {self.COLORS['primary'] + '20'};
- }}
-
- div[data-testid="stSidebarNav"] li div[aria-selected="true"] {{
- background-color: {self.COLORS['primary'] + '40'};
- }}
-
- div[data-testid="stSidebarNav"] li div[aria-selected="true"] a span {{
- color: {self.COLORS['primary']};
- font-weight: 500;
- }}
-
- .metric-card {{
- background-color: white;
- border-radius: 10px;
- padding: 20px;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
- text-align: center;
- transition: transform 0.3s ease;
- }}
-
- .metric-card:hover {{
- transform: translateY(-5px);
- }}
-
- .metric-value {{
- font-size: 2.5rem;
- font-weight: 700;
- margin: 10px 0;
- }}
-
- .metric-label {{
- font-size: 1rem;
- color: #666;
- }}
-
- .metric-change {{
- font-size: 0.9rem;
- margin-top: 5px;
- }}
-
- .metric-change-positive {{
- color: {self.COLORS['success']};
- }}
-
- .metric-change-negative {{
- color: {self.COLORS['danger']};
- }}
-
- .custom-button {{
- background-color: {self.COLORS['primary']};
- color: white;
- border: none;
- border-radius: 5px;
- padding: 10px 20px;
- font-size: 1rem;
- cursor: pointer;
- transition: background-color 0.3s ease;
- }}
-
- .custom-button:hover {{
- background-color: {self.COLORS['secondary']};
- }}
-
- .custom-card {{
- background-color: white;
- border-radius: 10px;
- padding: 20px;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
- margin-bottom: 20px;
- }}
-
- .header-container {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 2rem;
- padding-bottom: 1rem;
- border-bottom: 1px solid {self.COLORS['border']};
- }}
-
- .header-title {{
- color: {self.COLORS['primary']};
- font-size: {self.FONT_SIZES['3xl']};
- margin: 0;
- }}
-
- .header-subtitle {{
- color: {self.COLORS['dark']};
- font-size: {self.FONT_SIZES['lg']};
- margin: 0;
- }}
-
- .header-actions {{
- display: flex;
- gap: 10px;
- }}
-
- /* تنسيق الجداول */
- div[data-testid="stTable"] table {{
- width: 100%;
- border-collapse: collapse;
- }}
-
- div[data-testid="stTable"] thead tr th {{
- background-color: {self.COLORS['primary']};
- color: white;
- text-align: right;
- padding: 12px;
- }}
-
- div[data-testid="stTable"] tbody tr:nth-child(even) {{
- background-color: {self.COLORS['light']};
- }}
-
- div[data-testid="stTable"] tbody tr:hover {{
- background-color: {self.COLORS['primary'] + '10'};
- }}
-
- div[data-testid="stTable"] tbody tr td {{
- padding: 10px;
- text-align: right;
- }}
-
- /* تنسيق النماذج */
- div[data-testid="stForm"] {{
- background-color: white;
- border-radius: 10px;
- padding: 20px;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
- }}
-
- button[kind="primaryFormSubmit"] {{
- background-color: {self.COLORS['primary']};
- color: white;
- }}
-
- button[kind="secondaryFormSubmit"] {{
- background-color: {self.COLORS['light']};
- color: {self.COLORS['dark']};
- border: 1px solid {self.COLORS['border']};
- }}
-
- /* تنسيق الرسوم البيانية */
- div[data-testid="stVegaLiteChart"] {{
- background-color: white;
- border-radius: 10px;
- padding: 20px;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
- }}
- """
-
- # تطبيق CSS
- st.markdown(f'', unsafe_allow_html=True)
-
- def apply_theme_colors(self):
- """تطبيق ألوان السمة الحالية"""
- # تحديد ألوان السمة بناءً على الوضع
- if self.theme_mode == "dark":
- self.COLORS['background'] = '#121212'
- self.COLORS['text'] = '#FFFFFF'
- self.COLORS['border'] = '#333333'
- else:
- self.COLORS['background'] = '#FFFFFF'
- self.COLORS['text'] = '#212121'
- self.COLORS['border'] = '#E0E0E0'
-
- # تطبيق CSS للسمة
- theme_css = f"""
- body {{
- background-color: {self.COLORS['background']};
- color: {self.COLORS['text']};
- }}
- """
-
- st.markdown(f'', unsafe_allow_html=True)
-
- def toggle_theme(self):
- """تبديل وضع السمة بين الفاتح والداكن"""
- if self.theme_mode == "light":
- self.theme_mode = "dark"
- else:
- self.theme_mode = "light"
-
- self.apply_theme_colors()
-
- def create_sidebar(self, menu_items):
- """إنشاء الشريط الجانبي مع قائمة العناصر"""
- with st.sidebar:
- # إضافة الشعار
- st.markdown(
- f"""
-
-
{self.page_icon} {self.page_title}
-
- """,
- unsafe_allow_html=True
- )
-
- # إضافة معلومات المستخدم
- st.markdown(
- f"""
-
-
- م
-
-
مهندس تامر الجوهري
-
مدير المشاريع
-
- """,
- unsafe_allow_html=True
- )
-
- st.divider()
-
- # إنشاء القائمة
- selected = st.radio(
- "القائمة الرئيسية",
- [item["name"] for item in menu_items],
- format_func=lambda x: x,
- label_visibility="collapsed"
- )
-
- st.divider()
-
- # إضافة معلومات النظام
- st.markdown(
- """
-
-
نظام تحليل المناقصات | الإصدار 2.0.0
-
© 2025 جميع الحقوق محفوظة
-
- """,
- unsafe_allow_html=True
- )
-
- return selected
-
- def create_header(self, title, subtitle=None, show_actions=True):
- """إنشاء ترويسة الصفحة"""
- # إنشاء معرفات فريدة للأزرار
- add_button_key = f"add_button_{title}"
- update_button_key = f"update_button_{title}"
-
- col1, col2 = st.columns([3, 1])
-
- with col1:
- st.markdown(f'', unsafe_allow_html=True)
- if subtitle:
- st.markdown(f'', unsafe_allow_html=True)
-
- if show_actions:
- with col2:
- col2_1, col2_2 = st.columns(2)
- with col2_1:
- st.button("إضافة جديد", key=add_button_key)
- with col2_2:
- st.button("تحديث", key=update_button_key)
-
- st.divider()
-
- def create_metric_card(self, label, value, change=None, color=None):
- """إنشاء بطاقة مقياس"""
- if color is None:
- color = self.COLORS['primary']
-
- change_html = ""
- if change is not None:
- if change.startswith("+"):
- change_class = "metric-change-positive"
- change_icon = "↑"
- elif change.startswith("-"):
- change_class = "metric-change-negative"
- change_icon = "↓"
- else:
- change_class = ""
- change_icon = ""
-
- change_html = f'{change_icon} {change}
'
-
- st.markdown(
- f"""
-
-
{label}
-
{value}
- {change_html}
-
- """,
- unsafe_allow_html=True
- )
-
- def create_card(self, title, content, color=None):
- """إنشاء بطاقة عامة"""
- if color is None:
- color = self.COLORS['primary']
-
- st.markdown(
- f"""
-
- """,
- unsafe_allow_html=True
- )
-
- def create_button(self, label, color=None, icon=None, key=None):
- """إنشاء زر مخصص"""
- if color is None:
- color = self.COLORS['primary']
-
- # إنشاء معرف فريد للزر إذا لم يتم توفيره
- if key is None:
- key = f"button_{label}_{hash(label)}"
-
- icon_html = f"{icon} " if icon else ""
-
- return st.button(
- f"{icon_html}{label}",
- key=key
- )
-
- def create_tabs(self, tab_names):
- """إنشاء تبويبات"""
- return st.tabs(tab_names)
-
- def create_expander(self, title, expanded=False, key=None):
- """إنشاء عنصر قابل للتوسيع"""
- # إنشاء معرف فريد للعنصر إذا لم يتم توفيره
- if key is None:
- key = f"expander_{title}_{hash(title)}"
-
- return st.expander(title, expanded=expanded, key=key)
-
- def create_data_table(self, data, use_container_width=True, hide_index=True):
- """إنشاء جدول بيانات"""
- return st.dataframe(data, use_container_width=use_container_width, hide_index=hide_index)
-
- def create_chart(self, chart_type, data, **kwargs):
- """إنشاء رسم بياني"""
- if chart_type == "bar":
- return st.bar_chart(data, **kwargs)
- elif chart_type == "line":
- return st.line_chart(data, **kwargs)
- elif chart_type == "area":
- return st.area_chart(data, **kwargs)
- else:
- return st.bar_chart(data, **kwargs)
-
- def create_form(self, title, key=None):
- """إنشاء نموذج"""
- # إنشاء معرف فريد للنموذج إذا لم يتم توفيره
- if key is None:
- key = f"form_{title}_{hash(title)}"
-
- return st.form(key=key)
-
- def create_file_uploader(self, label, types=None, key=None):
- """إنشاء أداة رفع الملفات"""
- # إنشاء معرف فريد لأداة رفع الملفات إذا لم يتم توفيره
- if key is None:
- key = f"file_uploader_{label}_{hash(label)}"
-
- return st.file_uploader(label, type=types, key=key)
-
- def create_date_input(self, label, value=None, key=None):
- """إنشاء حقل إدخال تاريخ"""
- # إنشاء معرف فريد لحقل إدخال التاريخ إذا لم يتم توفيره
- if key is None:
- key = f"date_input_{label}_{hash(label)}"
-
- return st.date_input(label, value=value, key=key)
-
- def create_select_box(self, label, options, index=0, key=None):
- """إنشاء قائمة منسدلة"""
- # إنشاء معرف فريد للقائمة المنسدلة إذا لم يتم توفيره
- if key is None:
- key = f"select_box_{label}_{hash(label)}"
-
- return st.selectbox(label, options, index=index, key=key)
-
- def create_multi_select(self, label, options, default=None, key=None):
- """إنشاء قائمة اختيار متعدد"""
- # إنشاء معرف فريد لقائمة الاختيار المتعدد إذا لم يتم توفيره
- if key is None:
- key = f"multi_select_{label}_{hash(label)}"
-
- return st.multiselect(label, options, default=default, key=key)
-
- def create_slider(self, label, min_value, max_value, value=None, step=1, key=None):
- """إنشاء شريط تمرير"""
- # إنشاء معرف فريد لشريط التمرير إذا لم يتم توفيره
- if key is None:
- key = f"slider_{label}_{hash(label)}"
-
- return st.slider(label, min_value=min_value, max_value=max_value, value=value, step=step, key=key)
-
- def create_text_input(self, label, value="", key=None):
- """إنشاء حقل إدخال نصي"""
- # إنشاء معرف فريد لحقل الإدخال النصي إذا لم يتم توفيره
- if key is None:
- key = f"text_input_{label}_{hash(label)}"
-
- return st.text_input(label, value=value, key=key)
-
- def create_text_area(self, label, value="", height=None, key=None):
- """إنشاء منطقة نص"""
- # إنشاء معرف فريد لمنطقة النص إذا لم يتم توفيره
- if key is None:
- key = f"text_area_{label}_{hash(label)}"
-
- return st.text_area(label, value=value, height=height, key=key)
-
- def create_number_input(self, label, min_value=None, max_value=None, value=0, step=1, key=None):
- """إنشاء حقل إدخال رقمي"""
- # إنشاء معرف فريد لحقل الإدخال الرقمي إذا لم يتم توفيره
- if key is None:
- key = f"number_input_{label}_{hash(label)}"
-
- return st.number_input(label, min_value=min_value, max_value=max_value, value=value, step=step, key=key)
-
- def create_checkbox(self, label, value=False, key=None):
- """إنشاء خانة اختيار"""
- # إنشاء معرف فريد لخانة الاختيار إذا لم يتم توفيره
- if key is None:
- key = f"checkbox_{label}_{hash(label)}"
-
- return st.checkbox(label, value=value, key=key)
-
- def create_radio(self, label, options, index=0, key=None):
- """إنشاء أزرار راديو"""
- # إنشاء معرف فريد لأزرار الراديو إذا لم يتم توفيره
- if key is None:
- key = f"radio_{label}_{hash(label)}"
-
- return st.radio(label, options, index=index, key=key)
-
- def create_progress_bar(self, value, key=None):
- """إنشاء شريط تقدم"""
- # إنشاء معرف فريد لشريط التقدم إذا لم يتم توفيره
- if key is None:
- key = f"progress_bar_{value}_{hash(str(value))}"
-
- return st.progress(value, key=key)
-
- def create_spinner(self, text="جاري التحميل..."):
- """إنشاء مؤشر تحميل"""
- return st.spinner(text)
-
- def create_success_message(self, message):
- """إنشاء رسالة نجاح"""
- return st.success(message)
-
- def create_error_message(self, message):
- """إنشاء رسالة خطأ"""
- return st.error(message)
-
- def create_warning_message(self, message):
- """إنشاء رسالة تحذير"""
- return st.warning(message)
-
- def create_info_message(self, message):
- """إنشاء رسالة معلومات"""
- return st.info(message)
+"""
+محسن واجهة المستخدم - نظام تحليل المناقصات
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import base64
+from pathlib import Path
+import os
+
+class UIEnhancer:
+ """فئة لتحسين واجهة المستخدم وتوحيد التصميم عبر النظام"""
+
+ # ألوان النظام
+ COLORS = {
+ 'primary': '#1E88E5', # أزرق
+ 'secondary': '#5E35B1', # بنفسجي
+ 'success': '#43A047', # أخضر
+ 'warning': '#FB8C00', # برتقالي
+ 'danger': '#E53935', # أحمر
+ 'info': '#00ACC1', # سماوي
+ 'light': '#F5F5F5', # رمادي فاتح
+ 'dark': '#212121', # رمادي داكن
+ 'accent': '#FF4081', # وردي
+ 'background': '#FFFFFF', # أبيض
+ 'text': '#212121', # أسود
+ 'border': '#E0E0E0' # رمادي حدود
+ }
+
+ # أحجام الخطوط
+ FONT_SIZES = {
+ 'xs': '0.75rem',
+ 'sm': '0.875rem',
+ 'md': '1rem',
+ 'lg': '1.125rem',
+ 'xl': '1.25rem',
+ '2xl': '1.5rem',
+ '3xl': '1.875rem',
+ '4xl': '2.25rem',
+ '5xl': '3rem'
+ }
+
+ def __init__(self, page_title="نظام تحليل المناقصات", page_icon="📊"):
+ """تهيئة محسن واجهة المستخدم"""
+ self.page_title = page_title
+ self.page_icon = page_icon
+ self.theme_mode = "light" # الوضع الافتراضي هو الوضع الفاتح
+
+ # تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
+ if 'theme' not in st.session_state:
+ st.session_state.theme = 'light'
+
+ def apply_global_styles(self):
+ """تطبيق التنسيقات العامة على الصفحة"""
+ # تعريف CSS العام
+ css = f"""
+ @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700&display=swap');
+
+ * {{
+ font-family: 'Tajawal', sans-serif;
+ direction: rtl;
+ }}
+
+ h1, h2, h3, h4, h5, h6 {{
+ font-family: 'Tajawal', sans-serif;
+ font-weight: 700;
+ color: {self.COLORS['dark']};
+ }}
+
+ .module-title {{
+ color: {self.COLORS['primary']};
+ font-size: {self.FONT_SIZES['3xl']};
+ margin-bottom: 1rem;
+ border-bottom: 2px solid {self.COLORS['primary']};
+ padding-bottom: 0.5rem;
+ }}
+
+ .stTabs [data-baseweb="tab-list"] {{
+ gap: 2px;
+ }}
+
+ .stTabs [data-baseweb="tab"] {{
+ height: 50px;
+ white-space: pre-wrap;
+ background-color: {self.COLORS['light']};
+ border-radius: 4px 4px 0 0;
+ gap: 1px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ }}
+
+ .stTabs [aria-selected="true"] {{
+ background-color: {self.COLORS['primary']};
+ color: white;
+ }}
+
+ div[data-testid="stSidebarNav"] li div a span {{
+ direction: rtl;
+ text-align: right;
+ font-family: 'Tajawal', sans-serif;
+ }}
+
+ div[data-testid="stSidebarNav"] {{
+ background-color: {self.COLORS['light']};
+ }}
+
+ div[data-testid="stSidebarNav"] li div {{
+ margin-right: 0;
+ margin-left: auto;
+ }}
+
+ div[data-testid="stSidebarNav"] li div a {{
+ padding-right: 10px;
+ padding-left: 0;
+ }}
+
+ div[data-testid="stSidebarNav"] li div a:hover {{
+ background-color: {self.COLORS['primary'] + '20'};
+ }}
+
+ div[data-testid="stSidebarNav"] li div[aria-selected="true"] {{
+ background-color: {self.COLORS['primary'] + '40'};
+ }}
+
+ div[data-testid="stSidebarNav"] li div[aria-selected="true"] a span {{
+ color: {self.COLORS['primary']};
+ font-weight: 500;
+ }}
+
+ .metric-card {{
+ background-color: white;
+ border-radius: 10px;
+ padding: 20px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ text-align: center;
+ transition: transform 0.3s ease;
+ }}
+
+ .metric-card:hover {{
+ transform: translateY(-5px);
+ }}
+
+ .metric-value {{
+ font-size: 2.5rem;
+ font-weight: 700;
+ margin: 10px 0;
+ }}
+
+ .metric-label {{
+ font-size: 1rem;
+ color: #666;
+ }}
+
+ .metric-change {{
+ font-size: 0.9rem;
+ margin-top: 5px;
+ }}
+
+ .metric-change-positive {{
+ color: {self.COLORS['success']};
+ }}
+
+ .metric-change-negative {{
+ color: {self.COLORS['danger']};
+ }}
+
+ .custom-button {{
+ background-color: {self.COLORS['primary']};
+ color: white;
+ border: none;
+ border-radius: 5px;
+ padding: 10px 20px;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ }}
+
+ .custom-button:hover {{
+ background-color: {self.COLORS['secondary']};
+ }}
+
+ .custom-card {{
+ background-color: white;
+ border-radius: 10px;
+ padding: 20px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ margin-bottom: 20px;
+ }}
+
+ .header-container {{
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid {self.COLORS['border']};
+ }}
+
+ .header-title {{
+ color: {self.COLORS['primary']};
+ font-size: {self.FONT_SIZES['3xl']};
+ margin: 0;
+ }}
+
+ .header-subtitle {{
+ color: {self.COLORS['dark']};
+ font-size: {self.FONT_SIZES['lg']};
+ margin: 0;
+ }}
+
+ .header-actions {{
+ display: flex;
+ gap: 10px;
+ }}
+
+ /* تنسيق الجداول */
+ div[data-testid="stTable"] table {{
+ width: 100%;
+ border-collapse: collapse;
+ }}
+
+ div[data-testid="stTable"] thead tr th {{
+ background-color: {self.COLORS['primary']};
+ color: white;
+ text-align: right;
+ padding: 12px;
+ }}
+
+ div[data-testid="stTable"] tbody tr:nth-child(even) {{
+ background-color: {self.COLORS['light']};
+ }}
+
+ div[data-testid="stTable"] tbody tr:hover {{
+ background-color: {self.COLORS['primary'] + '10'};
+ }}
+
+ div[data-testid="stTable"] tbody tr td {{
+ padding: 10px;
+ text-align: right;
+ }}
+
+ /* تنسيق النماذج */
+ div[data-testid="stForm"] {{
+ background-color: white;
+ border-radius: 10px;
+ padding: 20px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ }}
+
+ button[kind="primaryFormSubmit"] {{
+ background-color: {self.COLORS['primary']};
+ color: white;
+ }}
+
+ button[kind="secondaryFormSubmit"] {{
+ background-color: {self.COLORS['light']};
+ color: {self.COLORS['dark']};
+ border: 1px solid {self.COLORS['border']};
+ }}
+
+ /* تنسيق الرسوم البيانية */
+ div[data-testid="stVegaLiteChart"] {{
+ background-color: white;
+ border-radius: 10px;
+ padding: 20px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ }}
+ """
+
+ # تطبيق CSS
+ st.markdown(f'', unsafe_allow_html=True)
+
+ def apply_theme_colors(self):
+ """تطبيق ألوان السمة الحالية"""
+ # تحديد ألوان السمة بناءً على الوضع
+ if self.theme_mode == "dark":
+ self.COLORS['background'] = '#121212'
+ self.COLORS['text'] = '#FFFFFF'
+ self.COLORS['border'] = '#333333'
+ else:
+ self.COLORS['background'] = '#FFFFFF'
+ self.COLORS['text'] = '#212121'
+ self.COLORS['border'] = '#E0E0E0'
+
+ # تطبيق CSS للسمة
+ theme_css = f"""
+ body {{
+ background-color: {self.COLORS['background']};
+ color: {self.COLORS['text']};
+ }}
+ """
+
+ st.markdown(f'', unsafe_allow_html=True)
+
+ def toggle_theme(self):
+ """تبديل وضع السمة بين الفاتح والداكن"""
+ if self.theme_mode == "light":
+ self.theme_mode = "dark"
+ else:
+ self.theme_mode = "light"
+
+ self.apply_theme_colors()
+
+ def create_sidebar(self, menu_items):
+ """إنشاء الشريط الجانبي مع قائمة العناصر"""
+ with st.sidebar:
+ # إضافة الشعار
+ st.markdown(
+ f"""
+
+
{self.page_icon} {self.page_title}
+
+ """,
+ unsafe_allow_html=True
+ )
+
+ # إضافة معلومات المستخدم
+ st.markdown(
+ f"""
+
+
+ م
+
+
مهندس تامر الجوهري
+
مدير المشاريع
+
+ """,
+ unsafe_allow_html=True
+ )
+
+ st.divider()
+
+ # إنشاء القائمة
+ selected = st.radio(
+ "القائمة الرئيسية",
+ [item["name"] for item in menu_items],
+ format_func=lambda x: x,
+ label_visibility="collapsed"
+ )
+
+ st.divider()
+
+ # إضافة معلومات النظام
+ st.markdown(
+ """
+
+
نظام تحليل المناقصات | الإصدار 2.0.0
+
© 2025 جميع الحقوق محفوظة
+
+ """,
+ unsafe_allow_html=True
+ )
+
+ return selected
+
+ def create_header(self, title, subtitle=None, show_actions=True):
+ """إنشاء ترويسة الصفحة"""
+ # إنشاء معرفات فريدة للأزرار
+ add_button_key = f"add_button_{title}"
+ update_button_key = f"update_button_{title}"
+
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.markdown(f'', unsafe_allow_html=True)
+ if subtitle:
+ st.markdown(f'', unsafe_allow_html=True)
+
+ if show_actions:
+ with col2:
+ col2_1, col2_2 = st.columns(2)
+ with col2_1:
+ st.button("إضافة جديد", key=add_button_key)
+ with col2_2:
+ st.button("تحديث", key=update_button_key)
+
+ st.divider()
+
+ def create_metric_card(self, label, value, change=None, color=None):
+ """إنشاء بطاقة مقياس"""
+ if color is None:
+ color = self.COLORS['primary']
+
+ change_html = ""
+ if change is not None:
+ if change.startswith("+"):
+ change_class = "metric-change-positive"
+ change_icon = "↑"
+ elif change.startswith("-"):
+ change_class = "metric-change-negative"
+ change_icon = "↓"
+ else:
+ change_class = ""
+ change_icon = ""
+
+ change_html = f'{change_icon} {change}
'
+
+ st.markdown(
+ f"""
+
+
{label}
+
{value}
+ {change_html}
+
+ """,
+ unsafe_allow_html=True
+ )
+
+ def create_card(self, title, content, color=None):
+ """إنشاء بطاقة عامة"""
+ if color is None:
+ color = self.COLORS['primary']
+
+ st.markdown(
+ f"""
+
+ """,
+ unsafe_allow_html=True
+ )
+
+ def create_button(self, label, color=None, icon=None, key=None):
+ """إنشاء زر مخصص"""
+ if color is None:
+ color = self.COLORS['primary']
+
+ # إنشاء معرف فريد للزر إذا لم يتم توفيره
+ if key is None:
+ key = f"button_{label}_{hash(label)}"
+
+ icon_html = f"{icon} " if icon else ""
+
+ return st.button(
+ f"{icon_html}{label}",
+ key=key
+ )
+
+ def create_tabs(self, tab_names):
+ """إنشاء تبويبات"""
+ return st.tabs(tab_names)
+
+ def create_expander(self, title, expanded=False, key=None):
+ """إنشاء عنصر قابل للتوسيع"""
+ # إنشاء معرف فريد للعنصر إذا لم يتم توفيره
+ if key is None:
+ key = f"expander_{title}_{hash(title)}"
+
+ return st.expander(title, expanded=expanded, key=key)
+
+ def create_data_table(self, data, use_container_width=True, hide_index=True):
+ """إنشاء جدول بيانات"""
+ return st.dataframe(data, use_container_width=use_container_width, hide_index=hide_index)
+
+ def create_chart(self, chart_type, data, **kwargs):
+ """إنشاء رسم بياني"""
+ if chart_type == "bar":
+ return st.bar_chart(data, **kwargs)
+ elif chart_type == "line":
+ return st.line_chart(data, **kwargs)
+ elif chart_type == "area":
+ return st.area_chart(data, **kwargs)
+ else:
+ return st.bar_chart(data, **kwargs)
+
+ def create_form(self, title, key=None):
+ """إنشاء نموذج"""
+ # إنشاء معرف فريد للنموذج إذا لم يتم توفيره
+ if key is None:
+ key = f"form_{title}_{hash(title)}"
+
+ return st.form(key=key)
+
+ def create_file_uploader(self, label, types=None, key=None):
+ """إنشاء أداة رفع الملفات"""
+ # إنشاء معرف فريد لأداة رفع الملفات إذا لم يتم توفيره
+ if key is None:
+ key = f"file_uploader_{label}_{hash(label)}"
+
+ return st.file_uploader(label, type=types, key=key)
+
+ def create_date_input(self, label, value=None, key=None):
+ """إنشاء حقل إدخال تاريخ"""
+ # إنشاء معرف فريد لحقل إدخال التاريخ إذا لم يتم توفيره
+ if key is None:
+ key = f"date_input_{label}_{hash(label)}"
+
+ return st.date_input(label, value=value, key=key)
+
+ def create_select_box(self, label, options, index=0, key=None):
+ """إنشاء قائمة منسدلة"""
+ # إنشاء معرف فريد للقائمة المنسدلة إذا لم يتم توفيره
+ if key is None:
+ key = f"select_box_{label}_{hash(label)}"
+
+ return st.selectbox(label, options, index=index, key=key)
+
+ def create_multi_select(self, label, options, default=None, key=None):
+ """إنشاء قائمة اختيار متعدد"""
+ # إنشاء معرف فريد لقائمة الاختيار المتعدد إذا لم يتم توفيره
+ if key is None:
+ key = f"multi_select_{label}_{hash(label)}"
+
+ return st.multiselect(label, options, default=default, key=key)
+
+ def create_slider(self, label, min_value, max_value, value=None, step=1, key=None):
+ """إنشاء شريط تمرير"""
+ # إنشاء معرف فريد لشريط التمرير إذا لم يتم توفيره
+ if key is None:
+ key = f"slider_{label}_{hash(label)}"
+
+ return st.slider(label, min_value=min_value, max_value=max_value, value=value, step=step, key=key)
+
+ def create_text_input(self, label, value="", key=None):
+ """إنشاء حقل إدخال نصي"""
+ # إنشاء معرف فريد لحقل الإدخال النصي إذا لم يتم توفيره
+ if key is None:
+ key = f"text_input_{label}_{hash(label)}"
+
+ return st.text_input(label, value=value, key=key)
+
+ def create_text_area(self, label, value="", height=None, key=None):
+ """إنشاء منطقة نص"""
+ # إنشاء معرف فريد لمنطقة النص إذا لم يتم توفيره
+ if key is None:
+ key = f"text_area_{label}_{hash(label)}"
+
+ return st.text_area(label, value=value, height=height, key=key)
+
+ def create_number_input(self, label, min_value=None, max_value=None, value=0, step=1, key=None):
+ """إنشاء حقل إدخال رقمي"""
+ # إنشاء معرف فريد لحقل الإدخال الرقمي إذا لم يتم توفيره
+ if key is None:
+ key = f"number_input_{label}_{hash(label)}"
+
+ return st.number_input(label, min_value=min_value, max_value=max_value, value=value, step=step, key=key)
+
+ def create_checkbox(self, label, value=False, key=None):
+ """إنشاء خانة اختيار"""
+ # إنشاء معرف فريد لخانة الاختيار إذا لم يتم توفيره
+ if key is None:
+ key = f"checkbox_{label}_{hash(label)}"
+
+ return st.checkbox(label, value=value, key=key)
+
+ def create_radio(self, label, options, index=0, key=None):
+ """إنشاء أزرار راديو"""
+ # إنشاء معرف فريد لأزرار الراديو إذا لم يتم توفيره
+ if key is None:
+ key = f"radio_{label}_{hash(label)}"
+
+ return st.radio(label, options, index=index, key=key)
+
+ def create_progress_bar(self, value, key=None):
+ """إنشاء شريط تقدم"""
+ # إنشاء معرف فريد لشريط التقدم إذا لم يتم توفيره
+ if key is None:
+ key = f"progress_bar_{value}_{hash(str(value))}"
+
+ return st.progress(value, key=key)
+
+ def create_spinner(self, text="جاري التحميل..."):
+ """إنشاء مؤشر تحميل"""
+ return st.spinner(text)
+
+ def create_success_message(self, message):
+ """إنشاء رسالة نجاح"""
+ return st.success(message)
+
+ def create_error_message(self, message):
+ """إنشاء رسالة خطأ"""
+ return st.error(message)
+
+ def create_warning_message(self, message):
+ """إنشاء رسالة تحذير"""
+ return st.warning(message)
+
+ def create_info_message(self, message):
+ """إنشاء رسالة معلومات"""
+ return st.info(message)
diff --git a/styling/icons.py b/styling/icons.py
index 6d28b551efd355fa9d567d406490ffe0f2e08f47..be8ed01eb3f51e6d58e77964d5a51b9ed5a73f12 100644
--- a/styling/icons.py
+++ b/styling/icons.py
@@ -1,609 +1,609 @@
-"""
-مولد الأيقونات لنظام إدارة المناقصات
-"""
-
-import os
-import math
-from PIL import Image, ImageDraw, ImageFont
-
-class IconGenerator:
- """فئة مولد الأيقونات"""
-
- def __init__(self):
- """تهيئة مولد الأيقونات"""
- # تحديد مسار مجلد الأيقونات
- self.icons_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "icons")
-
- # إنشاء مجلد الأيقونات إذا لم يكن موجودًا
- os.makedirs(self.icons_dir, exist_ok=True)
-
- # تحديد حجم الأيقونة الافتراضي
- self.icon_size = (64, 64)
-
- # تحديد الألوان الافتراضية
- self.colors = {
- "primary": "#2980B9",
- "secondary": "#1ABC9C",
- "accent": "#9B59B6",
- "warning": "#F39C12",
- "error": "#E74C3C",
- "success": "#2ECC71",
- "white": "#FFFFFF",
- "black": "#333333",
- "gray": "#95A5A6"
- }
-
- def generate_icon(self, name, color=None, background_color=None, size=None):
- """توليد أيقونة"""
- # تحديد الألوان
- if color is None:
- color = self.colors["primary"]
-
- if background_color is None:
- background_color = self.colors["white"]
-
- # تحديد الحجم
- if size is None:
- size = self.icon_size
-
- # إنشاء صورة جديدة
- icon = Image.new("RGBA", size, background_color)
- draw = ImageDraw.Draw(icon)
-
- # رسم الأيقونة بناءً على الاسم
- if name == "dashboard":
- self._draw_dashboard_icon(draw, size, color)
- elif name == "projects":
- self._draw_projects_icon(draw, size, color)
- elif name == "documents":
- self._draw_documents_icon(draw, size, color)
- elif name == "pricing":
- self._draw_pricing_icon(draw, size, color)
- elif name == "resources":
- self._draw_resources_icon(draw, size, color)
- elif name == "risk":
- self._draw_risk_icon(draw, size, color)
- elif name == "reports":
- self._draw_reports_icon(draw, size, color)
- elif name == "ai":
- self._draw_ai_icon(draw, size, color)
- elif name == "settings":
- self._draw_settings_icon(draw, size, color)
- elif name == "logout":
- self._draw_logout_icon(draw, size, color)
- elif name == "search":
- self._draw_search_icon(draw, size, color)
- elif name == "add":
- self._draw_add_icon(draw, size, color)
- elif name == "upload":
- self._draw_upload_icon(draw, size, color)
- elif name == "import":
- self._draw_import_icon(draw, size, color)
- elif name == "export":
- self._draw_export_icon(draw, size, color)
- elif name == "save":
- self._draw_save_icon(draw, size, color)
- else:
- # أيقونة افتراضية
- self._draw_default_icon(draw, size, color)
-
- # حفظ الأيقونة
- icon_path = os.path.join(self.icons_dir, f"{name}.png")
- icon.save(icon_path)
-
- return icon_path
-
- def _draw_dashboard_icon(self, draw, size, color):
- """رسم أيقونة لوحة التحكم"""
- width, height = size
- padding = width // 8
-
- # رسم المربعات الأربعة
- box_size = (width - 3 * padding) // 2
-
- # المربع العلوي الأيسر
- draw.rectangle(
- [(padding, padding), (padding + box_size, padding + box_size)],
- fill=color
- )
-
- # المربع العلوي الأيمن
- draw.rectangle(
- [(2 * padding + box_size, padding), (2 * padding + 2 * box_size, padding + box_size)],
- fill=color
- )
-
- # المربع السفلي الأيسر
- draw.rectangle(
- [(padding, 2 * padding + box_size), (padding + box_size, 2 * padding + 2 * box_size)],
- fill=color
- )
-
- # المربع السفلي الأيمن
- draw.rectangle(
- [(2 * padding + box_size, 2 * padding + box_size), (2 * padding + 2 * box_size, 2 * padding + 2 * box_size)],
- fill=color
- )
-
- def _draw_projects_icon(self, draw, size, color):
- """رسم أيقونة المشاريع"""
- width, height = size
- padding = width // 8
-
- # رسم مجلد
- folder_points = [
- (padding, height // 3),
- (width // 3, height // 3),
- (width // 2, padding),
- (width - padding, padding),
- (width - padding, height - padding),
- (padding, height - padding)
- ]
- draw.polygon(folder_points, fill=color)
-
- def _draw_documents_icon(self, draw, size, color):
- """رسم أيقونة المستندات"""
- width, height = size
- padding = width // 8
-
- # رسم ورقة
- draw.rectangle(
- [(padding, padding), (width - padding, height - padding)],
- fill=color
- )
-
- # رسم خطوط النص
- line_padding = height // 8
- line_height = height // 20
- for i in range(4):
- y = padding + line_padding + i * (line_height + line_padding)
- draw.rectangle(
- [(padding * 2, y), (width - padding * 2, y + line_height)],
- fill=self.colors["white"]
- )
-
- def _draw_pricing_icon(self, draw, size, color):
- """رسم أيقونة التسعير"""
- width, height = size
- padding = width // 8
-
- # رسم علامة الدولار
- center_x = width // 2
- center_y = height // 2
- radius = min(width, height) // 3
-
- # رسم دائرة
- draw.ellipse(
- [(center_x - radius, center_y - radius), (center_x + radius, center_y + radius)],
- fill=color
- )
-
- # رسم علامة الدولار
- line_width = radius // 4
- draw.rectangle(
- [(center_x - line_width // 2, center_y - radius * 2 // 3), (center_x + line_width // 2, center_y + radius * 2 // 3)],
- fill=self.colors["white"]
- )
- draw.rectangle(
- [(center_x - radius * 2 // 3, center_y - line_width // 2), (center_x + radius * 2 // 3, center_y + line_width // 2)],
- fill=self.colors["white"]
- )
-
- def _draw_resources_icon(self, draw, size, color):
- """رسم أيقونة الموارد"""
- width, height = size
- padding = width // 8
-
- # رسم ثلاثة أشخاص
- center_x = width // 2
- center_y = height // 2
- radius = min(width, height) // 10
-
- # الشخص الأول (في الوسط)
- head_center_y = center_y - radius * 2
- draw.ellipse(
- [(center_x - radius, head_center_y - radius), (center_x + radius, head_center_y + radius)],
- fill=color
- )
- draw.polygon(
- [
- (center_x, head_center_y + radius),
- (center_x - radius * 2, center_y + radius * 3),
- (center_x + radius * 2, center_y + radius * 3)
- ],
- fill=color
- )
-
- # الشخص الثاني (على اليسار)
- left_center_x = center_x - radius * 4
- head_center_y = center_y - radius * 2
- draw.ellipse(
- [(left_center_x - radius, head_center_y - radius), (left_center_x + radius, head_center_y + radius)],
- fill=color
- )
- draw.polygon(
- [
- (left_center_x, head_center_y + radius),
- (left_center_x - radius * 2, center_y + radius * 3),
- (left_center_x + radius * 2, center_y + radius * 3)
- ],
- fill=color
- )
-
- # الشخص الثالث (على اليمين)
- right_center_x = center_x + radius * 4
- head_center_y = center_y - radius * 2
- draw.ellipse(
- [(right_center_x - radius, head_center_y - radius), (right_center_x + radius, head_center_y + radius)],
- fill=color
- )
- draw.polygon(
- [
- (right_center_x, head_center_y + radius),
- (right_center_x - radius * 2, center_y + radius * 3),
- (right_center_x + radius * 2, center_y + radius * 3)
- ],
- fill=color
- )
-
- def _draw_risk_icon(self, draw, size, color):
- """رسم أيقونة المخاطر"""
- width, height = size
- padding = width // 8
-
- # رسم علامة تحذير (مثلث)
- draw.polygon(
- [
- (width // 2, padding),
- (padding, height - padding),
- (width - padding, height - padding)
- ],
- fill=color
- )
-
- # رسم علامة التعجب
- exclamation_width = width // 10
- exclamation_height = height // 3
- center_x = width // 2
- center_y = height // 2
-
- # الجزء العلوي من علامة التعجب
- draw.rectangle(
- [
- (center_x - exclamation_width // 2, center_y - exclamation_height),
- (center_x + exclamation_width // 2, center_y)
- ],
- fill=self.colors["white"]
- )
-
- # النقطة السفلية من علامة التعجب
- dot_radius = exclamation_width
- draw.ellipse(
- [
- (center_x - dot_radius // 2, center_y + exclamation_height // 4),
- (center_x + dot_radius // 2, center_y + exclamation_height // 4 + dot_radius)
- ],
- fill=self.colors["white"]
- )
-
- def _draw_reports_icon(self, draw, size, color):
- """رسم أيقونة التقارير"""
- width, height = size
- padding = width // 8
-
- # رسم ورقة
- draw.rectangle(
- [(padding, padding), (width - padding, height - padding)],
- fill=color
- )
-
- # رسم رسم بياني
- chart_padding = width // 6
- chart_width = width - 2 * chart_padding
- chart_height = height // 2
- chart_bottom = height - chart_padding
-
- # رسم الأعمدة
- bar_width = chart_width // 5
- bar_spacing = bar_width // 2
-
- for i in range(4):
- bar_height = (i + 1) * chart_height // 4
- bar_x = chart_padding + i * (bar_width + bar_spacing)
- bar_y = chart_bottom - bar_height
-
- draw.rectangle(
- [(bar_x, bar_y), (bar_x + bar_width, chart_bottom)],
- fill=self.colors["white"]
- )
-
- def _draw_ai_icon(self, draw, size, color):
- """رسم أيقونة الذكاء الاصطناعي"""
- width, height = size
- padding = width // 8
-
- # رسم دماغ (مجرد تمثيل مبسط)
- center_x = width // 2
- center_y = height // 2
- brain_width = width - 2 * padding
- brain_height = height - 2 * padding
-
- # رسم الجزء الخارجي من الدماغ
- draw.ellipse(
- [(center_x - brain_width // 2, center_y - brain_height // 2), (center_x + brain_width // 2, center_y + brain_height // 2)],
- fill=color
- )
-
- # رسم خطوط الدماغ
- line_width = brain_width // 10
- line_spacing = brain_width // 8
-
- for i in range(-2, 3):
- y = center_y + i * line_spacing
- draw.line(
- [(center_x - brain_width // 3, y), (center_x + brain_width // 3, y)],
- fill=self.colors["white"],
- width=line_width
- )
-
- def _draw_settings_icon(self, draw, size, color):
- """رسم أيقونة الإعدادات"""
- width, height = size
- padding = width // 8
-
- # رسم ترس
- center_x = width // 2
- center_y = height // 2
- outer_radius = min(width, height) // 2 - padding
- inner_radius = outer_radius * 2 // 3
-
- # رسم الدائرة الداخلية
- draw.ellipse(
- [(center_x - inner_radius, center_y - inner_radius), (center_x + inner_radius, center_y + inner_radius)],
- fill=color
- )
-
- # رسم الأسنان
- num_teeth = 8
- tooth_width = outer_radius - inner_radius
-
- for i in range(num_teeth):
- angle = 2 * math.pi * i / num_teeth
- tooth_center_x = center_x + (inner_radius + tooth_width // 2) * math.cos(angle)
- tooth_center_y = center_y + (inner_radius + tooth_width // 2) * math.sin(angle)
-
- draw.ellipse(
- [
- (tooth_center_x - tooth_width // 2, tooth_center_y - tooth_width // 2),
- (tooth_center_x + tooth_width // 2, tooth_center_y + tooth_width // 2)
- ],
- fill=color
- )
-
- def _draw_logout_icon(self, draw, size, color):
- """رسم أيقونة تسجيل الخروج"""
- width, height = size
- padding = width // 8
-
- # رسم سهم الخروج
- arrow_width = width - 2 * padding
- arrow_height = height - 2 * padding
-
- # رسم المستطيل الرئيسي
- draw.rectangle(
- [(padding, padding), (width // 2, height - padding)],
- fill=color
- )
-
- # رسم السهم
- arrow_points = [
- (width // 2, height // 3),
- (width - padding, height // 2),
- (width // 2, height * 2 // 3),
- (width // 2, height // 2 + height // 8),
- (width // 2 + width // 4, height // 2 + height // 8),
- (width // 2 + width // 4, height // 2 - height // 8),
- (width // 2, height // 2 - height // 8)
- ]
- draw.polygon(arrow_points, fill=color)
-
- def _draw_search_icon(self, draw, size, color):
- """رسم أيقونة البحث"""
- width, height = size
- padding = width // 8
-
- # رسم دائرة البحث
- center_x = width // 2 - padding
- center_y = height // 2 - padding
- radius = min(width, height) // 3
-
- draw.ellipse(
- [(center_x - radius, center_y - radius), (center_x + radius, center_y + radius)],
- outline=color,
- width=radius // 3
- )
-
- # رسم مقبض البحث
- handle_width = radius // 3
- handle_length = radius
- handle_angle = math.pi / 4 # 45 درجة
-
- handle_start_x = center_x + radius * math.cos(handle_angle)
- handle_start_y = center_y + radius * math.sin(handle_angle)
- handle_end_x = handle_start_x + handle_length * math.cos(handle_angle)
- handle_end_y = handle_start_y + handle_length * math.sin(handle_angle)
-
- draw.line(
- [(handle_start_x, handle_start_y), (handle_end_x, handle_end_y)],
- fill=color,
- width=handle_width
- )
-
- def _draw_add_icon(self, draw, size, color):
- """رسم أيقونة الإضافة"""
- width, height = size
- padding = width // 8
-
- # رسم علامة الزائد
- center_x = width // 2
- center_y = height // 2
- line_length = min(width, height) - 2 * padding
- line_width = line_length // 5
-
- # الخط الأفقي
- draw.rectangle(
- [
- (center_x - line_length // 2, center_y - line_width // 2),
- (center_x + line_length // 2, center_y + line_width // 2)
- ],
- fill=color
- )
-
- # الخط الرأسي
- draw.rectangle(
- [
- (center_x - line_width // 2, center_y - line_length // 2),
- (center_x + line_width // 2, center_y + line_length // 2)
- ],
- fill=color
- )
-
- def _draw_upload_icon(self, draw, size, color):
- """رسم أيقونة التحميل"""
- width, height = size
- padding = width // 8
-
- # رسم سهم لأعلى
- center_x = width // 2
- arrow_width = width // 3
- arrow_height = height // 2
-
- # رسم السهم
- arrow_points = [
- (center_x, padding),
- (center_x + arrow_width, padding + arrow_height),
- (center_x + arrow_width // 2, padding + arrow_height),
- (center_x + arrow_width // 2, height - padding),
- (center_x - arrow_width // 2, height - padding),
- (center_x - arrow_width // 2, padding + arrow_height),
- (center_x - arrow_width, padding + arrow_height)
- ]
- draw.polygon(arrow_points, fill=color)
-
- def _draw_import_icon(self, draw, size, color):
- """رسم أيقونة الاستيراد"""
- width, height = size
- padding = width // 8
-
- # رسم سهم للداخل
- center_y = height // 2
- arrow_width = width // 2
- arrow_height = height // 3
-
- # رسم المستطيل
- draw.rectangle(
- [(width - padding - arrow_width // 2, padding), (width - padding, height - padding)],
- fill=color
- )
-
- # رسم السهم
- arrow_points = [
- (padding, center_y),
- (padding + arrow_width, center_y - arrow_height // 2),
- (padding + arrow_width, center_y - arrow_height // 4),
- (width - padding - arrow_width // 2, center_y - arrow_height // 4),
- (width - padding - arrow_width // 2, center_y + arrow_height // 4),
- (padding + arrow_width, center_y + arrow_height // 4),
- (padding + arrow_width, center_y + arrow_height // 2)
- ]
- draw.polygon(arrow_points, fill=color)
-
- def _draw_export_icon(self, draw, size, color):
- """رسم أيقونة التصدير"""
- width, height = size
- padding = width // 8
-
- # رسم سهم للخارج
- center_y = height // 2
- arrow_width = width // 2
- arrow_height = height // 3
-
- # رسم المستطيل
- draw.rectangle(
- [(padding, padding), (padding + arrow_width // 2, height - padding)],
- fill=color
- )
-
- # رسم السهم
- arrow_points = [
- (width - padding, center_y),
- (width - padding - arrow_width, center_y - arrow_height // 2),
- (width - padding - arrow_width, center_y - arrow_height // 4),
- (padding + arrow_width // 2, center_y - arrow_height // 4),
- (padding + arrow_width // 2, center_y + arrow_height // 4),
- (width - padding - arrow_width, center_y + arrow_height // 4),
- (width - padding - arrow_width, center_y + arrow_height // 2)
- ]
- draw.polygon(arrow_points, fill=color)
-
- def _draw_save_icon(self, draw, size, color):
- """رسم أيقونة الحفظ"""
- width, height = size
- padding = width // 8
-
- # رسم أيقونة القرص
- draw.rectangle(
- [(padding, padding), (width - padding, height - padding)],
- fill=color
- )
-
- # رسم الشريط العلوي
- draw.rectangle(
- [(padding * 2, padding * 2), (width - padding * 2, padding * 4)],
- fill=self.colors["white"]
- )
-
- # رسم المستطيل الداخلي
- draw.rectangle(
- [(width // 3, height // 2), (width * 2 // 3, height - padding * 2)],
- fill=self.colors["white"]
- )
-
- def _draw_default_icon(self, draw, size, color):
- """رسم أيقونة افتراضية"""
- width, height = size
- padding = width // 8
-
- # رسم دائرة
- center_x = width // 2
- center_y = height // 2
- radius = min(width, height) // 2 - padding
-
- draw.ellipse(
- [(center_x - radius, center_y - radius), (center_x + radius, center_y + radius)],
- fill=color
- )
-
- def generate_default_icons(self):
- """توليد الأيقونات الافتراضية"""
- icons = [
- "dashboard",
- "projects",
- "documents",
- "pricing",
- "resources",
- "risk",
- "reports",
- "ai",
- "settings",
- "logout",
- "search",
- "add",
- "upload",
- "import",
- "export",
- "save"
- ]
-
- for icon in icons:
- self.generate_icon(icon)
+"""
+مولد الأيقونات لنظام إدارة المناقصات
+"""
+
+import os
+import math
+from PIL import Image, ImageDraw, ImageFont
+
+class IconGenerator:
+ """فئة مولد الأيقونات"""
+
+ def __init__(self):
+ """تهيئة مولد الأيقونات"""
+ # تحديد مسار مجلد الأيقونات
+ self.icons_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "icons")
+
+ # إنشاء مجلد الأيقونات إذا لم يكن موجودًا
+ os.makedirs(self.icons_dir, exist_ok=True)
+
+ # تحديد حجم الأيقونة الافتراضي
+ self.icon_size = (64, 64)
+
+ # تحديد الألوان الافتراضية
+ self.colors = {
+ "primary": "#2980B9",
+ "secondary": "#1ABC9C",
+ "accent": "#9B59B6",
+ "warning": "#F39C12",
+ "error": "#E74C3C",
+ "success": "#2ECC71",
+ "white": "#FFFFFF",
+ "black": "#333333",
+ "gray": "#95A5A6"
+ }
+
+ def generate_icon(self, name, color=None, background_color=None, size=None):
+ """توليد أيقونة"""
+ # تحديد الألوان
+ if color is None:
+ color = self.colors["primary"]
+
+ if background_color is None:
+ background_color = self.colors["white"]
+
+ # تحديد الحجم
+ if size is None:
+ size = self.icon_size
+
+ # إنشاء صورة جديدة
+ icon = Image.new("RGBA", size, background_color)
+ draw = ImageDraw.Draw(icon)
+
+ # رسم الأيقونة بناءً على الاسم
+ if name == "dashboard":
+ self._draw_dashboard_icon(draw, size, color)
+ elif name == "projects":
+ self._draw_projects_icon(draw, size, color)
+ elif name == "documents":
+ self._draw_documents_icon(draw, size, color)
+ elif name == "pricing":
+ self._draw_pricing_icon(draw, size, color)
+ elif name == "resources":
+ self._draw_resources_icon(draw, size, color)
+ elif name == "risk":
+ self._draw_risk_icon(draw, size, color)
+ elif name == "reports":
+ self._draw_reports_icon(draw, size, color)
+ elif name == "ai":
+ self._draw_ai_icon(draw, size, color)
+ elif name == "settings":
+ self._draw_settings_icon(draw, size, color)
+ elif name == "logout":
+ self._draw_logout_icon(draw, size, color)
+ elif name == "search":
+ self._draw_search_icon(draw, size, color)
+ elif name == "add":
+ self._draw_add_icon(draw, size, color)
+ elif name == "upload":
+ self._draw_upload_icon(draw, size, color)
+ elif name == "import":
+ self._draw_import_icon(draw, size, color)
+ elif name == "export":
+ self._draw_export_icon(draw, size, color)
+ elif name == "save":
+ self._draw_save_icon(draw, size, color)
+ else:
+ # أيقونة افتراضية
+ self._draw_default_icon(draw, size, color)
+
+ # حفظ الأيقونة
+ icon_path = os.path.join(self.icons_dir, f"{name}.png")
+ icon.save(icon_path)
+
+ return icon_path
+
+ def _draw_dashboard_icon(self, draw, size, color):
+ """رسم أيقونة لوحة التحكم"""
+ width, height = size
+ padding = width // 8
+
+ # رسم المربعات الأربعة
+ box_size = (width - 3 * padding) // 2
+
+ # المربع العلوي الأيسر
+ draw.rectangle(
+ [(padding, padding), (padding + box_size, padding + box_size)],
+ fill=color
+ )
+
+ # المربع العلوي الأيمن
+ draw.rectangle(
+ [(2 * padding + box_size, padding), (2 * padding + 2 * box_size, padding + box_size)],
+ fill=color
+ )
+
+ # المربع السفلي الأيسر
+ draw.rectangle(
+ [(padding, 2 * padding + box_size), (padding + box_size, 2 * padding + 2 * box_size)],
+ fill=color
+ )
+
+ # المربع السفلي الأيمن
+ draw.rectangle(
+ [(2 * padding + box_size, 2 * padding + box_size), (2 * padding + 2 * box_size, 2 * padding + 2 * box_size)],
+ fill=color
+ )
+
+ def _draw_projects_icon(self, draw, size, color):
+ """رسم أيقونة المشاريع"""
+ width, height = size
+ padding = width // 8
+
+ # رسم مجلد
+ folder_points = [
+ (padding, height // 3),
+ (width // 3, height // 3),
+ (width // 2, padding),
+ (width - padding, padding),
+ (width - padding, height - padding),
+ (padding, height - padding)
+ ]
+ draw.polygon(folder_points, fill=color)
+
+ def _draw_documents_icon(self, draw, size, color):
+ """رسم أيقونة المستندات"""
+ width, height = size
+ padding = width // 8
+
+ # رسم ورقة
+ draw.rectangle(
+ [(padding, padding), (width - padding, height - padding)],
+ fill=color
+ )
+
+ # رسم خطوط النص
+ line_padding = height // 8
+ line_height = height // 20
+ for i in range(4):
+ y = padding + line_padding + i * (line_height + line_padding)
+ draw.rectangle(
+ [(padding * 2, y), (width - padding * 2, y + line_height)],
+ fill=self.colors["white"]
+ )
+
+ def _draw_pricing_icon(self, draw, size, color):
+ """رسم أيقونة التسعير"""
+ width, height = size
+ padding = width // 8
+
+ # رسم علامة الدولار
+ center_x = width // 2
+ center_y = height // 2
+ radius = min(width, height) // 3
+
+ # رسم دائرة
+ draw.ellipse(
+ [(center_x - radius, center_y - radius), (center_x + radius, center_y + radius)],
+ fill=color
+ )
+
+ # رسم علامة الدولار
+ line_width = radius // 4
+ draw.rectangle(
+ [(center_x - line_width // 2, center_y - radius * 2 // 3), (center_x + line_width // 2, center_y + radius * 2 // 3)],
+ fill=self.colors["white"]
+ )
+ draw.rectangle(
+ [(center_x - radius * 2 // 3, center_y - line_width // 2), (center_x + radius * 2 // 3, center_y + line_width // 2)],
+ fill=self.colors["white"]
+ )
+
+ def _draw_resources_icon(self, draw, size, color):
+ """رسم أيقونة الموارد"""
+ width, height = size
+ padding = width // 8
+
+ # رسم ثلاثة أشخاص
+ center_x = width // 2
+ center_y = height // 2
+ radius = min(width, height) // 10
+
+ # الشخص الأول (في الوسط)
+ head_center_y = center_y - radius * 2
+ draw.ellipse(
+ [(center_x - radius, head_center_y - radius), (center_x + radius, head_center_y + radius)],
+ fill=color
+ )
+ draw.polygon(
+ [
+ (center_x, head_center_y + radius),
+ (center_x - radius * 2, center_y + radius * 3),
+ (center_x + radius * 2, center_y + radius * 3)
+ ],
+ fill=color
+ )
+
+ # الشخص الثاني (على اليسار)
+ left_center_x = center_x - radius * 4
+ head_center_y = center_y - radius * 2
+ draw.ellipse(
+ [(left_center_x - radius, head_center_y - radius), (left_center_x + radius, head_center_y + radius)],
+ fill=color
+ )
+ draw.polygon(
+ [
+ (left_center_x, head_center_y + radius),
+ (left_center_x - radius * 2, center_y + radius * 3),
+ (left_center_x + radius * 2, center_y + radius * 3)
+ ],
+ fill=color
+ )
+
+ # الشخص الثالث (على اليمين)
+ right_center_x = center_x + radius * 4
+ head_center_y = center_y - radius * 2
+ draw.ellipse(
+ [(right_center_x - radius, head_center_y - radius), (right_center_x + radius, head_center_y + radius)],
+ fill=color
+ )
+ draw.polygon(
+ [
+ (right_center_x, head_center_y + radius),
+ (right_center_x - radius * 2, center_y + radius * 3),
+ (right_center_x + radius * 2, center_y + radius * 3)
+ ],
+ fill=color
+ )
+
+ def _draw_risk_icon(self, draw, size, color):
+ """رسم أيقونة المخاطر"""
+ width, height = size
+ padding = width // 8
+
+ # رسم علامة تحذير (مثلث)
+ draw.polygon(
+ [
+ (width // 2, padding),
+ (padding, height - padding),
+ (width - padding, height - padding)
+ ],
+ fill=color
+ )
+
+ # رسم علامة التعجب
+ exclamation_width = width // 10
+ exclamation_height = height // 3
+ center_x = width // 2
+ center_y = height // 2
+
+ # الجزء العلوي من علامة التعجب
+ draw.rectangle(
+ [
+ (center_x - exclamation_width // 2, center_y - exclamation_height),
+ (center_x + exclamation_width // 2, center_y)
+ ],
+ fill=self.colors["white"]
+ )
+
+ # النقطة السفلية من علامة التعجب
+ dot_radius = exclamation_width
+ draw.ellipse(
+ [
+ (center_x - dot_radius // 2, center_y + exclamation_height // 4),
+ (center_x + dot_radius // 2, center_y + exclamation_height // 4 + dot_radius)
+ ],
+ fill=self.colors["white"]
+ )
+
+ def _draw_reports_icon(self, draw, size, color):
+ """رسم أيقونة التقارير"""
+ width, height = size
+ padding = width // 8
+
+ # رسم ورقة
+ draw.rectangle(
+ [(padding, padding), (width - padding, height - padding)],
+ fill=color
+ )
+
+ # رسم رسم بياني
+ chart_padding = width // 6
+ chart_width = width - 2 * chart_padding
+ chart_height = height // 2
+ chart_bottom = height - chart_padding
+
+ # رسم الأعمدة
+ bar_width = chart_width // 5
+ bar_spacing = bar_width // 2
+
+ for i in range(4):
+ bar_height = (i + 1) * chart_height // 4
+ bar_x = chart_padding + i * (bar_width + bar_spacing)
+ bar_y = chart_bottom - bar_height
+
+ draw.rectangle(
+ [(bar_x, bar_y), (bar_x + bar_width, chart_bottom)],
+ fill=self.colors["white"]
+ )
+
+ def _draw_ai_icon(self, draw, size, color):
+ """رسم أيقونة الذكاء الاصطناعي"""
+ width, height = size
+ padding = width // 8
+
+ # رسم دماغ (مجرد تمثيل مبسط)
+ center_x = width // 2
+ center_y = height // 2
+ brain_width = width - 2 * padding
+ brain_height = height - 2 * padding
+
+ # رسم الجزء الخارجي من الدماغ
+ draw.ellipse(
+ [(center_x - brain_width // 2, center_y - brain_height // 2), (center_x + brain_width // 2, center_y + brain_height // 2)],
+ fill=color
+ )
+
+ # رسم خطوط الدماغ
+ line_width = brain_width // 10
+ line_spacing = brain_width // 8
+
+ for i in range(-2, 3):
+ y = center_y + i * line_spacing
+ draw.line(
+ [(center_x - brain_width // 3, y), (center_x + brain_width // 3, y)],
+ fill=self.colors["white"],
+ width=line_width
+ )
+
+ def _draw_settings_icon(self, draw, size, color):
+ """رسم أيقونة الإعدادات"""
+ width, height = size
+ padding = width // 8
+
+ # رسم ترس
+ center_x = width // 2
+ center_y = height // 2
+ outer_radius = min(width, height) // 2 - padding
+ inner_radius = outer_radius * 2 // 3
+
+ # رسم الدائرة الداخلية
+ draw.ellipse(
+ [(center_x - inner_radius, center_y - inner_radius), (center_x + inner_radius, center_y + inner_radius)],
+ fill=color
+ )
+
+ # رسم الأسنان
+ num_teeth = 8
+ tooth_width = outer_radius - inner_radius
+
+ for i in range(num_teeth):
+ angle = 2 * math.pi * i / num_teeth
+ tooth_center_x = center_x + (inner_radius + tooth_width // 2) * math.cos(angle)
+ tooth_center_y = center_y + (inner_radius + tooth_width // 2) * math.sin(angle)
+
+ draw.ellipse(
+ [
+ (tooth_center_x - tooth_width // 2, tooth_center_y - tooth_width // 2),
+ (tooth_center_x + tooth_width // 2, tooth_center_y + tooth_width // 2)
+ ],
+ fill=color
+ )
+
+ def _draw_logout_icon(self, draw, size, color):
+ """رسم أيقونة تسجيل الخروج"""
+ width, height = size
+ padding = width // 8
+
+ # رسم سهم الخروج
+ arrow_width = width - 2 * padding
+ arrow_height = height - 2 * padding
+
+ # رسم المستطيل الرئيسي
+ draw.rectangle(
+ [(padding, padding), (width // 2, height - padding)],
+ fill=color
+ )
+
+ # رسم السهم
+ arrow_points = [
+ (width // 2, height // 3),
+ (width - padding, height // 2),
+ (width // 2, height * 2 // 3),
+ (width // 2, height // 2 + height // 8),
+ (width // 2 + width // 4, height // 2 + height // 8),
+ (width // 2 + width // 4, height // 2 - height // 8),
+ (width // 2, height // 2 - height // 8)
+ ]
+ draw.polygon(arrow_points, fill=color)
+
+ def _draw_search_icon(self, draw, size, color):
+ """رسم أيقونة البحث"""
+ width, height = size
+ padding = width // 8
+
+ # رسم دائرة البحث
+ center_x = width // 2 - padding
+ center_y = height // 2 - padding
+ radius = min(width, height) // 3
+
+ draw.ellipse(
+ [(center_x - radius, center_y - radius), (center_x + radius, center_y + radius)],
+ outline=color,
+ width=radius // 3
+ )
+
+ # رسم مقبض البحث
+ handle_width = radius // 3
+ handle_length = radius
+ handle_angle = math.pi / 4 # 45 درجة
+
+ handle_start_x = center_x + radius * math.cos(handle_angle)
+ handle_start_y = center_y + radius * math.sin(handle_angle)
+ handle_end_x = handle_start_x + handle_length * math.cos(handle_angle)
+ handle_end_y = handle_start_y + handle_length * math.sin(handle_angle)
+
+ draw.line(
+ [(handle_start_x, handle_start_y), (handle_end_x, handle_end_y)],
+ fill=color,
+ width=handle_width
+ )
+
+ def _draw_add_icon(self, draw, size, color):
+ """رسم أيقونة الإضافة"""
+ width, height = size
+ padding = width // 8
+
+ # رسم علامة الزائد
+ center_x = width // 2
+ center_y = height // 2
+ line_length = min(width, height) - 2 * padding
+ line_width = line_length // 5
+
+ # الخط الأفقي
+ draw.rectangle(
+ [
+ (center_x - line_length // 2, center_y - line_width // 2),
+ (center_x + line_length // 2, center_y + line_width // 2)
+ ],
+ fill=color
+ )
+
+ # الخط الرأسي
+ draw.rectangle(
+ [
+ (center_x - line_width // 2, center_y - line_length // 2),
+ (center_x + line_width // 2, center_y + line_length // 2)
+ ],
+ fill=color
+ )
+
+ def _draw_upload_icon(self, draw, size, color):
+ """رسم أيقونة التحميل"""
+ width, height = size
+ padding = width // 8
+
+ # رسم سهم لأعلى
+ center_x = width // 2
+ arrow_width = width // 3
+ arrow_height = height // 2
+
+ # رسم السهم
+ arrow_points = [
+ (center_x, padding),
+ (center_x + arrow_width, padding + arrow_height),
+ (center_x + arrow_width // 2, padding + arrow_height),
+ (center_x + arrow_width // 2, height - padding),
+ (center_x - arrow_width // 2, height - padding),
+ (center_x - arrow_width // 2, padding + arrow_height),
+ (center_x - arrow_width, padding + arrow_height)
+ ]
+ draw.polygon(arrow_points, fill=color)
+
+ def _draw_import_icon(self, draw, size, color):
+ """رسم أيقونة الاستيراد"""
+ width, height = size
+ padding = width // 8
+
+ # رسم سهم للداخل
+ center_y = height // 2
+ arrow_width = width // 2
+ arrow_height = height // 3
+
+ # رسم المستطيل
+ draw.rectangle(
+ [(width - padding - arrow_width // 2, padding), (width - padding, height - padding)],
+ fill=color
+ )
+
+ # رسم السهم
+ arrow_points = [
+ (padding, center_y),
+ (padding + arrow_width, center_y - arrow_height // 2),
+ (padding + arrow_width, center_y - arrow_height // 4),
+ (width - padding - arrow_width // 2, center_y - arrow_height // 4),
+ (width - padding - arrow_width // 2, center_y + arrow_height // 4),
+ (padding + arrow_width, center_y + arrow_height // 4),
+ (padding + arrow_width, center_y + arrow_height // 2)
+ ]
+ draw.polygon(arrow_points, fill=color)
+
+ def _draw_export_icon(self, draw, size, color):
+ """رسم أيقونة التصدير"""
+ width, height = size
+ padding = width // 8
+
+ # رسم سهم للخارج
+ center_y = height // 2
+ arrow_width = width // 2
+ arrow_height = height // 3
+
+ # رسم المستطيل
+ draw.rectangle(
+ [(padding, padding), (padding + arrow_width // 2, height - padding)],
+ fill=color
+ )
+
+ # رسم السهم
+ arrow_points = [
+ (width - padding, center_y),
+ (width - padding - arrow_width, center_y - arrow_height // 2),
+ (width - padding - arrow_width, center_y - arrow_height // 4),
+ (padding + arrow_width // 2, center_y - arrow_height // 4),
+ (padding + arrow_width // 2, center_y + arrow_height // 4),
+ (width - padding - arrow_width, center_y + arrow_height // 4),
+ (width - padding - arrow_width, center_y + arrow_height // 2)
+ ]
+ draw.polygon(arrow_points, fill=color)
+
+ def _draw_save_icon(self, draw, size, color):
+ """رسم أيقونة الحفظ"""
+ width, height = size
+ padding = width // 8
+
+ # رسم أيقونة القرص
+ draw.rectangle(
+ [(padding, padding), (width - padding, height - padding)],
+ fill=color
+ )
+
+ # رسم الشريط العلوي
+ draw.rectangle(
+ [(padding * 2, padding * 2), (width - padding * 2, padding * 4)],
+ fill=self.colors["white"]
+ )
+
+ # رسم المستطيل الداخلي
+ draw.rectangle(
+ [(width // 3, height // 2), (width * 2 // 3, height - padding * 2)],
+ fill=self.colors["white"]
+ )
+
+ def _draw_default_icon(self, draw, size, color):
+ """رسم أيقونة افتراضية"""
+ width, height = size
+ padding = width // 8
+
+ # رسم دائرة
+ center_x = width // 2
+ center_y = height // 2
+ radius = min(width, height) // 2 - padding
+
+ draw.ellipse(
+ [(center_x - radius, center_y - radius), (center_x + radius, center_y + radius)],
+ fill=color
+ )
+
+ def generate_default_icons(self):
+ """توليد الأيقونات الافتراضية"""
+ icons = [
+ "dashboard",
+ "projects",
+ "documents",
+ "pricing",
+ "resources",
+ "risk",
+ "reports",
+ "ai",
+ "settings",
+ "logout",
+ "search",
+ "add",
+ "upload",
+ "import",
+ "export",
+ "save"
+ ]
+
+ for icon in icons:
+ self.generate_icon(icon)
diff --git a/styling/theme.py b/styling/theme.py
index a1233b28e655531b6a3885297b5ea76bcaf903e7..1ed1a8a3ea9dae9a639cdfbf9251b6c3b783004e 100644
--- a/styling/theme.py
+++ b/styling/theme.py
@@ -1,541 +1,541 @@
-"""
-ملف النمط لنظام إدارة المناقصات
-"""
-
-import os
-import tkinter as tk
-import customtkinter as ctk
-from PIL import Image, ImageDraw
-import matplotlib.pyplot as plt
-from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
-
-class AppTheme:
- """فئة نمط التطبيق"""
-
- # ألوان النمط الفاتح
- LIGHT_BG_COLOR = "#F5F5F5"
- LIGHT_FG_COLOR = "#333333"
- LIGHT_CARD_BG_COLOR = "#FFFFFF"
- LIGHT_SIDEBAR_BG_COLOR = "#2C3E50"
- LIGHT_SIDEBAR_FG_COLOR = "#FFFFFF"
- LIGHT_SIDEBAR_HOVER_COLOR = "#34495E"
- LIGHT_SIDEBAR_ACTIVE_COLOR = "#1ABC9C"
- LIGHT_BUTTON_BG_COLOR = "#2980B9"
- LIGHT_BUTTON_HOVER_COLOR = "#3498DB"
- LIGHT_BUTTON_ACTIVE_COLOR = "#1F618D"
- LIGHT_INPUT_BG_COLOR = "#FFFFFF"
- LIGHT_INPUT_FG_COLOR = "#333333"
- LIGHT_BORDER_COLOR = "#E0E0E0"
-
- # ألوان النمط الداكن
- DARK_BG_COLOR = "#121212"
- DARK_FG_COLOR = "#E0E0E0"
- DARK_CARD_BG_COLOR = "#1E1E1E"
- DARK_SIDEBAR_BG_COLOR = "#1A1A2E"
- DARK_SIDEBAR_FG_COLOR = "#E0E0E0"
- DARK_SIDEBAR_HOVER_COLOR = "#16213E"
- DARK_SIDEBAR_ACTIVE_COLOR = "#0F3460"
- DARK_BUTTON_BG_COLOR = "#0F3460"
- DARK_BUTTON_HOVER_COLOR = "#16213E"
- DARK_BUTTON_ACTIVE_COLOR = "#1A1A2E"
- DARK_INPUT_BG_COLOR = "#2C2C2C"
- DARK_INPUT_FG_COLOR = "#E0E0E0"
- DARK_BORDER_COLOR = "#333333"
-
- # ألوان الأساسية
- PRIMARY_COLOR = {
- "light": "#2980B9",
- "dark": "#0F3460"
- }
-
- SECONDARY_COLOR = {
- "light": "#1ABC9C",
- "dark": "#16213E"
- }
-
- ACCENT_COLOR = {
- "light": "#9B59B6",
- "dark": "#533483"
- }
-
- WARNING_COLOR = {
- "light": "#F39C12",
- "dark": "#E58E26"
- }
-
- ERROR_COLOR = {
- "light": "#E74C3C",
- "dark": "#C0392B"
- }
-
- SUCCESS_COLOR = {
- "light": "#2ECC71",
- "dark": "#27AE60"
- }
-
- def __init__(self, config):
- """تهيئة النمط"""
- self.config = config
- self.current_theme = self.config.get_theme()
- self.font_family = self.config.get_font()
- self.font_size = self.config.get_font_size()
-
- # تهيئة النمط
- self._setup_theme()
-
- def _setup_theme(self):
- """إعداد النمط"""
- # تعيين نمط customtkinter
- ctk.set_appearance_mode(self.current_theme)
- ctk.set_default_color_theme("blue")
-
- # تهيئة الخطوط
- self.fonts = {
- "title": (self.font_family, self.font_size + 8, "bold"),
- "subtitle": (self.font_family, self.font_size + 4, "bold"),
- "heading": (self.font_family, self.font_size + 2, "bold"),
- "body": (self.font_family, self.font_size, "normal"),
- "small": (self.font_family, self.font_size - 2, "normal")
- }
-
- def apply_theme_to_app(self, app):
- """تطبيق النمط على التطبيق"""
- app.configure(fg_color=self.get_color("bg_color"))
-
- def get_color(self, color_name):
- """الحصول على لون معين"""
- if self.current_theme == "light":
- colors = {
- "bg_color": self.LIGHT_BG_COLOR,
- "fg_color": self.LIGHT_FG_COLOR,
- "card_bg_color": self.LIGHT_CARD_BG_COLOR,
- "sidebar_bg_color": self.LIGHT_SIDEBAR_BG_COLOR,
- "sidebar_fg_color": self.LIGHT_SIDEBAR_FG_COLOR,
- "sidebar_hover_color": self.LIGHT_SIDEBAR_HOVER_COLOR,
- "sidebar_active_color": self.LIGHT_SIDEBAR_ACTIVE_COLOR,
- "button_bg_color": self.LIGHT_BUTTON_BG_COLOR,
- "button_hover_color": self.LIGHT_BUTTON_HOVER_COLOR,
- "button_active_color": self.LIGHT_BUTTON_ACTIVE_COLOR,
- "input_bg_color": self.LIGHT_INPUT_BG_COLOR,
- "input_fg_color": self.LIGHT_INPUT_FG_COLOR,
- "border_color": self.LIGHT_BORDER_COLOR,
- "primary_color": self.PRIMARY_COLOR["light"],
- "secondary_color": self.SECONDARY_COLOR["light"],
- "accent_color": self.ACCENT_COLOR["light"],
- "warning_color": self.WARNING_COLOR["light"],
- "error_color": self.ERROR_COLOR["light"],
- "success_color": self.SUCCESS_COLOR["light"]
- }
- else:
- colors = {
- "bg_color": self.DARK_BG_COLOR,
- "fg_color": self.DARK_FG_COLOR,
- "card_bg_color": self.DARK_CARD_BG_COLOR,
- "sidebar_bg_color": self.DARK_SIDEBAR_BG_COLOR,
- "sidebar_fg_color": self.DARK_SIDEBAR_FG_COLOR,
- "sidebar_hover_color": self.DARK_SIDEBAR_HOVER_COLOR,
- "sidebar_active_color": self.DARK_SIDEBAR_ACTIVE_COLOR,
- "button_bg_color": self.DARK_BUTTON_BG_COLOR,
- "button_hover_color": self.DARK_BUTTON_HOVER_COLOR,
- "button_active_color": self.DARK_BUTTON_ACTIVE_COLOR,
- "input_bg_color": self.DARK_INPUT_BG_COLOR,
- "input_fg_color": self.DARK_INPUT_FG_COLOR,
- "border_color": self.DARK_BORDER_COLOR,
- "primary_color": self.PRIMARY_COLOR["dark"],
- "secondary_color": self.SECONDARY_COLOR["dark"],
- "accent_color": self.ACCENT_COLOR["dark"],
- "warning_color": self.WARNING_COLOR["dark"],
- "error_color": self.ERROR_COLOR["dark"],
- "success_color": self.SUCCESS_COLOR["dark"]
- }
-
- return colors.get(color_name, self.LIGHT_BG_COLOR)
-
- def get_font(self, font_type):
- """الحصول على خط معين"""
- return self.fonts.get(font_type, self.fonts["body"])
-
- def toggle_theme(self):
- """تبديل النمط بين الفاتح والداكن"""
- if self.current_theme == "light":
- self.current_theme = "dark"
- else:
- self.current_theme = "light"
-
- # تحديث النمط في الإعدادات
- self.config.set_theme(self.current_theme)
-
- # تحديث نمط customtkinter
- ctk.set_appearance_mode(self.current_theme)
-
- return self.current_theme
-
- def create_styled_frame(self, parent, **kwargs):
- """إنشاء إطار منسق"""
- default_kwargs = {
- "fg_color": self.get_color("bg_color"),
- "corner_radius": 10,
- "border_width": 0
- }
-
- # دمج الخصائص المخصصة مع الخصائص الافتراضية
- for key, value in kwargs.items():
- default_kwargs[key] = value
-
- return ctk.CTkFrame(parent, **default_kwargs)
-
- def create_styled_scrollable_frame(self, parent, **kwargs):
- """إنشاء إطار قابل للتمرير منسق"""
- default_kwargs = {
- "fg_color": self.get_color("bg_color"),
- "corner_radius": 10,
- "border_width": 0
- }
-
- # دمج الخصائص المخصصة مع الخصائص الافتراضية
- for key, value in kwargs.items():
- default_kwargs[key] = value
-
- return ctk.CTkScrollableFrame(parent, **default_kwargs)
-
- def create_styled_label(self, parent, text, **kwargs):
- """إنشاء تسمية منسقة"""
- default_kwargs = {
- "text": text,
- "font": self.get_font("body"),
- "text_color": self.get_color("fg_color")
- }
-
- # دمج الخصائص المخصصة مع الخصائص الافتراضية
- for key, value in kwargs.items():
- default_kwargs[key] = value
-
- return ctk.CTkLabel(parent, **default_kwargs)
-
- def create_styled_button(self, parent, text, **kwargs):
- """إنشاء زر منسق"""
- default_kwargs = {
- "text": text,
- "font": self.get_font("body"),
- "fg_color": self.get_color("button_bg_color"),
- "hover_color": self.get_color("button_hover_color"),
- "text_color": "white",
- "corner_radius": 8,
- "border_width": 0,
- "height": 36
- }
-
- # إضافة أيقونة إذا تم تحديدها
- if "icon" in kwargs:
- icon_name = kwargs.pop("icon")
- # هنا يمكن إضافة منطق لتحميل الأيقونة
-
- # دمج الخصائص المخصصة مع الخصائص الافتراضية
- for key, value in kwargs.items():
- default_kwargs[key] = value
-
- return ctk.CTkButton(parent, **default_kwargs)
-
- def create_styled_entry(self, parent, placeholder_text, **kwargs):
- """إنشاء حقل إدخال منسق"""
- default_kwargs = {
- "placeholder_text": placeholder_text,
- "font": self.get_font("body"),
- "fg_color": self.get_color("input_bg_color"),
- "text_color": self.get_color("input_fg_color"),
- "placeholder_text_color": self.get_color("border_color"),
- "corner_radius": 8,
- "border_width": 1,
- "border_color": self.get_color("border_color"),
- "height": 36
- }
-
- # دمج الخصائص المخصصة مع الخصائص الافتراضية
- for key, value in kwargs.items():
- default_kwargs[key] = value
-
- return ctk.CTkEntry(parent, **default_kwargs)
-
- def create_styled_textbox(self, parent, **kwargs):
- """إنشاء مربع نص منسق"""
- default_kwargs = {
- "font": self.get_font("body"),
- "fg_color": self.get_color("input_bg_color"),
- "text_color": self.get_color("input_fg_color"),
- "corner_radius": 8,
- "border_width": 1,
- "border_color": self.get_color("border_color")
- }
-
- # دمج الخصائص المخصصة مع الخصائص الافتراضية
- for key, value in kwargs.items():
- default_kwargs[key] = value
-
- return ctk.CTkTextbox(parent, **default_kwargs)
-
- def create_styled_combobox(self, parent, values, **kwargs):
- """إنشاء قائمة منسدلة منسقة"""
- default_kwargs = {
- "values": values,
- "font": self.get_font("body"),
- "fg_color": self.get_color("input_bg_color"),
- "text_color": self.get_color("input_fg_color"),
- "border_color": self.get_color("border_color"),
- "button_color": self.get_color("button_bg_color"),
- "button_hover_color": self.get_color("button_hover_color"),
- "dropdown_fg_color": self.get_color("card_bg_color"),
- "dropdown_text_color": self.get_color("fg_color"),
- "dropdown_hover_color": self.get_color("sidebar_hover_color"),
- "corner_radius": 8,
- "border_width": 1,
- "dropdown_font": self.get_font("body")
- }
-
- # دمج الخصائص المخصصة مع الخصائص الافتراضية
- for key, value in kwargs.items():
- default_kwargs[key] = value
-
- return ctk.CTkComboBox(parent, **default_kwargs)
-
- def create_styled_switch(self, parent, text, **kwargs):
- """إنشاء مفتاح تبديل منسق"""
- default_kwargs = {
- "text": text,
- "font": self.get_font("body"),
- "fg_color": self.get_color("border_color"),
- "progress_color": self.get_color("button_bg_color"),
- "button_color": "white",
- "button_hover_color": "white",
- "text_color": self.get_color("fg_color")
- }
-
- # دمج الخصائص المخصصة مع الخصائص الافتراضية
- for key, value in kwargs.items():
- default_kwargs[key] = value
-
- return ctk.CTkSwitch(parent, **default_kwargs)
-
- def create_styled_radio_button(self, parent, text, variable, value, **kwargs):
- """إنشاء زر راديو منسق"""
- default_kwargs = {
- "text": text,
- "font": self.get_font("body"),
- "fg_color": self.get_color("button_bg_color"),
- "border_color": self.get_color("border_color"),
- "hover_color": self.get_color("button_hover_color"),
- "text_color": self.get_color("fg_color"),
- "variable": variable,
- "value": value
- }
-
- # دمج الخصائص المخصصة مع الخصائص الافتراضية
- for key, value in kwargs.items():
- default_kwargs[key] = value
-
- return ctk.CTkRadioButton(parent, **default_kwargs)
-
- def create_styled_slider(self, parent, **kwargs):
- """إنشاء شريط تمرير منسق"""
- default_kwargs = {
- "fg_color": self.get_color("border_color"),
- "progress_color": self.get_color("button_bg_color"),
- "button_color": self.get_color("button_bg_color"),
- "button_hover_color": self.get_color("button_hover_color")
- }
-
- # دمج الخصائص المخصصة مع الخصائص الافتراضية
- for key, value in kwargs.items():
- default_kwargs[key] = value
-
- return ctk.CTkSlider(parent, **default_kwargs)
-
- def create_styled_tabview(self, parent, **kwargs):
- """إنشاء عرض تبويب منسق"""
- default_kwargs = {
- "fg_color": self.get_color("card_bg_color"),
- "segmented_button_fg_color": self.get_color("sidebar_bg_color"),
- "segmented_button_selected_color": self.get_color("button_bg_color"),
- "segmented_button_unselected_color": self.get_color("sidebar_bg_color"),
- "segmented_button_selected_hover_color": self.get_color("button_hover_color"),
- "segmented_button_unselected_hover_color": self.get_color("sidebar_hover_color"),
- "segmented_button_text_color": self.get_color("sidebar_fg_color"),
- "segmented_button_selected_text_color": "white",
- "text_color": self.get_color("fg_color"),
- "corner_radius": 10
- }
-
- # دمج الخصائص المخصصة مع الخصائص الافتراضية
- for key, value in kwargs.items():
- default_kwargs[key] = value
-
- return ctk.CTkTabview(parent, **default_kwargs)
-
- def create_styled_sidebar_button(self, parent, text, icon, command=None):
- """إنشاء زر الشريط الجانبي المنسق"""
- # إنشاء إطار للزر
- button_frame = ctk.CTkFrame(
- parent,
- fg_color="transparent",
- corner_radius=0
- )
-
- # إنشاء الزر
- button = ctk.CTkButton(
- button_frame,
- text=text,
- font=self.get_font("body"),
- fg_color="transparent",
- hover_color=self.get_color("sidebar_hover_color"),
- text_color=self.get_color("sidebar_fg_color"),
- anchor="w",
- corner_radius=0,
- border_width=0,
- height=40,
- command=command
- )
- button.pack(fill="x", padx=0, pady=0)
-
- return button_frame, button
-
- def create_styled_card(self, parent, title):
- """إنشاء بطاقة منسقة"""
- # إنشاء إطار البطاقة
- card = self.create_styled_frame(
- parent,
- fg_color=self.get_color("card_bg_color")
- )
-
- # إنشاء عنوان البطاقة
- title_label = self.create_styled_label(
- card,
- title,
- font=self.get_font("heading")
- )
- title_label.pack(anchor="w", padx=15, pady=(15, 5))
-
- # إنشاء خط فاصل
- separator = ctk.CTkFrame(
- card,
- height=1,
- fg_color=self.get_color("border_color")
- )
- separator.pack(fill="x", padx=15, pady=(5, 0))
-
- # إنشاء إطار المحتوى
- content_frame = self.create_styled_frame(
- card,
- fg_color="transparent"
- )
- content_frame.pack(fill="both", expand=True, padx=15, pady=15)
-
- return card, content_frame
-
- def create_styled_data_table(self, parent, columns, data):
- """إنشاء جدول بيانات منسق"""
- # إنشاء إطار الجدول
- table_frame = self.create_styled_frame(
- parent,
- fg_color="transparent"
- )
-
- # إنشاء إطار العناوين
- header_frame = self.create_styled_frame(
- table_frame,
- fg_color=self.get_color("sidebar_bg_color")
- )
- header_frame.pack(fill="x", padx=0, pady=(0, 1))
-
- # إنشاء عناوين الأعمدة
- for i, column in enumerate(columns):
- column_label = self.create_styled_label(
- header_frame,
- column,
- font=self.get_font("heading"),
- text_color=self.get_color("sidebar_fg_color")
- )
- column_label.grid(row=0, column=i, sticky="ew", padx=10, pady=10)
- header_frame.grid_columnconfigure(i, weight=1)
-
- # إنشاء إطار البيانات
- data_frame = self.create_styled_scrollable_frame(
- table_frame,
- fg_color="transparent"
- )
- data_frame.pack(fill="both", expand=True, padx=0, pady=0)
-
- # إنشاء صفوف البيانات
- for i, row in enumerate(data):
- row_frame = self.create_styled_frame(
- data_frame,
- fg_color=self.get_color("card_bg_color") if i % 2 == 0 else self.get_color("bg_color")
- )
- row_frame.pack(fill="x", padx=0, pady=(0, 1))
-
- for j, cell in enumerate(row):
- cell_label = self.create_styled_label(
- row_frame,
- cell,
- font=self.get_font("body")
- )
- cell_label.grid(row=0, column=j, sticky="ew", padx=10, pady=10)
- row_frame.grid_columnconfigure(j, weight=1)
-
- return table_frame, data_frame
-
- def create_styled_message_box(self, title, message, message_type="info"):
- """إنشاء مربع رسالة منسق"""
- # تحديد لون الرسالة بناءً على النوع
- if message_type == "error":
- color = self.get_color("error_color")
- elif message_type == "warning":
- color = self.get_color("warning_color")
- elif message_type == "success":
- color = self.get_color("success_color")
- else: # info
- color = self.get_color("primary_color")
-
- # إنشاء نافذة الرسالة
- message_window = ctk.CTkToplevel()
- message_window.title(title)
- message_window.geometry("400x200")
- message_window.resizable(False, False)
- message_window.grab_set() # جعل النافذة المنبثقة مركز الاهتمام
-
- # تطبيق النمط
- message_window.configure(fg_color=self.get_color("bg_color"))
-
- # إنشاء إطار الرسالة
- message_frame = self.create_styled_frame(
- message_window,
- fg_color=self.get_color("card_bg_color")
- )
- message_frame.pack(fill="both", expand=True, padx=20, pady=20)
-
- # إنشاء عنوان الرسالة
- title_label = self.create_styled_label(
- message_frame,
- title,
- font=self.get_font("heading"),
- text_color=color
- )
- title_label.pack(padx=20, pady=(20, 10))
-
- # إنشاء نص الرسالة
- message_label = self.create_styled_label(
- message_frame,
- message,
- font=self.get_font("body")
- )
- message_label.pack(padx=20, pady=(0, 20))
-
- # إنشاء زر موافق
- ok_button = self.create_styled_button(
- message_frame,
- "موافق",
- fg_color=color,
- hover_color=color,
- command=message_window.destroy
- )
- ok_button.pack(pady=(0, 20))
-
- return message_window
+"""
+ملف النمط لنظام إدارة المناقصات
+"""
+
+import os
+import tkinter as tk
+import customtkinter as ctk
+from PIL import Image, ImageDraw
+import matplotlib.pyplot as plt
+from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
+
+class AppTheme:
+ """فئة نمط التطبيق"""
+
+ # ألوان النمط الفاتح
+ LIGHT_BG_COLOR = "#F5F5F5"
+ LIGHT_FG_COLOR = "#333333"
+ LIGHT_CARD_BG_COLOR = "#FFFFFF"
+ LIGHT_SIDEBAR_BG_COLOR = "#2C3E50"
+ LIGHT_SIDEBAR_FG_COLOR = "#FFFFFF"
+ LIGHT_SIDEBAR_HOVER_COLOR = "#34495E"
+ LIGHT_SIDEBAR_ACTIVE_COLOR = "#1ABC9C"
+ LIGHT_BUTTON_BG_COLOR = "#2980B9"
+ LIGHT_BUTTON_HOVER_COLOR = "#3498DB"
+ LIGHT_BUTTON_ACTIVE_COLOR = "#1F618D"
+ LIGHT_INPUT_BG_COLOR = "#FFFFFF"
+ LIGHT_INPUT_FG_COLOR = "#333333"
+ LIGHT_BORDER_COLOR = "#E0E0E0"
+
+ # ألوان النمط الداكن
+ DARK_BG_COLOR = "#121212"
+ DARK_FG_COLOR = "#E0E0E0"
+ DARK_CARD_BG_COLOR = "#1E1E1E"
+ DARK_SIDEBAR_BG_COLOR = "#1A1A2E"
+ DARK_SIDEBAR_FG_COLOR = "#E0E0E0"
+ DARK_SIDEBAR_HOVER_COLOR = "#16213E"
+ DARK_SIDEBAR_ACTIVE_COLOR = "#0F3460"
+ DARK_BUTTON_BG_COLOR = "#0F3460"
+ DARK_BUTTON_HOVER_COLOR = "#16213E"
+ DARK_BUTTON_ACTIVE_COLOR = "#1A1A2E"
+ DARK_INPUT_BG_COLOR = "#2C2C2C"
+ DARK_INPUT_FG_COLOR = "#E0E0E0"
+ DARK_BORDER_COLOR = "#333333"
+
+ # ألوان الأساسية
+ PRIMARY_COLOR = {
+ "light": "#2980B9",
+ "dark": "#0F3460"
+ }
+
+ SECONDARY_COLOR = {
+ "light": "#1ABC9C",
+ "dark": "#16213E"
+ }
+
+ ACCENT_COLOR = {
+ "light": "#9B59B6",
+ "dark": "#533483"
+ }
+
+ WARNING_COLOR = {
+ "light": "#F39C12",
+ "dark": "#E58E26"
+ }
+
+ ERROR_COLOR = {
+ "light": "#E74C3C",
+ "dark": "#C0392B"
+ }
+
+ SUCCESS_COLOR = {
+ "light": "#2ECC71",
+ "dark": "#27AE60"
+ }
+
+ def __init__(self, config):
+ """تهيئة النمط"""
+ self.config = config
+ self.current_theme = self.config.get_theme()
+ self.font_family = self.config.get_font()
+ self.font_size = self.config.get_font_size()
+
+ # تهيئة النمط
+ self._setup_theme()
+
+ def _setup_theme(self):
+ """إعداد النمط"""
+ # تعيين نمط customtkinter
+ ctk.set_appearance_mode(self.current_theme)
+ ctk.set_default_color_theme("blue")
+
+ # تهيئة الخطوط
+ self.fonts = {
+ "title": (self.font_family, self.font_size + 8, "bold"),
+ "subtitle": (self.font_family, self.font_size + 4, "bold"),
+ "heading": (self.font_family, self.font_size + 2, "bold"),
+ "body": (self.font_family, self.font_size, "normal"),
+ "small": (self.font_family, self.font_size - 2, "normal")
+ }
+
+ def apply_theme_to_app(self, app):
+ """تطبيق النمط على التطبيق"""
+ app.configure(fg_color=self.get_color("bg_color"))
+
+ def get_color(self, color_name):
+ """الحصول على لون معين"""
+ if self.current_theme == "light":
+ colors = {
+ "bg_color": self.LIGHT_BG_COLOR,
+ "fg_color": self.LIGHT_FG_COLOR,
+ "card_bg_color": self.LIGHT_CARD_BG_COLOR,
+ "sidebar_bg_color": self.LIGHT_SIDEBAR_BG_COLOR,
+ "sidebar_fg_color": self.LIGHT_SIDEBAR_FG_COLOR,
+ "sidebar_hover_color": self.LIGHT_SIDEBAR_HOVER_COLOR,
+ "sidebar_active_color": self.LIGHT_SIDEBAR_ACTIVE_COLOR,
+ "button_bg_color": self.LIGHT_BUTTON_BG_COLOR,
+ "button_hover_color": self.LIGHT_BUTTON_HOVER_COLOR,
+ "button_active_color": self.LIGHT_BUTTON_ACTIVE_COLOR,
+ "input_bg_color": self.LIGHT_INPUT_BG_COLOR,
+ "input_fg_color": self.LIGHT_INPUT_FG_COLOR,
+ "border_color": self.LIGHT_BORDER_COLOR,
+ "primary_color": self.PRIMARY_COLOR["light"],
+ "secondary_color": self.SECONDARY_COLOR["light"],
+ "accent_color": self.ACCENT_COLOR["light"],
+ "warning_color": self.WARNING_COLOR["light"],
+ "error_color": self.ERROR_COLOR["light"],
+ "success_color": self.SUCCESS_COLOR["light"]
+ }
+ else:
+ colors = {
+ "bg_color": self.DARK_BG_COLOR,
+ "fg_color": self.DARK_FG_COLOR,
+ "card_bg_color": self.DARK_CARD_BG_COLOR,
+ "sidebar_bg_color": self.DARK_SIDEBAR_BG_COLOR,
+ "sidebar_fg_color": self.DARK_SIDEBAR_FG_COLOR,
+ "sidebar_hover_color": self.DARK_SIDEBAR_HOVER_COLOR,
+ "sidebar_active_color": self.DARK_SIDEBAR_ACTIVE_COLOR,
+ "button_bg_color": self.DARK_BUTTON_BG_COLOR,
+ "button_hover_color": self.DARK_BUTTON_HOVER_COLOR,
+ "button_active_color": self.DARK_BUTTON_ACTIVE_COLOR,
+ "input_bg_color": self.DARK_INPUT_BG_COLOR,
+ "input_fg_color": self.DARK_INPUT_FG_COLOR,
+ "border_color": self.DARK_BORDER_COLOR,
+ "primary_color": self.PRIMARY_COLOR["dark"],
+ "secondary_color": self.SECONDARY_COLOR["dark"],
+ "accent_color": self.ACCENT_COLOR["dark"],
+ "warning_color": self.WARNING_COLOR["dark"],
+ "error_color": self.ERROR_COLOR["dark"],
+ "success_color": self.SUCCESS_COLOR["dark"]
+ }
+
+ return colors.get(color_name, self.LIGHT_BG_COLOR)
+
+ def get_font(self, font_type):
+ """الحصول على خط معين"""
+ return self.fonts.get(font_type, self.fonts["body"])
+
+ def toggle_theme(self):
+ """تبديل النمط بين الفاتح والداكن"""
+ if self.current_theme == "light":
+ self.current_theme = "dark"
+ else:
+ self.current_theme = "light"
+
+ # تحديث النمط في الإعدادات
+ self.config.set_theme(self.current_theme)
+
+ # تحديث نمط customtkinter
+ ctk.set_appearance_mode(self.current_theme)
+
+ return self.current_theme
+
+ def create_styled_frame(self, parent, **kwargs):
+ """إنشاء إطار منسق"""
+ default_kwargs = {
+ "fg_color": self.get_color("bg_color"),
+ "corner_radius": 10,
+ "border_width": 0
+ }
+
+ # دمج الخصائص المخصصة مع الخصائص الافتراضية
+ for key, value in kwargs.items():
+ default_kwargs[key] = value
+
+ return ctk.CTkFrame(parent, **default_kwargs)
+
+ def create_styled_scrollable_frame(self, parent, **kwargs):
+ """إنشاء إطار قابل للتمرير منسق"""
+ default_kwargs = {
+ "fg_color": self.get_color("bg_color"),
+ "corner_radius": 10,
+ "border_width": 0
+ }
+
+ # دمج الخصائص المخصصة مع الخصائص الافتراضية
+ for key, value in kwargs.items():
+ default_kwargs[key] = value
+
+ return ctk.CTkScrollableFrame(parent, **default_kwargs)
+
+ def create_styled_label(self, parent, text, **kwargs):
+ """إنشاء تسمية منسقة"""
+ default_kwargs = {
+ "text": text,
+ "font": self.get_font("body"),
+ "text_color": self.get_color("fg_color")
+ }
+
+ # دمج الخصائص المخصصة مع الخصائص الافتراضية
+ for key, value in kwargs.items():
+ default_kwargs[key] = value
+
+ return ctk.CTkLabel(parent, **default_kwargs)
+
+ def create_styled_button(self, parent, text, **kwargs):
+ """إنشاء زر منسق"""
+ default_kwargs = {
+ "text": text,
+ "font": self.get_font("body"),
+ "fg_color": self.get_color("button_bg_color"),
+ "hover_color": self.get_color("button_hover_color"),
+ "text_color": "white",
+ "corner_radius": 8,
+ "border_width": 0,
+ "height": 36
+ }
+
+ # إضافة أيقونة إذا تم تحديدها
+ if "icon" in kwargs:
+ icon_name = kwargs.pop("icon")
+ # هنا يمكن إضافة منطق لتحميل الأيقونة
+
+ # دمج الخصائص المخصصة مع الخصائص الافتراضية
+ for key, value in kwargs.items():
+ default_kwargs[key] = value
+
+ return ctk.CTkButton(parent, **default_kwargs)
+
+ def create_styled_entry(self, parent, placeholder_text, **kwargs):
+ """إنشاء حقل إدخال منسق"""
+ default_kwargs = {
+ "placeholder_text": placeholder_text,
+ "font": self.get_font("body"),
+ "fg_color": self.get_color("input_bg_color"),
+ "text_color": self.get_color("input_fg_color"),
+ "placeholder_text_color": self.get_color("border_color"),
+ "corner_radius": 8,
+ "border_width": 1,
+ "border_color": self.get_color("border_color"),
+ "height": 36
+ }
+
+ # دمج الخصائص المخصصة مع الخصائص الافتراضية
+ for key, value in kwargs.items():
+ default_kwargs[key] = value
+
+ return ctk.CTkEntry(parent, **default_kwargs)
+
+ def create_styled_textbox(self, parent, **kwargs):
+ """إنشاء مربع نص منسق"""
+ default_kwargs = {
+ "font": self.get_font("body"),
+ "fg_color": self.get_color("input_bg_color"),
+ "text_color": self.get_color("input_fg_color"),
+ "corner_radius": 8,
+ "border_width": 1,
+ "border_color": self.get_color("border_color")
+ }
+
+ # دمج الخصائص المخصصة مع الخصائص الافتراضية
+ for key, value in kwargs.items():
+ default_kwargs[key] = value
+
+ return ctk.CTkTextbox(parent, **default_kwargs)
+
+ def create_styled_combobox(self, parent, values, **kwargs):
+ """إنشاء قائمة منسدلة منسقة"""
+ default_kwargs = {
+ "values": values,
+ "font": self.get_font("body"),
+ "fg_color": self.get_color("input_bg_color"),
+ "text_color": self.get_color("input_fg_color"),
+ "border_color": self.get_color("border_color"),
+ "button_color": self.get_color("button_bg_color"),
+ "button_hover_color": self.get_color("button_hover_color"),
+ "dropdown_fg_color": self.get_color("card_bg_color"),
+ "dropdown_text_color": self.get_color("fg_color"),
+ "dropdown_hover_color": self.get_color("sidebar_hover_color"),
+ "corner_radius": 8,
+ "border_width": 1,
+ "dropdown_font": self.get_font("body")
+ }
+
+ # دمج الخصائص المخصصة مع الخصائص الافتراضية
+ for key, value in kwargs.items():
+ default_kwargs[key] = value
+
+ return ctk.CTkComboBox(parent, **default_kwargs)
+
+ def create_styled_switch(self, parent, text, **kwargs):
+ """إنشاء مفتاح تبديل منسق"""
+ default_kwargs = {
+ "text": text,
+ "font": self.get_font("body"),
+ "fg_color": self.get_color("border_color"),
+ "progress_color": self.get_color("button_bg_color"),
+ "button_color": "white",
+ "button_hover_color": "white",
+ "text_color": self.get_color("fg_color")
+ }
+
+ # دمج الخصائص المخصصة مع الخصائص الافتراضية
+ for key, value in kwargs.items():
+ default_kwargs[key] = value
+
+ return ctk.CTkSwitch(parent, **default_kwargs)
+
+ def create_styled_radio_button(self, parent, text, variable, value, **kwargs):
+ """إنشاء زر راديو منسق"""
+ default_kwargs = {
+ "text": text,
+ "font": self.get_font("body"),
+ "fg_color": self.get_color("button_bg_color"),
+ "border_color": self.get_color("border_color"),
+ "hover_color": self.get_color("button_hover_color"),
+ "text_color": self.get_color("fg_color"),
+ "variable": variable,
+ "value": value
+ }
+
+ # دمج الخصائص المخصصة مع الخصائص الافتراضية
+ for key, value in kwargs.items():
+ default_kwargs[key] = value
+
+ return ctk.CTkRadioButton(parent, **default_kwargs)
+
+ def create_styled_slider(self, parent, **kwargs):
+ """إنشاء شريط تمرير منسق"""
+ default_kwargs = {
+ "fg_color": self.get_color("border_color"),
+ "progress_color": self.get_color("button_bg_color"),
+ "button_color": self.get_color("button_bg_color"),
+ "button_hover_color": self.get_color("button_hover_color")
+ }
+
+ # دمج الخصائص المخصصة مع الخصائص الافتراضية
+ for key, value in kwargs.items():
+ default_kwargs[key] = value
+
+ return ctk.CTkSlider(parent, **default_kwargs)
+
+ def create_styled_tabview(self, parent, **kwargs):
+ """إنشاء عرض تبويب منسق"""
+ default_kwargs = {
+ "fg_color": self.get_color("card_bg_color"),
+ "segmented_button_fg_color": self.get_color("sidebar_bg_color"),
+ "segmented_button_selected_color": self.get_color("button_bg_color"),
+ "segmented_button_unselected_color": self.get_color("sidebar_bg_color"),
+ "segmented_button_selected_hover_color": self.get_color("button_hover_color"),
+ "segmented_button_unselected_hover_color": self.get_color("sidebar_hover_color"),
+ "segmented_button_text_color": self.get_color("sidebar_fg_color"),
+ "segmented_button_selected_text_color": "white",
+ "text_color": self.get_color("fg_color"),
+ "corner_radius": 10
+ }
+
+ # دمج الخصائص المخصصة مع الخصائص الافتراضية
+ for key, value in kwargs.items():
+ default_kwargs[key] = value
+
+ return ctk.CTkTabview(parent, **default_kwargs)
+
+ def create_styled_sidebar_button(self, parent, text, icon, command=None):
+ """إنشاء زر الشريط الجانبي المنسق"""
+ # إنشاء إطار للزر
+ button_frame = ctk.CTkFrame(
+ parent,
+ fg_color="transparent",
+ corner_radius=0
+ )
+
+ # إنشاء الزر
+ button = ctk.CTkButton(
+ button_frame,
+ text=text,
+ font=self.get_font("body"),
+ fg_color="transparent",
+ hover_color=self.get_color("sidebar_hover_color"),
+ text_color=self.get_color("sidebar_fg_color"),
+ anchor="w",
+ corner_radius=0,
+ border_width=0,
+ height=40,
+ command=command
+ )
+ button.pack(fill="x", padx=0, pady=0)
+
+ return button_frame, button
+
+ def create_styled_card(self, parent, title):
+ """إنشاء بطاقة منسقة"""
+ # إنشاء إطار البطاقة
+ card = self.create_styled_frame(
+ parent,
+ fg_color=self.get_color("card_bg_color")
+ )
+
+ # إنشاء عنوان البطاقة
+ title_label = self.create_styled_label(
+ card,
+ title,
+ font=self.get_font("heading")
+ )
+ title_label.pack(anchor="w", padx=15, pady=(15, 5))
+
+ # إنشاء خط فاصل
+ separator = ctk.CTkFrame(
+ card,
+ height=1,
+ fg_color=self.get_color("border_color")
+ )
+ separator.pack(fill="x", padx=15, pady=(5, 0))
+
+ # إنشاء إطار المحتوى
+ content_frame = self.create_styled_frame(
+ card,
+ fg_color="transparent"
+ )
+ content_frame.pack(fill="both", expand=True, padx=15, pady=15)
+
+ return card, content_frame
+
+ def create_styled_data_table(self, parent, columns, data):
+ """إنشاء جدول بيانات منسق"""
+ # إنشاء إطار الجدول
+ table_frame = self.create_styled_frame(
+ parent,
+ fg_color="transparent"
+ )
+
+ # إنشاء إطار العناوين
+ header_frame = self.create_styled_frame(
+ table_frame,
+ fg_color=self.get_color("sidebar_bg_color")
+ )
+ header_frame.pack(fill="x", padx=0, pady=(0, 1))
+
+ # إنشاء عناوين الأعمدة
+ for i, column in enumerate(columns):
+ column_label = self.create_styled_label(
+ header_frame,
+ column,
+ font=self.get_font("heading"),
+ text_color=self.get_color("sidebar_fg_color")
+ )
+ column_label.grid(row=0, column=i, sticky="ew", padx=10, pady=10)
+ header_frame.grid_columnconfigure(i, weight=1)
+
+ # إنشاء إطار البيانات
+ data_frame = self.create_styled_scrollable_frame(
+ table_frame,
+ fg_color="transparent"
+ )
+ data_frame.pack(fill="both", expand=True, padx=0, pady=0)
+
+ # إنشاء صفوف البيانات
+ for i, row in enumerate(data):
+ row_frame = self.create_styled_frame(
+ data_frame,
+ fg_color=self.get_color("card_bg_color") if i % 2 == 0 else self.get_color("bg_color")
+ )
+ row_frame.pack(fill="x", padx=0, pady=(0, 1))
+
+ for j, cell in enumerate(row):
+ cell_label = self.create_styled_label(
+ row_frame,
+ cell,
+ font=self.get_font("body")
+ )
+ cell_label.grid(row=0, column=j, sticky="ew", padx=10, pady=10)
+ row_frame.grid_columnconfigure(j, weight=1)
+
+ return table_frame, data_frame
+
+ def create_styled_message_box(self, title, message, message_type="info"):
+ """إنشاء مربع رسالة منسق"""
+ # تحديد لون الرسالة بناءً على النوع
+ if message_type == "error":
+ color = self.get_color("error_color")
+ elif message_type == "warning":
+ color = self.get_color("warning_color")
+ elif message_type == "success":
+ color = self.get_color("success_color")
+ else: # info
+ color = self.get_color("primary_color")
+
+ # إنشاء نافذة الرسالة
+ message_window = ctk.CTkToplevel()
+ message_window.title(title)
+ message_window.geometry("400x200")
+ message_window.resizable(False, False)
+ message_window.grab_set() # جعل النافذة المنبثقة مركز الاهتمام
+
+ # تطبيق النمط
+ message_window.configure(fg_color=self.get_color("bg_color"))
+
+ # إنشاء إطار الرسالة
+ message_frame = self.create_styled_frame(
+ message_window,
+ fg_color=self.get_color("card_bg_color")
+ )
+ message_frame.pack(fill="both", expand=True, padx=20, pady=20)
+
+ # إنشاء عنوان الرسالة
+ title_label = self.create_styled_label(
+ message_frame,
+ title,
+ font=self.get_font("heading"),
+ text_color=color
+ )
+ title_label.pack(padx=20, pady=(20, 10))
+
+ # إنشاء نص الرسالة
+ message_label = self.create_styled_label(
+ message_frame,
+ message,
+ font=self.get_font("body")
+ )
+ message_label.pack(padx=20, pady=(0, 20))
+
+ # إنشاء زر موافق
+ ok_button = self.create_styled_button(
+ message_frame,
+ "موافق",
+ fg_color=color,
+ hover_color=color,
+ command=message_window.destroy
+ )
+ ok_button.pack(pady=(0, 20))
+
+ return message_window
diff --git a/tests/test_app.py b/tests/test_app.py
index cfe1f751292097e6ad341ab425ae1e5eca526672..afb943bfbdb58b978ef04b8f8a27f92af5ee1c8b 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -1,601 +1,601 @@
-"""
-وحدة اختبار التطبيق لنظام إدارة المناقصات - Hybrid Face
-"""
-
-import os
-import sys
-import logging
-import unittest
-import tkinter as tk
-from pathlib import Path
-
-# تهيئة السجل
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger('test_app')
-
-# إضافة المسار الرئيسي للتطبيق إلى مسار البحث
-sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-# استيراد الوحدات المطلوبة للاختبار
-from database.db_connector import DatabaseConnector
-from database.models import User, Project, Document, ProjectItem, Resource, Risk, Report, SystemLog
-from modules.document_analysis.analyzer import DocumentAnalyzer
-from modules.pricing.pricing_engine import PricingEngine
-from modules.risk_analysis.risk_analyzer import RiskAnalyzer
-from modules.ai_assistant.assistant import AIAssistant
-from styling.theme import AppTheme
-from styling.icons import IconGenerator
-from styling.charts import ChartGenerator
-from config import AppConfig
-
-class TestDatabaseConnector(unittest.TestCase):
- """اختبار وحدة اتصال قاعدة البيانات"""
-
- def setUp(self):
- """إعداد بيئة الاختبار"""
- # استخدام قاعدة بيانات اختبار مؤقتة
- self.config = AppConfig()
- self.config.DB_NAME = "test_tender_system.db"
- self.db = DatabaseConnector(self.config)
-
- def tearDown(self):
- """تنظيف بيئة الاختبار"""
- self.db.disconnect()
- # حذف قاعدة البيانات المؤقتة
- if os.path.exists(self.config.DB_NAME):
- os.remove(self.config.DB_NAME)
-
- def test_connection(self):
- """اختبار الاتصال بقاعدة البيانات"""
- self.assertTrue(self.db.is_connected)
-
- def test_execute_query(self):
- """اختبار تنفيذ استعلام"""
- cursor = self.db.execute("SELECT 1")
- self.assertIsNotNone(cursor)
- result = cursor.fetchone()
- self.assertEqual(result[0], 1)
-
- def test_insert_and_fetch(self):
- """اختبار إدراج واسترجاع البيانات"""
- # إدراج بيانات
- user_data = {
- "username": "test_user",
- "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
- "full_name": "مستخدم اختبار",
- "email": "test@example.com",
- "role": "user"
- }
- user_id = self.db.insert("users", user_data)
- self.assertIsNotNone(user_id)
-
- # استرجاع البيانات
- user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
- self.assertIsNotNone(user)
- self.assertEqual(user["username"], "test_user")
- self.assertEqual(user["full_name"], "مستخدم اختبار")
-
- def test_update(self):
- """اختبار تحديث البيانات"""
- # إدراج بيانات
- user_data = {
- "username": "update_user",
- "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
- "full_name": "مستخدم للتحديث",
- "email": "update@example.com",
- "role": "user"
- }
- user_id = self.db.insert("users", user_data)
-
- # تحديث البيانات
- updated_data = {
- "full_name": "مستخدم تم تحديثه",
- "role": "admin"
- }
- rows_affected = self.db.update("users", updated_data, "id = ?", (user_id,))
- self.assertEqual(rows_affected, 1)
-
- # التحقق من التحديث
- user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
- self.assertEqual(user["full_name"], "مستخدم تم تحديثه")
- self.assertEqual(user["role"], "admin")
-
- def test_delete(self):
- """اختبار حذف البيانات"""
- # إدراج بيانات
- user_data = {
- "username": "delete_user",
- "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
- "full_name": "مستخدم للحذف",
- "email": "delete@example.com",
- "role": "user"
- }
- user_id = self.db.insert("users", user_data)
-
- # حذف البيانات
- rows_affected = self.db.delete("users", "id = ?", (user_id,))
- self.assertEqual(rows_affected, 1)
-
- # التحقق من الحذف
- user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
- self.assertIsNone(user)
-
-
-class TestModels(unittest.TestCase):
- """اختبار نماذج البيانات"""
-
- def setUp(self):
- """إعداد بيئة الاختبار"""
- # استخدام قاعدة بيانات اختبار مؤقتة
- self.config = AppConfig()
- self.config.DB_NAME = "test_models.db"
- self.db = DatabaseConnector(self.config)
-
- def tearDown(self):
- """تنظيف بيئة الاختبار"""
- self.db.disconnect()
- # حذف قاعدة البيانات المؤقتة
- if os.path.exists(self.config.DB_NAME):
- os.remove(self.config.DB_NAME)
-
- def test_user_model(self):
- """اختبار نموذج المستخدم"""
- # إنشاء مستخدم جديد
- user = User(self.db)
- user.data = {
- "username": "model_user",
- "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
- "full_name": "مستخدم نموذج",
- "email": "model@example.com",
- "role": "user",
- "is_active": 1
- }
-
- # حفظ المستخدم
- self.assertTrue(user.save())
- self.assertIsNotNone(user.data.get("id"))
-
- # استرجاع المستخدم
- retrieved_user = User.get_by_id(user.data["id"], self.db)
- self.assertIsNotNone(retrieved_user)
- self.assertEqual(retrieved_user.data["username"], "model_user")
-
- # مصادقة المستخدم
- authenticated_user = User.authenticate("model_user", "admin", self.db)
- self.assertIsNotNone(authenticated_user)
- self.assertEqual(authenticated_user.data["username"], "model_user")
-
- # تعيين كلمة مرور جديدة
- user.set_password("newpassword")
- user.save()
-
- # مصادقة المستخدم بكلمة المرور الجديدة
- authenticated_user = User.authenticate("model_user", "newpassword", self.db)
- self.assertIsNotNone(authenticated_user)
-
- # حذف المستخدم
- self.assertTrue(user.delete())
-
- def test_project_model(self):
- """اختبار نموذج المشروع"""
- # إنشاء مستخدم للمشروع
- user = User(self.db)
- user.data = {
- "username": "project_user",
- "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
- "full_name": "مستخدم المشروع",
- "email": "project@example.com",
- "role": "user",
- "is_active": 1
- }
- user.save()
-
- # إنشاء مشروع جديد
- project = Project(self.db)
- project.data = {
- "name": "مشروع اختبار",
- "client": "عميل اختبار",
- "description": "وصف مشروع الاختبار",
- "start_date": "2025-01-01",
- "end_date": "2025-12-31",
- "status": "تخطيط",
- "created_by": user.data["id"]
- }
-
- # حفظ المشروع
- self.assertTrue(project.save())
- self.assertIsNotNone(project.data.get("id"))
-
- # استرجاع المشروع
- retrieved_project = Project.get_by_id(project.data["id"], self.db)
- self.assertIsNotNone(retrieved_project)
- self.assertEqual(retrieved_project.data["name"], "مشروع اختبار")
-
- # إضافة بند للمشروع
- item = ProjectItem(self.db)
- item.data = {
- "project_id": project.data["id"],
- "name": "بند اختبار",
- "description": "وصف بند الاختبار",
- "unit": "م²",
- "quantity": 100,
- "unit_price": 500,
- "total_price": 50000
- }
- item.save()
-
- # حساب التكلفة الإجمالية للمشروع
- total_cost = project.calculate_total_cost()
- self.assertEqual(total_cost, 50000)
-
- # حذف المشروع
- self.assertTrue(project.delete())
-
- # حذف المستخدم
- self.assertTrue(user.delete())
-
-
-class TestDocumentAnalyzer(unittest.TestCase):
- """اختبار محلل المستندات"""
-
- def setUp(self):
- """إعداد بيئة الاختبار"""
- self.config = AppConfig()
- self.analyzer = DocumentAnalyzer(self.config)
-
- # إنشاء مجلد المستندات للاختبار
- self.test_docs_dir = Path("test_documents")
- self.test_docs_dir.mkdir(exist_ok=True)
-
- # إنشاء ملف مستند اختبار
- self.test_doc_path = self.test_docs_dir / "test_document.txt"
- with open(self.test_doc_path, "w", encoding="utf-8") as f:
- f.write("هذا مستند اختبار لمحلل المستندات")
-
- def tearDown(self):
- """تنظيف بيئة الاختبار"""
- # حذف ملف المستند
- if self.test_doc_path.exists():
- self.test_doc_path.unlink()
-
- # حذف مجلد المستندات
- if self.test_docs_dir.exists():
- self.test_docs_dir.rmdir()
-
- def test_analyze_document(self):
- """اختبار تحليل المستند"""
- # تحليل المستند
- result = self.analyzer.analyze_document(str(self.test_doc_path), "tender")
- self.assertTrue(result)
-
- # انتظار اكتمال التحليل
- import time
- max_wait = 5 # ثوانٍ
- waited = 0
- while self.analyzer.analysis_in_progress and waited < max_wait:
- time.sleep(0.5)
- waited += 0.5
-
- # التحقق من نتائج التحليل
- self.assertFalse(self.analyzer.analysis_in_progress)
- results = self.analyzer.get_analysis_results()
- self.assertEqual(results["status"], "اكتمل التحليل")
- self.assertEqual(results["document_path"], str(self.test_doc_path))
-
- def test_export_analysis_results(self):
- """اختبار تصدير نتائج التحليل"""
- # تحليل المستند
- self.analyzer.analyze_document(str(self.test_doc_path), "tender")
-
- # انتظار اكتمال التحليل
- import time
- max_wait = 5 # ثوانٍ
- waited = 0
- while self.analyzer.analysis_in_progress and waited < max_wait:
- time.sleep(0.5)
- waited += 0.5
-
- # تصدير النتائج
- export_path = self.test_docs_dir / "analysis_results.json"
- result_path = self.analyzer.export_analysis_results(str(export_path))
- self.assertIsNotNone(result_path)
-
- # التحقق من وجود ملف التصدير
- self.assertTrue(export_path.exists())
-
- # حذف ملف التصدير
- if export_path.exists():
- export_path.unlink()
-
-
-class TestPricingEngine(unittest.TestCase):
- """اختبار محرك التسعير"""
-
- def setUp(self):
- """إعداد بيئة الاختبار"""
- self.config = AppConfig()
- self.pricing_engine = PricingEngine(self.config)
-
- def test_calculate_pricing(self):
- """اختبار حساب التسعير"""
- # حساب التسعير
- result = self.pricing_engine.calculate_pricing(1, "comprehensive")
- self.assertTrue(result)
-
- # انتظار اكتمال التسعير
- import time
- max_wait = 5 # ثوانٍ
- waited = 0
- while self.pricing_engine.pricing_in_progress and waited < max_wait:
- time.sleep(0.5)
- waited += 0.5
-
- # التحقق من نتائج التسعير
- self.assertFalse(self.pricing_engine.pricing_in_progress)
- results = self.pricing_engine.get_pricing_results()
- self.assertEqual(results["status"], "اكتمل التسعير")
- self.assertEqual(results["project_id"], 1)
- self.assertEqual(results["strategy"], "comprehensive")
-
- # التحقق من وجود التكاليف المباشرة
- self.assertIn("direct_costs", results)
- self.assertIn("total_direct_costs", results["direct_costs"])
-
- # التحقق من وجود التكاليف غير المباشرة
- self.assertIn("indirect_costs", results)
- self.assertIn("total_indirect_costs", results["indirect_costs"])
-
- # التحقق من وجود تكاليف المخاطر
- self.assertIn("risk_costs", results)
- self.assertIn("total_risk_cost", results["risk_costs"])
-
- # التحقق من وجود ملخص التسعير
- self.assertIn("summary", results)
- self.assertIn("final_price", results["summary"])
-
-
-class TestRiskAnalyzer(unittest.TestCase):
- """اختبار محلل المخاطر"""
-
- def setUp(self):
- """إعداد بيئة الاختبار"""
- self.config = AppConfig()
- self.risk_analyzer = RiskAnalyzer(self.config)
-
- def test_analyze_risks(self):
- """اختبار تحليل المخاطر"""
- # تحليل المخاطر
- result = self.risk_analyzer.analyze_risks(1, "comprehensive")
- self.assertTrue(result)
-
- # انتظار اكتمال التحليل
- import time
- max_wait = 5 # ثوانٍ
- waited = 0
- while self.risk_analyzer.analysis_in_progress and waited < max_wait:
- time.sleep(0.5)
- waited += 0.5
-
- # التحقق من نتائج التحليل
- self.assertFalse(self.risk_analyzer.analysis_in_progress)
- results = self.risk_analyzer.get_analysis_results()
- self.assertEqual(results["status"], "اكتمل التحليل")
- self.assertEqual(results["project_id"], 1)
- self.assertEqual(results["method"], "comprehensive")
-
- # التحقق من وجود المخاطر المحددة
- self.assertIn("identified_risks", results)
- self.assertTrue(len(results["identified_risks"]) > 0)
-
- # التحقق من وجود فئات المخاطر
- self.assertIn("risk_categories", results)
-
- # التحقق من وجود مصفوفة المخاطر
- self.assertIn("risk_matrix", results)
-
- # التحقق من وجود استراتيجيات التخفيف
- self.assertIn("mitigation_strategies", results)
- self.assertTrue(len(results["mitigation_strategies"]) > 0)
-
- # التحقق من وجود ملخص التحليل
- self.assertIn("summary", results)
- self.assertIn("overall_risk_level", results["summary"])
-
-
-class TestAIAssistant(unittest.TestCase):
- """اختبار المساعد الذكي"""
-
- def setUp(self):
- """إعداد بيئة الاختبار"""
- self.config = AppConfig()
- self.assistant = AIAssistant(self.config)
-
- def test_process_query(self):
- """اختبار معالجة الاستعلام"""
- # معالجة استعلام
- query = "كيف يمكنني تحليل مستند مناقصة؟"
- result = self.assistant.process_query(query)
- self.assertTrue(result)
-
- # انتظار اكتمال المعالجة
- import time
- max_wait = 5 # ثوانٍ
- waited = 0
- while self.assistant.processing_in_progress and waited < max_wait:
- time.sleep(0.5)
- waited += 0.5
-
- # التحقق من نتائج المعالجة
- self.assertFalse(self.assistant.processing_in_progress)
- results = self.assistant.get_processing_results()
- self.assertEqual(results["status"], "اكتملت المعالجة")
- self.assertEqual(results["query"], query)
-
- # التحقق من وجود استجابة
- self.assertIn("response", results)
- self.assertTrue(len(results["response"]) > 0)
-
- # التحقق من وجود اقتراحات
- self.assertIn("suggestions", results)
- self.assertTrue(len(results["suggestions"]) > 0)
-
- def test_conversation_history(self):
- """اختبار سجل المحادثة"""
- # معالجة استعلام
- query = "ما هي استراتيجيات التسعير المتاحة؟"
- self.assistant.process_query(query)
-
- # انتظار اكتمال المعالجة
- import time
- max_wait = 5 # ثوانٍ
- waited = 0
- while self.assistant.processing_in_progress and waited < max_wait:
- time.sleep(0.5)
- waited += 0.5
-
- # التحقق من سجل المحادثة
- history = self.assistant.get_conversation_history()
- self.assertEqual(len(history), 2) # استعلام المستخدم واستجابة المساعد
- self.assertEqual(history[0]["role"], "user")
- self.assertEqual(history[0]["content"], query)
- self.assertEqual(history[1]["role"], "assistant")
-
- # مسح سجل المحادثة
- self.assertTrue(self.assistant.clear_conversation_history())
- history = self.assistant.get_conversation_history()
- self.assertEqual(len(history), 0)
-
-
-class TestStyling(unittest.TestCase):
- """اختبار وحدات التصميم"""
-
- def test_app_theme(self):
- """اختبار نمط التطبيق"""
- theme = AppTheme()
-
- # التحقق من الألوان
- self.assertIsNotNone(theme.get_color("bg_color"))
- self.assertIsNotNone(theme.get_color("fg_color"))
-
- # التحقق من الخطوط
- self.assertIsNotNone(theme.get_font("body"))
- self.assertIsNotNone(theme.get_font("title"))
-
- # التحقق من الأحجام
- self.assertIsNotNone(theme.get_size("padding_medium"))
- self.assertIsNotNone(theme.get_size("border_radius"))
-
- # تغيير النمط
- self.assertTrue(theme.set_theme("dark"))
- self.assertEqual(theme.current_theme, "dark")
-
- # تغيير اللغة
- self.assertTrue(theme.set_language("en"))
- self.assertEqual(theme.current_language, "en")
-
- def test_icon_generator(self):
- """اختبار مولد الأيقونات"""
- icon_generator = IconGenerator()
-
- # توليد الأيقونات الافتراضية
- icon_generator.generate_default_icons()
-
- # التحقق من وجود مجلد الأيقونات
- self.assertTrue(Path('assets/icons').exists())
-
- # التحقق من وجود بعض الأيقونات
- self.assertTrue(Path('assets/icons/dashboard.png').exists())
- self.assertTrue(Path('assets/icons/projects.png').exists())
-
- def test_chart_generator(self):
- """اختبار مولد الرسوم البيانية"""
- theme = AppTheme()
- chart_generator = ChartGenerator(theme)
-
- # إنشاء بيانات للرسم البياني الشريطي
- bar_data = {
- 'labels': ['الربع الأول', 'الربع الثاني', 'الربع الثالث', 'الربع الرابع'],
- 'values': [15000, 20000, 18000, 25000]
- }
-
- # إنشاء رسم بياني شريطي
- fig = chart_generator.create_bar_chart(
- bar_data,
- 'الإيرادات الفصلية',
- 'الفصل',
- 'الإيرادات (ريال)'
- )
-
- # التحقق من إنشاء الرسم البياني
- self.assertIsNotNone(fig)
-
- # حفظ الرسم البياني
- save_path = 'test_chart.png'
- chart_generator.create_bar_chart(
- bar_data,
- 'الإيرادات الفصلية',
- 'الفصل',
- 'الإيرادات (ريال)',
- save_path=save_path
- )
-
- # التحقق من وجود ملف الرسم البياني
- self.assertTrue(Path(save_path).exists())
-
- # حذف ملف الرسم البياني
- if Path(save_path).exists():
- Path(save_path).unlink()
-
-
-def run_tests():
- """تشغيل الاختبارات"""
- # إنشاء مجلد الاختبارات
- test_dir = Path('test_results')
- test_dir.mkdir(exist_ok=True)
-
- # إنشاء ملف لنتائج الاختبارات
- test_results_file = test_dir / 'test_results.txt'
-
- # تشغيل الاختبارات وحفظ النتائج
- with open(test_results_file, 'w', encoding='utf-8') as f:
- runner = unittest.TextTestRunner(stream=f, verbosity=2)
- suite = unittest.TestSuite()
-
- # إضافة اختبارات قاعدة البيانات
- suite.addTest(unittest.makeSuite(TestDatabaseConnector))
- suite.addTest(unittest.makeSuite(TestModels))
-
- # إضافة اختبارات الوحدات
- suite.addTest(unittest.makeSuite(TestDocumentAnalyzer))
- suite.addTest(unittest.makeSuite(TestPricingEngine))
- suite.addTest(unittest.makeSuite(TestRiskAnalyzer))
- suite.addTest(unittest.makeSuite(TestAIAssistant))
-
- # إضافة اختبارات التصميم
- suite.addTest(unittest.makeSuite(TestStyling))
-
- # تشغيل الاختبارات
- result = runner.run(suite)
-
- # كتابة ملخص النتائج
- f.write("\n\n=== ملخص نتائج الاختبارات ===\n")
- f.write(f"عدد الاختبارات: {result.testsRun}\n")
- f.write(f"عدد النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}\n")
- f.write(f"عدد الإخفاقات: {len(result.failures)}\n")
- f.write(f"عدد الأخطاء: {len(result.errors)}\n")
-
- # طباعة ملخص النتائج
- logger.info(f"تم تشغيل {result.testsRun} اختبار")
- logger.info(f"النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}")
- logger.info(f"الإخفاقات: {len(result.failures)}")
- logger.info(f"الأخطاء: {len(result.errors)}")
- logger.info(f"تم حفظ نتائج الاختبارات في: {test_results_file}")
-
- return result
-
-
-if __name__ == "__main__":
- run_tests()
+"""
+وحدة اختبار التطبيق لنظام إدارة المناقصات - Hybrid Face
+"""
+
+import os
+import sys
+import logging
+import unittest
+import tkinter as tk
+from pathlib import Path
+
+# تهيئة السجل
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger('test_app')
+
+# إضافة المسار الرئيسي للتطبيق إلى مسار البحث
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+# استيراد الوحدات المطلوبة للاختبار
+from database.db_connector import DatabaseConnector
+from database.models import User, Project, Document, ProjectItem, Resource, Risk, Report, SystemLog
+from modules.document_analysis.analyzer import DocumentAnalyzer
+from modules.pricing.pricing_engine import PricingEngine
+from modules.risk_analysis.risk_analyzer import RiskAnalyzer
+from modules.ai_assistant.assistant import AIAssistant
+from styling.theme import AppTheme
+from styling.icons import IconGenerator
+from styling.charts import ChartGenerator
+from config import AppConfig
+
+class TestDatabaseConnector(unittest.TestCase):
+ """اختبار وحدة اتصال قاعدة البيانات"""
+
+ def setUp(self):
+ """إعداد بيئة الاختبار"""
+ # استخدام قاعدة بيانات اختبار مؤقتة
+ self.config = AppConfig()
+ self.config.DB_NAME = "test_tender_system.db"
+ self.db = DatabaseConnector(self.config)
+
+ def tearDown(self):
+ """تنظيف بيئة الاختبار"""
+ self.db.disconnect()
+ # حذف قاعدة البيانات المؤقتة
+ if os.path.exists(self.config.DB_NAME):
+ os.remove(self.config.DB_NAME)
+
+ def test_connection(self):
+ """اختبار الاتصال بقاعدة البيانات"""
+ self.assertTrue(self.db.is_connected)
+
+ def test_execute_query(self):
+ """اختبار تنفيذ استعلام"""
+ cursor = self.db.execute("SELECT 1")
+ self.assertIsNotNone(cursor)
+ result = cursor.fetchone()
+ self.assertEqual(result[0], 1)
+
+ def test_insert_and_fetch(self):
+ """اختبار إدراج واسترجاع البيانات"""
+ # إدراج بيانات
+ user_data = {
+ "username": "test_user",
+ "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
+ "full_name": "مستخدم اختبار",
+ "email": "test@example.com",
+ "role": "user"
+ }
+ user_id = self.db.insert("users", user_data)
+ self.assertIsNotNone(user_id)
+
+ # استرجاع البيانات
+ user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
+ self.assertIsNotNone(user)
+ self.assertEqual(user["username"], "test_user")
+ self.assertEqual(user["full_name"], "مستخدم اختبار")
+
+ def test_update(self):
+ """اختبار تحديث البيانات"""
+ # إدراج بيانات
+ user_data = {
+ "username": "update_user",
+ "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
+ "full_name": "مستخدم للتحديث",
+ "email": "update@example.com",
+ "role": "user"
+ }
+ user_id = self.db.insert("users", user_data)
+
+ # تحديث البيانات
+ updated_data = {
+ "full_name": "مستخدم تم تحديثه",
+ "role": "admin"
+ }
+ rows_affected = self.db.update("users", updated_data, "id = ?", (user_id,))
+ self.assertEqual(rows_affected, 1)
+
+ # التحقق من التحديث
+ user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
+ self.assertEqual(user["full_name"], "مستخدم تم تحديثه")
+ self.assertEqual(user["role"], "admin")
+
+ def test_delete(self):
+ """اختبار حذف البيانات"""
+ # إدراج بيانات
+ user_data = {
+ "username": "delete_user",
+ "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
+ "full_name": "مستخدم للحذف",
+ "email": "delete@example.com",
+ "role": "user"
+ }
+ user_id = self.db.insert("users", user_data)
+
+ # حذف البيانات
+ rows_affected = self.db.delete("users", "id = ?", (user_id,))
+ self.assertEqual(rows_affected, 1)
+
+ # التحقق من الحذف
+ user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
+ self.assertIsNone(user)
+
+
+class TestModels(unittest.TestCase):
+ """اختبار نماذج البيانات"""
+
+ def setUp(self):
+ """إعداد بيئة الاختبار"""
+ # استخدام قاعدة بيانات اختبار مؤقتة
+ self.config = AppConfig()
+ self.config.DB_NAME = "test_models.db"
+ self.db = DatabaseConnector(self.config)
+
+ def tearDown(self):
+ """تنظيف بيئة الاختبار"""
+ self.db.disconnect()
+ # حذف قاعدة البيانات المؤقتة
+ if os.path.exists(self.config.DB_NAME):
+ os.remove(self.config.DB_NAME)
+
+ def test_user_model(self):
+ """اختبار نموذج المستخدم"""
+ # إنشاء مستخدم جديد
+ user = User(self.db)
+ user.data = {
+ "username": "model_user",
+ "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
+ "full_name": "مستخدم نموذج",
+ "email": "model@example.com",
+ "role": "user",
+ "is_active": 1
+ }
+
+ # حفظ المستخدم
+ self.assertTrue(user.save())
+ self.assertIsNotNone(user.data.get("id"))
+
+ # استرجاع المستخدم
+ retrieved_user = User.get_by_id(user.data["id"], self.db)
+ self.assertIsNotNone(retrieved_user)
+ self.assertEqual(retrieved_user.data["username"], "model_user")
+
+ # مصادقة المستخدم
+ authenticated_user = User.authenticate("model_user", "admin", self.db)
+ self.assertIsNotNone(authenticated_user)
+ self.assertEqual(authenticated_user.data["username"], "model_user")
+
+ # تعيين كلمة مرور جديدة
+ user.set_password("newpassword")
+ user.save()
+
+ # مصادقة المستخدم بكلمة المرور الجديدة
+ authenticated_user = User.authenticate("model_user", "newpassword", self.db)
+ self.assertIsNotNone(authenticated_user)
+
+ # حذف المستخدم
+ self.assertTrue(user.delete())
+
+ def test_project_model(self):
+ """اختبار نموذج المشروع"""
+ # إنشاء مستخدم للمشروع
+ user = User(self.db)
+ user.data = {
+ "username": "project_user",
+ "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
+ "full_name": "مستخدم المشروع",
+ "email": "project@example.com",
+ "role": "user",
+ "is_active": 1
+ }
+ user.save()
+
+ # إنشاء مشروع جديد
+ project = Project(self.db)
+ project.data = {
+ "name": "مشروع اختبار",
+ "client": "عميل اختبار",
+ "description": "وصف مشروع الاختبار",
+ "start_date": "2025-01-01",
+ "end_date": "2025-12-31",
+ "status": "تخطيط",
+ "created_by": user.data["id"]
+ }
+
+ # حفظ المشروع
+ self.assertTrue(project.save())
+ self.assertIsNotNone(project.data.get("id"))
+
+ # استرجاع المشروع
+ retrieved_project = Project.get_by_id(project.data["id"], self.db)
+ self.assertIsNotNone(retrieved_project)
+ self.assertEqual(retrieved_project.data["name"], "مشروع اختبار")
+
+ # إضافة بند للمشروع
+ item = ProjectItem(self.db)
+ item.data = {
+ "project_id": project.data["id"],
+ "name": "بند اختبار",
+ "description": "وصف بند الاختبار",
+ "unit": "م²",
+ "quantity": 100,
+ "unit_price": 500,
+ "total_price": 50000
+ }
+ item.save()
+
+ # حساب التكلفة الإجمالية للمشروع
+ total_cost = project.calculate_total_cost()
+ self.assertEqual(total_cost, 50000)
+
+ # حذف المشروع
+ self.assertTrue(project.delete())
+
+ # حذف المستخدم
+ self.assertTrue(user.delete())
+
+
+class TestDocumentAnalyzer(unittest.TestCase):
+ """اختبار محلل المستندات"""
+
+ def setUp(self):
+ """إعداد بيئة الاختبار"""
+ self.config = AppConfig()
+ self.analyzer = DocumentAnalyzer(self.config)
+
+ # إنشاء مجلد المستندات للاختبار
+ self.test_docs_dir = Path("test_documents")
+ self.test_docs_dir.mkdir(exist_ok=True)
+
+ # إنشاء ملف مستند اختبار
+ self.test_doc_path = self.test_docs_dir / "test_document.txt"
+ with open(self.test_doc_path, "w", encoding="utf-8") as f:
+ f.write("هذا مستند اختبار لمحلل المستندات")
+
+ def tearDown(self):
+ """تنظيف بيئة الاختبار"""
+ # حذف ملف المستند
+ if self.test_doc_path.exists():
+ self.test_doc_path.unlink()
+
+ # حذف مجلد المستندات
+ if self.test_docs_dir.exists():
+ self.test_docs_dir.rmdir()
+
+ def test_analyze_document(self):
+ """اختبار تحليل المستند"""
+ # تحليل المستند
+ result = self.analyzer.analyze_document(str(self.test_doc_path), "tender")
+ self.assertTrue(result)
+
+ # انتظار اكتمال التحليل
+ import time
+ max_wait = 5 # ثوانٍ
+ waited = 0
+ while self.analyzer.analysis_in_progress and waited < max_wait:
+ time.sleep(0.5)
+ waited += 0.5
+
+ # التحقق من نتائج التحليل
+ self.assertFalse(self.analyzer.analysis_in_progress)
+ results = self.analyzer.get_analysis_results()
+ self.assertEqual(results["status"], "اكتمل التحليل")
+ self.assertEqual(results["document_path"], str(self.test_doc_path))
+
+ def test_export_analysis_results(self):
+ """اختبار تصدير نتائج التحليل"""
+ # تحليل المستند
+ self.analyzer.analyze_document(str(self.test_doc_path), "tender")
+
+ # انتظار اكتمال التحليل
+ import time
+ max_wait = 5 # ثوانٍ
+ waited = 0
+ while self.analyzer.analysis_in_progress and waited < max_wait:
+ time.sleep(0.5)
+ waited += 0.5
+
+ # تصدير النتائج
+ export_path = self.test_docs_dir / "analysis_results.json"
+ result_path = self.analyzer.export_analysis_results(str(export_path))
+ self.assertIsNotNone(result_path)
+
+ # التحقق من وجود ملف التصدير
+ self.assertTrue(export_path.exists())
+
+ # حذف ملف التصدير
+ if export_path.exists():
+ export_path.unlink()
+
+
+class TestPricingEngine(unittest.TestCase):
+ """اختبار محرك التسعير"""
+
+ def setUp(self):
+ """إعداد بيئة الاختبار"""
+ self.config = AppConfig()
+ self.pricing_engine = PricingEngine(self.config)
+
+ def test_calculate_pricing(self):
+ """اختبار حساب التسعير"""
+ # حساب التسعير
+ result = self.pricing_engine.calculate_pricing(1, "comprehensive")
+ self.assertTrue(result)
+
+ # انتظار اكتمال التسعير
+ import time
+ max_wait = 5 # ثوانٍ
+ waited = 0
+ while self.pricing_engine.pricing_in_progress and waited < max_wait:
+ time.sleep(0.5)
+ waited += 0.5
+
+ # التحقق من نتائج التسعير
+ self.assertFalse(self.pricing_engine.pricing_in_progress)
+ results = self.pricing_engine.get_pricing_results()
+ self.assertEqual(results["status"], "اكتمل التسعير")
+ self.assertEqual(results["project_id"], 1)
+ self.assertEqual(results["strategy"], "comprehensive")
+
+ # التحقق من وجود التكاليف المباشرة
+ self.assertIn("direct_costs", results)
+ self.assertIn("total_direct_costs", results["direct_costs"])
+
+ # التحقق من وجود التكاليف غير المباشرة
+ self.assertIn("indirect_costs", results)
+ self.assertIn("total_indirect_costs", results["indirect_costs"])
+
+ # التحقق من وجود تكاليف المخاطر
+ self.assertIn("risk_costs", results)
+ self.assertIn("total_risk_cost", results["risk_costs"])
+
+ # التحقق من وجود ملخص التسعير
+ self.assertIn("summary", results)
+ self.assertIn("final_price", results["summary"])
+
+
+class TestRiskAnalyzer(unittest.TestCase):
+ """اختبار محلل المخاطر"""
+
+ def setUp(self):
+ """إعداد بيئة الاختبار"""
+ self.config = AppConfig()
+ self.risk_analyzer = RiskAnalyzer(self.config)
+
+ def test_analyze_risks(self):
+ """اختبار تحليل المخاطر"""
+ # تحليل المخاطر
+ result = self.risk_analyzer.analyze_risks(1, "comprehensive")
+ self.assertTrue(result)
+
+ # انتظار اكتمال التحليل
+ import time
+ max_wait = 5 # ثوانٍ
+ waited = 0
+ while self.risk_analyzer.analysis_in_progress and waited < max_wait:
+ time.sleep(0.5)
+ waited += 0.5
+
+ # التحقق من نتائج التحليل
+ self.assertFalse(self.risk_analyzer.analysis_in_progress)
+ results = self.risk_analyzer.get_analysis_results()
+ self.assertEqual(results["status"], "اكتمل التحليل")
+ self.assertEqual(results["project_id"], 1)
+ self.assertEqual(results["method"], "comprehensive")
+
+ # التحقق من وجود المخاطر المحددة
+ self.assertIn("identified_risks", results)
+ self.assertTrue(len(results["identified_risks"]) > 0)
+
+ # التحقق من وجود فئات المخاطر
+ self.assertIn("risk_categories", results)
+
+ # التحقق من وجود مصفوفة المخاطر
+ self.assertIn("risk_matrix", results)
+
+ # التحقق من وجود استراتيجيات التخفيف
+ self.assertIn("mitigation_strategies", results)
+ self.assertTrue(len(results["mitigation_strategies"]) > 0)
+
+ # التحقق من وجود ملخص التحليل
+ self.assertIn("summary", results)
+ self.assertIn("overall_risk_level", results["summary"])
+
+
+class TestAIAssistant(unittest.TestCase):
+ """اختبار المساعد الذكي"""
+
+ def setUp(self):
+ """إعداد بيئة الاختبار"""
+ self.config = AppConfig()
+ self.assistant = AIAssistant(self.config)
+
+ def test_process_query(self):
+ """اختبار معالجة الاستعلام"""
+ # معالجة استعلام
+ query = "كيف يمكنني تحليل مستند مناقصة؟"
+ result = self.assistant.process_query(query)
+ self.assertTrue(result)
+
+ # انتظار اكتمال المعالجة
+ import time
+ max_wait = 5 # ثوانٍ
+ waited = 0
+ while self.assistant.processing_in_progress and waited < max_wait:
+ time.sleep(0.5)
+ waited += 0.5
+
+ # التحقق من نتائج المعالجة
+ self.assertFalse(self.assistant.processing_in_progress)
+ results = self.assistant.get_processing_results()
+ self.assertEqual(results["status"], "اكتملت المعالجة")
+ self.assertEqual(results["query"], query)
+
+ # التحقق من وجود استجابة
+ self.assertIn("response", results)
+ self.assertTrue(len(results["response"]) > 0)
+
+ # التحقق من وجود اقتراحات
+ self.assertIn("suggestions", results)
+ self.assertTrue(len(results["suggestions"]) > 0)
+
+ def test_conversation_history(self):
+ """اختبار سجل المحادثة"""
+ # معالجة استعلام
+ query = "ما هي استراتيجيات التسعير المتاحة؟"
+ self.assistant.process_query(query)
+
+ # انتظار اكتمال المعالجة
+ import time
+ max_wait = 5 # ثوانٍ
+ waited = 0
+ while self.assistant.processing_in_progress and waited < max_wait:
+ time.sleep(0.5)
+ waited += 0.5
+
+ # التحقق من سجل المحادثة
+ history = self.assistant.get_conversation_history()
+ self.assertEqual(len(history), 2) # استعلام المستخدم واستجابة المساعد
+ self.assertEqual(history[0]["role"], "user")
+ self.assertEqual(history[0]["content"], query)
+ self.assertEqual(history[1]["role"], "assistant")
+
+ # مسح سجل المحادثة
+ self.assertTrue(self.assistant.clear_conversation_history())
+ history = self.assistant.get_conversation_history()
+ self.assertEqual(len(history), 0)
+
+
+class TestStyling(unittest.TestCase):
+ """اختبار وحدات التصميم"""
+
+ def test_app_theme(self):
+ """اختبار نمط التطبيق"""
+ theme = AppTheme()
+
+ # التحقق من الألوان
+ self.assertIsNotNone(theme.get_color("bg_color"))
+ self.assertIsNotNone(theme.get_color("fg_color"))
+
+ # التحقق من الخطوط
+ self.assertIsNotNone(theme.get_font("body"))
+ self.assertIsNotNone(theme.get_font("title"))
+
+ # التحقق من الأحجام
+ self.assertIsNotNone(theme.get_size("padding_medium"))
+ self.assertIsNotNone(theme.get_size("border_radius"))
+
+ # تغيير النمط
+ self.assertTrue(theme.set_theme("dark"))
+ self.assertEqual(theme.current_theme, "dark")
+
+ # تغيير اللغة
+ self.assertTrue(theme.set_language("en"))
+ self.assertEqual(theme.current_language, "en")
+
+ def test_icon_generator(self):
+ """اختبار مولد الأيقونات"""
+ icon_generator = IconGenerator()
+
+ # توليد الأيقونات الافتراضية
+ icon_generator.generate_default_icons()
+
+ # التحقق من وجود مجلد الأيقونات
+ self.assertTrue(Path('assets/icons').exists())
+
+ # التحقق من وجود بعض الأيقونات
+ self.assertTrue(Path('assets/icons/dashboard.png').exists())
+ self.assertTrue(Path('assets/icons/projects.png').exists())
+
+ def test_chart_generator(self):
+ """اختبار مولد الرسوم البيانية"""
+ theme = AppTheme()
+ chart_generator = ChartGenerator(theme)
+
+ # إنشاء بيانات للرسم البياني الشريطي
+ bar_data = {
+ 'labels': ['الربع الأول', 'الربع الثاني', 'الربع الثالث', 'الربع الرابع'],
+ 'values': [15000, 20000, 18000, 25000]
+ }
+
+ # إنشاء رسم بياني شريطي
+ fig = chart_generator.create_bar_chart(
+ bar_data,
+ 'الإيرادات الفصلية',
+ 'الفصل',
+ 'الإيرادات (ريال)'
+ )
+
+ # التحقق من إنشاء الرسم البياني
+ self.assertIsNotNone(fig)
+
+ # حفظ الرسم البياني
+ save_path = 'test_chart.png'
+ chart_generator.create_bar_chart(
+ bar_data,
+ 'الإيرادات الفصلية',
+ 'الفصل',
+ 'الإيرادات (ريال)',
+ save_path=save_path
+ )
+
+ # التحقق من وجود ملف الرسم البياني
+ self.assertTrue(Path(save_path).exists())
+
+ # حذف ملف الرسم البياني
+ if Path(save_path).exists():
+ Path(save_path).unlink()
+
+
+def run_tests():
+ """تشغيل الاختبارات"""
+ # إنشاء مجلد الاختبارات
+ test_dir = Path('test_results')
+ test_dir.mkdir(exist_ok=True)
+
+ # إنشاء ملف لنتائج الاختبارات
+ test_results_file = test_dir / 'test_results.txt'
+
+ # تشغيل الاختبارات وحفظ النتائج
+ with open(test_results_file, 'w', encoding='utf-8') as f:
+ runner = unittest.TextTestRunner(stream=f, verbosity=2)
+ suite = unittest.TestSuite()
+
+ # إضافة اختبارات قاعدة البيانات
+ suite.addTest(unittest.makeSuite(TestDatabaseConnector))
+ suite.addTest(unittest.makeSuite(TestModels))
+
+ # إضافة اختبارات الوحدات
+ suite.addTest(unittest.makeSuite(TestDocumentAnalyzer))
+ suite.addTest(unittest.makeSuite(TestPricingEngine))
+ suite.addTest(unittest.makeSuite(TestRiskAnalyzer))
+ suite.addTest(unittest.makeSuite(TestAIAssistant))
+
+ # إضافة اختبارات التصميم
+ suite.addTest(unittest.makeSuite(TestStyling))
+
+ # تشغيل الاختبارات
+ result = runner.run(suite)
+
+ # كتابة ملخص النتائج
+ f.write("\n\n=== ملخص نتائج الاختبارات ===\n")
+ f.write(f"عدد الاختبارات: {result.testsRun}\n")
+ f.write(f"عدد النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}\n")
+ f.write(f"عدد الإخفاقات: {len(result.failures)}\n")
+ f.write(f"عدد الأخطاء: {len(result.errors)}\n")
+
+ # طباعة ملخص النتائج
+ logger.info(f"تم تشغيل {result.testsRun} اختبار")
+ logger.info(f"النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}")
+ logger.info(f"الإخفاقات: {len(result.failures)}")
+ logger.info(f"الأخطاء: {len(result.errors)}")
+ logger.info(f"تم حفظ نتائج الاختبارات في: {test_results_file}")
+
+ return result
+
+
+if __name__ == "__main__":
+ run_tests()
diff --git a/tests/test_integrated_system.py b/tests/test_integrated_system.py
index 2fd47305e0ac2f2da2139e01ec4a283dcbe7a9a3..f0aa420d5d8bc42cf6d71477c5b580e631375c31 100644
--- a/tests/test_integrated_system.py
+++ b/tests/test_integrated_system.py
@@ -1,96 +1,96 @@
-"""
-وحدة اختبار النظام المتكامل
-"""
-
-import unittest
-import os
-import sys
-from pathlib import Path
-
-# إضافة المسار الرئيسي للنظام
-sys.path.append(str(Path(__file__).parent.parent))
-
-# استيراد الوحدات
-from modules.pricing.pricing_app import PricingApp
-from modules.ai_assistant.ai_app import AIAssistantApp
-from modules.document_analysis.document_app import DocumentAnalysisApp
-from modules.data_analysis.data_analysis_app import DataAnalysisApp
-from modules.resources.resources_app import ResourcesApp
-
-class TestIntegratedSystem(unittest.TestCase):
- """اختبارات النظام المتكامل"""
-
- def setUp(self):
- """إعداد بيئة الاختبار"""
- # التأكد من وجود جميع الملفات الرئيسية
- self.main_files = [
- "app.py",
- "config.py",
- "requirements.txt"
- ]
-
- # التأكد من وجود جميع المجلدات الرئيسية
- self.main_directories = [
- "modules",
- "assets",
- "data",
- "utils"
- ]
-
- # التأكد من وجود جميع وحدات النظام
- self.modules = [
- "modules/pricing",
- "modules/ai_assistant",
- "modules/document_analysis",
- "modules/data_analysis",
- "modules/resources",
- "modules/project_management",
- "modules/reports",
- "modules/risk_analysis"
- ]
-
- def test_main_files_exist(self):
- """اختبار وجود الملفات الرئيسية"""
- for file in self.main_files:
- file_path = Path(__file__).parent.parent / file
- self.assertTrue(file_path.exists(), f"الملف {file} غير موجود")
-
- def test_main_directories_exist(self):
- """اختبار وجود المجلدات الرئيسية"""
- for directory in self.main_directories:
- dir_path = Path(__file__).parent.parent / directory
- self.assertTrue(dir_path.exists(), f"المجلد {directory} غير موجود")
-
- def test_modules_exist(self):
- """اختبار وجود وحدات النظام"""
- for module in self.modules:
- module_path = Path(__file__).parent.parent / module
- self.assertTrue(module_path.exists(), f"الوحدة {module} غير موجودة")
-
- def test_pricing_module(self):
- """اختبار وحدة التسعير"""
- pricing_app = PricingApp()
- self.assertIsNotNone(pricing_app, "فشل إنشاء وحدة التسعير")
-
- def test_ai_assistant_module(self):
- """اختبار وحدة الذكاء الاصطناعي"""
- ai_app = AIAssistantApp()
- self.assertIsNotNone(ai_app, "فشل إنشاء وحدة الذكاء الاصطناعي")
-
- def test_document_analysis_module(self):
- """اختبار وحدة تحليل المستندات"""
- document_app = DocumentAnalysisApp()
- self.assertIsNotNone(document_app, "فشل إنشاء وحدة تحليل المستندات")
-
- def test_data_analysis_module(self):
- """اختبار وحدة تحليل البيانات"""
- data_analysis_app = DataAnalysisApp()
- self.assertIsNotNone(data_analysis_app, "فشل إنشاء وحدة تحليل البيانات")
-
- def test_resources_module(self):
- """اختبار وحدة الموارد"""
- resources_app = ResourcesApp()
- self.assertIsNotNone(resources_app, "فشل إنشاء وحدة الموارد")
-
-if __name__ == "__main__":
- unittest.main()
+"""
+وحدة اختبار النظام المتكامل
+"""
+
+import unittest
+import os
+import sys
+from pathlib import Path
+
+# إضافة المسار الرئيسي للنظام
+sys.path.append(str(Path(__file__).parent.parent))
+
+# استيراد الوحدات
+from modules.pricing.pricing_app import PricingApp
+from modules.ai_assistant.ai_app import AIAssistantApp
+from modules.document_analysis.document_app import DocumentAnalysisApp
+from modules.data_analysis.data_analysis_app import DataAnalysisApp
+from modules.resources.resources_app import ResourcesApp
+
+class TestIntegratedSystem(unittest.TestCase):
+ """اختبارات النظام المتكامل"""
+
+ def setUp(self):
+ """إعداد بيئة الاختبار"""
+ # التأكد من وجود جميع الملفات الرئيسية
+ self.main_files = [
+ "app.py",
+ "config.py",
+ "requirements.txt"
+ ]
+
+ # التأكد من وجود جميع المجلدات الرئيسية
+ self.main_directories = [
+ "modules",
+ "assets",
+ "data",
+ "utils"
+ ]
+
+ # التأكد من وجود جميع وحدات النظام
+ self.modules = [
+ "modules/pricing",
+ "modules/ai_assistant",
+ "modules/document_analysis",
+ "modules/data_analysis",
+ "modules/resources",
+ "modules/project_management",
+ "modules/reports",
+ "modules/risk_analysis"
+ ]
+
+ def test_main_files_exist(self):
+ """اختبار وجود الملفات الرئيسية"""
+ for file in self.main_files:
+ file_path = Path(__file__).parent.parent / file
+ self.assertTrue(file_path.exists(), f"الملف {file} غير موجود")
+
+ def test_main_directories_exist(self):
+ """اختبار وجود المجلدات الرئيسية"""
+ for directory in self.main_directories:
+ dir_path = Path(__file__).parent.parent / directory
+ self.assertTrue(dir_path.exists(), f"المجلد {directory} غير موجود")
+
+ def test_modules_exist(self):
+ """اختبار وجود وحدات النظام"""
+ for module in self.modules:
+ module_path = Path(__file__).parent.parent / module
+ self.assertTrue(module_path.exists(), f"الوحدة {module} غير موجودة")
+
+ def test_pricing_module(self):
+ """اختبار وحدة التسعير"""
+ pricing_app = PricingApp()
+ self.assertIsNotNone(pricing_app, "فشل إنشاء وحدة التسعير")
+
+ def test_ai_assistant_module(self):
+ """اختبار وحدة الذكاء الاصطناعي"""
+ ai_app = AIAssistantApp()
+ self.assertIsNotNone(ai_app, "فشل إنشاء وحدة الذكاء الاصطناعي")
+
+ def test_document_analysis_module(self):
+ """اختبار وحدة تحليل المستندات"""
+ document_app = DocumentAnalysisApp()
+ self.assertIsNotNone(document_app, "فشل إنشاء وحدة تحليل المستندات")
+
+ def test_data_analysis_module(self):
+ """اختبار وحدة تحليل البيانات"""
+ data_analysis_app = DataAnalysisApp()
+ self.assertIsNotNone(data_analysis_app, "فشل إنشاء وحدة تحليل البيانات")
+
+ def test_resources_module(self):
+ """اختبار وحدة الموارد"""
+ resources_app = ResourcesApp()
+ self.assertIsNotNone(resources_app, "فشل إنشاء وحدة الموارد")
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_modules.py b/tests/test_modules.py
index 1c6107f28ff75091bd944a8fbc9d754a68d24f64..6f7b8ebc626c3a8ca3d9322a39e4001b970d11c4 100644
--- a/tests/test_modules.py
+++ b/tests/test_modules.py
@@ -1,244 +1,244 @@
-"""
-اختبارات وحدات نظام المناقصات
-
-هذا الملف يحتوي على اختبارات للتحقق من عمل وحدات نظام المناقصات بشكل صحيح.
-"""
-
-import os
-import sys
-import unittest
-from unittest.mock import patch, MagicMock
-import pandas as pd
-import numpy as np
-
-# إضافة مسار المشروع إلى مسار النظام
-sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
-
-# محاكاة Streamlit
-class StreamlitMock:
- def __init__(self):
- self.session_state = {"theme": "light"}
- self.sidebar_items = []
- self.page_items = []
-
- def title(self, text):
- self.page_items.append(f"TITLE: {text}")
- return None
-
- def header(self, text):
- self.page_items.append(f"HEADER: {text}")
- return None
-
- def subheader(self, text):
- self.page_items.append(f"SUBHEADER: {text}")
- return None
-
- def write(self, text):
- self.page_items.append(f"WRITE: {text}")
- return None
-
- def sidebar(self):
- return self
-
- def selectbox(self, label, options, index=0):
- self.page_items.append(f"SELECTBOX: {label}, OPTIONS: {options}")
- return options[index] if options else None
-
- def radio(self, label, options, index=0):
- self.page_items.append(f"RADIO: {label}, OPTIONS: {options}")
- return options[index] if options else None
-
- def checkbox(self, label, value=False):
- self.page_items.append(f"CHECKBOX: {label}, VALUE: {value}")
- return value
-
- def button(self, label):
- self.page_items.append(f"BUTTON: {label}")
- return False
-
- def file_uploader(self, label, type=None, accept_multiple_files=False):
- self.page_items.append(f"FILE_UPLOADER: {label}, TYPE: {type}")
- return None
-
- def columns(self, spec):
- cols = [StreamlitMock() for _ in range(len(spec))]
- self.page_items.append(f"COLUMNS: {spec}")
- return cols
-
- def tabs(self, tabs):
- tab_objects = [StreamlitMock() for _ in tabs]
- self.page_items.append(f"TABS: {tabs}")
- return tab_objects
-
- def expander(self, label):
- exp = StreamlitMock()
- self.page_items.append(f"EXPANDER: {label}")
- return exp
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- return False
-
- def success(self, text):
- self.page_items.append(f"SUCCESS: {text}")
- return None
-
- def error(self, text):
- self.page_items.append(f"ERROR: {text}")
- return None
-
- def warning(self, text):
- self.page_items.append(f"WARNING: {text}")
- return None
-
- def info(self, text):
- self.page_items.append(f"INFO: {text}")
- return None
-
- def markdown(self, text):
- self.page_items.append(f"MARKDOWN: {text}")
- return None
-
- def metric(self, label, value, delta=None):
- self.page_items.append(f"METRIC: {label}, VALUE: {value}, DELTA: {delta}")
- return None
-
- def progress(self, value):
- self.page_items.append(f"PROGRESS: {value}")
- return None
-
- def json(self, data):
- self.page_items.append(f"JSON: {data}")
- return None
-
- def dataframe(self, data):
- self.page_items.append(f"DATAFRAME: {type(data)}")
- return None
-
- def line_chart(self, data):
- self.page_items.append(f"LINE_CHART: {type(data)}")
- return None
-
- def bar_chart(self, data):
- self.page_items.append(f"BAR_CHART: {type(data)}")
- return None
-
- def pyplot(self, fig):
- self.page_items.append(f"PYPLOT: {type(fig)}")
- return None
-
- def download_button(self, label, data, file_name, mime=None):
- self.page_items.append(f"DOWNLOAD_BUTTON: {label}, FILE_NAME: {file_name}")
- return False
-
- def form(self, key):
- form = StreamlitMock()
- self.page_items.append(f"FORM: {key}")
- return form
-
- def form_submit_button(self, label):
- self.page_items.append(f"FORM_SUBMIT_BUTTON: {label}")
- return False
-
- def rerun(self):
- self.page_items.append("RERUN")
- return None
-
-# تعريف الاختبارات
-class TestContractAnalyzer(unittest.TestCase):
- """اختبارات محلل العقود"""
-
- def test_contract_analyzer_initialization(self):
- """اختبار تهيئة محلل العقود"""
- from modules.ai_assistant.contract_analyzer import ContractAnalyzer
-
- # تهيئة المحلل
- analyzer = ContractAnalyzer()
-
- # التحقق من تهيئة الخصائص
- self.assertEqual(analyzer.api_key_source, "security_section")
- self.assertIsNotNone(analyzer.openai_api_key)
- self.assertIsNotNone(analyzer.claude_api_key)
- self.assertTrue(analyzer.hybrid_environment)
-
- def test_contract_analysis(self):
- """اختبار تحليل العقود"""
- from modules.ai_assistant.contract_analyzer import ContractAnalyzer
-
- # تهيئة المحلل
- analyzer = ContractAnalyzer()
-
- # إنشاء ملف عقد وهمي
- contract_file = "/tmp/test_contract.txt"
- with open(contract_file, "w") as f:
- f.write("عقد إنشاء مبنى إداري")
-
- # تحليل العقد
- result = analyzer.analyze_contract(contract_file)
-
- # التحقق من النتائج
- self.assertIsNotNone(result)
- self.assertIn("title", result)
- self.assertIn("summary", result)
- self.assertIn("key_points", result)
- self.assertIn("entities", result)
-
- # حذف الملف الوهمي
- os.remove(contract_file)
-
- def test_tender_analysis(self):
- """اختبار تحليل المناقصات"""
- from modules.ai_assistant.contract_analyzer import ContractAnalyzer
-
- # تهيئة المحلل
- analyzer = ContractAnalyzer()
-
- # إنشاء ملف مناقصة وهمي
- tender_file = "/tmp/test_tender.txt"
- with open(tender_file, "w") as f:
- f.write("مناقصة إنشاء مبنى إداري")
-
- # تحليل المناقصة
- result = analyzer.analyze_tender(tender_file)
-
- # التحقق من النتائج
- self.assertIsNotNone(result)
- self.assertIn("title", result)
- self.assertIn("summary", result)
- self.assertIn("key_points", result)
- self.assertIn("entities", result)
-
- # حذف الملف الوهمي
- os.remove(tender_file)
-
- def test_dwg_analysis(self):
- """اختبار تحليل ملفات DWG"""
- from modules.ai_assistant.contract_analyzer import ContractAnalyzer
-
- # تهيئة المحلل
- analyzer = ContractAnalyzer()
-
- # إنشاء ملف DWG وهمي
- dwg_file = "/tmp/test_drawing.dwg"
- with open(dwg_file, "w") as f:
- f.write("DWG FILE CONTENT")
-
- # تحليل ملف DWG
- result = analyzer.analyze_dwg_file(dwg_file)
-
- # التحقق من النتائج
- self.assertIsNotNone(result)
- self.assertIn("file_name", result)
- self.assertIn("elements_count", result)
- self.assertIn("dimensions", result)
- self.assertIn("materials", result)
- self.assertIn("cost_estimate", result)
-
- # حذف الملف الوهمي
- os.remove(dwg_file)
-
-# تشغيل الاختبارات
-if __name__ == '__main__':
- unittest.main()
+"""
+اختبارات وحدات نظام المناقصات
+
+هذا الملف يحتوي على اختبارات للتحقق من عمل وحدات نظام المناقصات بشكل صحيح.
+"""
+
+import os
+import sys
+import unittest
+from unittest.mock import patch, MagicMock
+import pandas as pd
+import numpy as np
+
+# إضافة مسار المشروع إلى مسار النظام
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+
+# محاكاة Streamlit
+class StreamlitMock:
+ def __init__(self):
+ self.session_state = {"theme": "light"}
+ self.sidebar_items = []
+ self.page_items = []
+
+ def title(self, text):
+ self.page_items.append(f"TITLE: {text}")
+ return None
+
+ def header(self, text):
+ self.page_items.append(f"HEADER: {text}")
+ return None
+
+ def subheader(self, text):
+ self.page_items.append(f"SUBHEADER: {text}")
+ return None
+
+ def write(self, text):
+ self.page_items.append(f"WRITE: {text}")
+ return None
+
+ def sidebar(self):
+ return self
+
+ def selectbox(self, label, options, index=0):
+ self.page_items.append(f"SELECTBOX: {label}, OPTIONS: {options}")
+ return options[index] if options else None
+
+ def radio(self, label, options, index=0):
+ self.page_items.append(f"RADIO: {label}, OPTIONS: {options}")
+ return options[index] if options else None
+
+ def checkbox(self, label, value=False):
+ self.page_items.append(f"CHECKBOX: {label}, VALUE: {value}")
+ return value
+
+ def button(self, label):
+ self.page_items.append(f"BUTTON: {label}")
+ return False
+
+ def file_uploader(self, label, type=None, accept_multiple_files=False):
+ self.page_items.append(f"FILE_UPLOADER: {label}, TYPE: {type}")
+ return None
+
+ def columns(self, spec):
+ cols = [StreamlitMock() for _ in range(len(spec))]
+ self.page_items.append(f"COLUMNS: {spec}")
+ return cols
+
+ def tabs(self, tabs):
+ tab_objects = [StreamlitMock() for _ in tabs]
+ self.page_items.append(f"TABS: {tabs}")
+ return tab_objects
+
+ def expander(self, label):
+ exp = StreamlitMock()
+ self.page_items.append(f"EXPANDER: {label}")
+ return exp
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ return False
+
+ def success(self, text):
+ self.page_items.append(f"SUCCESS: {text}")
+ return None
+
+ def error(self, text):
+ self.page_items.append(f"ERROR: {text}")
+ return None
+
+ def warning(self, text):
+ self.page_items.append(f"WARNING: {text}")
+ return None
+
+ def info(self, text):
+ self.page_items.append(f"INFO: {text}")
+ return None
+
+ def markdown(self, text):
+ self.page_items.append(f"MARKDOWN: {text}")
+ return None
+
+ def metric(self, label, value, delta=None):
+ self.page_items.append(f"METRIC: {label}, VALUE: {value}, DELTA: {delta}")
+ return None
+
+ def progress(self, value):
+ self.page_items.append(f"PROGRESS: {value}")
+ return None
+
+ def json(self, data):
+ self.page_items.append(f"JSON: {data}")
+ return None
+
+ def dataframe(self, data):
+ self.page_items.append(f"DATAFRAME: {type(data)}")
+ return None
+
+ def line_chart(self, data):
+ self.page_items.append(f"LINE_CHART: {type(data)}")
+ return None
+
+ def bar_chart(self, data):
+ self.page_items.append(f"BAR_CHART: {type(data)}")
+ return None
+
+ def pyplot(self, fig):
+ self.page_items.append(f"PYPLOT: {type(fig)}")
+ return None
+
+ def download_button(self, label, data, file_name, mime=None):
+ self.page_items.append(f"DOWNLOAD_BUTTON: {label}, FILE_NAME: {file_name}")
+ return False
+
+ def form(self, key):
+ form = StreamlitMock()
+ self.page_items.append(f"FORM: {key}")
+ return form
+
+ def form_submit_button(self, label):
+ self.page_items.append(f"FORM_SUBMIT_BUTTON: {label}")
+ return False
+
+ def rerun(self):
+ self.page_items.append("RERUN")
+ return None
+
+# تعريف الاختبارات
+class TestContractAnalyzer(unittest.TestCase):
+ """اختبارات محلل العقود"""
+
+ def test_contract_analyzer_initialization(self):
+ """اختبار تهيئة محلل العقود"""
+ from modules.ai_assistant.contract_analyzer import ContractAnalyzer
+
+ # تهيئة المحلل
+ analyzer = ContractAnalyzer()
+
+ # التحقق من تهيئة الخصائص
+ self.assertEqual(analyzer.api_key_source, "security_section")
+ self.assertIsNotNone(analyzer.openai_api_key)
+ self.assertIsNotNone(analyzer.claude_api_key)
+ self.assertTrue(analyzer.hybrid_environment)
+
+ def test_contract_analysis(self):
+ """اختبار تحليل العقود"""
+ from modules.ai_assistant.contract_analyzer import ContractAnalyzer
+
+ # تهيئة المحلل
+ analyzer = ContractAnalyzer()
+
+ # إنشاء ملف عقد وهمي
+ contract_file = "/tmp/test_contract.txt"
+ with open(contract_file, "w") as f:
+ f.write("عقد إنشاء مبنى إداري")
+
+ # تحليل العقد
+ result = analyzer.analyze_contract(contract_file)
+
+ # التحقق من النتائج
+ self.assertIsNotNone(result)
+ self.assertIn("title", result)
+ self.assertIn("summary", result)
+ self.assertIn("key_points", result)
+ self.assertIn("entities", result)
+
+ # حذف الملف الوهمي
+ os.remove(contract_file)
+
+ def test_tender_analysis(self):
+ """اختبار تحليل المناقصات"""
+ from modules.ai_assistant.contract_analyzer import ContractAnalyzer
+
+ # تهيئة المحلل
+ analyzer = ContractAnalyzer()
+
+ # إنشاء ملف مناقصة وهمي
+ tender_file = "/tmp/test_tender.txt"
+ with open(tender_file, "w") as f:
+ f.write("مناقصة إنشاء مبنى إداري")
+
+ # تحليل المناقصة
+ result = analyzer.analyze_tender(tender_file)
+
+ # التحقق من النتائج
+ self.assertIsNotNone(result)
+ self.assertIn("title", result)
+ self.assertIn("summary", result)
+ self.assertIn("key_points", result)
+ self.assertIn("entities", result)
+
+ # حذف الملف الوهمي
+ os.remove(tender_file)
+
+ def test_dwg_analysis(self):
+ """اختبار تحليل ملفات DWG"""
+ from modules.ai_assistant.contract_analyzer import ContractAnalyzer
+
+ # تهيئة المحلل
+ analyzer = ContractAnalyzer()
+
+ # إنشاء ملف DWG وهمي
+ dwg_file = "/tmp/test_drawing.dwg"
+ with open(dwg_file, "w") as f:
+ f.write("DWG FILE CONTENT")
+
+ # تحليل ملف DWG
+ result = analyzer.analyze_dwg_file(dwg_file)
+
+ # التحقق من النتائج
+ self.assertIsNotNone(result)
+ self.assertIn("file_name", result)
+ self.assertIn("elements_count", result)
+ self.assertIn("dimensions", result)
+ self.assertIn("materials", result)
+ self.assertIn("cost_estimate", result)
+
+ # حذف الملف الوهمي
+ os.remove(dwg_file)
+
+# تشغيل الاختبارات
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_ui.py b/tests/test_ui.py
index bd4af237613aa2b0b8fd098093c0b301e8bb6cf4..9d1aee30d0c22642661330df79e6f7d1fc160e28 100644
--- a/tests/test_ui.py
+++ b/tests/test_ui.py
@@ -1,413 +1,413 @@
-"""
-وحدة اختبار واجهة المستخدم لنظام إدارة المناقصات - Hybrid Face
-"""
-
-import os
-import sys
-import logging
-import unittest
-import tkinter as tk
-import customtkinter as ctk
-from pathlib import Path
-
-# تهيئة السجل
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger('test_ui')
-
-# إضافة المسار الرئيسي للتطبيق إلى مسار البحث
-sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-# استيراد الوحدات المطلوبة للاختبار
-from styling.theme import AppTheme
-from styling.icons import IconGenerator
-from styling.charts import ChartGenerator
-from config import AppConfig
-
-class TestUIComponents(unittest.TestCase):
- """اختبار مكونات واجهة المستخدم"""
-
- def setUp(self):
- """إعداد بيئة الاختبار"""
- self.root = ctk.CTk()
- self.root.withdraw() # إخفاء النافذة أثناء الاختبار
- self.theme = AppTheme()
-
- def tearDown(self):
- """تنظيف بيئة الاختبار"""
- self.root.destroy()
-
- def test_styled_frame(self):
- """اختبار الإطار المنسق"""
- frame = self.theme.create_styled_frame(self.root)
- self.assertIsNotNone(frame)
- self.assertEqual(frame.cget("fg_color"), self.theme.get_color("card_bg_color"))
- self.assertEqual(frame.cget("corner_radius"), self.theme.get_size("border_radius"))
-
- def test_styled_button(self):
- """اختبار الزر المنسق"""
- button = self.theme.create_styled_button(self.root, "زر اختبار")
- self.assertIsNotNone(button)
- self.assertEqual(button.cget("text"), "زر اختبار")
- self.assertEqual(button.cget("fg_color"), self.theme.get_color("button_bg_color"))
- self.assertEqual(button.cget("text_color"), self.theme.get_color("button_fg_color"))
-
- def test_styled_label(self):
- """اختبار التسمية المنسقة"""
- label = self.theme.create_styled_label(self.root, "تسمية اختبار")
- self.assertIsNotNone(label)
- self.assertEqual(label.cget("text"), "تسمية اختبار")
- self.assertEqual(label.cget("text_color"), self.theme.get_color("fg_color"))
-
- def test_styled_entry(self):
- """اختبار حقل الإدخال المنسق"""
- entry = self.theme.create_styled_entry(self.root, "نص توضيحي")
- self.assertIsNotNone(entry)
- self.assertEqual(entry.cget("placeholder_text"), "نص توضيحي")
- self.assertEqual(entry.cget("fg_color"), self.theme.get_color("input_bg_color"))
- self.assertEqual(entry.cget("text_color"), self.theme.get_color("input_fg_color"))
-
- def test_styled_combobox(self):
- """اختبار القائمة المنسدلة المنسقة"""
- values = ["الخيار الأول", "الخيار الثاني", "الخيار الثالث"]
- combobox = self.theme.create_styled_combobox(self.root, values)
- self.assertIsNotNone(combobox)
- self.assertEqual(combobox.cget("values"), values)
- self.assertEqual(combobox.cget("fg_color"), self.theme.get_color("input_bg_color"))
- self.assertEqual(combobox.cget("text_color"), self.theme.get_color("input_fg_color"))
-
- def test_styled_checkbox(self):
- """اختبار خانة الاختيار المنسقة"""
- checkbox = self.theme.create_styled_checkbox(self.root, "خانة اختبار")
- self.assertIsNotNone(checkbox)
- self.assertEqual(checkbox.cget("text"), "خانة اختبار")
- self.assertEqual(checkbox.cget("fg_color"), self.theme.get_color("button_bg_color"))
- self.assertEqual(checkbox.cget("text_color"), self.theme.get_color("fg_color"))
-
- def test_styled_radio_button(self):
- """اختبار زر الراديو المنسق"""
- var = ctk.StringVar(value="1")
- radio_button = self.theme.create_styled_radio_button(self.root, "زر راديو اختبار", var, "1")
- self.assertIsNotNone(radio_button)
- self.assertEqual(radio_button.cget("text"), "زر راديو اختبار")
- self.assertEqual(radio_button.cget("fg_color"), self.theme.get_color("button_bg_color"))
- self.assertEqual(radio_button.cget("text_color"), self.theme.get_color("fg_color"))
-
- def test_styled_switch(self):
- """اختبار مفتاح التبديل المنسق"""
- switch = self.theme.create_styled_switch(self.root, "مفتاح اختبار")
- self.assertIsNotNone(switch)
- self.assertEqual(switch.cget("text"), "مفتاح اختبار")
- self.assertEqual(switch.cget("progress_color"), self.theme.get_color("button_bg_color"))
- self.assertEqual(switch.cget("text_color"), self.theme.get_color("fg_color"))
-
- def test_styled_slider(self):
- """اختبار شريط التمرير المنسق"""
- slider = self.theme.create_styled_slider(self.root)
- self.assertIsNotNone(slider)
- self.assertEqual(slider.cget("fg_color"), self.theme.get_color("input_border_color"))
- self.assertEqual(slider.cget("progress_color"), self.theme.get_color("button_bg_color"))
-
- def test_styled_progressbar(self):
- """اختبار شريط التقدم المنسق"""
- progressbar = self.theme.create_styled_progressbar(self.root)
- self.assertIsNotNone(progressbar)
- self.assertEqual(progressbar.cget("fg_color"), self.theme.get_color("input_border_color"))
- self.assertEqual(progressbar.cget("progress_color"), self.theme.get_color("button_bg_color"))
-
- def test_styled_tabview(self):
- """اختبار عرض التبويب المنسق"""
- tabview = self.theme.create_styled_tabview(self.root)
- self.assertIsNotNone(tabview)
- self.assertEqual(tabview.cget("fg_color"), self.theme.get_color("card_bg_color"))
-
- def test_styled_scrollable_frame(self):
- """اختبار الإطار القابل للتمرير المنسق"""
- scrollable_frame = self.theme.create_styled_scrollable_frame(self.root)
- self.assertIsNotNone(scrollable_frame)
- self.assertEqual(scrollable_frame.cget("fg_color"), "transparent")
-
- def test_styled_textbox(self):
- """اختبار مربع النص المنسق"""
- textbox = self.theme.create_styled_textbox(self.root)
- self.assertIsNotNone(textbox)
- self.assertEqual(textbox.cget("fg_color"), self.theme.get_color("input_bg_color"))
- self.assertEqual(textbox.cget("text_color"), self.theme.get_color("input_fg_color"))
-
- def test_styled_card(self):
- """اختبار البطاقة المنسقة"""
- card, content_frame = self.theme.create_styled_card(self.root, "بطاقة اختبار")
- self.assertIsNotNone(card)
- self.assertIsNotNone(content_frame)
- self.assertEqual(card.cget("fg_color"), self.theme.get_color("card_bg_color"))
-
- def test_styled_data_table(self):
- """اختبار جدول البيانات المنسق"""
- columns = ["العمود الأول", "العمود الثاني", "العمود الثالث"]
- data = [
- ["بيانات 1-1", "بيانات 1-2", "بيانات 1-3"],
- ["بيانات 2-1", "بيانات 2-2", "بيانات 2-3"]
- ]
- table_frame, data_frame = self.theme.create_styled_data_table(self.root, columns, data)
- self.assertIsNotNone(table_frame)
- self.assertIsNotNone(data_frame)
- self.assertEqual(table_frame.cget("fg_color"), self.theme.get_color("card_bg_color"))
-
- def test_theme_switching(self):
- """اختبار تبديل النمط"""
- # تعيين النمط الفاتح
- self.theme.set_theme("light")
- light_bg_color = self.theme.get_color("bg_color")
-
- # تعيين النمط الداكن
- self.theme.set_theme("dark")
- dark_bg_color = self.theme.get_color("bg_color")
-
- # التحقق من اختلاف الألوان
- self.assertNotEqual(light_bg_color, dark_bg_color)
-
- def test_language_switching(self):
- """اختبار تبديل اللغة"""
- # تعيين اللغة العربية
- self.theme.set_language("ar")
- ar_font = self.theme.get_font("body")
-
- # تعيين اللغة الإنجليزية
- self.theme.set_language("en")
- en_font = self.theme.get_font("body")
-
- # التحقق من اختلاف الخطوط
- self.assertNotEqual(ar_font[0], en_font[0])
-
-
-class TestUILayout(unittest.TestCase):
- """اختبار تخطيط واجهة المستخدم"""
-
- def setUp(self):
- """إعداد بيئة الاختبار"""
- self.root = ctk.CTk()
- self.root.withdraw() # إخفاء النافذة أثناء الاختبار
- self.theme = AppTheme()
-
- # إنشاء الإطار الرئيسي
- self.main_frame = self.theme.create_styled_frame(self.root)
- self.main_frame.pack(fill="both", expand=True)
-
- # إنشاء الشريط الجانبي
- self.sidebar_frame = self.theme.create_styled_frame(
- self.main_frame,
- fg_color=self.theme.get_color("sidebar_bg_color")
- )
- self.sidebar_frame.pack(side="left", fill="y", padx=0, pady=0)
-
- # إنشاء إطار المحتوى
- self.content_frame = self.theme.create_styled_frame(
- self.main_frame,
- fg_color=self.theme.get_color("bg_color")
- )
- self.content_frame.pack(side="right", fill="both", expand=True, padx=0, pady=0)
-
- def tearDown(self):
- """تنظيف بيئة الاختبار"""
- self.root.destroy()
-
- def test_sidebar_layout(self):
- """اختبار تخطيط الشريط الجانبي"""
- # إنشاء شعار التطبيق
- logo_label = self.theme.create_styled_label(
- self.sidebar_frame,
- "نظام إدارة المناقصات",
- font=self.theme.get_font("title"),
- text_color=self.theme.get_color("sidebar_fg_color")
- )
- logo_label.pack(padx=20, pady=20)
-
- # إنشاء أزرار الشريط الجانبي
- sidebar_buttons = []
- button_texts = [
- "لوحة التحكم", "المشاريع", "المستندات", "التسعير",
- "الموارد", "المخاطر", "التقارير", "الذكاء الاصطناعي"
- ]
-
- for text in button_texts:
- button_frame, button = self.theme.create_styled_sidebar_button(
- self.sidebar_frame,
- text
- )
- button_frame.pack(fill="x", padx=0, pady=2)
- sidebar_buttons.append(button)
-
- # التحقق من إنشاء الأزرار
- self.assertEqual(len(sidebar_buttons), len(button_texts))
- for i, button in enumerate(sidebar_buttons):
- self.assertEqual(button.cget("text"), button_texts[i])
-
- def test_content_layout(self):
- """اختبار تخطيط المحتوى"""
- # إنشاء شريط العنوان
- header_frame = self.theme.create_styled_frame(
- self.content_frame,
- fg_color=self.theme.get_color("card_bg_color")
- )
- header_frame.pack(fill="x", padx=20, pady=20)
-
- # إنشاء عنوان الصفحة
- page_title = self.theme.create_styled_label(
- header_frame,
- "لوحة التحكم",
- font=self.theme.get_font("title")
- )
- page_title.pack(side="left", padx=20, pady=20)
-
- # إنشاء زر البحث
- search_button = self.theme.create_styled_button(
- header_frame,
- "بحث"
- )
- search_button.pack(side="right", padx=20, pady=20)
-
- # إنشاء إطار البطاقات
- cards_frame = self.theme.create_styled_frame(
- self.content_frame,
- fg_color="transparent"
- )
- cards_frame.pack(fill="both", expand=True, padx=20, pady=20)
-
- # إنشاء بطاقات
- cards = []
- card_titles = [
- "المشاريع النشطة", "المناقصات الجديدة", "المخاطر العالية", "التقارير المعلقة"
- ]
-
- for i, title in enumerate(card_titles):
- card, card_content = self.theme.create_styled_card(
- cards_frame,
- title
- )
- card.grid(row=i//2, column=i%2, padx=10, pady=10, sticky="nsew")
- cards.append(card)
-
- # التحقق من إنشاء البطاقات
- self.assertEqual(len(cards), len(card_titles))
-
- # تهيئة أوزان الصفوف والأعمدة
- cards_frame.grid_columnconfigure(0, weight=1)
- cards_frame.grid_columnconfigure(1, weight=1)
- cards_frame.grid_rowconfigure(0, weight=1)
- cards_frame.grid_rowconfigure(1, weight=1)
-
- def test_responsive_layout(self):
- """اختبار التخطيط المتجاوب"""
- # تغيير حجم النافذة
- self.root.geometry("800x600")
- self.root.update()
-
- # التحقق من أن الإطار الرئيسي يملأ النافذة
- self.assertEqual(self.main_frame.winfo_width(), 800)
- self.assertEqual(self.main_frame.winfo_height(), 600)
-
- # تغيير حجم النافذة مرة أخرى
- self.root.geometry("1024x768")
- self.root.update()
-
- # التحقق من أن الإطار الرئيسي يملأ النافذة
- self.assertEqual(self.main_frame.winfo_width(), 1024)
- self.assertEqual(self.main_frame.winfo_height(), 768)
-
-
-class TestUIArabicSupport(unittest.TestCase):
- """اختبار دعم اللغة العربية في واجهة المستخدم"""
-
- def setUp(self):
- """إعداد بيئة الاختبار"""
- self.root = ctk.CTk()
- self.root.withdraw() # إخفاء النافذة أثناء الاختبار
- self.theme = AppTheme()
- self.theme.set_language("ar") # تعيين اللغة العربية
-
- def tearDown(self):
- """تنظيف بيئة الاختبار"""
- self.root.destroy()
-
- def test_arabic_text_display(self):
- """اختبار عرض النص العربي"""
- # إنشاء تسمية بنص عربي
- arabic_text = "هذا نص عربي للاختبار"
- label = self.theme.create_styled_label(self.root, arabic_text)
- self.assertEqual(label.cget("text"), arabic_text)
-
- # إنشاء زر بنص عربي
- button = self.theme.create_styled_button(self.root, "زر باللغة العربية")
- self.assertEqual(button.cget("text"), "زر باللغة العربية")
-
- # إنشاء حقل إدخال بنص توضيحي عربي
- entry = self.theme.create_styled_entry(self.root, "أدخل النص هنا")
- self.assertEqual(entry.cget("placeholder_text"), "أدخل النص هنا")
-
- def test_arabic_font(self):
- """اختبار الخط العربي"""
- # التحقق من استخدام خط يدعم العربية
- ar_font = self.theme.get_font("body")
- self.assertEqual(ar_font[0], "Cairo")
-
- def test_rtl_support(self):
- """اختبار دعم الكتابة من اليمين إلى اليسار"""
- # إنشاء إطار
- frame = self.theme.create_styled_frame(self.root)
- frame.pack(fill="both", expand=True)
-
- # إنشاء تسمية بنص عربي
- label = self.theme.create_styled_label(frame, "نص عربي من اليمين إلى اليسار")
- label.pack(anchor="e", padx=20, pady=20) # محاذاة إلى اليمين
-
- # التحقق من المحاذاة
- self.assertEqual(label.cget("anchor"), "w") # w تعني غرب (يسار)، لكن النص سيظهر من اليمين إلى اليسار
-
-
-def run_ui_tests():
- """تشغيل اختبارات واجهة المستخدم"""
- # إنشاء مجلد الاختبارات
- test_dir = Path('test_results')
- test_dir.mkdir(exist_ok=True)
-
- # إنشاء ملف لنتائج الاختبارات
- test_results_file = test_dir / 'ui_test_results.txt'
-
- # تشغيل الاختبارات وحفظ النتائج
- with open(test_results_file, 'w', encoding='utf-8') as f:
- runner = unittest.TextTestRunner(stream=f, verbosity=2)
- suite = unittest.TestSuite()
-
- # إضافة اختبارات مكونات واجهة المستخدم
- suite.addTest(unittest.makeSuite(TestUIComponents))
-
- # إضافة اختبارات تخطيط واجهة المستخدم
- suite.addTest(unittest.makeSuite(TestUILayout))
-
- # إضافة اختبارات دعم اللغة العربية
- suite.addTest(unittest.makeSuite(TestUIArabicSupport))
-
- # تشغيل الاختبارات
- result = runner.run(suite)
-
- # كتابة ملخص النتائج
- f.write("\n\n=== ملخص نتائج اختبارات واجهة المستخدم ===\n")
- f.write(f"عدد الاختبارات: {result.testsRun}\n")
- f.write(f"عدد النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}\n")
- f.write(f"عدد الإخفاقات: {len(result.failures)}\n")
- f.write(f"عدد الأخطاء: {len(result.errors)}\n")
-
- # طباعة ملخص النتائج
- logger.info(f"تم تشغيل {result.testsRun} اختبار لواجهة المستخدم")
- logger.info(f"النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}")
- logger.info(f"الإخفاقات: {len(result.failures)}")
- logger.info(f"الأخطاء: {len(result.errors)}")
- logger.info(f"تم حفظ نتائج الاختبارات في: {test_results_file}")
-
- return result
-
-
-if __name__ == "__main__":
- run_ui_tests()
+"""
+وحدة اختبار واجهة المستخدم لنظام إدارة المناقصات - Hybrid Face
+"""
+
+import os
+import sys
+import logging
+import unittest
+import tkinter as tk
+import customtkinter as ctk
+from pathlib import Path
+
+# تهيئة السجل
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger('test_ui')
+
+# إضافة المسار الرئيسي للتطبيق إلى مسار البحث
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+# استيراد الوحدات المطلوبة للاختبار
+from styling.theme import AppTheme
+from styling.icons import IconGenerator
+from styling.charts import ChartGenerator
+from config import AppConfig
+
+class TestUIComponents(unittest.TestCase):
+ """اختبار مكونات واجهة المستخدم"""
+
+ def setUp(self):
+ """إعداد بيئة الاختبار"""
+ self.root = ctk.CTk()
+ self.root.withdraw() # إخفاء النافذة أثناء الاختبار
+ self.theme = AppTheme()
+
+ def tearDown(self):
+ """تنظيف بيئة الاختبار"""
+ self.root.destroy()
+
+ def test_styled_frame(self):
+ """اختبار الإطار المنسق"""
+ frame = self.theme.create_styled_frame(self.root)
+ self.assertIsNotNone(frame)
+ self.assertEqual(frame.cget("fg_color"), self.theme.get_color("card_bg_color"))
+ self.assertEqual(frame.cget("corner_radius"), self.theme.get_size("border_radius"))
+
+ def test_styled_button(self):
+ """اختبار الزر المنسق"""
+ button = self.theme.create_styled_button(self.root, "زر اختبار")
+ self.assertIsNotNone(button)
+ self.assertEqual(button.cget("text"), "زر اختبار")
+ self.assertEqual(button.cget("fg_color"), self.theme.get_color("button_bg_color"))
+ self.assertEqual(button.cget("text_color"), self.theme.get_color("button_fg_color"))
+
+ def test_styled_label(self):
+ """اختبار التسمية المنسقة"""
+ label = self.theme.create_styled_label(self.root, "تسمية اختبار")
+ self.assertIsNotNone(label)
+ self.assertEqual(label.cget("text"), "تسمية اختبار")
+ self.assertEqual(label.cget("text_color"), self.theme.get_color("fg_color"))
+
+ def test_styled_entry(self):
+ """اختبار حقل الإدخال المنسق"""
+ entry = self.theme.create_styled_entry(self.root, "نص توضيحي")
+ self.assertIsNotNone(entry)
+ self.assertEqual(entry.cget("placeholder_text"), "نص توضيحي")
+ self.assertEqual(entry.cget("fg_color"), self.theme.get_color("input_bg_color"))
+ self.assertEqual(entry.cget("text_color"), self.theme.get_color("input_fg_color"))
+
+ def test_styled_combobox(self):
+ """اختبار القائمة المنسدلة المنسقة"""
+ values = ["الخيار الأول", "الخيار الثاني", "الخيار الثالث"]
+ combobox = self.theme.create_styled_combobox(self.root, values)
+ self.assertIsNotNone(combobox)
+ self.assertEqual(combobox.cget("values"), values)
+ self.assertEqual(combobox.cget("fg_color"), self.theme.get_color("input_bg_color"))
+ self.assertEqual(combobox.cget("text_color"), self.theme.get_color("input_fg_color"))
+
+ def test_styled_checkbox(self):
+ """اختبار خانة الاختيار المنسقة"""
+ checkbox = self.theme.create_styled_checkbox(self.root, "خانة اختبار")
+ self.assertIsNotNone(checkbox)
+ self.assertEqual(checkbox.cget("text"), "خانة اختبار")
+ self.assertEqual(checkbox.cget("fg_color"), self.theme.get_color("button_bg_color"))
+ self.assertEqual(checkbox.cget("text_color"), self.theme.get_color("fg_color"))
+
+ def test_styled_radio_button(self):
+ """اختبار زر الراديو المنسق"""
+ var = ctk.StringVar(value="1")
+ radio_button = self.theme.create_styled_radio_button(self.root, "زر راديو اختبار", var, "1")
+ self.assertIsNotNone(radio_button)
+ self.assertEqual(radio_button.cget("text"), "زر راديو اختبار")
+ self.assertEqual(radio_button.cget("fg_color"), self.theme.get_color("button_bg_color"))
+ self.assertEqual(radio_button.cget("text_color"), self.theme.get_color("fg_color"))
+
+ def test_styled_switch(self):
+ """اختبار مفتاح التبديل المنسق"""
+ switch = self.theme.create_styled_switch(self.root, "مفتاح اختبار")
+ self.assertIsNotNone(switch)
+ self.assertEqual(switch.cget("text"), "مفتاح اختبار")
+ self.assertEqual(switch.cget("progress_color"), self.theme.get_color("button_bg_color"))
+ self.assertEqual(switch.cget("text_color"), self.theme.get_color("fg_color"))
+
+ def test_styled_slider(self):
+ """اختبار شريط التمرير المنسق"""
+ slider = self.theme.create_styled_slider(self.root)
+ self.assertIsNotNone(slider)
+ self.assertEqual(slider.cget("fg_color"), self.theme.get_color("input_border_color"))
+ self.assertEqual(slider.cget("progress_color"), self.theme.get_color("button_bg_color"))
+
+ def test_styled_progressbar(self):
+ """اختبار شريط التقدم المنسق"""
+ progressbar = self.theme.create_styled_progressbar(self.root)
+ self.assertIsNotNone(progressbar)
+ self.assertEqual(progressbar.cget("fg_color"), self.theme.get_color("input_border_color"))
+ self.assertEqual(progressbar.cget("progress_color"), self.theme.get_color("button_bg_color"))
+
+ def test_styled_tabview(self):
+ """اختبار عرض التبويب المنسق"""
+ tabview = self.theme.create_styled_tabview(self.root)
+ self.assertIsNotNone(tabview)
+ self.assertEqual(tabview.cget("fg_color"), self.theme.get_color("card_bg_color"))
+
+ def test_styled_scrollable_frame(self):
+ """اختبار الإطار القابل للتمرير المنسق"""
+ scrollable_frame = self.theme.create_styled_scrollable_frame(self.root)
+ self.assertIsNotNone(scrollable_frame)
+ self.assertEqual(scrollable_frame.cget("fg_color"), "transparent")
+
+ def test_styled_textbox(self):
+ """اختبار مربع النص المنسق"""
+ textbox = self.theme.create_styled_textbox(self.root)
+ self.assertIsNotNone(textbox)
+ self.assertEqual(textbox.cget("fg_color"), self.theme.get_color("input_bg_color"))
+ self.assertEqual(textbox.cget("text_color"), self.theme.get_color("input_fg_color"))
+
+ def test_styled_card(self):
+ """اختبار البطاقة المنسقة"""
+ card, content_frame = self.theme.create_styled_card(self.root, "بطاقة اختبار")
+ self.assertIsNotNone(card)
+ self.assertIsNotNone(content_frame)
+ self.assertEqual(card.cget("fg_color"), self.theme.get_color("card_bg_color"))
+
+ def test_styled_data_table(self):
+ """اختبار جدول البيانات المنسق"""
+ columns = ["العمود الأول", "العمود الثاني", "العمود الثالث"]
+ data = [
+ ["بيانات 1-1", "بيانات 1-2", "بيانات 1-3"],
+ ["بيانات 2-1", "بيانات 2-2", "بيانات 2-3"]
+ ]
+ table_frame, data_frame = self.theme.create_styled_data_table(self.root, columns, data)
+ self.assertIsNotNone(table_frame)
+ self.assertIsNotNone(data_frame)
+ self.assertEqual(table_frame.cget("fg_color"), self.theme.get_color("card_bg_color"))
+
+ def test_theme_switching(self):
+ """اختبار تبديل النمط"""
+ # تعيين النمط الفاتح
+ self.theme.set_theme("light")
+ light_bg_color = self.theme.get_color("bg_color")
+
+ # تعيين النمط الداكن
+ self.theme.set_theme("dark")
+ dark_bg_color = self.theme.get_color("bg_color")
+
+ # التحقق من اختلاف الألوان
+ self.assertNotEqual(light_bg_color, dark_bg_color)
+
+ def test_language_switching(self):
+ """اختبار تبديل اللغة"""
+ # تعيين اللغة العربية
+ self.theme.set_language("ar")
+ ar_font = self.theme.get_font("body")
+
+ # تعيين اللغة الإنجليزية
+ self.theme.set_language("en")
+ en_font = self.theme.get_font("body")
+
+ # التحقق من اختلاف الخطوط
+ self.assertNotEqual(ar_font[0], en_font[0])
+
+
+class TestUILayout(unittest.TestCase):
+ """اختبار تخطيط واجهة المستخدم"""
+
+ def setUp(self):
+ """إعداد بيئة الاختبار"""
+ self.root = ctk.CTk()
+ self.root.withdraw() # إخفاء النافذة أثناء الاختبار
+ self.theme = AppTheme()
+
+ # إنشاء الإطار الرئيسي
+ self.main_frame = self.theme.create_styled_frame(self.root)
+ self.main_frame.pack(fill="both", expand=True)
+
+ # إنشاء الشريط الجانبي
+ self.sidebar_frame = self.theme.create_styled_frame(
+ self.main_frame,
+ fg_color=self.theme.get_color("sidebar_bg_color")
+ )
+ self.sidebar_frame.pack(side="left", fill="y", padx=0, pady=0)
+
+ # إنشاء إطار المحتوى
+ self.content_frame = self.theme.create_styled_frame(
+ self.main_frame,
+ fg_color=self.theme.get_color("bg_color")
+ )
+ self.content_frame.pack(side="right", fill="both", expand=True, padx=0, pady=0)
+
+ def tearDown(self):
+ """تنظيف بيئة الاختبار"""
+ self.root.destroy()
+
+ def test_sidebar_layout(self):
+ """اختبار تخطيط الشريط الجانبي"""
+ # إنشاء شعار التطبيق
+ logo_label = self.theme.create_styled_label(
+ self.sidebar_frame,
+ "نظام إدارة المناقصات",
+ font=self.theme.get_font("title"),
+ text_color=self.theme.get_color("sidebar_fg_color")
+ )
+ logo_label.pack(padx=20, pady=20)
+
+ # إنشاء أزرار الشريط الجانبي
+ sidebar_buttons = []
+ button_texts = [
+ "لوحة التحكم", "المشاريع", "المستندات", "التسعير",
+ "الموارد", "المخاطر", "التقارير", "الذكاء الاصطناعي"
+ ]
+
+ for text in button_texts:
+ button_frame, button = self.theme.create_styled_sidebar_button(
+ self.sidebar_frame,
+ text
+ )
+ button_frame.pack(fill="x", padx=0, pady=2)
+ sidebar_buttons.append(button)
+
+ # التحقق من إنشاء الأزرار
+ self.assertEqual(len(sidebar_buttons), len(button_texts))
+ for i, button in enumerate(sidebar_buttons):
+ self.assertEqual(button.cget("text"), button_texts[i])
+
+ def test_content_layout(self):
+ """اختبار تخطيط المحتوى"""
+ # إنشاء شريط العنوان
+ header_frame = self.theme.create_styled_frame(
+ self.content_frame,
+ fg_color=self.theme.get_color("card_bg_color")
+ )
+ header_frame.pack(fill="x", padx=20, pady=20)
+
+ # إنشاء عنوان الصفحة
+ page_title = self.theme.create_styled_label(
+ header_frame,
+ "لوحة التحكم",
+ font=self.theme.get_font("title")
+ )
+ page_title.pack(side="left", padx=20, pady=20)
+
+ # إنشاء زر البحث
+ search_button = self.theme.create_styled_button(
+ header_frame,
+ "بحث"
+ )
+ search_button.pack(side="right", padx=20, pady=20)
+
+ # إنشاء إطار البطاقات
+ cards_frame = self.theme.create_styled_frame(
+ self.content_frame,
+ fg_color="transparent"
+ )
+ cards_frame.pack(fill="both", expand=True, padx=20, pady=20)
+
+ # إنشاء بطاقات
+ cards = []
+ card_titles = [
+ "المشاريع النشطة", "المناقصات الجديدة", "المخاطر العالية", "التقارير المعلقة"
+ ]
+
+ for i, title in enumerate(card_titles):
+ card, card_content = self.theme.create_styled_card(
+ cards_frame,
+ title
+ )
+ card.grid(row=i//2, column=i%2, padx=10, pady=10, sticky="nsew")
+ cards.append(card)
+
+ # التحقق من إنشاء البطاقات
+ self.assertEqual(len(cards), len(card_titles))
+
+ # تهيئة أوزان الصفوف والأعمدة
+ cards_frame.grid_columnconfigure(0, weight=1)
+ cards_frame.grid_columnconfigure(1, weight=1)
+ cards_frame.grid_rowconfigure(0, weight=1)
+ cards_frame.grid_rowconfigure(1, weight=1)
+
+ def test_responsive_layout(self):
+ """اختبار التخطيط المتجاوب"""
+ # تغيير حجم النافذة
+ self.root.geometry("800x600")
+ self.root.update()
+
+ # التحقق من أن الإطار الرئيسي يملأ النافذة
+ self.assertEqual(self.main_frame.winfo_width(), 800)
+ self.assertEqual(self.main_frame.winfo_height(), 600)
+
+ # تغيير حجم النافذة مرة أخرى
+ self.root.geometry("1024x768")
+ self.root.update()
+
+ # التحقق من أن الإطار الرئيسي يملأ النافذة
+ self.assertEqual(self.main_frame.winfo_width(), 1024)
+ self.assertEqual(self.main_frame.winfo_height(), 768)
+
+
+class TestUIArabicSupport(unittest.TestCase):
+ """اختبار دعم اللغة العربية في واجهة المستخدم"""
+
+ def setUp(self):
+ """إعداد بيئة الاختبار"""
+ self.root = ctk.CTk()
+ self.root.withdraw() # إخفاء النافذة أثناء الاختبار
+ self.theme = AppTheme()
+ self.theme.set_language("ar") # تعيين اللغة العربية
+
+ def tearDown(self):
+ """تنظيف بيئة الاختبار"""
+ self.root.destroy()
+
+ def test_arabic_text_display(self):
+ """اختبار عرض النص العربي"""
+ # إنشاء تسمية بنص عربي
+ arabic_text = "هذا نص عربي للاختبار"
+ label = self.theme.create_styled_label(self.root, arabic_text)
+ self.assertEqual(label.cget("text"), arabic_text)
+
+ # إنشاء زر بنص عربي
+ button = self.theme.create_styled_button(self.root, "زر باللغة العربية")
+ self.assertEqual(button.cget("text"), "زر باللغة العربية")
+
+ # إنشاء حقل إدخال بنص توضيحي عربي
+ entry = self.theme.create_styled_entry(self.root, "أدخل النص هنا")
+ self.assertEqual(entry.cget("placeholder_text"), "أدخل النص هنا")
+
+ def test_arabic_font(self):
+ """اختبار الخط العربي"""
+ # التحقق من استخدام خط يدعم العربية
+ ar_font = self.theme.get_font("body")
+ self.assertEqual(ar_font[0], "Cairo")
+
+ def test_rtl_support(self):
+ """اختبار دعم الكتابة من اليمين إلى اليسار"""
+ # إنشاء إطار
+ frame = self.theme.create_styled_frame(self.root)
+ frame.pack(fill="both", expand=True)
+
+ # إنشاء تسمية بنص عربي
+ label = self.theme.create_styled_label(frame, "نص عربي من اليمين إلى اليسار")
+ label.pack(anchor="e", padx=20, pady=20) # محاذاة إلى اليمين
+
+ # التحقق من المحاذاة
+ self.assertEqual(label.cget("anchor"), "w") # w تعني غرب (يسار)، لكن النص سيظهر من اليمين إلى اليسار
+
+
+def run_ui_tests():
+ """تشغيل اختبارات واجهة المستخدم"""
+ # إنشاء مجلد الاختبارات
+ test_dir = Path('test_results')
+ test_dir.mkdir(exist_ok=True)
+
+ # إنشاء ملف لنتائج الاختبارات
+ test_results_file = test_dir / 'ui_test_results.txt'
+
+ # تشغيل الاختبارات وحفظ النتائج
+ with open(test_results_file, 'w', encoding='utf-8') as f:
+ runner = unittest.TextTestRunner(stream=f, verbosity=2)
+ suite = unittest.TestSuite()
+
+ # إضافة اختبارات مكونات واجهة المستخدم
+ suite.addTest(unittest.makeSuite(TestUIComponents))
+
+ # إضافة اختبارات تخطيط واجهة المستخدم
+ suite.addTest(unittest.makeSuite(TestUILayout))
+
+ # إضافة اختبارات دعم اللغة العربية
+ suite.addTest(unittest.makeSuite(TestUIArabicSupport))
+
+ # تشغيل الاختبارات
+ result = runner.run(suite)
+
+ # كتابة ملخص النتائج
+ f.write("\n\n=== ملخص نتائج اختبارات واجهة المستخدم ===\n")
+ f.write(f"عدد الاختبارات: {result.testsRun}\n")
+ f.write(f"عدد النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}\n")
+ f.write(f"عدد الإخفاقات: {len(result.failures)}\n")
+ f.write(f"عدد الأخطاء: {len(result.errors)}\n")
+
+ # طباعة ملخص النتائج
+ logger.info(f"تم تشغيل {result.testsRun} اختبار لواجهة المستخدم")
+ logger.info(f"النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}")
+ logger.info(f"الإخفاقات: {len(result.failures)}")
+ logger.info(f"الأخطاء: {len(result.errors)}")
+ logger.info(f"تم حفظ نتائج الاختبارات في: {test_results_file}")
+
+ return result
+
+
+if __name__ == "__main__":
+ run_ui_tests()