diff --git a/modules/achievements/__init__.py b/modules/achievements/__init__.py deleted file mode 100644 index a90f2266bb52cfe152316012068d7d797b6a7a0c..0000000000000000000000000000000000000000 --- a/modules/achievements/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -""" -وحدة نظام الإنجازات المحفز لمراحل المشروع -""" \ No newline at end of file diff --git a/modules/achievements/achievement_system.py b/modules/achievements/achievement_system.py deleted file mode 100644 index 1e882fc6e37e64ec689e4438aade888759985410..0000000000000000000000000000000000000000 --- a/modules/achievements/achievement_system.py +++ /dev/null @@ -1,1033 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -نظام الإنجازات المحفز لمراحل المشروع -""" - -import os -import sys -import json -import streamlit as st -import pandas as pd -import numpy as np -import time -from datetime import datetime, timedelta -import random - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات قاعدة البيانات -try: - from database.db_connector import get_connection -except ImportError: - from utils.helpers import get_connection - -from utils.helpers import format_time, get_user_info, load_icons - - -class AchievementSystem: - """نظام الإنجازات المحفز لمراحل المشروع""" - - def __init__(self, user_id=None): - """تهيئة نظام الإنجازات المحفز""" - self.user_id = user_id or 1 # استخدام المستخدم الافتراضي إذا لم يتم توفير معرف المستخدم - self.conn = get_connection() - self.achievements_path = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'achievements') - os.makedirs(self.achievements_path, exist_ok=True) - self.user_data_file = os.path.join(self.achievements_path, f'user_{self.user_id}_achievements.json') - self.icons = load_icons() - - # تحميل بيانات المستخدم - self.load_user_data() - - # تعريف قائمة الإنجازات - self.define_achievements() - - def load_user_data(self): - """تحميل بيانات إنجازات المستخدم""" - try: - if os.path.exists(self.user_data_file): - with open(self.user_data_file, 'r', encoding='utf-8') as f: - self.user_data = json.load(f) - else: - # بيانات افتراضية عند عدم وجود ملف - self.user_data = { - 'user_id': self.user_id, - 'total_points': 0, - 'level': 1, - 'unlocked_achievements': [], - 'in_progress_achievements': [], - 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - self.save_user_data() - except Exception as e: - st.error(f"خطأ في تحميل بيانات المستخدم: {e}") - self.user_data = { - 'user_id': self.user_id, - 'total_points': 0, - 'level': 1, - 'unlocked_achievements': [], - 'in_progress_achievements': [], - 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - - def save_user_data(self): - """حفظ بيانات إنجازات المستخدم""" - try: - with open(self.user_data_file, 'w', encoding='utf-8') as f: - json.dump(self.user_data, f, ensure_ascii=False, indent=2) - except Exception as e: - st.error(f"خطأ في حفظ بيانات المستخدم: {e}") - - def define_achievements(self): - """تعريف قائمة الإنجازات المتاحة""" - self.achievements = [ - { - 'id': 'first_project', - 'name': 'بداية الرحلة', - 'description': 'قم بإنشاء مشروعك الأول', - 'icon': '🏆', - 'points': 100, - 'category': 'مشاريع', - 'difficulty': 'سهل' - }, - { - 'id': 'five_projects', - 'name': 'محترف المشاريع', - 'description': 'قم بإنشاء خمسة مشاريع', - 'icon': '🏅', - 'points': 500, - 'category': 'مشاريع', - 'difficulty': 'متوسط' - }, - { - 'id': 'ten_projects', - 'name': 'خبير المشاريع', - 'description': 'قم بإنشاء عشرة مشاريع', - 'icon': '🎖️', - 'points': 1000, - 'category': 'مشاريع', - 'difficulty': 'صعب' - }, - { - 'id': 'first_document_analysis', - 'name': 'المحلل الأول', - 'description': 'قم بتحليل مستند للمرة الأولى', - 'icon': '📊', - 'points': 150, - 'category': 'تحليل', - 'difficulty': 'سهل' - }, - { - 'id': 'five_document_analysis', - 'name': 'محلل متمرس', - 'description': 'قم بتحليل خمسة مستندات', - 'icon': '📈', - 'points': 600, - 'category': 'تحليل', - 'difficulty': 'متوسط' - }, - { - 'id': 'complete_boq', - 'name': 'خبير جداول الكميات', - 'description': 'أكمل تحليل جدول كميات كامل', - 'icon': '📋', - 'points': 300, - 'category': 'تحليل', - 'difficulty': 'متوسط' - }, - { - 'id': 'risk_analysis', - 'name': 'محلل المخاطر', - 'description': 'أكمل تحليل مخاطر متقدم', - 'icon': '⚠️', - 'points': 400, - 'category': 'مخاطر', - 'difficulty': 'متوسط' - }, - { - 'id': 'ten_risk_identified', - 'name': 'متنبئ المخاطر', - 'description': 'تعرف على عشرة مخاطر في المشاريع', - 'icon': '🔍', - 'points': 700, - 'category': 'مخاطر', - 'difficulty': 'صعب' - }, - { - 'id': 'first_terms_analysis', - 'name': 'محلل الشروط', - 'description': 'قم بتحليل بنود الشروط والأحكام', - 'icon': '📝', - 'points': 250, - 'category': 'تحليل', - 'difficulty': 'متوسط' - }, - { - 'id': 'quick_analysis', - 'name': 'محلل سريع', - 'description': 'أكمل تحليل مستند في أقل من 5 دقائق', - 'icon': '⚡', - 'points': 500, - 'category': 'كفاءة', - 'difficulty': 'صعب' - }, - { - 'id': 'voice_narration', - 'name': 'مترجم صوتي', - 'description': 'استخدم ميزة الترجمة الصوتية لأول مرة', - 'icon': '🎙️', - 'points': 200, - 'category': 'ترجمة', - 'difficulty': 'سهل' - }, - { - 'id': 'multilingual_expert', - 'name': 'خبير متعدد اللغات', - 'description': 'استخدم الترجمة الصوتية بخمس لغات مختلفة', - 'icon': '🌍', - 'points': 800, - 'category': 'ترجمة', - 'difficulty': 'صعب' - }, - { - 'id': 'first_map', - 'name': 'مستكشف الخرائط', - 'description': 'استخدم ميزة الخريطة التفاعلية لأول مرة', - 'icon': '🗺️', - 'points': 200, - 'category': 'خرائط', - 'difficulty': 'سهل' - }, - { - 'id': 'ai_fine_tuning', - 'name': 'مدرب الذكاء', - 'description': 'قم بتدريب نموذج ذكاء اصطناعي مخصص', - 'icon': '🧠', - 'points': 1000, - 'category': 'ذكاء اصطناعي', - 'difficulty': 'خبير' - }, - { - 'id': 'pricing_master', - 'name': 'سيد التسعير', - 'description': 'أكمل حساب تكلفة مشروع بالكامل', - 'icon': '💰', - 'points': 500, - 'category': 'تسعير', - 'difficulty': 'متوسط' - } - ] - - def calculate_level(self, points): - """حساب مستوى المستخدم بناءً على النقاط""" - # صيغة بسيطة لحساب المستوى: كل 1000 نقطة = مستوى واحد - level = 1 + int(points / 1000) - return level - - def unlock_achievement(self, achievement_id): - """إلغاء قفل إنجاز جديد""" - # التحقق من وجود الإنجاز في القائمة - achievement = next((a for a in self.achievements if a['id'] == achievement_id), None) - if not achievement: - return False - - # التحقق من عدم وجود الإنجاز مسبقاً - if achievement_id in [a['id'] for a in self.user_data['unlocked_achievements']]: - return False - - # إزالة الإنجاز من قائمة "قيد التقدم" إذا كان موجوداً - self.user_data['in_progress_achievements'] = [ - a for a in self.user_data['in_progress_achievements'] - if a['id'] != achievement_id - ] - - # إضافة الإنجاز إلى القائمة - achievement['unlocked_date'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - self.user_data['unlocked_achievements'].append(achievement) - - # تحديث النقاط والمستوى - self.user_data['total_points'] += achievement['points'] - self.user_data['level'] = self.calculate_level(self.user_data['total_points']) - - # حفظ البيانات - self.save_user_data() - - return achievement - - def update_achievement_progress(self, achievement_id, progress, total): - """تحديث تقدم إنجاز معين""" - # التحقق من وجود الإنجاز في القائمة - achievement = next((a for a in self.achievements if a['id'] == achievement_id), None) - if not achievement: - return False - - # التحقق من عدم وجود الإنجاز في قائمة "تم إلغاء قفله" - if achievement_id in [a['id'] for a in self.user_data['unlocked_achievements']]: - return False - - # البحث عن الإنجاز في قائمة "قيد التقدم" - in_progress_achievement = next( - (a for a in self.user_data['in_progress_achievements'] if a['id'] == achievement_id), - None - ) - - if in_progress_achievement: - # تحديث التقدم - in_progress_achievement['progress'] = progress - in_progress_achievement['total'] = total - in_progress_achievement['percentage'] = min(100, int((progress / total) * 100)) - in_progress_achievement['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - else: - # إضافة الإنجاز إلى قائمة "قيد التقدم" - progress_data = achievement.copy() - progress_data['progress'] = progress - progress_data['total'] = total - progress_data['percentage'] = min(100, int((progress / total) * 100)) - progress_data['start_date'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - progress_data['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - self.user_data['in_progress_achievements'].append(progress_data) - - # إذا اكتمل التقدم، قم بإلغاء قفل الإنجاز - if progress >= total: - return self.unlock_achievement(achievement_id) - - # حفظ البيانات - self.save_user_data() - - return True - - def check_and_award_achievements(self, action_type, data=None): - """التحقق من ومنح الإنجازات بناءً على إجراءات المستخدم""" - try: - if action_type == 'create_project': - # حساب عدد المشاريع - projects_count = self._get_projects_count() - - # منح إنجازات المشاريع - if projects_count == 1: - self.unlock_achievement('first_project') - elif projects_count == 5: - self.unlock_achievement('five_projects') - elif projects_count == 10: - self.unlock_achievement('ten_projects') - - # تحديث تقدم الإنجاز - self.update_achievement_progress('five_projects', min(projects_count, 5), 5) - self.update_achievement_progress('ten_projects', min(projects_count, 10), 10) - - elif action_type == 'analyze_document': - # حساب عدد تحليلات المستندات - analysis_count = self._get_document_analysis_count() - - # منح إنجازات تحليل المستندات - if analysis_count == 1: - self.unlock_achievement('first_document_analysis') - elif analysis_count == 5: - self.unlock_achievement('five_document_analysis') - - # تحديث تقدم الإنجاز - self.update_achievement_progress('five_document_analysis', min(analysis_count, 5), 5) - - # التحقق من الوقت المستغرق للتحليل - if data and 'duration_seconds' in data and data['duration_seconds'] < 300: # أقل من 5 دقائق - self.unlock_achievement('quick_analysis') - - elif action_type == 'analyze_boq': - self.unlock_achievement('complete_boq') - - elif action_type == 'analyze_terms': - self.unlock_achievement('first_terms_analysis') - - elif action_type == 'analyze_risks': - self.unlock_achievement('risk_analysis') - - # حساب عدد المخاطر المحددة - if data and 'risks_count' in data: - risks_count = data['risks_count'] - risk_total = self._get_total_risks_identified() - new_total = risk_total + risks_count - - # تحديث تقدم إنجاز "متنبئ المخاطر" - self.update_achievement_progress('ten_risk_identified', min(new_total, 10), 10) - - if new_total >= 10 and risk_total < 10: - self.unlock_achievement('ten_risk_identified') - - elif action_type == 'use_voice_narration': - self.unlock_achievement('voice_narration') - - # حساب عدد اللغات المستخدمة - if data and 'language' in data: - languages_used = self._get_languages_used() - if data['language'] not in languages_used: - languages_used.append(data['language']) - self._save_languages_used(languages_used) - - # تحديث تقدم إنجاز "خبير متعدد اللغات" - self.update_achievement_progress('multilingual_expert', len(languages_used), 5) - - if len(languages_used) >= 5: - self.unlock_achievement('multilingual_expert') - - elif action_type == 'use_map': - self.unlock_achievement('first_map') - - elif action_type == 'train_ai_model': - self.unlock_achievement('ai_fine_tuning') - - elif action_type == 'complete_pricing': - self.unlock_achievement('pricing_master') - - except Exception as e: - st.error(f"خطأ في التحقق من الإنجازات: {e}") - - def _get_projects_count(self): - """الحصول على عدد المشاريع""" - try: - cursor = self.conn.cursor() - cursor.execute("SELECT COUNT(*) FROM documents WHERE user_id = %s AND type = 'project'", (self.user_id,)) - count = cursor.fetchone()[0] - cursor.close() - return count - except Exception: - # في حالة عدم وجود جدول أو خطأ في الاتصال، استخدم بيانات تقديرية - projects_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'projects') - if os.path.exists(projects_dir): - return len([f for f in os.listdir(projects_dir) if os.path.isdir(os.path.join(projects_dir, f))]) - return 0 - - def _get_document_analysis_count(self): - """الحصول على عدد تحليلات المستندات""" - try: - cursor = self.conn.cursor() - cursor.execute("SELECT COUNT(*) FROM document_analysis WHERE document_id IN (SELECT id FROM documents WHERE user_id = %s)", (self.user_id,)) - count = cursor.fetchone()[0] - cursor.close() - return count - except Exception: - # في حالة عدم وجود جدول أو خطأ في الاتصال، استخدم بيانات تقديرية - return len(self.user_data['unlocked_achievements']) - - def _get_total_risks_identified(self): - """الحصول على إجمالي عدد المخاطر المحددة""" - risks_file = os.path.join(self.achievements_path, f'user_{self.user_id}_risks.json') - if os.path.exists(risks_file): - try: - with open(risks_file, 'r', encoding='utf-8') as f: - risks_data = json.load(f) - return risks_data.get('total_risks', 0) - except Exception: - return 0 - return 0 - - def _save_total_risks_identified(self, total): - """حفظ إجمالي عدد المخاطر المحددة""" - risks_file = os.path.join(self.achievements_path, f'user_{self.user_id}_risks.json') - try: - with open(risks_file, 'w', encoding='utf-8') as f: - json.dump({'total_risks': total}, f, ensure_ascii=False, indent=2) - except Exception: - pass - - def _get_languages_used(self): - """الحصول على قائمة اللغات المستخدمة""" - languages_file = os.path.join(self.achievements_path, f'user_{self.user_id}_languages.json') - if os.path.exists(languages_file): - try: - with open(languages_file, 'r', encoding='utf-8') as f: - languages_data = json.load(f) - return languages_data.get('languages', []) - except Exception: - return [] - return [] - - def _save_languages_used(self, languages): - """حفظ قائمة اللغات المستخدمة""" - languages_file = os.path.join(self.achievements_path, f'user_{self.user_id}_languages.json') - try: - with open(languages_file, 'w', encoding='utf-8') as f: - json.dump({'languages': languages}, f, ensure_ascii=False, indent=2) - except Exception: - pass - - def render_achievements_tab(self): - """عرض علامة تبويب الإنجازات""" - st.markdown("

إنجازاتك

", unsafe_allow_html=True) - - # عرض مستوى المستخدم والنقاط - col1, col2 = st.columns([1, 3]) - with col1: - st.markdown(f"
المستوى {self.user_data['level']}
", unsafe_allow_html=True) - with col2: - # حساب النقاط المطلوبة للمستوى التالي - next_level_points = (self.user_data['level']) * 1000 - current_level_points = (self.user_data['level'] - 1) * 1000 - progress = (self.user_data['total_points'] - current_level_points) / (next_level_points - current_level_points) - - st.markdown(f"
{self.user_data['total_points']} نقطة
", unsafe_allow_html=True) - st.progress(progress, text=f"المستوى التالي: {next_level_points} نقطة") - - # تقسيم الإنجازات إلى مجموعات - st.markdown("

الإنجازات المفتوحة

", unsafe_allow_html=True) - - if not self.user_data['unlocked_achievements']: - st.info("لم تقم بفتح أي إنجازات حتى الآن. أكمل المهام للحصول على الإنجازات!") - else: - # عرض الإنجازات المفتوحة بتنسيق الشبكة - cols = st.columns(3) - for i, achievement in enumerate(self.user_data['unlocked_achievements']): - with cols[i % 3]: - self._render_achievement_card(achievement, is_unlocked=True) - - # عرض الإنجازات قيد التقدم - st.markdown("

الإنجازات قيد التقدم

", unsafe_allow_html=True) - - if not self.user_data['in_progress_achievements']: - st.info("ليس لديك أي إنجازات قيد التقدم حالياً.") - else: - # عرض الإنجازات قيد التقدم - for achievement in self.user_data['in_progress_achievements']: - self._render_progress_achievement(achievement) - - # عرض الإنجازات المتاحة - st.markdown("

الإنجازات المتاحة

", unsafe_allow_html=True) - - # فلترة الإنجازات غير المفتوحة وغير قيد التقدم - unlocked_ids = [a['id'] for a in self.user_data['unlocked_achievements']] - in_progress_ids = [a['id'] for a in self.user_data['in_progress_achievements']] - available_achievements = [a for a in self.achievements if a['id'] not in unlocked_ids and a['id'] not in in_progress_ids] - - if not available_achievements: - st.success("رائع! لقد حققت جميع الإنجازات المتاحة.") - else: - # تقسيم الإنجازات المتاحة حسب الفئات - categories = sorted(set(a['category'] for a in available_achievements)) - for category in categories: - st.markdown(f"
{category}
", unsafe_allow_html=True) - - category_achievements = [a for a in available_achievements if a['category'] == category] - cols = st.columns(3) - for i, achievement in enumerate(category_achievements): - with cols[i % 3]: - self._render_achievement_card(achievement, is_unlocked=False) - - def _render_achievement_card(self, achievement, is_unlocked): - """عرض بطاقة إنجاز""" - if is_unlocked: - card_class = "achievement-card unlocked" - icon_class = "achievement-icon unlocked" - title_class = "achievement-name unlocked" - points_display = f"{achievement['points']} نقطة" - date_display = f"تم الفتح: {achievement.get('unlocked_date', 'غير معروف')}" - else: - card_class = "achievement-card locked" - icon_class = "achievement-icon locked" - title_class = "achievement-name locked" - points_display = f"{achievement['points']} نقطة" - date_display = f"صعوبة: {achievement['difficulty']}" - - html = f""" -
-
{achievement['icon']}
-
{achievement['name']}
-
{achievement['description']}
- -
- """ - st.markdown(html, unsafe_allow_html=True) - - def _render_progress_achievement(self, achievement): - """عرض إنجاز قيد التقدم""" - progress = achievement.get('percentage', 0) - - html = f""" -
-
-
{achievement['icon']}
-
-
{achievement['name']}
-
{achievement['description']}
-
-
{achievement['points']} نقطة
-
-
- """ - st.markdown(html, unsafe_allow_html=True) - - st.progress(progress / 100, text=f"{progress}% ({achievement.get('progress', 0)}/{achievement.get('total', 1)})") - - def render_achievements_summary(self): - """عرض ملخص الإنجازات في لوحة التحكم""" - # حساب الإحصائيات - total_achievements = len(self.achievements) - unlocked_count = len(self.user_data['unlocked_achievements']) - in_progress_count = len(self.user_data['in_progress_achievements']) - - st.markdown(f""" -
-
-
الإنجازات
-
المستوى {self.user_data['level']}
-
-
-
{int((unlocked_count / total_achievements) * 100)}%
-
{unlocked_count} / {total_achievements}
-
- -
- """, unsafe_allow_html=True) - - # عرض آخر 3 إنجازات تم فتحها - if self.user_data['unlocked_achievements']: - st.markdown("
آخر الإنجازات
", unsafe_allow_html=True) - - recent_achievements = sorted( - self.user_data['unlocked_achievements'], - key=lambda x: x.get('unlocked_date', ''), - reverse=True - )[:3] - - for achievement in recent_achievements: - st.markdown(f""" -
-
{achievement['icon']}
-
-
{achievement['name']}
-
{achievement.get('unlocked_date', '')}
-
-
+{achievement['points']}
-
- """, unsafe_allow_html=True) - - def render(self): - """عرض واجهة نظام الإنجازات""" - st.markdown("

نظام الإنجازات المحفز لمراحل المشروع

", unsafe_allow_html=True) - - st.markdown(""" -
- نظام الإنجازات يحفزك على إكمال المهام وتحقيق أهداف المشروع من خلال مكافآت - وإنجازات قابلة للفتح. اكتسب النقاط وارتقِ بمستواك وافتح إنجازات جديدة كلما تقدمت في استخدام نظام تحليل المناقصات. -
- """, unsafe_allow_html=True) - - # عرض صندوق معلومات عند تشغيل الوحدة لأول مرة - if not self.user_data['unlocked_achievements'] and not self.user_data['in_progress_achievements']: - st.info(""" - 👋 مرحباً بك في نظام الإنجازات! - - استكشف الإنجازات المتاحة وابدأ في تحقيقها عن طريق إكمال المهام في أنحاء النظام المختلفة. - كلما حققت المزيد من الإنجازات، حصلت على نقاط أكثر وارتقيت في المستويات. - - ابدأ الآن بإنشاء مشروع جديد أو تحليل مستند! - """) - - # إنشاء علامات تبويب لعرض محتوى مختلف - tab1, tab2, tab3 = st.tabs(["الإنجازات", "المستويات والمكافآت", "الإحصائيات"]) - - with tab1: - self.render_achievements_tab() - - with tab2: - st.markdown("

المستويات والمكافآت

", unsafe_allow_html=True) - - # عرض معلومات عن نظام المستويات - st.markdown(""" -
-

نظام المستويات يعتمد على النقاط التي تكتسبها من إنجاز المهام وفتح الإنجازات:

- -

كلما ارتقيت في المستويات، تفتح مكافآت وميزات جديدة في النظام!

-
- """, unsafe_allow_html=True) - - # عرض قائمة المكافآت - st.markdown("

المكافآت المتاحة

", unsafe_allow_html=True) - - rewards = [ - {"level": 2, "name": "قوالب مخصصة", "description": "الوصول إلى قوالب مخصصة للتقارير والتحليلات"}, - {"level": 3, "name": "تنبيهات متقدمة", "description": "إعدادات إشعارات متقدمة للمشاريع والمواعيد النهائية"}, - {"level": 5, "name": "تحليل معزز", "description": "خيارات إضافية لتحليل المستندات والعقود"}, - {"level": 7, "name": "تخصيص متقدم", "description": "خيارات إضافية لتخصيص واجهة النظام والتقارير"}, - {"level": 10, "name": "وضع الخبراء", "description": "وضع متقدم مع ميزات خاصة متاحة فقط للمستخدمين المخضرمين"} - ] - - for reward in rewards: - status = "متاح" if self.user_data['level'] >= reward['level'] else "مقفل" - status_class = "available" if self.user_data['level'] >= reward['level'] else "locked" - - st.markdown(f""" -
-
المستوى {reward['level']}
-
-
{reward['name']}
-
{reward['description']}
-
-
{status}
-
- """, unsafe_allow_html=True) - - with tab3: - st.markdown("

إحصائيات الإنجازات

", unsafe_allow_html=True) - - # إعداد بيانات للرسم البياني - categories = {} - for achievement in self.achievements: - category = achievement['category'] - if category not in categories: - categories[category] = {"total": 0, "unlocked": 0} - categories[category]["total"] += 1 - - # حساب الإنجازات المفتوحة لكل فئة - for achievement in self.user_data['unlocked_achievements']: - category = achievement['category'] - if category in categories: - categories[category]["unlocked"] += 1 - - # تحويل البيانات إلى DataFrame - df = pd.DataFrame([ - { - "الفئة": category, - "المفتوحة": data["unlocked"], - "الإجمالي": data["total"], - "النسبة": round((data["unlocked"] / data["total"]) * 100 if data["total"] > 0 else 0) - } - for category, data in categories.items() - ]) - - # عرض البيانات في جدول - st.dataframe( - df, - column_config={ - "النسبة": st.column_config.ProgressColumn( - "نسبة الإنجاز", - format="%d%%", - min_value=0, - max_value=100 - ) - }, - hide_index=True - ) - - # عرض معلومات إضافية - col1, col2, col3 = st.columns(3) - with col1: - total_points_possible = sum(a['points'] for a in self.achievements) - st.metric( - "إجمالي النقاط المحتملة", - f"{total_points_possible}", - f"{int((self.user_data['total_points'] / total_points_possible) * 100)}%" - ) - - with col2: - days_since_first = 0 - if self.user_data['unlocked_achievements']: - first_date = min([ - datetime.strptime(a.get('unlocked_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')), '%Y-%m-%d %H:%M:%S') - for a in self.user_data['unlocked_achievements'] - ]) - days_since_first = (datetime.now() - first_date).days - - st.metric("أيام النشاط", f"{days_since_first}") - - with col3: - if self.user_data['unlocked_achievements']: - achievements_per_day = round(len(self.user_data['unlocked_achievements']) / max(1, days_since_first), 2) - st.metric("معدل الإنجازات اليومي", f"{achievements_per_day}") - else: - st.metric("معدل الإنجازات اليومي", "0") - - # إضافة CSS مخصص للصفحة - st.markdown(""" - - """, unsafe_allow_html=True) \ No newline at end of file diff --git a/modules/achievements/achievements_app.py b/modules/achievements/achievements_app.py deleted file mode 100644 index 908c740742312d5b2f9e45f4c13334b3c436500d..0000000000000000000000000000000000000000 --- a/modules/achievements/achievements_app.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة تطبيق نظام الإنجازات المحفز لمراحل المشروع -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np -import time -from datetime import datetime, timedelta - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات نظام الإنجازات -from modules.achievements.achievement_system import AchievementSystem - - -class AchievementsApp: - """وحدة تطبيق نظام الإنجازات المحفز لمراحل المشروع""" - - def __init__(self, user_id=None): - """تهيئة وحدة تطبيق نظام الإنجازات المحفز""" - self.achievement_system = AchievementSystem(user_id) - - def render(self): - """عرض واجهة وحدة تطبيق نظام الإنجازات المحفز""" - self.achievement_system.render() - - def render_dashboard_summary(self): - """عرض ملخص الإنجازات في لوحة التحكم""" - self.achievement_system.render_achievements_summary() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="نظام الإنجازات المحفز | WAHBi AI", - page_icon="🏆", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = AchievementsApp() - app.render() \ No newline at end of file diff --git a/modules/ai_assistant/__init__.py b/modules/ai_assistant/__init__.py deleted file mode 100644 index d6e1f9b02e95d00ffe620203226b5ec04b5495a6..0000000000000000000000000000000000000000 --- a/modules/ai_assistant/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -وحدة المساعد الذكي -""" - -__version__ = '1.0.0' \ No newline at end of file diff --git a/modules/ai_assistant/ai_app.py b/modules/ai_assistant/ai_app.py deleted file mode 100644 index 7ebd95fac567e489ef7b02f03065a0bd1710c26f..0000000000000000000000000000000000000000 --- a/modules/ai_assistant/ai_app.py +++ /dev/null @@ -1,1067 +0,0 @@ -""" -وحدة الذكاء الاصطناعي - التطبيق الرئيسي -""" - -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 AIAssistantApp: - """وحدة الذكاء الاصطناعي""" - - def __init__(self): - """تهيئة وحدة الذكاء الاصطناعي""" - - # تهيئة حالة الجلسة - if 'chat_history' not in st.session_state: - st.session_state.chat_history = [ - { - 'role': 'assistant', - 'content': 'مرحباً! أنا مساعدك الذكي لإدارة المناقصات. كيف يمكنني مساعدتك اليوم؟' - } - ] - - if 'document_summaries' not in st.session_state: - st.session_state.document_summaries = [ - { - 'id': 1, - 'title': 'كراسة الشروط والمواصفات - مشروع إنشاء مبنى إداري', - 'date': '2024-03-15', - 'summary': 'تتضمن كراسة الشروط والمواصفات لمشروع إنشاء مبنى إداري متطلبات المشروع وشروط التنفيذ والمواصفات الفنية للأعمال المطلوبة. يتكون المبنى من 5 طوابق بمساحة إجمالية 5000 متر مربع. تشمل الأعمال الأساسية: الأعمال الإنشائية، الأعمال المعمارية، الأعمال الكهربائية، الأعمال الميكانيكية، وأعمال التشطيبات.', - 'key_points': [ - 'مدة التنفيذ: 18 شهراً', - 'قيمة الضمان الابتدائي: 2% من قيمة العطاء', - 'قيمة الضمان النهائي: 5% من قيمة العقد', - 'غرامة التأخير: 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%', - 'شروط الدفع: دفعات شهرية حسب نسبة الإنجاز' - ], - 'entities': { - 'الجهة المالكة': 'وزارة المالية', - 'موقع المشروع': 'الرياض - حي العليا', - 'رقم المناقصة': 'T-2024-001', - 'تاريخ الطرح': '2024-03-01', - 'تاريخ الإقفال': '2024-04-15' - } - }, - { - 'id': 2, - 'title': 'جدول الكميات - مشروع تطوير شبكة طرق', - 'date': '2024-03-20', - 'summary': 'يتضمن جدول الكميات لمشروع تطوير شبكة طرق تفاصيل الأعمال المطلوبة والكميات التقديرية. يشمل المشروع إنشاء طرق جديدة بطول 15 كم وتطوير طرق قائمة بطول 10 كم، بالإضافة إلى إنشاء 3 جسور و5 أنفاق.', - 'key_points': [ - 'إجمالي أعمال الحفر: 250,000 م3', - 'إجمالي أعمال الردم: 180,000 م3', - 'إجمالي أعمال الخرسانة: 45,000 م3', - 'إجمالي أعمال الأسفلت: 120,000 م2', - 'إجمالي أعمال الإنارة: 500 عمود إنارة' - ], - 'entities': { - 'الجهة المالكة': 'وزارة النقل', - 'موقع المشروع': 'جدة', - 'رقم المناقصة': 'T-2024-002', - 'تاريخ الطرح': '2024-03-10', - 'تاريخ الإقفال': '2024-04-20' - } - }, - { - 'id': 3, - 'title': 'المواصفات الفنية - مشروع بناء مدرسة', - 'date': '2024-03-25', - 'summary': 'تتضمن المواصفات الفنية لمشروع بناء مدرسة تفاصيل المتطلبات الفنية للمشروع. تتكون المدرسة من 3 طوابق بمساحة إجمالية 3000 متر مربع، وتشمل 20 فصلاً دراسياً، ومختبرات علوم، وقاعة متعددة الأغراض، ومكتبة، وغرف إدارية.', - 'key_points': [ - 'نوع الهيكل: خرساني مسلح', - 'نظام التكييف: نظام مركزي', - 'نظام الإنارة: LED موفر للطاقة', - 'نظام مكافحة الحريق: نظام رش آلي', - 'متطلبات خاصة: نظام طاقة شمسية لتوفير 30% من احتياجات الطاقة' - ], - 'entities': { - 'الجهة المالكة': 'وزارة التعليم', - 'موقع المشروع': 'الدمام', - 'رقم المناقصة': 'T-2024-003', - 'تاريخ الطرح': '2024-03-15', - 'تاريخ الإقفال': '2024-04-25' - } - } - ] - - if 'ai_models' not in st.session_state: - st.session_state.ai_models = [ - { - 'id': 1, - 'name': 'نموذج تحليل المستندات', - 'description': 'نموذج ذكاء اصطناعي لتحليل مستندات المناقصات واستخراج المعلومات الرئيسية منها.', - 'type': 'معالجة اللغة الطبيعية', - 'accuracy': 92, - 'last_updated': '2024-03-01' - }, - { - 'id': 2, - 'name': 'نموذج تقدير التكاليف', - 'description': 'نموذج ذكاء اصطناعي لتقدير تكاليف المشاريع بناءً على بيانات المشاريع السابقة.', - 'type': 'تعلم آلي', - 'accuracy': 85, - 'last_updated': '2024-02-15' - }, - { - 'id': 3, - 'name': 'نموذج تحليل المخاطر', - 'description': 'نموذج ذكاء اصطناعي لتحليل المخاطر المحتملة للمشاريع وتقديم توصيات للتخفيف منها.', - 'type': 'تعلم آلي', - 'accuracy': 88, - 'last_updated': '2024-02-20' - }, - { - 'id': 4, - 'name': 'نموذج تحليل المنافسين', - 'description': 'نموذج ذكاء اصطناعي لتحليل بيانات المنافسين وتقديم توصيات للتسعير التنافسي.', - 'type': 'تعلم آلي', - 'accuracy': 80, - 'last_updated': '2024-03-10' - }, - { - 'id': 5, - 'name': 'نموذج المساعد الذكي', - 'description': 'نموذج ذكاء اصطناعي للإجابة على الاستفسارات وتقديم المساعدة في إدارة المناقصات.', - 'type': 'معالجة اللغة الطبيعية', - 'accuracy': 90, - 'last_updated': '2024-03-15' - } - ] - - 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_document_analysis_tab() - - with tabs[2]: - self._render_cost_estimation_tab() - - with tabs[3]: - self._render_risk_analysis_tab() - - with tabs[4]: - self._render_ai_models_tab() - - def _render_ai_assistant_tab(self): - """عرض تبويب المساعد الذكي""" - - st.markdown("### المساعد الذكي") - - # عرض محادثة المساعد الذكي - chat_container = st.container() - - with chat_container: - for message in st.session_state.chat_history: - if message['role'] == 'user': - st.markdown(f"
أنت: {message['content']}
", unsafe_allow_html=True) - else: - st.markdown(f"
المساعد: {message['content']}
", unsafe_allow_html=True) - - # إدخال رسالة جديدة - with st.form(key="chat_form"): - user_input = st.text_area("اكتب رسالتك هنا:", key="user_input", height=100) - submit_button = st.form_submit_button("إرسال") - - if submit_button and user_input: - # إضافة رسالة المستخدم إلى المحادثة - st.session_state.chat_history.append({ - 'role': 'user', - 'content': user_input - }) - - # محاكاة استجابة المساعد الذكي - ai_responses = { - "تكلفة": "بناءً على تحليل بيانات المشاريع السابقة، أتوقع أن تكون تكلفة هذا المشروع في حدود 15-18 مليون ريال. يمكنني تقديم تحليل تفصيلي إذا وفرت لي المزيد من المعلومات عن نطاق المشروع والمواصفات المطلوبة.", - "مخاطر": "من أهم المخاطر المحتملة لهذا النوع من المشاريع: تأخر التوريدات، نقص العمالة الماهرة، التغييرات في نطاق العمل، والظروف الجوية غير المتوقعة. أنصح بوضع خطة إدارة مخاطر شاملة وتخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع.", - "منافس": "بناءً على تحليل المناقصات السابقة، يبدو أن المنافس الرئيسي يقدم أسعاراً أقل بنسبة 5-8% من متوسط السوق، لكنه يواجه تحديات في الالتزام بالجداول الزمنية. يمكنك التركيز على نقاط قوتك في الالتزام بالمواعيد وجودة التنفيذ في عرضك.", - "مستند": "يمكنني تحليل مستندات المناقصة لاستخراج المعلومات الرئيسية مثل نطاق العمل، الشروط والمواصفات، الجداول الزمنية، وشروط الدفع. يرجى تحميل المستندات في تبويب تحليل المستندات.", - "تسعير": "لتحسين استراتيجية التسعير، أنصح بتحليل هيكل التكاليف بدقة، ودراسة أسعار المنافسين، وتقييم القيمة المضافة التي تقدمها. يمكنك استخدام وحدة التسعير لإنشاء سيناريوهات تسعير مختلفة ومقارنتها.", - "موارد": "بناءً على نطاق المشروع، أتوقع أنك ستحتاج إلى فريق من 15-20 مهندساً وفنياً، بالإضافة إلى معدات إنشائية رئيسية. يمكنك استخدام وحدة الموارد لتخطيط احتياجات المشروع بشكل تفصيلي." - } - - # تحديد الاستجابة المناسبة بناءً على كلمات مفتاحية في رسالة المستخدم - response = "أشكرك على رسالتك. يمكنني مساعدتك في إدارة المناقصات وتحليل المستندات وتقدير التكاليف وتحليل المخاطر. يرجى توضيح ما تحتاجه بالتحديد." - - for keyword, resp in ai_responses.items(): - if keyword in user_input: - response = resp - break - - # إضافة استجابة المساعد الذكي إلى المحادثة - st.session_state.chat_history.append({ - 'role': 'assistant', - 'content': response - }) - - # إعادة تحميل الصفحة لعرض المحادثة المحدثة - st.rerun() - - # عرض اقتراحات للأسئلة - st.markdown("### اقتراحات للأسئلة") - - suggestions = [ - "كيف يمكنني تقدير تكلفة مشروع إنشاء مبنى إداري؟", - "ما هي المخاطر المحتملة لمشروع تطوير شبكة طرق؟", - "كيف يمكنني تحليل استراتيجية المنافس الرئيسي؟", - "كيف يمكنني تحليل مستندات المناقصة بسرعة؟", - "ما هي أفضل استراتيجية للتسعير التنافسي؟", - "كيف يمكنني تخطيط الموارد اللازمة للمشروع؟" - ] - - col1, col2 = st.columns(2) - - with col1: - for i in range(0, len(suggestions), 2): - if st.button(suggestions[i], key=f"suggestion_{i}"): - # إضافة السؤال المقترح إلى المحادثة - st.session_state.chat_history.append({ - 'role': 'user', - 'content': suggestions[i] - }) - - # تحديد الاستجابة المناسبة - for keyword, resp in ai_responses.items(): - if keyword in suggestions[i].lower(): - response = resp - break - else: - response = "أشكرك على سؤالك. يمكنني مساعدتك في ذلك. يرجى تقديم المزيد من التفاصيل حول احتياجاتك المحددة." - - # إضافة استجابة المساعد الذكي إلى المحادثة - st.session_state.chat_history.append({ - 'role': 'assistant', - 'content': response - }) - - # إعادة تحميل الصفحة لعرض المحادثة المحدثة - st.rerun() - - with col2: - for i in range(1, len(suggestions), 2): - if st.button(suggestions[i], key=f"suggestion_{i}"): - # إضافة السؤال المقترح إلى المحادثة - st.session_state.chat_history.append({ - 'role': 'user', - 'content': suggestions[i] - }) - - # تحديد الاستجابة المناسبة - for keyword, resp in ai_responses.items(): - if keyword in suggestions[i].lower(): - response = resp - break - else: - response = "أشكرك على سؤالك. يمكنني مساعدتك في ذلك. يرجى تقديم المزيد من التفاصيل حول احتياجاتك المحددة." - - # إضافة استجابة المساعد الذكي إلى المحادثة - st.session_state.chat_history.append({ - 'role': 'assistant', - 'content': response - }) - - # إعادة تحميل الصفحة لعرض المحادثة المحدثة - st.rerun() - - def _render_document_analysis_tab(self): - """عرض تبويب تحليل المستندات""" - - st.markdown("### تحليل المستندات باستخدام الذكاء الاصطناعي") - - # تحميل المستندات - st.markdown("#### تحميل المستندات") - - uploaded_file = st.file_uploader("قم بتحميل مستند المناقصة (PDF, DOCX)", type=["pdf", "docx"]) - - if uploaded_file is not None: - if st.button("تحليل المستند"): - # محاكاة تحليل المستند - with st.spinner("جاري تحليل المستند..."): - time.sleep(2) # محاكاة وقت المعالجة - st.success("تم تحليل المستند بنجاح!") - - # إضافة ملخص المستند إلى قائمة الملخصات - new_id = max([item['id'] for item in st.session_state.document_summaries], default=0) + 1 - - st.session_state.document_summaries.append({ - 'id': new_id, - 'title': uploaded_file.name, - 'date': time.strftime("%Y-%m-%d"), - 'summary': 'تم تحليل المستند واستخراج المعلومات الرئيسية منه. يتضمن المستند شروط ومواصفات المناقصة، ونطاق العمل، والجدول الزمني، وشروط الدفع.', - 'key_points': [ - 'مدة التنفيذ: 12 شهراً', - 'قيمة الضمان الابتدائي: 2% من قيمة العطاء', - 'قيمة الضمان النهائي: 5% من قيمة العقد', - 'غرامة التأخير: 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%', - 'شروط الدفع: دفعات شهرية حسب نسبة الإنجاز' - ], - 'entities': { - 'الجهة المالكة': 'وزارة الإسكان', - 'موقع المشروع': 'الرياض', - 'رقم المناقصة': 'T-2024-004', - 'تاريخ الطرح': '2024-03-25', - 'تاريخ الإقفال': '2024-05-01' - } - }) - - # عرض ملخصات المستندات - st.markdown("#### ملخصات المستندات") - - for summary in st.session_state.document_summaries: - with st.expander(f"{summary['title']} - {summary['date']}"): - st.markdown(f"**ملخص المستند:** {summary['summary']}") - - st.markdown("**النقاط الرئيسية:**") - for point in summary['key_points']: - st.markdown(f"- {point}") - - st.markdown("**الكيانات المستخرجة:**") - for entity, value in summary['entities'].items(): - st.markdown(f"- **{entity}:** {value}") - - col1, col2, col3 = st.columns(3) - - with col1: - if st.button("تصدير الملخص", key=f"export_summary_{summary['id']}"): - st.success("تم تصدير الملخص بنجاح!") - - with col2: - if st.button("إرسال إلى وحدة التسعير", key=f"send_to_pricing_{summary['id']}"): - st.success("تم إرسال البيانات إلى وحدة التسعير بنجاح!") - - with col3: - if st.button("إرسال إلى وحدة المخاطر", key=f"send_to_risk_{summary['id']}"): - st.success("تم إرسال البيانات إلى وحدة المخاطر بنجاح!") - - # استخراج جدول الكميات - st.markdown("#### استخراج جدول الكميات") - - boq_file = st.file_uploader("قم بتحميل جدول الكميات (PDF, XLSX)", type=["pdf", "xlsx"]) - - if boq_file is not None: - if st.button("استخراج جدول الكميات"): - # محاكاة استخراج جدول الكميات - with st.spinner("جاري استخراج جدول الكميات..."): - time.sleep(2) # محاكاة وقت المعالجة - st.success("تم استخراج جدول الكميات بنجاح!") - - # عرض جدول الكميات المستخرج - boq_data = { - 'الكود': ['A-001', 'A-002', 'A-003', 'B-001', 'B-002'], - 'الوصف': [ - 'أعمال الحفر والردم', - 'توريد وصب خرسانة عادية', - 'توريد وصب خرسانة مسلحة للأساسات', - 'توريد وتركيب حديد تسليح', - 'توريد وبناء طابوق' - ], - 'الوحدة': ['م3', 'م3', 'م3', 'طن', 'م2'], - 'الكمية': [2000, 300, 200, 20, 500], - 'سعر الوحدة': [45, 350, 450, 3500, 120], - 'الإجمالي': [90000, 105000, 90000, 70000, 60000] - } - - boq_df = pd.DataFrame(boq_data) - st.dataframe(boq_df, use_container_width=True, hide_index=True) - - if st.button("إرسال إلى وحدة التسعير", key="send_boq_to_pricing"): - st.success("تم إرسال جدول الكميات إلى وحدة التسعير بنجاح!") - - # تحليل الشروط والمواصفات - st.markdown("#### تحليل الشروط والمواصفات") - - specs_file = st.file_uploader("قم بتحميل الشروط والمواصفات (PDF, DOCX)", type=["pdf", "docx"]) - - if specs_file is not None: - if st.button("تحليل الشروط والمواصفات"): - # محاكاة تحليل الشروط والمواصفات - with st.spinner("جاري تحليل الشروط والمواصفات..."): - time.sleep(2) # محاكاة وقت المعالجة - st.success("تم تحليل الشروط والمواصفات بنجاح!") - - # عرض نتائج التحليل - st.markdown("**الشروط الرئيسية:**") - st.markdown("- مدة التنفيذ: 12 شهراً") - st.markdown("- قيمة الضمان الابتدائي: 2% من قيمة العطاء") - st.markdown("- قيمة الضمان النهائي: 5% من قيمة العقد") - st.markdown("- غرامة التأخير: 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%") - st.markdown("- شروط الدفع: دفعات شهرية حسب نسبة الإنجاز") - - st.markdown("**المواصفات الفنية الرئيسية:**") - st.markdown("- نوع الهيكل: خرساني مسلح") - st.markdown("- نظام التكييف: نظام مركزي") - st.markdown("- نظام الإنارة: LED موفر للطاقة") - st.markdown("- نظام مكافحة الحريق: نظام رش آلي") - st.markdown("- متطلبات خاصة: نظام طاقة شمسية لتوفير 30% من احتياجات الطاقة") - - if st.button("إرسال إلى وحدة المخاطر", key="send_specs_to_risk"): - st.success("تم إرسال تحليل الشروط والمواصفات إلى وحدة المخاطر بنجاح!") - - def _render_cost_estimation_tab(self): - """عرض تبويب تقدير التكاليف""" - - st.markdown("### تقدير التكاليف باستخدام الذكاء الاصطناعي") - - # إدخال معلومات المشروع - st.markdown("#### معلومات المشروع") - - col1, col2 = st.columns(2) - - with col1: - project_type = st.selectbox( - "نوع المشروع", - ["مبنى إداري", "مبنى سكني", "مدرسة", "مستشفى", "طرق", "جسور", "بنية تحتية", "أخرى"] - ) - - project_area = st.number_input("المساحة الإجمالية (م2)", min_value=0, value=5000) - - project_location = st.selectbox( - "موقع المشروع", - ["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك", "أخرى"] - ) - - with col2: - project_duration = st.number_input("مدة التنفيذ (شهر)", min_value=1, value=18) - - project_quality = st.select_slider( - "مستوى الجودة", - options=["اقتصادي", "متوسط", "عالي", "ممتاز"] - ) - - project_complexity = st.select_slider( - "مستوى التعقيد", - options=["بسيط", "متوسط", "معقد", "معقد جداً"] - ) - - # تقدير التكاليف - if st.button("تقدير التكاليف"): - # محاكاة تقدير التكاليف - with st.spinner("جاري تقدير التكاليف..."): - time.sleep(2) # محاكاة وقت المعالجة - st.success("تم تقدير التكاليف بنجاح!") - - # عرض نتائج التقدير - st.markdown("#### نتائج تقدير التكاليف") - - # تحديد التكلفة التقديرية بناءً على نوع المشروع والمساحة - base_cost_per_sqm = { - "مبنى إداري": 3500, - "مبنى سكني": 3000, - "مدرسة": 3200, - "مستشفى": 5000, - "طرق": 1500, - "جسور": 8000, - "بنية تحتية": 2500, - "أخرى": 3000 - } - - # تعديل التكلفة بناءً على الموقع - location_factor = { - "الرياض": 1.0, - "جدة": 1.05, - "الدمام": 0.95, - "مكة": 1.1, - "المدينة": 1.0, - "أبها": 0.9, - "تبوك": 0.85, - "أخرى": 1.0 - } - - # تعديل التكلفة بناءً على مستوى الجودة - quality_factor = { - "اقتصادي": 0.8, - "متوسط": 1.0, - "عالي": 1.2, - "ممتاز": 1.5 - } - - # تعديل التكلفة بناءً على مستوى التعقيد - complexity_factor = { - "بسيط": 0.9, - "متوسط": 1.0, - "معقد": 1.2, - "معقد جداً": 1.4 - } - - # حساب التكلفة التقديرية - base_cost = base_cost_per_sqm[project_type] * project_area - adjusted_cost = base_cost * location_factor[project_location] * quality_factor[project_quality] * complexity_factor[project_complexity] - - # عرض التكلفة التقديرية - col1, col2 = st.columns(2) - - with col1: - st.metric("التكلفة التقديرية", f"{adjusted_cost:,.0f} ريال") - - with col2: - st.metric("التكلفة لكل متر مربع", f"{adjusted_cost / project_area:,.0f} ريال/م2") - - # عرض تفاصيل التكاليف - st.markdown("#### تفاصيل التكاليف") - - # تقسيم التكاليف إلى فئات - cost_breakdown = { - "الأعمال الإنشائية": 0.35, - "الأعمال المعمارية": 0.25, - "الأعمال الكهربائية": 0.15, - "الأعمال الميكانيكية": 0.15, - "أعمال الموقع والتجهيزات": 0.10 - } - - cost_details = { - "الفئة": list(cost_breakdown.keys()), - "النسبة": [f"{v * 100:.0f}%" for v in cost_breakdown.values()], - "التكلفة": [adjusted_cost * v for v in cost_breakdown.values()] - } - - cost_df = pd.DataFrame(cost_details) - st.dataframe(cost_df, use_container_width=True, hide_index=True) - - # عرض رسم بياني للتكاليف - fig = px.pie( - cost_df, - values="التكلفة", - names="الفئة", - title="توزيع التكاليف حسب الفئة" - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض تحليل التكاليف المباشرة وغير المباشرة - st.markdown("#### تحليل التكاليف المباشرة وغير المباشرة") - - direct_cost = adjusted_cost * 0.85 - indirect_cost = adjusted_cost * 0.15 - - direct_indirect_data = { - "نوع التكلفة": ["تكاليف مباشرة", "تكاليف غير مباشرة"], - "النسبة": ["85%", "15%"], - "التكلفة": [direct_cost, indirect_cost] - } - - direct_indirect_df = pd.DataFrame(direct_indirect_data) - st.dataframe(direct_indirect_df, use_container_width=True, hide_index=True) - - # عرض رسم بياني للتكاليف المباشرة وغير المباشرة - fig = px.bar( - direct_indirect_df, - x="نوع التكلفة", - y="التكلفة", - title="التكاليف المباشرة وغير المباشرة", - color="نوع التكلفة", - text_auto='.2s' - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض توصيات لتحسين التكاليف - st.markdown("#### توصيات لتحسين التكاليف") - - st.markdown("1. **تحسين تصميم المشروع:** يمكن تحسين التصميم لتقليل التكاليف مع الحفاظ على الجودة.") - st.markdown("2. **استخدام مواد بديلة:** يمكن استخدام مواد بديلة بتكلفة أقل مع الحفاظ على الجودة.") - st.markdown("3. **تحسين جدولة المشروع:** يمكن تحسين جدولة المشروع لتقليل مدة التنفيذ وبالتالي تقليل التكاليف غير المباشرة.") - st.markdown("4. **تحسين إدارة الموارد:** يمكن تحسين إدارة الموارد لتقليل الهدر وزيادة الإنتاجية.") - st.markdown("5. **التفاوض مع الموردين:** يمكن التفاوض مع الموردين للحصول على أسعار أفضل.") - - # إرسال التقدير إلى وحدة التسعير - if st.button("إرسال إلى وحدة التسعير", key="send_estimate_to_pricing"): - st.success("تم إرسال تقدير التكاليف إلى وحدة التسعير بنجاح!") - - # مقارنة التكاليف مع المشاريع السابقة - st.markdown("#### مقارنة التكاليف مع المشاريع السابقة") - - # بيانات افتراضية للمشاريع السابقة - previous_projects_data = { - "المشروع": ["مبنى إداري - الرياض", "مبنى إداري - جدة", "مبنى إداري - الدمام", "مبنى سكني - الرياض", "مدرسة - جدة"], - "المساحة (م2)": [4500, 5200, 4800, 6000, 3500], - "التكلفة الإجمالية": [16200000, 19500000, 15800000, 18500000, 11200000], - "التكلفة لكل متر مربع": [3600, 3750, 3290, 3080, 3200] - } - - previous_projects_df = pd.DataFrame(previous_projects_data) - st.dataframe(previous_projects_df, use_container_width=True, hide_index=True) - - # عرض رسم بياني لمقارنة التكاليف - fig = px.bar( - previous_projects_df, - x="المشروع", - y="التكلفة لكل متر مربع", - title="مقارنة التكلفة لكل متر مربع للمشاريع السابقة", - color="المشروع", - text_auto='.0f' - ) - - st.plotly_chart(fig, use_container_width=True) - - 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" - ) - - project_budget = st.number_input("ميزانية المشروع (ريال)", min_value=0, value=15000000) - - project_location = st.selectbox( - "موقع المشروع", - ["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك", "أخرى"], - key="risk_project_location" - ) - - with col2: - project_duration = st.number_input("مدة التنفيذ (شهر)", min_value=1, value=18, key="risk_project_duration") - - project_complexity = st.select_slider( - "مستوى التعقيد", - options=["بسيط", "متوسط", "معقد", "معقد جداً"], - key="risk_project_complexity" - ) - - project_experience = st.select_slider( - "مستوى الخبرة في هذا النوع من المشاريع", - options=["منخفض", "متوسط", "عالي", "ممتاز"] - ) - - # تحليل المخاطر - if st.button("تحليل المخاطر"): - # محاكاة تحليل المخاطر - with st.spinner("جاري تحليل المخاطر..."): - time.sleep(2) # محاكاة وقت المعالجة - st.success("تم تحليل المخاطر بنجاح!") - - # عرض نتائج التحليل - st.markdown("#### نتائج تحليل المخاطر") - - # بيانات افتراضية للمخاطر - risks_data = { - "المخاطرة": [ - "تأخر التوريدات", - "نقص العمالة الماهرة", - "التغييرات في نطاق العمل", - "الظروف الجوية غير المتوقعة", - "مشاكل في التصميم", - "تأخر الدفعات", - "مشاكل في الموقع", - "تغيير الأنظمة واللوائح", - "مشاكل في الجودة", - "مشاكل في التنسيق مع الجهات الحكومية" - ], - "الاحتمالية": [ - "متوسطة", - "عالية", - "متوسطة", - "منخفضة", - "منخفضة", - "متوسطة", - "منخفضة", - "منخفضة", - "متوسطة", - "عالية" - ], - "التأثير": [ - "عالي", - "عالي", - "عالي", - "متوسط", - "عالي", - "متوسط", - "متوسط", - "عالي", - "عالي", - "متوسط" - ], - "درجة المخاطرة": [ - "عالية", - "عالية", - "عالية", - "متوسطة", - "متوسطة", - "متوسطة", - "منخفضة", - "متوسطة", - "عالية", - "عالية" - ] - } - - risks_df = pd.DataFrame(risks_data) - st.dataframe(risks_df, use_container_width=True, hide_index=True) - - # عرض مصفوفة المخاطر - st.markdown("#### مصفوفة المخاطر") - - # تحويل الاحتمالية والتأثير إلى قيم عددية - probability_map = {"منخفضة": 1, "متوسطة": 2, "عالية": 3} - impact_map = {"منخفض": 1, "متوسط": 2, "عالي": 3} - - risk_matrix_data = [] - - for i, risk in enumerate(risks_data["المخاطرة"]): - prob = probability_map[risks_data["الاحتمالية"][i]] - impact = impact_map[risks_data["التأثير"][i]] - risk_matrix_data.append({ - "المخاطرة": risk, - "الاحتمالية": prob, - "التأثير": impact, - "درجة المخاطرة": prob * impact - }) - - # إنشاء مصفوفة المخاطر - risk_matrix = np.zeros((3, 3)) - - for risk in risk_matrix_data: - prob = risk["الاحتمالية"] - 1 # تعديل الفهرس ليبدأ من 0 - impact = risk["التأثير"] - 1 # تعديل الفهرس ليبدأ من 0 - risk_matrix[prob, impact] += 1 - - # عرض مصفوفة المخاطر كرسم بياني حراري - fig, ax = plt.subplots(figsize=(10, 8)) - - im = ax.imshow(risk_matrix, cmap="YlOrRd") - - # إضافة النص إلى الخلايا - for i in range(3): - for j in range(3): - text = ax.text(j, i, int(risk_matrix[i, j]), ha="center", va="center", color="black") - - # إضافة العناوين - ax.set_xticks(np.arange(3)) - ax.set_yticks(np.arange(3)) - ax.set_xticklabels(["منخفض", "متوسط", "عالي"]) - ax.set_yticklabels(["منخفضة", "متوسطة", "عالية"]) - - # إضافة العناوين الرئيسية - ax.set_xlabel("التأثير") - ax.set_ylabel("الاحتمالية") - ax.set_title("مصفوفة المخاطر") - - # إضافة شريط الألوان - cbar = ax.figure.colorbar(im, ax=ax) - cbar.ax.set_ylabel("عدد المخاطر", rotation=-90, va="bottom") - - # عرض الرسم البياني - st.pyplot(fig) - - # عرض توزيع المخاطر حسب الدرجة - st.markdown("#### توزيع المخاطر حسب الدرجة") - - risk_degree_counts = { - "منخفضة": sum(1 for degree in risks_data["درجة المخاطرة"] if degree == "منخفضة"), - "متوسطة": sum(1 for degree in risks_data["درجة المخاطرة"] if degree == "متوسطة"), - "عالية": sum(1 for degree in risks_data["درجة المخاطرة"] if degree == "عالية") - } - - risk_degree_df = pd.DataFrame({ - "درجة المخاطرة": list(risk_degree_counts.keys()), - "العدد": list(risk_degree_counts.values()) - }) - - fig = px.pie( - risk_degree_df, - values="العدد", - names="درجة المخاطرة", - title="توزيع المخاطر حسب الدرجة", - color="درجة المخاطرة", - color_discrete_map={"منخفضة": "green", "متوسطة": "orange", "عالية": "red"} - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض خطة إدارة المخاطر - st.markdown("#### خطة إدارة المخاطر") - - # بيانات افتراضية لخطة إدارة المخاطر - risk_management_data = { - "المخاطرة": [ - "تأخر التوريدات", - "نقص العمالة الماهرة", - "التغييرات في نطاق العمل", - "مشاكل في الجودة", - "مشاكل في التنسيق مع الجهات الحكومية" - ], - "استراتيجية المواجهة": [ - "تخفيف", - "تخفيف", - "تجنب", - "تخفيف", - "نقل" - ], - "الإجراءات": [ - "التعاقد مع موردين متعددين، وضع جدول زمني للتوريدات مع هامش أمان، متابعة التوريدات بشكل دوري", - "التعاقد مع شركات توريد عمالة موثوقة، تدريب العمالة الحالية، وضع حوافز للعمالة الماهرة", - "توثيق نطاق العمل بشكل دقيق، وضع إجراءات للتغييرات في نطاق العمل، تحديد صلاحيات اعتماد التغييرات", - "وضع خطة لضبط الجودة، تعيين مسؤول للجودة، إجراء اختبارات دورية للجودة", - "التعاقد مع استشاري متخصص في التنسيق مع الجهات الحكومية، تحديد متطلبات الجهات الحكومية مسبقاً" - ], - "المسؤول": [ - "مدير المشتريات", - "مدير الموارد البشرية", - "مدير المشروع", - "مدير الجودة", - "مدير العلاقات الحكومية" - ], - "الموعد النهائي": [ - "قبل بدء المشروع بشهر", - "قبل بدء المشروع بشهرين", - "قبل بدء المشروع بأسبوعين", - "مستمر طوال فترة المشروع", - "قبل بدء المشروع بشهر" - ] - } - - risk_management_df = pd.DataFrame(risk_management_data) - st.dataframe(risk_management_df, use_container_width=True, hide_index=True) - - # عرض توصيات لإدارة المخاطر - st.markdown("#### توصيات لإدارة المخاطر") - - st.markdown("1. **تخصيص احتياطي للطوارئ:** يوصى بتخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع.") - st.markdown("2. **مراجعة خطة إدارة المخاطر بشكل دوري:** يجب مراجعة خطة إدارة المخاطر بشكل دوري وتحديثها حسب الحاجة.") - st.markdown("3. **تعيين مسؤول لإدارة المخاطر:** يوصى بتعيين مسؤول لإدارة المخاطر في المشروع.") - st.markdown("4. **توثيق الدروس المستفادة:** يجب توثيق الدروس المستفادة من إدارة المخاطر في المشاريع السابقة.") - st.markdown("5. **التواصل المستمر مع أصحاب المصلحة:** يجب التواصل المستمر مع أصحاب المصلحة لتحديد المخاطر المحتملة.") - - # إرسال تحليل المخاطر إلى وحدة التسعير - if st.button("إرسال إلى وحدة التسعير", key="send_risk_to_pricing"): - st.success("تم إرسال تحليل المخاطر إلى وحدة التسعير بنجاح!") - - def _render_ai_models_tab(self): - """عرض تبويب نماذج الذكاء الاصطناعي""" - - st.markdown("### نماذج الذكاء الاصطناعي") - - # عرض نماذج الذكاء الاصطناعي - st.markdown("#### قائمة نماذج الذكاء الاصطناعي") - - # تحويل قائمة النماذج إلى DataFrame - models_df = pd.DataFrame(st.session_state.ai_models) - - # عرض النماذج كجدول - st.dataframe( - models_df, - column_config={ - "id": st.column_config.NumberColumn("الرقم"), - "name": st.column_config.TextColumn("اسم النموذج"), - "description": st.column_config.TextColumn("الوصف"), - "type": st.column_config.TextColumn("النوع"), - "accuracy": st.column_config.ProgressColumn("الدقة (%)", min_value=0, max_value=100), - "last_updated": st.column_config.DateColumn("تاريخ التحديث") - }, - use_container_width=True, - hide_index=True - ) - - # عرض تفاصيل النماذج - st.markdown("#### تفاصيل النماذج") - - for model in st.session_state.ai_models: - with st.expander(f"{model['name']} - دقة {model['accuracy']}%"): - st.markdown(f"**الوصف:** {model['description']}") - st.markdown(f"**النوع:** {model['type']}") - st.markdown(f"**تاريخ التحديث:** {model['last_updated']}") - - if model['name'] == "نموذج تحليل المستندات": - st.markdown("**القدرات:**") - st.markdown("- استخراج المعلومات الرئيسية من مستندات المناقصات") - st.markdown("- تحليل الشروط والمواصفات") - st.markdown("- استخراج جداول الكميات") - st.markdown("- تحديد الكيانات المهمة مثل الجهة المالكة، موقع المشروع، تواريخ المناقصة") - st.markdown("- تلخيص المستندات الطويلة") - - elif model['name'] == "نموذج تقدير التكاليف": - st.markdown("**القدرات:**") - st.markdown("- تقدير تكاليف المشاريع بناءً على بيانات المشاريع السابقة") - st.markdown("- تحليل العوامل المؤثرة على التكاليف") - st.markdown("- تقديم توصيات لتحسين التكاليف") - st.markdown("- مقارنة التكاليف مع المشاريع المماثلة") - st.markdown("- تحليل التكاليف المباشرة وغير المباشرة") - - elif model['name'] == "نموذج تحليل المخاطر": - st.markdown("**القدرات:**") - st.markdown("- تحديد المخاطر المحتملة للمشاريع") - st.markdown("- تقييم احتمالية وتأثير المخاطر") - st.markdown("- إنشاء مصفوفة المخاطر") - st.markdown("- تقديم توصيات لإدارة المخاطر") - st.markdown("- تحليل المخاطر بناءً على بيانات المشاريع السابقة") - - elif model['name'] == "نموذج تحليل المنافسين": - st.markdown("**القدرات:**") - st.markdown("- تحليل بيانات المنافسين") - st.markdown("- تحديد نقاط القوة والضعف للمنافسين") - st.markdown("- تقديم توصيات للتسعير التنافسي") - st.markdown("- تحليل استراتيجيات المنافسين") - st.markdown("- تحليل حصص السوق") - - elif model['name'] == "نموذج المساعد الذكي": - st.markdown("**القدرات:**") - st.markdown("- الإجابة على الاستفسارات المتعلقة بإدارة المناقصات") - st.markdown("- تقديم توصيات لتحسين إدارة المناقصات") - st.markdown("- مساعدة المستخدمين في استخدام النظام") - st.markdown("- تقديم معلومات عن المشاريع والمناقصات") - st.markdown("- تقديم إحصائيات وتحليلات عن المناقصات") - - # عرض أداء النماذج - st.markdown("#### أداء النماذج") - - # إنشاء رسم بياني لأداء النماذج - performance_df = pd.DataFrame({ - "النموذج": [model['name'] for model in st.session_state.ai_models], - "الدقة (%)": [model['accuracy'] for model in st.session_state.ai_models] - }) - - fig = px.bar( - performance_df, - x="النموذج", - y="الدقة (%)", - title="أداء نماذج الذكاء الاصطناعي", - color="الدقة (%)", - text_auto='.0f' - ) - - fig.update_layout(yaxis_range=[0, 100]) - - st.plotly_chart(fig, use_container_width=True) - - # تدريب النماذج - st.markdown("#### تدريب النماذج") - - col1, col2 = st.columns(2) - - with col1: - model_to_train = st.selectbox( - "اختر النموذج للتدريب", - [model['name'] for model in st.session_state.ai_models] - ) - - with col2: - training_data = st.file_uploader("قم بتحميل بيانات التدريب (CSV, XLSX)", type=["csv", "xlsx"]) - - if st.button("تدريب النموذج"): - # محاكاة تدريب النموذج - with st.spinner(f"جاري تدريب {model_to_train}..."): - time.sleep(3) # محاكاة وقت التدريب - st.success(f"تم تدريب {model_to_train} بنجاح!") - - # تحديث دقة النموذج - for i, model in enumerate(st.session_state.ai_models): - if model['name'] == model_to_train: - # زيادة الدقة بنسبة عشوائية بين 1% و 3% - import random - accuracy_increase = random.uniform(1, 3) - new_accuracy = min(model['accuracy'] + accuracy_increase, 99) - st.session_state.ai_models[i]['accuracy'] = new_accuracy - st.session_state.ai_models[i]['last_updated'] = time.strftime("%Y-%m-%d") - - st.metric( - "الدقة الجديدة", - f"{new_accuracy:.1f}%", - f"+{accuracy_increase:.1f}%" - ) - - break - - # تقييم النماذج - st.markdown("#### تقييم النماذج") - - col1, col2 = st.columns(2) - - with col1: - model_to_evaluate = st.selectbox( - "اختر النموذج للتقييم", - [model['name'] for model in st.session_state.ai_models], - key="model_to_evaluate" - ) - - with col2: - evaluation_data = st.file_uploader("قم بتحميل بيانات التقييم (CSV, XLSX)", type=["csv", "xlsx"], key="evaluation_data") - - if st.button("تقييم النموذج"): - # محاكاة تقييم النموذج - with st.spinner(f"جاري تقييم {model_to_evaluate}..."): - time.sleep(2) # محاكاة وقت التقييم - st.success(f"تم تقييم {model_to_evaluate} بنجاح!") - - # عرض نتائج التقييم - for model in st.session_state.ai_models: - if model['name'] == model_to_evaluate: - accuracy = model['accuracy'] - break - - evaluation_metrics = { - "المقياس": ["الدقة", "الاستدعاء", "F1", "AUC-ROC"], - "القيمة": [accuracy / 100, (accuracy - 5) / 100, (accuracy - 3) / 100, (accuracy - 2) / 100] - } - - evaluation_df = pd.DataFrame(evaluation_metrics) - st.dataframe(evaluation_df, use_container_width=True, hide_index=True) - - # عرض مصفوفة الارتباك - st.markdown("##### مصفوفة الارتباك") - - # إنشاء مصفوفة ارتباك افتراضية - confusion_matrix = np.array([ - [85, 10, 5], - [8, 80, 12], - [7, 13, 80] - ]) - - fig, ax = plt.subplots(figsize=(10, 8)) - - im = ax.imshow(confusion_matrix, cmap="Blues") - - # إضافة النص إلى الخلايا - for i in range(3): - for j in range(3): - text = ax.text(j, i, confusion_matrix[i, j], ha="center", va="center", color="black") - - # إضافة العناوين - ax.set_xticks(np.arange(3)) - ax.set_yticks(np.arange(3)) - ax.set_xticklabels(["الفئة 1", "الفئة 2", "الفئة 3"]) - ax.set_yticklabels(["الفئة 1", "الفئة 2", "الفئة 3"]) - - # إضافة العناوين الرئيسية - ax.set_xlabel("الفئة المتوقعة") - ax.set_ylabel("الفئة الحقيقية") - ax.set_title("مصفوفة الارتباك") - - # إضافة شريط الألوان - cbar = ax.figure.colorbar(im, ax=ax) - cbar.ax.set_ylabel("عدد العينات", rotation=-90, va="bottom") - - # عرض الرسم البياني - st.pyplot(fig) diff --git a/modules/ai_assistant/ai_assistant.py b/modules/ai_assistant/ai_assistant.py deleted file mode 100644 index 9ac352b74035d2f4360a95e1fb1913eef4b154c7..0000000000000000000000000000000000000000 --- a/modules/ai_assistant/ai_assistant.py +++ /dev/null @@ -1,773 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة المساعد الذكي التفاعلية -تتيح للمستخدمين التفاعل مع نماذج الذكاء الاصطناعي المتقدمة للحصول على مساعدة في تحليل العقود والمناقصات -""" - -import os -import sys -import json -import re -import time -import base64 -import tempfile -import logging -from datetime import datetime -import streamlit as st -import pandas as pd -import numpy as np -import requests -from io import BytesIO -from PIL import Image -import openai -import plotly.express as px -import plotly.graph_objects as go - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد المكونات المساعدة -from utils.helpers import create_directory_if_not_exists, format_time, get_user_info, render_credits, load_css - - -class AIAssistant: - """فئة المساعد الذكي التفاعلية""" - - def __init__(self): - """تهيئة المساعد الذكي""" - self.conversations_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'assistant_conversations') - create_directory_if_not_exists(self.conversations_dir) - - # تهيئة مفتاح OpenAI API - self.openai_api_key = os.environ.get("OPENAI_API_KEY") - if self.openai_api_key: - openai.api_key = self.openai_api_key - self.is_api_available = True - else: - self.is_api_available = False - - # نموذج OpenAI المستخدم - self.model = "gpt-4o" # النموذج الأحدث من OpenAI - - # تهيئة حالة المحادثة في الجلسة - if "assistant_messages" not in st.session_state: - st.session_state.assistant_messages = [] - - if "assistant_mode" not in st.session_state: - st.session_state.assistant_mode = "general" - - if "document_context" not in st.session_state: - st.session_state.document_context = None - - # الأنماط المتاحة للمساعد - self.assistant_modes = { - "general": "مساعد عام", - "contract_analysis": "تحليل العقود", - "cost_estimation": "تقدير التكاليف", - "risk_assessment": "تقييم المخاطر", - "project_planning": "تخطيط المشاريع" - } - - # توجيهات النظام للمساعد - self.system_prompts = { - "general": """ - أنت مساعد ذكي متخصص في شركة شبه الجزيرة للمقاولات، وتعمل ضمن نظام WAHBi لتحليل العقود والمناقصات. - دورك هو مساعدة المستخدمين في: - 1. تحليل المستندات والعقود، وتوضيح بنود العقود وفهم الالتزامات والشروط. - 2. المساعدة في تسعير المشاريع وحساب التكاليف والموارد. - 3. تقييم مخاطر العقود والمشاريع والمساعدة في اتخاذ القرارات. - 4. المساعدة في إدارة المشاريع ومتابعة الإنجاز. - - استخدم لغة مهنية واضحة ومباشرة. قدم إجابات دقيقة ومختصرة. - عند قيام المستخدم بسؤال عن كيفية استخدام النظام، قم بإرشاده إلى الوحدة المناسبة في النظام. - - معلومات هامة عن وحدات النظام: - - وحدة تحليل المستندات: لتحليل العقود والمناقصات باستخدام الذكاء الاصطناعي. - - وحدة مقارنة المستندات: لمقارنة نسخ مختلفة من المستندات وتحديد التغييرات. - - وحدة التسعير المتكاملة: لحساب تكاليف المشاريع بناءً على الموارد والمواد والعمالة. - - وحدة تقييم مخاطر العقود: لتحليل وتقييم المخاطر المحتملة في العقود والمشاريع. - - وحدة متتبع حالة المشروع: لمتابعة تقدم المشاريع وعرض مؤشرات الأداء. - - وحدة خريطة المشاريع: لعرض مواقع المشاريع على الخريطة بشكل تفاعلي. - - وحدة الإشعارات الذكية: لإرسال تنبيهات وإشعارات للمستخدمين حول المشاريع. - - تذكر أن تكون مفيداً ودقيقاً ومهنياً في جميع إجاباتك. - """, - - "contract_analysis": """ - أنت محلل عقود متخصص في تحليل العقود والمناقصات لشركات المقاولات. - مهمتك هي تحليل العقود وتحديد: - - الالتزامات الرئيسية - - المواعيد النهائية والتسليمات - - الشروط الجزائية والغرامات - - آلية الدفع والمستحقات المالية - - الشروط الخاصة والاستثناءات - - المخاطر المحتملة وكيفية التخفيف منها - - عند تحليل عقد، قم بتوضيح البنود غير المواتية التي قد تسبب مشاكل مستقبلية. - استخدم لغة قانونية دقيقة مع شرح المصطلحات القانونية بلغة مبسطة. - قدم توصيات عملية لكيفية التعامل مع بنود العقد وتجنب المخاطر. - """, - - "cost_estimation": """ - أنت خبير في تقدير تكاليف مشاريع البناء والمقاولات. - مهمتك هي مساعدة المستخدم في: - - تقدير تكاليف المشاريع بناءً على وصف المشروع ومتطلباته - - حساب تكاليف المواد والعمالة والمعدات والنفقات العامة - - توضيح كيفية تخصيص الميزانية بين مختلف عناصر المشروع - - تحديد التكاليف غير المباشرة التي قد يغفل عنها المستخدم - - اقتراح طرق لتقليل التكاليف دون التأثير على جودة المشروع - - استخدم أسلوب منهجي في تقدير التكاليف واشرح افتراضاتك بوضوح. - قدم نطاقات تقديرية بدلاً من أرقام دقيقة للتكاليف حيثما كان ذلك مناسباً. - عند الإشارة إلى تكاليف، وضح ما إذا كانت التكاليف تشمل ضريبة القيمة المضافة أم لا. - """, - - "risk_assessment": """ - أنت خبير في تقييم مخاطر مشاريع البناء والمقاولات. - مهمتك هي مساعدة المستخدم في: - - تحديد المخاطر المحتملة في المشاريع والعقود - - تقييم احتمالية وتأثير كل خطر - - اقتراح استراتيجيات للتخفيف من المخاطر - - تحليل السيناريوهات المحتملة وخطط الطوارئ - - تقديم أفضل الممارسات لإدارة المخاطر في مشاريع المقاولات - - صنف المخاطر إلى فئات (عالية، متوسطة، منخفضة) بناءً على احتماليتها وتأثيرها. - اشرح كيف يمكن للشركة أن تحول بعض المخاطر إلى فرص. - قدم أمثلة عملية من مشاريع مماثلة لتوضيح كيفية إدارة المخاطر المحددة. - """, - - "project_planning": """ - أنت خبير في تخطيط وإدارة مشاريع البناء والمقاولات. - مهمتك هي مساعدة المستخدم في: - - تخطيط المشاريع وتقسيمها إلى مراحل ومهام - - تحديد الموارد اللازمة والجداول الزمنية - - إنشاء مخطط جانت وتحديد المسار الحرج - - التخطيط للموارد البشرية والمعدات والمواد - - متابعة تقدم المشروع ومؤشرات الأداء - - قدم نصائح عملية لإدارة المشاريع بكفاءة وتجنب التأخيرات. - اشرح كيفية التعامل مع التغييرات والمطالبات خلال تنفيذ المشروع. - قدم أفضل الممارسات للتواصل مع أصحاب المصلحة وإدارة التوقعات. - """ - } - - def _call_openai_api(self, messages, model=None, max_tokens=2000): - """استدعاء OpenAI API للحصول على استجابة""" - if not self.is_api_available: - return { - "choices": [{"message": {"content": "عذراً، مفتاح OpenAI API غير متوفر. يرجى التواصل مع مسؤول النظام."}}] - } - - try: - if model is None: - model = self.model - - response = openai.ChatCompletion.create( - model=model, - messages=messages, - max_tokens=max_tokens, - temperature=0.7, - top_p=0.9, - frequency_penalty=0, - presence_penalty=0 - ) - - return response - except Exception as e: - logging.error(f"خطأ في استدعاء OpenAI API: {e}") - return { - "choices": [{"message": {"content": f"عذراً، حدث خطأ في الاتصال بـ OpenAI API: {str(e)}"}}] - } - - def _call_backend_api(self, endpoint, data): - """استدعاء واجهة API الخلفية للنظام""" - try: - response = requests.post( - f"http://localhost:5000/api/{endpoint}", - json=data, - timeout=60 - ) - - if response.status_code == 200: - return response.json() - else: - logging.error(f"خطأ في استدعاء واجهة API الخلفية: {response.status_code} - {response.text}") - return {"error": f"خطأ في استدعاء واجهة API الخلفية: {response.status_code}"} - except Exception as e: - logging.error(f"خطأ في الاتصال بواجهة API الخلفية: {e}") - return {"error": f"خطأ في الاتصال بواجهة API الخلفية: {str(e)}"} - - def _process_user_message(self, user_message, mode=None): - """معالجة رسالة المستخدم والحصول على رد من المساعد الذكي""" - if mode is None: - mode = st.session_state.assistant_mode - - # إنشاء قائمة الرسائل للمحادثة - messages = [ - {"role": "system", "content": self.system_prompts[mode]} - ] - - # إضافة سياق المستند إذا كان متاحاً - if st.session_state.document_context: - messages.append({ - "role": "system", - "content": f"معلومات سياقية عن المستند: {st.session_state.document_context}" - }) - - # إضافة المحادثة السابقة - for msg in st.session_state.assistant_messages: - messages.append({ - "role": msg["role"], - "content": msg["content"] - }) - - # إضافة رسالة المستخدم الحالية - messages.append({ - "role": "user", - "content": user_message - }) - - # استدعاء API - response = self._call_openai_api(messages) - - # استخراج الرد - assistant_response = response["choices"][0]["message"]["content"] - - # تحديث سجل المحادثة - st.session_state.assistant_messages.append({"role": "user", "content": user_message}) - st.session_state.assistant_messages.append({"role": "assistant", "content": assistant_response}) - - return assistant_response - - def _clear_chat(self): - """مسح المحادثة الحالية""" - st.session_state.assistant_messages = [] - st.session_state.document_context = None - - def _save_conversation(self): - """حفظ المحادثة الحالية""" - if not st.session_state.assistant_messages: - st.warning("لا توجد محادثة لحفظها.") - return False - - timestamp = datetime.now().strftime("%Y%m%d%H%M%S") - user_info = get_user_info() - - conversation_data = { - "timestamp": timestamp, - "user": user_info["username"], - "mode": st.session_state.assistant_mode, - "messages": st.session_state.assistant_messages, - "document_context": st.session_state.document_context - } - - filename = f"conversation_{user_info['username']}_{timestamp}.json" - file_path = os.path.join(self.conversations_dir, filename) - - try: - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(conversation_data, f, ensure_ascii=False, indent=2) - - return True - except Exception as e: - logging.error(f"خطأ في حفظ المحادثة: {e}") - return False - - def _load_conversation(self, filename): - """تحميل محادثة محفوظة""" - file_path = os.path.join(self.conversations_dir, filename) - - try: - with open(file_path, 'r', encoding='utf-8') as f: - conversation_data = json.load(f) - - st.session_state.assistant_messages = conversation_data["messages"] - st.session_state.assistant_mode = conversation_data["mode"] - st.session_state.document_context = conversation_data.get("document_context") - - return True - except Exception as e: - logging.error(f"خطأ في تحميل المحادثة: {e}") - return False - - def _get_saved_conversations(self): - """الحصول على قائمة المحادثات المحفوظة""" - conversations = [] - - try: - for filename in os.listdir(self.conversations_dir): - if filename.endswith(".json") and filename.startswith("conversation_"): - file_path = os.path.join(self.conversations_dir, filename) - - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - conversations.append({ - "filename": filename, - "timestamp": data.get("timestamp", ""), - "user": data.get("user", ""), - "mode": data.get("mode", "general"), - "message_count": len(data.get("messages", [])) - }) - except Exception as e: - logging.error(f"خطأ في قراءة المحادثات المحفوظة: {e}") - - # ترتيب المحادثات حسب التاريخ (الأحدث أولاً) - conversations.sort(key=lambda x: x["timestamp"], reverse=True) - - return conversations - - def render_chat_interface(self): - """عرض واجهة المحادثة الرئيسية""" - st.markdown("

المساعد الذكي

", unsafe_allow_html=True) - - # التحقق من توفر OpenAI API - if not self.is_api_available: - st.warning("⚠️ مفتاح OpenAI API غير متوفر. لن يكون المساعد الذكي قادراً على الرد. يرجى التواصل مع مسؤول النظام.") - - # إضافة CSS - st.markdown(""" - - """, unsafe_allow_html=True) - - # عرض أوضاع المساعد - st.markdown("#### اختر وضع المساعد الذكي") - - col1, col2, col3, col4, col5 = st.columns(5) - - with col1: - if st.button("مساعد عام", key="mode_general", - help="مساعد عام للإجابة على الأسئلة المتعلقة بالعقود والمناقصات"): - st.session_state.assistant_mode = "general" - st.rerun() - - with col2: - if st.button("تحليل العقود", key="mode_contract_analysis", - help="متخصص في تحليل العقود وتحديد البنود والشروط والمخاطر"): - st.session_state.assistant_mode = "contract_analysis" - st.rerun() - - with col3: - if st.button("تقدير التكاليف", key="mode_cost_estimation", - help="متخصص في تقدير تكاليف المشاريع والبنود"): - st.session_state.assistant_mode = "cost_estimation" - st.rerun() - - with col4: - if st.button("تقييم المخاطر", key="mode_risk_assessment", - help="متخصص في تحديد وتقييم المخاطر المحتملة في المشاريع والعقود"): - st.session_state.assistant_mode = "risk_assessment" - st.rerun() - - with col5: - if st.button("تخطيط المشاريع", key="mode_project_planning", - help="متخصص في تخطيط وإدارة المشاريع وتحديد المراحل والموارد"): - st.session_state.assistant_mode = "project_planning" - st.rerun() - - st.markdown(f"**الوضع الحالي:** {self.assistant_modes[st.session_state.assistant_mode]}") - - # تحميل سياق من مستند (اختياري) - st.markdown("---") - with st.expander("إضافة سياق من مستند", expanded=False): - context_text = st.text_area( - "نص المستند (اختياري)", - value=st.session_state.document_context if st.session_state.document_context else "", - height=150, - help="أضف نص المستند هنا ليتم استخدامه كسياق للمحادثة" - ) - - uploaded_file = st.file_uploader( - "أو قم بتحميل ملف نصي أو PDF", - type=["txt", "pdf"], - help="يمكنك تحميل ملف نصي أو PDF ليتم استخدامه كسياق للمحادثة" - ) - - doc_col1, doc_col2 = st.columns(2) - - with doc_col1: - if st.button("إضافة السياق", disabled=not context_text and not uploaded_file): - if uploaded_file: - try: - # قراءة الملف المرفوع - if uploaded_file.name.endswith(".pdf"): - import PyPDF2 - reader = PyPDF2.PdfReader(uploaded_file) - context = "" - for page in reader.pages: - context += page.extract_text() + "\n" - else: - context = uploaded_file.read().decode("utf-8") - - st.session_state.document_context = context - st.success("تم إضافة نص المستند كسياق للمحادثة بنجاح.") - except Exception as e: - st.error(f"حدث خطأ أثناء قراءة الملف: {str(e)}") - elif context_text: - st.session_state.document_context = context_text - st.success("تم إضافة نص المستند كسياق للمحادثة بنجاح.") - - with doc_col2: - if st.button("مسح السياق", disabled=not st.session_state.document_context): - st.session_state.document_context = None - st.success("تم مسح سياق المستند بنجاح.") - - # عرض المحادثة - st.markdown("---") - st.markdown("#### المحادثة مع المساعد الذكي") - - # عرض رسائل المحادثة - chat_container = st.container() - - with chat_container: - with st.container(): - if not st.session_state.assistant_messages: - st.markdown(""" -
-

مرحباً بك في المساعد الذكي!

-

يمكنك البدء بطرح سؤال أو طلب مساعدة.

-
- """, unsafe_allow_html=True) - else: - message_html = "" - - for msg in st.session_state.assistant_messages: - if msg["role"] == "user": - message_html += f""" -
-
-
{msg["content"]}
-
-
أ
-
- """ - else: - message_html += f""" -
-
W
-
-
{msg["content"]}
-
-
- """ - - st.markdown(f""" -
- {message_html} -
- """, unsafe_allow_html=True) - - # ادخال الرسالة - st.markdown("#### أدخل رسالتك") - - with st.container(): - with st.form(key="chat_form"): - user_message = st.text_area("رسالتك", height=100, placeholder="اكتب سؤالك أو طلبك هنا...") - - col1, col2, col3 = st.columns([2, 2, 1]) - - with col1: - send_button = st.form_submit_button( - "إرسال", - help="إرسال الرسالة إلى المساعد الذكي" - ) - - with col2: - suggested_questions = [ - "كيف يمكنني تحليل بنود الدفع في العقد؟", - "ما هي أفضل طريقة لتقدير تكاليف مشروع بناء؟", - "كيف أحدد المخاطر المحتملة في مشروع جديد؟", - "كيف يمكنني إنشاء جدول زمني فعال للمشروع؟", - "ما هي أهم البنود التي يجب الانتباه إليها في عقود المقاولات؟" - ] - - if st.session_state.assistant_mode == "contract_analysis": - suggested_questions = [ - "كيف أحدد البنود غير المواتية في العقد؟", - "ما هي العناصر الأساسية التي يجب أن يتضمنها عقد المقاولة؟", - "كيف أتعامل مع بنود الغرامات والتعويضات؟", - "كيف يمكنني التفاوض على تحسين شروط الدفع؟", - "ما هي الفروق الرئيسية بين عقد الثمن الثابت وعقد التكلفة زائد أتعاب؟" - ] - elif st.session_state.assistant_mode == "cost_estimation": - suggested_questions = [ - "كيف أقدر تكلفة المواد في مشروع بناء؟", - "ما هي نسبة النفقات العامة المعقولة لمشروع مقاولات؟", - "كيف أحسب تكلفة العمالة بدقة؟", - "ما هي العوامل التي تؤثر على تكلفة المعدات؟", - "كيف أقدر هامش الربح المناسب للمشروع؟" - ] - - selected_question = st.selectbox( - "أو اختر سؤال مقترح", - [""] + suggested_questions, - index=0 - ) - - with col3: - clear_button = st.form_submit_button( - "مسح المحادثة", - help="مسح جميع الرسائل في المحادثة الحالية" - ) - - if send_button and user_message: - # معالجة رسالة المستخدم - with st.spinner("جاري معالجة الرسالة..."): - self._process_user_message(user_message) - st.rerun() - - if send_button and selected_question and not user_message: - # استخدام السؤال المقترح - with st.spinner("جاري معالجة الرسالة..."): - self._process_user_message(selected_question) - st.rerun() - - if clear_button: - self._clear_chat() - st.rerun() - - # زر لحفظ المحادثة - col1, col2, col3 = st.columns([1, 1, 2]) - - with col1: - if st.button("حفظ المحادثة", key="save_conversation", disabled=not st.session_state.assistant_messages): - if self._save_conversation(): - st.success("تم حفظ المحادثة بنجاح.") - else: - st.error("حدث خطأ أثناء حفظ المحادثة.") - - with col2: - if st.button("تحميل محادثة سابقة", key="show_load_conversation"): - st.session_state.show_conversations = True - st.rerun() - - # عرض المحادثات المحفوظة - if "show_conversations" in st.session_state and st.session_state.show_conversations: - st.markdown("---") - st.markdown("#### المحادثات المحفوظة") - - conversations = self._get_saved_conversations() - - if not conversations: - st.info("لا توجد محادثات محفوظة.") - else: - # عرض المحادثات في جدول - conversation_data = [] - for conv in conversations: - timestamp = datetime.strptime(conv["timestamp"], "%Y%m%d%H%M%S") if conv["timestamp"] else "" - formatted_time = timestamp.strftime("%Y-%m-%d %H:%M:%S") if timestamp else "" - - conversation_data.append({ - "التاريخ": formatted_time, - "المستخدم": conv["user"], - "وضع المساعد": self.assistant_modes.get(conv["mode"], "غير معروف"), - "عدد الرسائل": conv["message_count"], - "الملف": conv["filename"] - }) - - df = pd.DataFrame(conversation_data) - st.dataframe(df, height=300) - - # اختيار محادثة لتحميلها - selected_filename = st.selectbox( - "اختر محادثة لتحميلها", - options=[""] + [conv["filename"] for conv in conversations], - format_func=lambda x: next((f"{c['user']} - {datetime.strptime(c['timestamp'], '%Y%m%d%H%M%S').strftime('%Y-%m-%d %H:%M:%S')}" for c in conversations if c["filename"] == x), x), - index=0 - ) - - col1, col2 = st.columns(2) - - with col1: - if st.button("تحميل المحادثة المختارة", disabled=not selected_filename): - if self._load_conversation(selected_filename): - st.success("تم تحميل المحادثة بنجاح.") - st.session_state.show_conversations = False - st.rerun() - else: - st.error("حدث خطأ أثناء تحميل المحادثة.") - - with col2: - if st.button("إلغاء", key="cancel_load_conversation"): - st.session_state.show_conversations = False - st.rerun() - - # عرض المعلومات عن وضع المساعد الحالي - st.markdown("---") - st.markdown(f"#### معلومات عن وضع المساعد: {self.assistant_modes[st.session_state.assistant_mode]}") - - if st.session_state.assistant_mode == "general": - st.markdown(""" - المساعد العام يمكنه مساعدتك في مجموعة متنوعة من المهام المتعلقة بالعقود والمناقصات وإدارة المشاريع. يمكنه: - - الإجابة على الأسئلة العامة حول العقود والمناقصات - - توجيهك إلى الوحدات المناسبة في النظام - - تقديم معلومات عامة عن إدارة المشاريع وأفضل الممارسات - - المساعدة في فهم المصطلحات والمفاهيم المتعلقة بمجال المقاولات - """) - elif st.session_state.assistant_mode == "contract_analysis": - st.markdown(""" - مساعد تحليل العقود متخصص في: - - تحليل بنود العقود وتوضيح معانيها - - تحديد الالتزامات والحقوق لكل طرف - - تسليط الضوء على البنود غير المواتية أو الغامضة - - تقديم توصيات للتفاوض على تحسين شروط العقد - - مقارنة العقد مع أفضل الممارسات في القطاع - """) - elif st.session_state.assistant_mode == "cost_estimation": - st.markdown(""" - مساعد تقدير التكاليف متخصص في: - - حساب تكاليف المشاريع بناءً على المتطلبات والمواصفات - - تقدير تكاليف المواد والعمالة والمعدات - - تحليل التكاليف المباشرة وغير المباشرة - - تقديم نصائح لتقليل التكاليف وزيادة الكفاءة - - تحديد العوامل التي قد تؤثر على التكلفة الإجمالية - """) - elif st.session_state.assistant_mode == "risk_assessment": - st.markdown(""" - مساعد تقييم المخاطر متخصص في: - - تحديد المخاطر المحتملة في المشاريع والعقود - - تقييم احتمالية وتأثير كل خطر - - اقتراح استراتيجيات للتخفيف من المخاطر - - إنشاء خطط للطوارئ والاستجابة للمخاطر - - تحليل تأثير المخاطر على الجدول الزمني والتكلفة - """) - elif st.session_state.assistant_mode == "project_planning": - st.markdown(""" - مساعد تخطيط المشاريع متخصص في: - - تقسيم المشروع إلى مراحل ومهام وأنشطة - - تحديد الموارد اللازمة لكل نشاط - - إنشاء الجداول الزمنية والمسار الحرج - - التخطيط للموارد البشرية والمعدات والمواد - - مراقبة تقدم المشروع وإدارة التغييرات - """) - - # عرض معلومات حقوق الملكية - render_credits() - - def render(self): - """عرض واجهة المساعد الذكي الرئيسية""" - # تحميل CSS المخصص - load_css() - - # عرض واجهة المحادثة - self.render_chat_interface() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="المساعد الذكي | WAHBi AI", - page_icon="🤖", - layout="wide", - initial_sidebar_state="expanded" - ) - - assistant = AIAssistant() - assistant.render() \ No newline at end of file diff --git a/modules/ai_assistant/ai_assistant_app.py b/modules/ai_assistant/ai_assistant_app.py deleted file mode 100644 index 4f667ece6e9a58e33ef6805b933cd551aeb2d2be..0000000000000000000000000000000000000000 --- a/modules/ai_assistant/ai_assistant_app.py +++ /dev/null @@ -1,3175 +0,0 @@ -# -*- coding: utf-8 -*- -""" -وحدة المساعد الذكي - -هذا الملف يحتوي على الفئة الرئيسية لتطبيق المساعد الذكي مع دعم نموذج Claude AI. -""" - -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 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 - أسرع نموذج للمهام اليومية" - } - - 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: - # الحصول على مفتاح 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: - 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: - 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": "يمكنك استخدام وحدة تحليل المستندات لمعالجة مستندات المناقصة ضخمة الحجم، حيث تقوم الوحدة بتحليل المستندات واستخراج المعلومات المهمة مثل مواصفات المشروع ومتطلباته وشروطه تلقائياً." - } - ] - - 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""" -
-
- {user_input} -
-
- """, 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""" -
-
- {response} -
-
- """, 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: - 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'] - ] - }) - - 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": "" - } - - # تقييم شامل للمخاطر - 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/assistant.py b/modules/ai_assistant/assistant.py deleted file mode 100644 index 00df7a6113f2597ccfebd77eb38bb0b24e77eaf1..0000000000000000000000000000000000000000 --- a/modules/ai_assistant/assistant.py +++ /dev/null @@ -1,444 +0,0 @@ -""" -وحدة المساعد الذكي لنظام إدارة المناقصات - Hybrid Face -""" - -import os -import logging -import threading -import datetime -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": [] - } - - # إضافة الاستعلام إلى سجل المحادثة - 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): - """خيط معالجة الاستعلام""" - 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) - 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]}...") - - 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"} - ] - - 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 = [ - "ما هي أنواع التقارير المتاحة في النظام؟", - "كيف يمكنني تخصيص تقرير المشروع؟", - "كيف أقوم بجدولة إنشاء تقارير دورية؟" - ] - else: - suggestions = [ - "كيف يمكنني البدء باستخدام النظام؟", - "ما هي الوحدات المتاحة في النظام؟", - "كيف يمكنني إنشاء مشروع جديد؟" - ] - - 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") - } - - 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 diff --git a/modules/ai_assistant/assistant_app.py b/modules/ai_assistant/assistant_app.py deleted file mode 100644 index 8306a78f6923fe730ef0c096c1e95c238e59ebe9..0000000000000000000000000000000000000000 --- a/modules/ai_assistant/assistant_app.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -تطبيق المساعد الذكي التفاعلي -يتيح للمستخدمين التفاعل مع نماذج الذكاء الاصطناعي المتقدمة للحصول على مساعدة في تحليل العقود والمناقصات -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات المساعد الذكي -from modules.ai_assistant.ai_assistant import AIAssistant - - -class AssistantApp: - """تطبيق المساعد الذكي التفاعلي""" - - def __init__(self): - """تهيئة تطبيق المساعد الذكي""" - self.assistant = AIAssistant() - - def render(self): - """عرض واجهة المستخدم الرئيسية للتطبيق""" - self.assistant.render() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="المساعد الذكي | WAHBi AI", - page_icon="🤖", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = AssistantApp() - app.render() \ No newline at end of file diff --git a/modules/ai_finetuning/__init__.py b/modules/ai_finetuning/__init__.py deleted file mode 100644 index 070419ec30c2df5c5ba01ff2fa55212770dd1391..0000000000000000000000000000000000000000 --- a/modules/ai_finetuning/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# ملف تهيئة حزمة ضبط نماذج الذكاء الاصطناعي \ No newline at end of file diff --git a/modules/ai_finetuning/finetuning_app.py b/modules/ai_finetuning/finetuning_app.py deleted file mode 100644 index a1dc1a7b8e4982246bd7567707b37e0b762a5b74..0000000000000000000000000000000000000000 --- a/modules/ai_finetuning/finetuning_app.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي للمصطلحات التعاقدية المتخصصة -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات تخصيص وضبط نماذج الذكاء الاصطناعي -from modules.ai_finetuning.model_finetuning import ModelFinetuning - - -class FinetuningApp: - """وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي""" - - def __init__(self): - """تهيئة وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي""" - self.model_finetuning = ModelFinetuning() - - def render(self): - """عرض واجهة وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي""" - st.markdown("

وحدة تخصيص وضبط نماذج الذكاء الاصطناعي

", unsafe_allow_html=True) - - st.markdown(""" -
- تمكنك هذه الوحدة من تخصيص وضبط نماذج الذكاء الاصطناعي للتعرف بدقة على المصطلحات التعاقدية والهندسية المتخصصة باللغة العربية. - يمكنك إنشاء قاموس للمصطلحات، وإعداد أمثلة التدريب، وتدريب النماذج واختبارها. -
- """, unsafe_allow_html=True) - - # عرض نموذج تخصيص وضبط نماذج الذكاء الاصطناعي - self.model_finetuning.render() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="تخصيص وضبط نماذج الذكاء الاصطناعي | WAHBi AI", - page_icon="🧠", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = FinetuningApp() - app.render() \ No newline at end of file diff --git a/modules/ai_finetuning/model_finetuning.py b/modules/ai_finetuning/model_finetuning.py deleted file mode 100644 index 464b6c2455ca7b925a2920a3fab386449e407243..0000000000000000000000000000000000000000 --- a/modules/ai_finetuning/model_finetuning.py +++ /dev/null @@ -1,2081 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة تخصيص وضبط نماذج الذكاء الاصطناعي للمصطلحات التعاقدية المتخصصة -تتيح هذه الوحدة إمكانية تدريب نماذج الذكاء الاصطناعي على المصطلحات المتخصصة في مجال العقود والمناقصات -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np -import json -import time -import datetime -from typing import List, Dict, Any, Optional, Tuple -import openai -import matplotlib.pyplot as plt -import tempfile -import csv -import re -import random -from pathlib import Path - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات واجهة المستخدم -from utils.components.header import render_header -from utils.components.credits import render_credits -from utils.helpers import format_number, format_currency, styled_button - - -class ModelFinetuning: - """فئة تخصيص وضبط نماذج الذكاء الاصطناعي""" - - def __init__(self): - """تهيئة وحدة تخصيص وضبط نماذج الذكاء الاصطناعي""" - # تهيئة مجلدات حفظ البيانات - self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/finetuning")) - os.makedirs(self.data_dir, exist_ok=True) - - # تهيئة الملفات والمجلدات الفرعية - self.training_data_dir = os.path.join(self.data_dir, "training_data") - os.makedirs(self.training_data_dir, exist_ok=True) - - self.models_dir = os.path.join(self.data_dir, "models") - os.makedirs(self.models_dir, exist_ok=True) - - self.terminology_file = os.path.join(self.data_dir, "terminology.json") - - # تهيئة حالة الجلسة - if 'terminology_data' not in st.session_state: - if os.path.exists(self.terminology_file): - with open(self.terminology_file, 'r', encoding='utf-8') as f: - st.session_state.terminology_data = json.load(f) - else: - st.session_state.terminology_data = { - "terms": [], - "training_examples": [], - "models": [] - } - - if 'active_training_job' not in st.session_state: - st.session_state.active_training_job = None - - if 'training_results' not in st.session_state: - st.session_state.training_results = [] - - # ضبط API مفاتيح الذكاء الاصطناعي - self.api_key = os.environ.get("OPENAI_API_KEY") - self.anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY") - - def render(self): - """عرض واجهة وحدة تخصيص وضبط نماذج الذكاء الاصطناعي""" - # عرض الشعار والعنوان الرئيسي - render_header("تخصيص وضبط نماذج الذكاء الاصطناعي للمصطلحات التعاقدية المتخصصة") - - # تبويبات الوحدة - tabs = st.tabs([ - "قاموس المصطلحات المتخصصة", - "إعداد بيانات التدريب", - "تدريب النموذج", - "اختبار النموذج", - "المساعد المتخصص" - ]) - - # تبويب قاموس المصطلحات المتخصصة - with tabs[0]: - self._render_terminology_dictionary() - - # تبويب إعداد بيانات التدريب - with tabs[1]: - self._render_training_data_setup() - - # تبويب تدريب النموذج - with tabs[2]: - self._render_model_training() - - # تبويب اختبار النموذج - with tabs[3]: - self._render_model_testing() - - # تبويب المساعد المتخصص - with tabs[4]: - self._render_specialized_assistant() - - # عرض حقوق النشر - render_credits() - - def _render_terminology_dictionary(self): - """عرض قاموس المصطلحات المتخصصة""" - st.markdown(""" -
-

📚 قاموس المصطلحات المتخصصة

-

أضف وحرر المصطلحات الفنية المتخصصة في مجال العقود والمناقصات باللغة العربية.

-

هذه المصطلحات ستستخدم لتدريب وضبط نماذج الذكاء الاصطناعي للتعرف عليها بدقة عالية.

-
- """, unsafe_allow_html=True) - - # إضافة مصطلح جديد - st.markdown("### إضافة مصطلح جديد") - - col1, col2 = st.columns(2) - - with col1: - term = st.text_input("المصطلح", key="new_term") - category = st.selectbox( - "الفئة", - options=[ - "شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", - "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", - "محتوى محلي", "أخرى" - ], - key="new_term_category" - ) - - with col2: - english_term = st.text_input("المصطلح بالإنجليزية (اختياري)", key="new_term_english") - importance = st.slider("مستوى الأهمية", 1, 5, 3, key="new_term_importance") - - definition = st.text_area("التعريف", key="new_term_definition") - examples = st.text_area("أمثلة على استخدام المصطلح (فصل بين الأمثلة بسطر جديد)", key="new_term_examples") - - # زر إضافة المصطلح - if styled_button("إضافة المصطلح", key="add_term", type="primary", icon="➕"): - if not term or not definition: - st.error("يرجى تعبئة المصطلح والتعريف على الأقل.") - else: - # إنشاء كائن المصطلح - new_term = { - "term": term, - "definition": definition, - "category": category, - "english_term": english_term, - "importance": importance, - "examples": [ex.strip() for ex in examples.split("\n") if ex.strip()], - "added_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - # إضافة المصطلح للقائمة - st.session_state.terminology_data["terms"].append(new_term) - - # حفظ البيانات - self._save_terminology_data() - - st.success(f"تمت إضافة المصطلح '{term}' بنجاح!") - st.rerun() - - # عرض المصطلحات الموجودة - st.markdown("### المصطلحات الموجودة") - - terms = st.session_state.terminology_data.get("terms", []) - - if not terms: - st.info("لا توجد مصطلحات مضافة. يرجى إضافة مصطلحات جديدة.") - else: - # تصفية المصطلحات - filter_col1, filter_col2 = st.columns(2) - - with filter_col1: - filter_category = st.selectbox( - "تصفية حسب الفئة", - options=["الكل"] + list(set(t.get("category") for t in terms)), - key="filter_term_category" - ) - - with filter_col2: - search_query = st.text_input("بحث", key="search_term") - - # تطبيق التصفية - filtered_terms = terms - if filter_category != "الكل": - filtered_terms = [t for t in filtered_terms if t.get("category") == filter_category] - - if search_query: - filtered_terms = [ - t for t in filtered_terms - if search_query.lower() in t.get("term", "").lower() or - search_query.lower() in t.get("definition", "").lower() or - search_query.lower() in t.get("english_term", "").lower() - ] - - # عرض المصطلحات المصفاة - if not filtered_terms: - st.warning("لا توجد مصطلحات تطابق معايير التصفية.") - else: - # إعداد بيانات للعرض - for i, term in enumerate(filtered_terms): - with st.expander(f"{term.get('term')} ({term.get('english_term', '')})", expanded=i==0 and len(filtered_terms)<5): - term_col1, term_col2 = st.columns([3, 1]) - - with term_col1: - st.markdown(f"**التعريف:** {term.get('definition')}") - st.markdown(f"**الفئة:** {term.get('category')}") - st.markdown(f"**المصطلح بالإنجليزية:** {term.get('english_term', '-')}") - - if "examples" in term and term["examples"]: - st.markdown("**أمثلة:**") - for ex in term["examples"]: - st.markdown(f"- {ex}") - - with term_col2: - st.markdown(f"**مستوى الأهمية:** {'⭐' * term.get('importance', 3)}") - st.markdown(f"**تاريخ الإضافة:** {term.get('added_at', '-')}") - - # أزرار التحرير والحذف - if styled_button("تحرير", key=f"edit_term_{i}", type="secondary", icon="✏️"): - st.session_state.term_to_edit = i - - if styled_button("حذف", key=f"delete_term_{i}", type="danger", icon="🗑️"): - st.session_state.term_to_delete = i - - # معالجة تحرير أو حذف المصطلح - if "term_to_edit" in st.session_state: - self._render_edit_term_form(st.session_state.term_to_edit, filtered_terms) - - if "term_to_delete" in st.session_state: - if st.warning(f"هل أنت متأكد من حذف المصطلح '{filtered_terms[st.session_state.term_to_delete].get('term')}'؟"): - if styled_button("تأكيد الحذف", key="confirm_delete", type="danger", icon="🗑️"): - # حذف المصطلح - term_index = terms.index(filtered_terms[st.session_state.term_to_delete]) - del st.session_state.terminology_data["terms"][term_index] - - # حفظ البيانات - self._save_terminology_data() - - # إعادة ضبط حالة الحذف - del st.session_state.term_to_delete - - st.success("تم حذف المصطلح بنجاح!") - st.rerun() - - if styled_button("إلغاء", key="cancel_delete", type="secondary", icon="❌"): - del st.session_state.term_to_delete - st.rerun() - - # تصدير المصطلحات - st.markdown("### تصدير وتوريد المصطلحات") - - col1, col2 = st.columns(2) - - with col1: - if styled_button("تصدير المصطلحات إلى CSV", key="export_terms", type="primary", icon="📤"): - self._export_terms_to_csv() - - with col2: - uploaded_file = st.file_uploader("استيراد المصطلحات من ملف CSV", type=["csv"], key="import_terms_file") - - if uploaded_file is not None: - if styled_button("استيراد المصطلحات", key="import_terms", type="success", icon="📥"): - self._import_terms_from_csv(uploaded_file) - - def _render_edit_term_form(self, term_index, terms_list): - """عرض نموذج تحرير المصطلح""" - term = terms_list[term_index] - - st.markdown("### تحرير المصطلح") - - col1, col2 = st.columns(2) - - with col1: - edited_term = st.text_input("المصطلح", value=term.get("term", ""), key="edit_term_name") - edited_category = st.selectbox( - "الفئة", - options=[ - "شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", - "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", - "محتوى محلي", "أخرى" - ], - index=["شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", "محتوى محلي", "أخرى"].index(term.get("category", "أخرى")), - key="edit_term_category" - ) - - with col2: - edited_english_term = st.text_input("المصطلح بالإنجليزية (اختياري)", value=term.get("english_term", ""), key="edit_term_english") - edited_importance = st.slider("مستوى الأهمية", 1, 5, term.get("importance", 3), key="edit_term_importance") - - edited_definition = st.text_area("التعريف", value=term.get("definition", ""), key="edit_term_definition") - edited_examples = st.text_area("أمثلة على استخدام المصطلح (فصل بين الأمثلة بسطر جديد)", value="\n".join(term.get("examples", [])), key="edit_term_examples") - - col1, col2 = st.columns(2) - - with col1: - if styled_button("حفظ التغييرات", key="save_edited_term", type="primary", icon="💾"): - if not edited_term or not edited_definition: - st.error("يرجى تعبئة المصطلح والتعريف على الأقل.") - else: - # تحديث المصطلح - updated_term = { - "term": edited_term, - "definition": edited_definition, - "category": edited_category, - "english_term": edited_english_term, - "importance": edited_importance, - "examples": [ex.strip() for ex in edited_examples.split("\n") if ex.strip()], - "added_at": term.get("added_at", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), - "updated_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - # الحصول على المؤشر الفعلي في القائمة الكاملة - all_terms = st.session_state.terminology_data["terms"] - actual_index = all_terms.index(term) - - # تحديث المصطلح - st.session_state.terminology_data["terms"][actual_index] = updated_term - - # حفظ البيانات - self._save_terminology_data() - - # إعادة ضبط حالة التحرير - del st.session_state.term_to_edit - - st.success(f"تم تحديث المصطلح '{edited_term}' بنجاح!") - st.rerun() - - with col2: - if styled_button("إلغاء", key="cancel_edit_term", type="secondary", icon="❌"): - del st.session_state.term_to_edit - st.rerun() - - def _export_terms_to_csv(self): - """تصدير المصطلحات إلى ملف CSV""" - terms = st.session_state.terminology_data.get("terms", []) - - if not terms: - st.error("لا توجد مصطلحات للتصدير.") - return - - # إنشاء ملف CSV مؤقت - with tempfile.NamedTemporaryFile(mode='w+', suffix='.csv', newline='', encoding='utf-8', delete=False) as f: - writer = csv.writer(f) - - # كتابة الترويسة - writer.writerow([ - 'المصطلح', 'التعريف', 'الفئة', 'المصطلح بالإنجليزية', - 'مستوى الأهمية', 'الأمثلة', 'تاريخ الإضافة' - ]) - - # كتابة المصطلحات - for term in terms: - writer.writerow([ - term.get('term', ''), - term.get('definition', ''), - term.get('category', ''), - term.get('english_term', ''), - term.get('importance', 3), - '|'.join(term.get('examples', [])), - term.get('added_at', '') - ]) - - # الحصول على مسار الملف - csv_path = f.name - - # قراءة الملف وتقديمه للتنزيل - with open(csv_path, 'r', encoding='utf-8') as f: - csv_data = f.read() - - # تقديم الملف للتنزيل - st.download_button( - label="تنزيل ملف CSV", - data=csv_data, - file_name="terminology_dictionary.csv", - mime="text/csv" - ) - - # حذف الملف المؤقت - os.unlink(csv_path) - - def _import_terms_from_csv(self, uploaded_file): - """استيراد المصطلحات من ملف CSV""" - try: - # قراءة الملف - df = pd.read_csv(uploaded_file, encoding='utf-8') - - # التحقق من وجود الأعمدة المطلوبة - required_columns = ['المصطلح', 'التعريف'] - missing_columns = [col for col in required_columns if col not in df.columns] - - if missing_columns: - st.error(f"الملف لا يحتوي على الأعمدة التالية: {', '.join(missing_columns)}") - return - - # إضافة المصطلحات - terms_added = 0 - terms_updated = 0 - - for _, row in df.iterrows(): - term = row['المصطلح'] - - # البحث عن المصطلح الموجود - existing_term = next((t for t in st.session_state.terminology_data["terms"] if t.get("term") == term), None) - - # تحضير كائن المصطلح - term_obj = { - "term": term, - "definition": row.get('التعريف', ''), - "category": row.get('الفئة', 'أخرى'), - "english_term": row.get('المصطلح بالإنجليزية', ''), - "importance": int(row.get('مستوى الأهمية', 3)), - "examples": row.get('الأمثلة', '').split('|') if 'الأمثلة' in row else [], - "added_at": row.get('تاريخ الإضافة', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) - } - - if existing_term: - # تحديث المصطلح الموجود - index = st.session_state.terminology_data["terms"].index(existing_term) - st.session_state.terminology_data["terms"][index] = term_obj - terms_updated += 1 - else: - # إضافة مصطلح جديد - st.session_state.terminology_data["terms"].append(term_obj) - terms_added += 1 - - # حفظ البيانات - self._save_terminology_data() - - st.success(f"تم استيراد المصطلحات بنجاح! (تمت إضافة {terms_added} مصطلح جديد، وتحديث {terms_updated} مصطلح موجود)") - st.rerun() - - except Exception as e: - st.error(f"حدث خطأ أثناء استيراد المصطلحات: {str(e)}") - - def _render_training_data_setup(self): - """عرض إعداد بيانات التدريب""" - st.markdown(""" -
-

🔬 إعداد بيانات التدريب

-

قم بإنشاء وتحرير أمثلة التدريب لضبط نماذج الذكاء الاصطناعي على المصطلحات المتخصصة.

-

يمكنك إنشاء أمثلة يدوياً أو استيرادها من ملف أو توليدها تلقائياً باستخدام نماذج الذكاء الاصطناعي الحالية.

-
- """, unsafe_allow_html=True) - - # تبويبات إعداد البيانات - training_tabs = st.tabs(["أمثلة التدريب الحالية", "إنشاء أمثلة يدوياً", "توليد أمثلة تلقائياً", "استيراد وتصدير البيانات"]) - - # عرض أمثلة التدريب الحالية - with training_tabs[0]: - self._render_existing_training_examples() - - # إنشاء أمثلة يدوياً - with training_tabs[1]: - self._render_manual_example_creation() - - # توليد أمثلة تلقائياً - with training_tabs[2]: - self._render_automatic_example_generation() - - # استيراد وتصدير البيانات - with training_tabs[3]: - self._render_import_export_training_data() - - def _render_existing_training_examples(self): - """عرض أمثلة التدريب الحالية""" - st.markdown("### أمثلة التدريب الحالية") - - examples = st.session_state.terminology_data.get("training_examples", []) - - if not examples: - st.info("لا توجد أمثلة تدريب. يرجى إنشاء أمثلة جديدة.") - return - - # عرض إحصائيات البيانات - st.markdown("#### إحصائيات البيانات") - - total_examples = len(examples) - categories = {} - terms_used = set() - - for ex in examples: - cat = ex.get("category", "غير مصنف") - categories[cat] = categories.get(cat, 0) + 1 - - for term in ex.get("terms", []): - terms_used.add(term) - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي الأمثلة", total_examples) - - with col2: - st.metric("عدد المصطلحات المستخدمة", len(terms_used)) - - with col3: - st.metric("عدد الفئات", len(categories)) - - # عرض توزيع الفئات - st.markdown("#### توزيع الأمثلة حسب الفئة") - - categories_df = pd.DataFrame({ - "الفئة": list(categories.keys()), - "عدد الأمثلة": list(categories.values()) - }) - - fig, ax = plt.subplots(figsize=(10, 6)) - ax.bar(categories_df["الفئة"], categories_df["عدد الأمثلة"]) - ax.set_title("توزيع أمثلة التدريب حسب الفئة") - ax.set_xlabel("الفئة") - ax.set_ylabel("عدد الأمثلة") - - # تدوير أسماء الفئات لتسهيل القراءة - plt.xticks(rotation=45, ha='right') - plt.tight_layout() - - st.pyplot(fig) - - # تصفية الأمثلة - st.markdown("#### تصفية الأمثلة") - - filter_col1, filter_col2 = st.columns(2) - - with filter_col1: - filter_category = st.selectbox( - "تصفية حسب الفئة", - options=["الكل"] + list(categories.keys()), - key="filter_example_category" - ) - - with filter_col2: - search_query = st.text_input("بحث في النص", key="search_example") - - # تطبيق التصفية - filtered_examples = examples - if filter_category != "الكل": - filtered_examples = [ex for ex in filtered_examples if ex.get("category") == filter_category] - - if search_query: - filtered_examples = [ - ex for ex in filtered_examples - if search_query.lower() in ex.get("input", "").lower() or - search_query.lower() in ex.get("output", "").lower() - ] - - # عرض الأمثلة المصفاة - if not filtered_examples: - st.warning("لا توجد أمثلة تطابق معايير التصفية.") - else: - # عرض عدد محدود من الأمثلة في كل صفحة - examples_per_page = 10 - total_pages = (len(filtered_examples) - 1) // examples_per_page + 1 - - # التنقل بين الصفحات - col1, col2, col3 = st.columns([1, 3, 1]) - - with col2: - page = st.slider("الصفحة", 1, max(1, total_pages), 1, key="examples_page") - - start_idx = (page - 1) * examples_per_page - end_idx = min(start_idx + examples_per_page, len(filtered_examples)) - - page_examples = filtered_examples[start_idx:end_idx] - - # عرض الأمثلة - for i, example in enumerate(page_examples): - example_idx = start_idx + i - with st.expander(f"مثال #{example_idx+1} - {example.get('category', 'غير مصنف')}", expanded=i==0 and len(page_examples)<5): - ex_col1, ex_col2 = st.columns([3, 1]) - - with ex_col1: - st.markdown("**النص المدخل:**") - st.markdown(f"```\n{example.get('input', '')}\n```") - - st.markdown("**النص المتوقع:**") - st.markdown(f"```\n{example.get('output', '')}\n```") - - with ex_col2: - st.markdown("**الفئة:** " + example.get('category', 'غير مصنف')) - st.markdown("**المصطلحات المستخدمة:**") - for term in example.get("terms", []): - st.markdown(f"- {term}") - - # تاريخ الإنشاء - if "created_at" in example: - st.markdown(f"**تاريخ الإنشاء:** {example['created_at']}") - - # أزرار التحرير والحذف - if styled_button("تحرير", key=f"edit_example_{example_idx}", type="secondary", icon="✏️"): - st.session_state.example_to_edit = example_idx - - if styled_button("حذف", key=f"delete_example_{example_idx}", type="danger", icon="🗑️"): - st.session_state.example_to_delete = example_idx - - # معالجة تحرير أو حذف مثال - if "example_to_edit" in st.session_state: - self._render_edit_example_form(st.session_state.example_to_edit, filtered_examples) - - if "example_to_delete" in st.session_state: - if st.warning(f"هل أنت متأكد من حذف المثال #{st.session_state.example_to_delete+1}؟"): - if styled_button("تأكيد الحذف", key="confirm_delete_example", type="danger", icon="🗑️"): - # حذف المثال - example_index = examples.index(filtered_examples[st.session_state.example_to_delete]) - del st.session_state.terminology_data["training_examples"][example_index] - - # حفظ البيانات - self._save_terminology_data() - - # إعادة ضبط حالة الحذف - del st.session_state.example_to_delete - - st.success("تم حذف المثال بنجاح!") - st.rerun() - - if styled_button("إلغاء", key="cancel_delete_example", type="secondary", icon="❌"): - del st.session_state.example_to_delete - st.rerun() - - def _render_edit_example_form(self, example_index, examples_list): - """عرض نموذج تحرير مثال التدريب""" - example = examples_list[example_index] - - st.markdown("### تحرير مثال التدريب") - - # اختيار المصطلحات المستخدمة - all_terms = [term.get("term") for term in st.session_state.terminology_data.get("terms", [])] - selected_terms = st.multiselect( - "المصطلحات المستخدمة في المثال", - options=all_terms, - default=example.get("terms", []), - key="edit_example_terms" - ) - - # إدخال النص المدخل والمتوقع - input_text = st.text_area("النص المدخل", value=example.get("input", ""), key="edit_example_input", height=150) - output_text = st.text_area("النص المتوقع", value=example.get("output", ""), key="edit_example_output", height=150) - - # اختيار الفئة - category = st.selectbox( - "الفئة", - options=["شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", "محتوى محلي", "أخرى"], - index=["شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", "محتوى محلي", "أخرى"].index(example.get("category", "أخرى")), - key="edit_example_category" - ) - - col1, col2 = st.columns(2) - - with col1: - if styled_button("حفظ التغييرات", key="save_edited_example", type="primary", icon="💾"): - if not input_text or not output_text: - st.error("يرجى تعبئة النص المدخل والنص المتوقع.") - else: - # تحديث المثال - updated_example = { - "input": input_text, - "output": output_text, - "category": category, - "terms": selected_terms, - "created_at": example.get("created_at", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), - "updated_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - # الحصول على المؤشر الفعلي في القائمة الكاملة - all_examples = st.session_state.terminology_data["training_examples"] - actual_index = all_examples.index(example) - - # تحديث المثال - st.session_state.terminology_data["training_examples"][actual_index] = updated_example - - # حفظ البيانات - self._save_terminology_data() - - # إعادة ضبط حالة التحرير - del st.session_state.example_to_edit - - st.success("تم تحديث مثال التدريب بنجاح!") - st.rerun() - - with col2: - if styled_button("إلغاء", key="cancel_edit_example", type="secondary", icon="❌"): - del st.session_state.example_to_edit - st.rerun() - - def _render_manual_example_creation(self): - """عرض نموذج إنشاء أمثلة يدوياً""" - st.markdown("### إنشاء مثال تدريب جديد") - - # اختيار المصطلحات المستخدمة - all_terms = [term.get("term") for term in st.session_state.terminology_data.get("terms", [])] - selected_terms = st.multiselect( - "المصطلحات المستخدمة في المثال", - options=all_terms, - key="new_example_terms" - ) - - # اختيار الفئة - category = st.selectbox( - "الفئة", - options=["شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", "محتوى محلي", "أخرى"], - key="new_example_category" - ) - - # إدخال النص المدخل والمتوقع - st.markdown("**النص المدخل** (نص السؤال أو الطلب)") - input_text = st.text_area("", key="new_example_input", height=150, placeholder="مثال: قم بشرح معنى مصطلح 'محتوى محلي' وكيفية حسابه في المشاريع الحكومية.") - - st.markdown("**النص المتوقع** (الإجابة المثالية التي يجب أن يقدمها النموذج)") - output_text = st.text_area("", key="new_example_output", height=150, placeholder="مثال: المحتوى المحلي (Local Content) هو نسبة القيمة المحلية المضافة في المنتجات والخدمات المقدمة في المشروع...") - - # عرض تعريفات المصطلحات المختارة للمساعدة - if selected_terms: - with st.expander("تعريفات المصطلحات المختارة", expanded=True): - for term_name in selected_terms: - term = next((t for t in st.session_state.terminology_data.get("terms", []) if t.get("term") == term_name), None) - if term: - st.markdown(f"**{term_name}**: {term.get('definition', '')}") - - # زر إضافة المثال - if styled_button("إضافة مثال التدريب", key="add_example", type="primary", icon="➕"): - if not input_text or not output_text: - st.error("يرجى تعبئة النص المدخل والنص المتوقع.") - else: - # إنشاء كائن المثال - new_example = { - "input": input_text, - "output": output_text, - "category": category, - "terms": selected_terms, - "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - - # إضافة المثال للقائمة - if "training_examples" not in st.session_state.terminology_data: - st.session_state.terminology_data["training_examples"] = [] - - st.session_state.terminology_data["training_examples"].append(new_example) - - # حفظ البيانات - self._save_terminology_data() - - st.success("تم إضافة مثال التدريب بنجاح!") - st.rerun() - - def _render_automatic_example_generation(self): - """عرض واجهة توليد أمثلة تلقائياً""" - st.markdown("### توليد أمثلة تدريب تلقائياً") - - # التحقق من وجود مفاتيح API - if not self.api_key and not self.anthropic_api_key: - st.warning("لم يتم العثور على مفاتيح API للذكاء الاصطناعي. يرجى إضافة OPENAI_API_KEY أو ANTHROPIC_API_KEY إلى المتغيرات البيئية.") - return - - # اختيار نموذج الذكاء الاصطناعي - ai_models = [] - - if self.api_key: - ai_models.extend(["gpt-4o", "gpt-3.5-turbo"]) - - if self.anthropic_api_key: - ai_models.extend(["claude-3-7-sonnet-20250219"]) - - selected_model = st.selectbox( - "اختر نموذج الذكاء الاصطناعي", - options=ai_models, - key="auto_gen_model" - ) - - # اختيار المصطلحات لتوليد أمثلة حولها - all_terms = [term.get("term") for term in st.session_state.terminology_data.get("terms", [])] - selected_terms = st.multiselect( - "اختر المصطلحات لتوليد أمثلة حولها", - options=all_terms, - key="auto_gen_terms" - ) - - # اختيار عدد الأمثلة المراد توليدها - num_examples = st.slider("عدد الأمثلة لكل مصطلح", 1, 5, 2, key="auto_gen_count") - - # اختيار الفئات المرغوبة - selected_categories = st.multiselect( - "اختر الفئات المرغوبة للأمثلة", - options=["شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", "محتوى محلي", "أخرى"], - default=["شروط تعاقدية", "مستندات مناقصة", "مصطلحات قانونية"], - key="auto_gen_categories" - ) - - # زر توليد الأمثلة - if styled_button("توليد الأمثلة", key="generate_examples", type="primary", icon="✨"): - if not selected_terms: - st.error("يرجى اختيار مصطلح واحد على الأقل.") - elif not selected_categories: - st.error("يرجى اختيار فئة واحدة على الأقل.") - else: - # عرض شريط التقدم - progress_bar = st.progress(0) - status_text = st.empty() - - # تجهيز المصطلحات وتعريفاتها - terms_with_definitions = {} - for term_name in selected_terms: - term = next((t for t in st.session_state.terminology_data.get("terms", []) if t.get("term") == term_name), None) - if term: - terms_with_definitions[term_name] = term.get('definition', '') - - # توليد الأمثلة - generated_examples = [] - total_iterations = len(selected_terms) * len(selected_categories) - current_iteration = 0 - - for term_name, definition in terms_with_definitions.items(): - for category in selected_categories: - current_iteration += 1 - progress = current_iteration / total_iterations - progress_bar.progress(progress) - status_text.text(f"جاري توليد أمثلة للمصطلح '{term_name}' في الفئة '{category}'...") - - # توليد أمثلة لهذا المصطلح والفئة - examples = self._generate_examples_with_ai( - term_name, - definition, - category, - num_examples, - selected_model - ) - - generated_examples.extend(examples) - - # إضافة الأمثلة المولدة إلى البيانات - if "training_examples" not in st.session_state.terminology_data: - st.session_state.terminology_data["training_examples"] = [] - - st.session_state.terminology_data["training_examples"].extend(generated_examples) - - # حفظ البيانات - self._save_terminology_data() - - # إكمال شريط التقدم - progress_bar.progress(1.0) - status_text.text(f"تم توليد {len(generated_examples)} مثال بنجاح!") - - st.success(f"تم توليد {len(generated_examples)} مثال بنجاح!") - st.rerun() - - def _generate_examples_with_ai(self, term_name, definition, category, num_examples, model): - """توليد أمثلة باستخدام الذكاء الاصطناعي""" - # تحضير الرسالة - prompt = f""" - أنت خبير في توليد أمثلة تدريب لضبط نماذج الذكاء الاصطناعي على المصطلحات التعاقدية والهندسية المتخصصة باللغة العربية. - - أريد منك توليد {num_examples} مثال تدريب للمصطلح التالي: - - المصطلح: {term_name} - التعريف: {definition} - الفئة: {category} - - لكل مثال، قم بتوليد: - 1. نص المدخل (سؤال أو طلب حول المصطلح) - 2. نص المخرج المتوقع (الإجابة المثالية التي يجب أن يقدمها النموذج) - - تأكد من: - - جعل الأمثلة متنوعة وواقعية - - تضمين سياقات مختلفة لاستخدام المصطلح - - استخدام أسلوب مناسب لوثائق المناقصات والعقود - - تضمين تفاصيل تقنية دقيقة عند الحاجة - - قم بإرجاع النتائج بتنسيق JSON كما يلي: - - ```json - [ - { - "input": "نص المدخل للمثال الأول", - "output": "نص المخرج المتوقع للمثال الأول" - }, - { - "input": "نص المدخل للمثال الثاني", - "output": "نص المخرج المتوقع للمثال الثاني" - }, - ... - ] - ``` - - أرجع البيانات بتنسيق JSON فقط. - """ - - try: - # استدعاء API المناسب حسب النموذج المختار - if "gpt" in model and self.api_key: - # استخدام OpenAI API - openai.api_key = self.api_key - - response = openai.chat.completions.create( - model=model, - messages=[ - {"role": "system", "content": "أنت مساعد محترف متخصص في توليد بيانات تدريب لضبط نماذج الذكاء الاصطناعي."}, - {"role": "user", "content": prompt} - ], - temperature=0.7, - response_format={"type": "json_object"} - ) - - # استخراج النتيجة - result_text = response.choices[0].message.content - - # تنظيف النص واستخراج JSON - json_match = re.search(r'```json\s*(.*?)\s*```', result_text, re.DOTALL) - if json_match: - result_json = json_match.group(1) - else: - result_json = result_text - - # تحليل JSON - examples_data = json.loads(result_json) - - # إذا كان الناتج كائن JSON بخاصية examples - if isinstance(examples_data, dict) and "examples" in examples_data: - examples_data = examples_data["examples"] - - elif "claude" in model and self.anthropic_api_key: - # استخدام Anthropic API - from anthropic import Anthropic - - anthropic_client = Anthropic(api_key=self.anthropic_api_key) - - response = anthropic_client.messages.create( - model=model, - max_tokens=4000, - messages=[ - {"role": "user", "content": prompt} - ], - temperature=0.7 - ) - - # استخراج النتيجة - result_text = response.content[0].text - - # تنظيف النص واستخراج JSON - json_match = re.search(r'```json\s*(.*?)\s*```', result_text, re.DOTALL) - if json_match: - result_json = json_match.group(1) - else: - result_json = result_text - - # تحليل JSON - examples_data = json.loads(result_json) - - # إذا كان الناتج كائن JSON بخاصية examples - if isinstance(examples_data, dict) and "examples" in examples_data: - examples_data = examples_data["examples"] - - else: - # في حالة عدم توفر النموذج المطلوب - return [] - - # تحويل البيانات إلى الصيغة المطلوبة للأمثلة - formatted_examples = [] - - for example in examples_data: - formatted_examples.append({ - "input": example["input"], - "output": example["output"], - "category": category, - "terms": [term_name], - "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "generated_by": model - }) - - return formatted_examples - - except Exception as e: - st.error(f"حدث خطأ أثناء توليد الأمثلة: {str(e)}") - return [] - - def _render_import_export_training_data(self): - """عرض واجهة استيراد وتصدير بيانات التدريب""" - st.markdown("### استيراد وتصدير بيانات التدريب") - - col1, col2 = st.columns(2) - - with col1: - st.markdown("#### تصدير بيانات التدريب") - - export_format = st.selectbox( - "صيغة التصدير", - options=["JSON", "JSONL", "CSV"], - key="export_format" - ) - - if styled_button("تصدير البيانات", key="export_data", type="primary", icon="📤"): - self._export_training_data(export_format) - - with col2: - st.markdown("#### استيراد بيانات التدريب") - - import_format = st.selectbox( - "صيغة الاستيراد", - options=["JSON", "JSONL", "CSV"], - key="import_format" - ) - - uploaded_file = st.file_uploader("استيراد بيانات التدريب", type=["json", "jsonl", "csv"], key="import_data_file") - - if uploaded_file is not None: - if styled_button("استيراد البيانات", key="import_data", type="success", icon="📥"): - self._import_training_data(uploaded_file, import_format) - - def _export_training_data(self, format): - """تصدير بيانات التدريب إلى ملف""" - examples = st.session_state.terminology_data.get("training_examples", []) - - if not examples: - st.error("لا توجد بيانات تدريب للتصدير.") - return - - try: - if format == "JSON": - # تصدير إلى ملف JSON - with tempfile.NamedTemporaryFile(mode='w+', suffix='.json', encoding='utf-8', delete=False) as f: - json.dump(examples, f, ensure_ascii=False, indent=2) - json_path = f.name - - # قراءة الملف وتقديمه للتنزيل - with open(json_path, 'r', encoding='utf-8') as f: - json_data = f.read() - - st.download_button( - label="تنزيل ملف JSON", - data=json_data, - file_name="training_data.json", - mime="application/json" - ) - - # حذف الملف المؤقت - os.unlink(json_path) - - elif format == "JSONL": - # تصدير إلى ملف JSONL - jsonl_content = "" - for example in examples: - jsonl_content += json.dumps(example, ensure_ascii=False) + "\n" - - st.download_button( - label="تنزيل ملف JSONL", - data=jsonl_content, - file_name="training_data.jsonl", - mime="application/jsonl" - ) - - elif format == "CSV": - # تصدير إلى ملف CSV - with tempfile.NamedTemporaryFile(mode='w+', suffix='.csv', newline='', encoding='utf-8', delete=False) as f: - writer = csv.writer(f) - - # كتابة الترويسة - writer.writerow([ - 'النص المدخل', 'النص المتوقع', 'الفئة', 'المصطلحات', 'تاريخ الإنشاء', 'تم التوليد بواسطة' - ]) - - # كتابة البيانات - for example in examples: - writer.writerow([ - example.get('input', ''), - example.get('output', ''), - example.get('category', ''), - '|'.join(example.get('terms', [])), - example.get('created_at', ''), - example.get('generated_by', 'يدوي') - ]) - - # الحصول على مسار الملف - csv_path = f.name - - # قراءة الملف وتقديمه للتنزيل - with open(csv_path, 'r', encoding='utf-8') as f: - csv_data = f.read() - - st.download_button( - label="تنزيل ملف CSV", - data=csv_data, - file_name="training_data.csv", - mime="text/csv" - ) - - # حذف الملف المؤقت - os.unlink(csv_path) - - except Exception as e: - st.error(f"حدث خطأ أثناء تصدير البيانات: {str(e)}") - - def _import_training_data(self, uploaded_file, format): - """استيراد بيانات التدريب من ملف""" - try: - examples = [] - - if format == "JSON": - # استيراد من ملف JSON - content = uploaded_file.read().decode('utf-8') - examples = json.loads(content) - - elif format == "JSONL": - # استيراد من ملف JSONL - content = uploaded_file.read().decode('utf-8') - - for line in content.strip().split('\n'): - if line.strip(): - examples.append(json.loads(line)) - - elif format == "CSV": - # استيراد من ملف CSV - df = pd.read_csv(uploaded_file, encoding='utf-8') - - for _, row in df.iterrows(): - example = { - "input": row.get('النص المدخل', ''), - "output": row.get('النص المتوقع', ''), - "category": row.get('الفئة', 'أخرى'), - "terms": row.get('المصطلحات', '').split('|') if pd.notna(row.get('المصطلحات', '')) else [], - "created_at": row.get('تاريخ الإنشاء', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), - "generated_by": row.get('تم التوليد بواسطة', 'مستورد') - } - - examples.append(example) - - # إضافة الأمثلة المستوردة - if "training_examples" not in st.session_state.terminology_data: - st.session_state.terminology_data["training_examples"] = [] - - # فلترة الأمثلة الصحيحة - valid_examples = [] - for ex in examples: - if "input" in ex and "output" in ex: - valid_examples.append(ex) - - # إضافة الأمثلة وحفظ البيانات - if valid_examples: - st.session_state.terminology_data["training_examples"].extend(valid_examples) - self._save_terminology_data() - - st.success(f"تم استيراد {len(valid_examples)} مثال بنجاح!") - st.rerun() - else: - st.error("لم يتم العثور على أمثلة صالحة في الملف.") - - except Exception as e: - st.error(f"حدث خطأ أثناء استيراد البيانات: {str(e)}") - - def _render_model_training(self): - """عرض واجهة تدريب النموذج""" - st.markdown(""" -
-

🧠 تدريب النموذج

-

قم بتدريب نموذج الذكاء الاصطناعي على المصطلحات المتخصصة باستخدام أمثلة التدريب.

-

يمكنك اختيار النموذج الأساسي والإعدادات المناسبة لعملية التدريب.

-
- """, unsafe_allow_html=True) - - # التحقق من وجود بيانات تدريب كافية - examples = st.session_state.terminology_data.get("training_examples", []) - if len(examples) < 10: - st.warning(f"عدد أمثلة التدريب الحالية ({len(examples)}) غير كافٍ للتدريب. يُنصح بوجود 10 أمثلة على الأقل.") - - # تبويبات تدريب النموذج - training_tabs = st.tabs(["إعداد التدريب", "نماذج سابقة", "وظائف التدريب النشطة"]) - - # تبويب إعداد التدريب - with training_tabs[0]: - self._render_training_setup() - - # تبويب النماذج السابقة - with training_tabs[1]: - self._render_previous_models() - - # تبويب وظائف التدريب النشطة - with training_tabs[2]: - self._render_active_training_jobs() - - def _render_training_setup(self): - """عرض إعدادات تدريب النموذج""" - st.markdown("### إعداد عملية التدريب") - - # التحقق من وجود مفاتيح API - if not self.api_key and not self.anthropic_api_key: - st.warning("لم يتم العثور على مفاتيح API للذكاء الاصطناعي. يرجى إضافة OPENAI_API_KEY أو ANTHROPIC_API_KEY إلى المتغيرات البيئية.") - return - - # اختيار نموذج الذكاء الاصطناعي الأساسي - provider_options = [] - if self.api_key: - provider_options.append("OpenAI") - if self.anthropic_api_key: - provider_options.append("Anthropic") - - provider = st.selectbox( - "مزود الذكاء الاصطناعي", - options=provider_options, - key="training_provider" - ) - - # الإعدادات حسب المزود - if provider == "OpenAI": - # نماذج OpenAI المتاحة للضبط - base_model = st.selectbox( - "النموذج الأساسي", - options=["gpt-3.5-turbo-0125", "gpt-4o-mini"], - key="openai_base_model" - ) - - # إعدادات التدريب - col1, col2 = st.columns(2) - - with col1: - n_epochs = st.slider("عدد الحقب (Epochs)", 1, 4, 2, key="openai_epochs") - batch_size = st.selectbox("حجم الدفعة (Batch Size)", options=[1, 2, 4, 8], index=1, key="openai_batch_size") - - with col2: - learning_rate_multiplier = st.slider("مضاعف معدل التعلم", 0.1, 2.0, 1.0, 0.1, key="openai_lr") - suffix = st.text_input("لاحقة اسم النموذج", value="arabic-contracts-expert", key="openai_suffix") - - # زر بدء التدريب - if styled_button("بدء التدريب", key="start_openai_training", type="primary", icon="🚀"): - # التحقق من وجود بيانات كافية - examples = st.session_state.terminology_data.get("training_examples", []) - if len(examples) < 10: - st.error("عدد أمثلة التدريب الحالية قليل جداً. يُفضل وجود على الأقل 10 أمثلة للتدريب.") - else: - # التأكيد على بدء التدريب - confirm = st.warning(f"سيتم بدء عملية تدريب نموذج {base_model} باستخدام {len(examples)} مثال. هل أنت متأكد؟") - if styled_button("تأكيد بدء التدريب", key="confirm_openai_training", type="success", icon="✅"): - # توجيه بيانات التدريب لصيغة OpenAI - formatted_data = self._format_training_data_for_openai(examples) - - # بدء التدريب - self._start_openai_training( - base_model=base_model, - training_data=formatted_data, - n_epochs=n_epochs, - batch_size=batch_size, - learning_rate_multiplier=learning_rate_multiplier, - suffix=suffix - ) - - elif provider == "Anthropic": - st.info("ضبط نماذج Anthropic غير متاح حالياً في واجهة البرمجة العامة. يمكنك استخدام أمثلة التدريب مع المساعد المتخصص.") - - def _format_training_data_for_openai(self, examples): - """تنسيق بيانات التدريب لواجهة برمجة OpenAI""" - formatted_examples = [] - - for example in examples: - formatted_examples.append({ - "messages": [ - {"role": "user", "content": example.get("input", "")}, - {"role": "assistant", "content": example.get("output", "")} - ] - }) - - return formatted_examples - - def _start_openai_training(self, base_model, training_data, n_epochs, batch_size, learning_rate_multiplier, suffix): - """بدء عملية تدريب نموذج OpenAI""" - try: - # تهيئة واجهة برمجة OpenAI - openai.api_key = self.api_key - - # إنشاء ملف تدريب - training_file_path = os.path.join(self.training_data_dir, f"training_data_{int(time.time())}.jsonl") - - with open(training_file_path, 'w', encoding='utf-8') as f: - for example in training_data: - f.write(json.dumps(example, ensure_ascii=False) + "\n") - - # رفع ملف التدريب إلى OpenAI - with open(training_file_path, 'rb') as f: - response = openai.files.create( - file=f, - purpose="fine-tune" - ) - - file_id = response.id - - # بدء وظيفة التدريب - response = openai.fine_tuning.jobs.create( - training_file=file_id, - model=base_model, - hyperparameters={ - "n_epochs": n_epochs, - "batch_size": batch_size, - "learning_rate_multiplier": learning_rate_multiplier - }, - suffix=suffix - ) - - job_id = response.id - - # تخزين معلومات وظيفة التدريب - training_job = { - "job_id": job_id, - "provider": "OpenAI", - "base_model": base_model, - "n_epochs": n_epochs, - "batch_size": batch_size, - "learning_rate_multiplier": learning_rate_multiplier, - "suffix": suffix, - "status": "running", - "file_id": file_id, - "file_path": training_file_path, - "examples_count": len(training_data), - "started_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "finished_at": None, - "fine_tuned_model": None - } - - # إضافة الوظيفة إلى حالة الجلسة - st.session_state.active_training_job = training_job - - # إضافة الوظيفة إلى قائمة وظائف التدريب - if "training_jobs" not in st.session_state.terminology_data: - st.session_state.terminology_data["training_jobs"] = [] - - st.session_state.terminology_data["training_jobs"].append(training_job) - - # حفظ البيانات - self._save_terminology_data() - - st.success(f"تم بدء وظيفة التدريب بنجاح! معرف الوظيفة: {job_id}") - st.info("يمكنك متابعة حالة التدريب من تبويب 'وظائف التدريب النشطة'.") - - except Exception as e: - st.error(f"حدث خطأ أثناء بدء عملية التدريب: {str(e)}") - - def _render_previous_models(self): - """عرض النماذج المدربة سابقاً""" - st.markdown("### النماذج المدربة سابقاً") - - # الحصول على النماذج المدربة - models = st.session_state.terminology_data.get("models", []) - - if not models: - st.info("لا توجد نماذج مدربة سابقاً.") - return - - # عرض النماذج - for i, model in enumerate(models): - with st.expander(f"{model.get('model_id')} - {model.get('base_model')}", expanded=i==0): - col1, col2 = st.columns([3, 1]) - - with col1: - st.markdown(f"**معرف النموذج:** {model.get('model_id')}") - st.markdown(f"**النموذج الأساسي:** {model.get('base_model')}") - st.markdown(f"**عدد أمثلة التدريب:** {model.get('examples_count')}") - st.markdown(f"**تاريخ الإنشاء:** {model.get('created_at')}") - - # عرض مؤشرات الأداء إن وجدت - if "metrics" in model: - st.markdown("#### مؤشرات الأداء") - - metrics = model.get("metrics", {}) - for metric_name, metric_value in metrics.items(): - st.markdown(f"**{metric_name}:** {metric_value}") - - with col2: - # أزرار الاستخدام والحذف - if styled_button("استخدام النموذج", key=f"use_model_{i}", type="primary", icon="✅"): - st.session_state.selected_model = model.get('model_id') - st.success(f"تم اختيار النموذج {model.get('model_id')} للاستخدام.") - - if styled_button("حذف النموذج", key=f"delete_model_{i}", type="danger", icon="🗑️"): - st.session_state.model_to_delete = i - - # عرض الوصف والملاحظات - st.markdown(f"**الوصف:** {model.get('description', 'لا يوجد وصف.')}") - - # عرض النماذج المستخدمة في التدريب - if "examples_preview" in model and model["examples_preview"]: - with st.expander("عينة من أمثلة التدريب"): - for j, example in enumerate(model["examples_preview"]): - st.markdown(f"**مثال #{j+1}**") - st.markdown(f"**المدخل:** {example.get('input')}") - st.markdown(f"**المخرج:** {example.get('output')}") - st.markdown("---") - - # معالجة حذف النموذج - if "model_to_delete" in st.session_state: - if st.warning(f"هل أنت متأكد من حذف النموذج '{models[st.session_state.model_to_delete].get('model_id')}'؟"): - if styled_button("تأكيد الحذف", key="confirm_delete_model", type="danger", icon="🗑️"): - # حذف النموذج - del st.session_state.terminology_data["models"][st.session_state.model_to_delete] - - # حفظ البيانات - self._save_terminology_data() - - # إعادة ضبط حالة الحذف - del st.session_state.model_to_delete - - st.success("تم حذف النموذج بنجاح!") - st.rerun() - - if styled_button("إلغاء", key="cancel_delete_model", type="secondary", icon="❌"): - del st.session_state.model_to_delete - st.rerun() - - def _render_active_training_jobs(self): - """عرض وظائف التدريب النشطة""" - st.markdown("### وظائف التدريب النشطة") - - # الحصول على وظائف التدريب - jobs = st.session_state.terminology_data.get("training_jobs", []) - - # فرز الوظائف حسب الحالة - active_jobs = [job for job in jobs if job.get("status") in ["running", "validating_files", "queued"]] - completed_jobs = [job for job in jobs if job.get("status") == "succeeded"] - failed_jobs = [job for job in jobs if job.get("status") in ["failed", "cancelled"]] - - # زر تحديث حالة الوظائف - if styled_button("تحديث حالة الوظائف", key="refresh_jobs", type="primary", icon="🔄"): - self._refresh_training_jobs_status() - - # عرض الوظائف النشطة - if active_jobs: - st.markdown("#### الوظائف النشطة") - - for i, job in enumerate(active_jobs): - with st.expander(f"{job.get('job_id')} - {job.get('base_model')}", expanded=True): - col1, col2 = st.columns([3, 1]) - - with col1: - st.markdown(f"**معرف الوظيفة:** {job.get('job_id')}") - st.markdown(f"**النموذج الأساسي:** {job.get('base_model')}") - st.markdown(f"**الحالة:** {job.get('status')}") - st.markdown(f"**تاريخ البدء:** {job.get('started_at')}") - - # عرض تقدم التدريب إن وجد - if "progress" in job: - progress = job.get("progress", 0) - st.progress(progress) - st.markdown(f"**التقدم:** {progress*100:.1f}%") - - with col2: - # زر إلغاء الوظيفة - if styled_button("إلغاء الوظيفة", key=f"cancel_job_{i}", type="danger", icon="⛔"): - st.session_state.job_to_cancel = i - - # عرض معلومات إضافية - st.markdown(f"**عدد الحقب:** {job.get('n_epochs')}") - st.markdown(f"**حجم الدفعة:** {job.get('batch_size')}") - st.markdown(f"**مضاعف معدل التعلم:** {job.get('learning_rate_multiplier')}") - else: - st.info("لا توجد وظائف تدريب نشطة حالياً.") - - # عرض الوظائف المكتملة - if completed_jobs: - st.markdown("#### الوظائف المكتملة") - - for i, job in enumerate(completed_jobs): - with st.expander(f"{job.get('job_id')} - {job.get('base_model')}", expanded=False): - col1, col2 = st.columns([3, 1]) - - with col1: - st.markdown(f"**معرف الوظيفة:** {job.get('job_id')}") - st.markdown(f"**النموذج الأساسي:** {job.get('base_model')}") - st.markdown(f"**تاريخ البدء:** {job.get('started_at')}") - st.markdown(f"**تاريخ الانتهاء:** {job.get('finished_at')}") - st.markdown(f"**النموذج المدرب:** {job.get('fine_tuned_model')}") - - with col2: - # زر استخدام النموذج المدرب - if styled_button("استخدام النموذج", key=f"use_trained_model_{i}", type="primary", icon="✅"): - st.session_state.selected_model = job.get('fine_tuned_model') - st.success(f"تم اختيار النموذج {job.get('fine_tuned_model')} للاستخدام.") - - # زر حذف الوظيفة - if styled_button("حذف الوظيفة", key=f"delete_completed_job_{i}", type="danger", icon="🗑️"): - st.session_state.completed_job_to_delete = len(active_jobs) + i - - # عرض الوظائف الفاشلة - if failed_jobs: - st.markdown("#### الوظائف الفاشلة") - - for i, job in enumerate(failed_jobs): - with st.expander(f"{job.get('job_id')} - {job.get('base_model')}", expanded=False): - col1, col2 = st.columns([3, 1]) - - with col1: - st.markdown(f"**معرف الوظيفة:** {job.get('job_id')}") - st.markdown(f"**النموذج الأساسي:** {job.get('base_model')}") - st.markdown(f"**الحالة:** {job.get('status')}") - st.markdown(f"**تاريخ البدء:** {job.get('started_at')}") - - # عرض سبب الفشل إن وجد - if "error" in job: - st.error(f"سبب الفشل: {job.get('error')}") - - with col2: - # زر حذف الوظيفة - if styled_button("حذف الوظيفة", key=f"delete_failed_job_{i}", type="danger", icon="🗑️"): - st.session_state.failed_job_to_delete = len(active_jobs) + len(completed_jobs) + i - - # معالجة إلغاء الوظيفة - if "job_to_cancel" in st.session_state: - if st.warning(f"هل أنت متأكد من إلغاء وظيفة التدريب '{active_jobs[st.session_state.job_to_cancel].get('job_id')}'؟"): - if styled_button("تأكيد الإلغاء", key="confirm_cancel_job", type="danger", icon="🗑️"): - # إلغاء الوظيفة - self._cancel_training_job(active_jobs[st.session_state.job_to_cancel]) - - # إعادة ضبط حالة الإلغاء - del st.session_state.job_to_cancel - - st.success("تم إلغاء وظيفة التدريب بنجاح!") - st.rerun() - - if styled_button("إلغاء", key="cancel_job_cancellation", type="secondary", icon="❌"): - del st.session_state.job_to_cancel - st.rerun() - - # معالجة حذف الوظائف المكتملة - if "completed_job_to_delete" in st.session_state: - idx = st.session_state.completed_job_to_delete - if 0 <= idx < len(jobs): - if st.warning(f"هل أنت متأكد من حذف وظيفة التدريب '{jobs[idx].get('job_id')}'؟"): - if styled_button("تأكيد الحذف", key="confirm_delete_completed_job", type="danger", icon="🗑️"): - # حذف الوظيفة - del st.session_state.terminology_data["training_jobs"][idx] - - # حفظ البيانات - self._save_terminology_data() - - # إعادة ضبط حالة الحذف - del st.session_state.completed_job_to_delete - - st.success("تم حذف وظيفة التدريب بنجاح!") - st.rerun() - - if styled_button("إلغاء", key="cancel_completed_job_deletion", type="secondary", icon="❌"): - del st.session_state.completed_job_to_delete - st.rerun() - - # معالجة حذف الوظائف الفاشلة - if "failed_job_to_delete" in st.session_state: - idx = st.session_state.failed_job_to_delete - if 0 <= idx < len(jobs): - if st.warning(f"هل أنت متأكد من حذف وظيفة التدريب '{jobs[idx].get('job_id')}'؟"): - if styled_button("تأكيد الحذف", key="confirm_delete_failed_job", type="danger", icon="🗑️"): - # حذف الوظيفة - del st.session_state.terminology_data["training_jobs"][idx] - - # حفظ البيانات - self._save_terminology_data() - - # إعادة ضبط حالة الحذف - del st.session_state.failed_job_to_delete - - st.success("تم حذف وظيفة التدريب بنجاح!") - st.rerun() - - if styled_button("إلغاء", key="cancel_failed_job_deletion", type="secondary", icon="❌"): - del st.session_state.failed_job_to_delete - st.rerun() - - def _refresh_training_jobs_status(self): - """تحديث حالة وظائف التدريب""" - jobs = st.session_state.terminology_data.get("training_jobs", []) - - # فلترة الوظائف النشطة - active_jobs = [job for job in jobs if job.get("status") in ["running", "validating_files", "queued"]] - - if not active_jobs: - st.info("لا توجد وظائف تدريب نشطة للتحديث.") - return - - try: - # تحديث حالة كل وظيفة نشطة - for job in active_jobs: - if job.get("provider") == "OpenAI" and self.api_key: - # تحديث حالة وظيفة OpenAI - job_id = job.get("job_id") - - # استعلام عن حالة الوظيفة - openai.api_key = self.api_key - response = openai.fine_tuning.jobs.retrieve(job_id) - - # تحديث حالة الوظيفة - job["status"] = response.status - - # تحديث التقدم إن وجد - if hasattr(response, "progress") and response.progress is not None: - job["progress"] = response.progress - - # إذا اكتملت الوظيفة، تحديث معلومات النموذج المدرب - if response.status == "succeeded": - job["finished_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - job["fine_tuned_model"] = response.fine_tuned_model - - # إضافة النموذج المدرب إلى قائمة النماذج - self._add_trained_model(job, response) - - # إذا فشلت الوظيفة، تسجيل سبب الفشل - elif response.status == "failed" and hasattr(response, "error"): - job["error"] = response.error - - # حفظ البيانات - self._save_terminology_data() - - st.success("تم تحديث حالة وظائف التدريب بنجاح!") - - except Exception as e: - st.error(f"حدث خطأ أثناء تحديث حالة وظائف التدريب: {str(e)}") - - def _cancel_training_job(self, job): - """إلغاء وظيفة تدريب""" - try: - if job.get("provider") == "OpenAI" and self.api_key: - # إلغاء وظيفة OpenAI - job_id = job.get("job_id") - - # استدعاء واجهة برمجة OpenAI - openai.api_key = self.api_key - openai.fine_tuning.jobs.cancel(job_id) - - # تحديث حالة الوظيفة - job["status"] = "cancelled" - job["finished_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - # حفظ البيانات - self._save_terminology_data() - - return True - - return False - - except Exception as e: - st.error(f"حدث خطأ أثناء إلغاء وظيفة التدريب: {str(e)}") - return False - - def _add_trained_model(self, job, response): - """إضافة النموذج المدرب إلى قائمة النماذج""" - # إنشاء كائن النموذج - model = { - "model_id": response.fine_tuned_model, - "base_model": job.get("base_model"), - "provider": job.get("provider"), - "training_job_id": job.get("job_id"), - "description": f"النموذج المدرب على المصطلحات المتخصصة في {job.get('suffix')}", - "examples_count": job.get("examples_count"), - "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "metrics": {} - } - - # إضافة مؤشرات الأداء إن وجدت - if hasattr(response, "result_files") and response.result_files: - # تنزيل ملف النتائج وقراءة مؤشرات الأداء - pass - - # إضافة عينة من أمثلة التدريب - examples = st.session_state.terminology_data.get("training_examples", []) - if examples: - # أخذ 5 أمثلة كعينة - sample_examples = random.sample(examples, min(5, len(examples))) - model["examples_preview"] = sample_examples - - # إضافة النموذج إلى قائمة النماذج - if "models" not in st.session_state.terminology_data: - st.session_state.terminology_data["models"] = [] - - st.session_state.terminology_data["models"].append(model) - - def _render_model_testing(self): - """عرض واجهة اختبار النموذج""" - st.markdown(""" -
-

🧪 اختبار النموذج

-

اختبر نموذج الذكاء الاصطناعي المدرب على المصطلحات المتخصصة.

-

يمكنك تجريب أسئلة مختلفة ومقارنة النتائج مع النماذج الأخرى.

-
- """, unsafe_allow_html=True) - - # التحقق من وجود مفاتيح API - if not self.api_key and not self.anthropic_api_key: - st.warning("لم يتم العثور على مفاتيح API للذكاء الاصطناعي. يرجى إضافة OPENAI_API_KEY أو ANTHROPIC_API_KEY إلى المتغيرات البيئية.") - return - - # الحصول على قائمة النماذج المتاحة - available_models = [] - - # OpenAI - if self.api_key: - available_models.extend(["gpt-4o", "gpt-3.5-turbo"]) - - # إضافة النماذج المدربة إن وجدت - for model in st.session_state.terminology_data.get("models", []): - if model.get("provider") == "OpenAI": - available_models.append(model.get("model_id")) - - # Anthropic - if self.anthropic_api_key: - available_models.extend(["claude-3-7-sonnet-20250219"]) - - # اختيار النموذج - selected_model = st.selectbox( - "اختر النموذج", - options=available_models, - key="test_model" - ) - - # إدخال النص للاختبار - test_input = st.text_area( - "أدخل نص الاختبار", - value="ما هو مفهوم المحتوى المحلي في المشاريع الحكومية وكيف يتم حسابه؟", - height=150, - key="test_input" - ) - - # خيارات متقدمة - with st.expander("خيارات متقدمة"): - temperature = st.slider("درجة الإبداعية (Temperature)", 0.0, 1.0, 0.7, 0.1, key="test_temperature") - max_tokens = st.slider("الحد الأقصى للرموز (Max Tokens)", 100, 2000, 500, 100, key="test_max_tokens") - - # تحميل المصطلحات والتعريفات - terms_with_definitions = {} - for term in st.session_state.terminology_data.get("terms", []): - terms_with_definitions[term.get("term")] = term.get("definition") - - # زر إجراء الاختبار - if styled_button("إجراء الاختبار", key="run_test", type="primary", icon="🧪"): - if not test_input: - st.error("يرجى إدخال نص للاختبار.") - else: - # عرض شريط التقدم - with st.spinner("جاري معالجة النص..."): - # إجراء الاختبار - response = self._test_model( - model=selected_model, - input_text=test_input, - temperature=temperature, - max_tokens=max_tokens, - terms_with_definitions=terms_with_definitions - ) - - # عرض النتيجة - st.markdown("### نتيجة الاختبار") - st.markdown(response) - - # تحليل الاستجابة لاكتشاف المصطلحات المستخدمة - used_terms = [] - for term in terms_with_definitions: - if term in response: - used_terms.append(term) - - if used_terms: - st.markdown("### المصطلحات المكتشفة في الاستجابة") - for term in used_terms: - st.markdown(f"- **{term}**: {terms_with_definitions[term]}") - - def _test_model(self, model, input_text, temperature, max_tokens, terms_with_definitions): - """اختبار النموذج""" - try: - # تجهيز المحتوى النظامي - system_prompt = "أنت مساعد متخصص في عقود المقاولات والمناقصات باللغة العربية. قم بالإجابة بدقة على الأسئلة والطلبات مع مراعاة المصطلحات الفنية المتخصصة." - - # إضافة المصطلحات إلى المحتوى النظامي - if terms_with_definitions: - system_prompt += "\n\nفيما يلي قائمة بالمصطلحات المتخصصة وتعريفاتها:\n\n" - for term, definition in terms_with_definitions.items(): - system_prompt += f"- {term}: {definition}\n" - - # OpenAI - if "gpt" in model or any(model_data.get("model_id") == model for model_data in st.session_state.terminology_data.get("models", [])): - # استخدام OpenAI API - openai.api_key = self.api_key - - response = openai.chat.completions.create( - model=model, - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": input_text} - ], - temperature=temperature, - max_tokens=max_tokens - ) - - return response.choices[0].message.content - - # Anthropic - elif "claude" in model and self.anthropic_api_key: - # استخدام Anthropic API - from anthropic import Anthropic - - anthropic_client = Anthropic(api_key=self.anthropic_api_key) - - response = anthropic_client.messages.create( - model=model, - max_tokens=max_tokens, - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": input_text} - ], - temperature=temperature - ) - - return response.content[0].text - - else: - return "النموذج المختار غير مدعوم حالياً." - - except Exception as e: - return f"حدث خطأ أثناء اختبار النموذج: {str(e)}" - - def _render_specialized_assistant(self): - """عرض واجهة المساعد المتخصص""" - st.markdown(""" -
-

🤖 المساعد المتخصص

-

استخدم المساعد الذكي المتخصص في المصطلحات التعاقدية الهندسية.

-

يمكنك طرح أسئلة حول المصطلحات وتفسيراتها واستخداماتها.

-
- """, unsafe_allow_html=True) - - # التحقق من وجود مفاتيح API - if not self.api_key and not self.anthropic_api_key: - st.warning("لم يتم العثور على مفاتيح API للذكاء الاصطناعي. يرجى إضافة OPENAI_API_KEY أو ANTHROPIC_API_KEY إلى المتغيرات البيئية.") - return - - # الحصول على قائمة النماذج المتاحة - available_models = [] - - # OpenAI - if self.api_key: - available_models.extend(["gpt-4o", "gpt-3.5-turbo"]) - - # إضافة النماذج المدربة إن وجدت - for model in st.session_state.terminology_data.get("models", []): - if model.get("provider") == "OpenAI": - available_models.append(model.get("model_id")) - - # Anthropic - if self.anthropic_api_key: - available_models.extend(["claude-3-7-sonnet-20250219"]) - - # تهيئة حالة المحادثة - if "chat_history" not in st.session_state: - st.session_state.chat_history = [] - - if "assistant_model" not in st.session_state: - st.session_state.assistant_model = available_models[0] if available_models else "" - - # اختيار النموذج - selected_model = st.selectbox( - "اختر نموذج المساعد", - options=available_models, - index=available_models.index(st.session_state.assistant_model) if st.session_state.assistant_model in available_models else 0, - key="assistant_model_selector" - ) - - # تحديث النموذج المختار - if selected_model != st.session_state.assistant_model: - st.session_state.assistant_model = selected_model - st.rerun() - - # عرض المحادثة - st.markdown("### المحادثة") - - for message in st.session_state.chat_history: - if message["role"] == "user": - st.markdown(f""" -
- أنت: {message["content"]} -
- """, unsafe_allow_html=True) - else: - st.markdown(f""" -
- المساعد: {message["content"]} -
- """, unsafe_allow_html=True) - - # إدخال رسالة جديدة - user_input = st.text_area("اكتب رسالتك هنا", key="assistant_input", height=100) - - # أزرار التحكم - col1, col2, col3 = st.columns([1, 1, 1]) - - with col1: - if styled_button("إرسال", key="send_message", type="primary", icon="✉️"): - if not user_input: - st.error("يرجى كتابة رسالة للإرسال.") - else: - # إضافة رسالة المستخدم إلى المحادثة - st.session_state.chat_history.append({ - "role": "user", - "content": user_input - }) - - # الحصول على الرد من النموذج - with st.spinner("المساعد يفكر..."): - # تحميل المصطلحات والتعريفات - terms_with_definitions = {} - for term in st.session_state.terminology_data.get("terms", []): - terms_with_definitions[term.get("term")] = term.get("definition") - - # الحصول على رد المساعد - response = self._get_assistant_response( - model=st.session_state.assistant_model, - chat_history=st.session_state.chat_history, - terms_with_definitions=terms_with_definitions - ) - - # إضافة رد المساعد إلى المحادثة - st.session_state.chat_history.append({ - "role": "assistant", - "content": response - }) - - # إعادة تشغيل لتحديث واجهة المستخدم - st.rerun() - - with col2: - if styled_button("مسح المحادثة", key="clear_chat", type="danger", icon="🗑️"): - st.session_state.chat_history = [] - st.rerun() - - with col3: - if styled_button("اقتراح أسئلة", key="suggest_questions", type="secondary", icon="💡"): - # عرض أسئلة مقترحة - st.markdown("### أسئلة مقترحة") - - # الحصول على المصطلحات المتاحة - terms = [term.get("term") for term in st.session_state.terminology_data.get("terms", [])] - - # إنشاء أسئلة مقترحة - suggested_questions = [ - f"ما هو تعريف مصطلح {term}؟" for term in random.sample(terms, min(3, len(terms))) - ] - - suggested_questions.extend([ - "كيف يتم حساب المحتوى المحلي في مشاريع البنية التحتية؟", - "ما هي أهم البنود التي يجب الانتباه لها في العقود الهندسية؟", - "ما الفرق بين الضمان الابتدائي والضمان النهائي؟", - "كيف يمكن تقييم المخاطر في مشاريع البناء؟" - ]) - - # عرض الأسئلة المقترحة - for i, question in enumerate(suggested_questions): - if styled_button(question, key=f"suggested_q_{i}", type="info", icon="❓"): - # إضافة السؤال المقترح إلى المحادثة - st.session_state.chat_history.append({ - "role": "user", - "content": question - }) - - # الحصول على الرد من النموذج - with st.spinner("المساعد يفكر..."): - # تحميل المصطلحات والتعريفات - terms_with_definitions = {} - for term in st.session_state.terminology_data.get("terms", []): - terms_with_definitions[term.get("term")] = term.get("definition") - - # الحصول على رد المساعد - response = self._get_assistant_response( - model=st.session_state.assistant_model, - chat_history=st.session_state.chat_history, - terms_with_definitions=terms_with_definitions - ) - - # إضافة رد المساعد إلى المحادثة - st.session_state.chat_history.append({ - "role": "assistant", - "content": response - }) - - # إعادة تشغيل لتحديث واجهة المستخدم - st.rerun() - - def _get_assistant_response(self, model, chat_history, terms_with_definitions): - """الحصول على رد المساعد المتخصص""" - try: - # تجهيز المحتوى النظامي - system_prompt = """أنت مساعد متخصص في عقود المقاولات والمناقصات باللغة العربية. - مهمتك هي تقديم معلومات دقيقة ومفصلة حول المصطلحات التعاقدية والهندسية المتخصصة. - قدم شرحاً واضحاً ومفهوماً للمصطلحات، مع أمثلة عملية عند الإمكان. - استخدم لغة مهنية ودقيقة، مع مراعاة السياق الهندسي والقانوني للمصطلحات.""" - - # إضافة المصطلحات إلى المحتوى النظامي - if terms_with_definitions: - system_prompt += "\n\nفيما يلي قائمة بالمصطلحات المتخصصة وتعريفاتها التي يجب عليك استخدامها في إجاباتك:\n\n" - for term, definition in terms_with_definitions.items(): - system_prompt += f"- {term}: {definition}\n" - - # تحويل محادثة streamlit إلى صيغة مناسبة للـ API - messages = [{"role": "system", "content": system_prompt}] - - for msg in chat_history: - messages.append({"role": msg["role"], "content": msg["content"]}) - - # OpenAI - if "gpt" in model or any(model_data.get("model_id") == model for model_data in st.session_state.terminology_data.get("models", [])): - # استخدام OpenAI API - openai.api_key = self.api_key - - response = openai.chat.completions.create( - model=model, - messages=messages, - temperature=0.7 - ) - - return response.choices[0].message.content - - # Anthropic - elif "claude" in model and self.anthropic_api_key: - # استخدام Anthropic API - from anthropic import Anthropic - - # تعديل الرسائل لتتناسب مع صيغة Anthropic - anthropic_messages = [] - for msg in messages[1:]: # تخطي رسالة النظام - anthropic_messages.append({"role": msg["role"], "content": msg["content"]}) - - anthropic_client = Anthropic(api_key=self.anthropic_api_key) - - response = anthropic_client.messages.create( - model=model, - max_tokens=2000, - system=system_prompt, - messages=anthropic_messages, - temperature=0.7 - ) - - return response.content[0].text - - else: - return "النموذج المختار غير مدعوم حالياً." - - except Exception as e: - return f"عذراً، حدث خطأ أثناء معالجة طلبك: {str(e)}" - - def _save_terminology_data(self): - """حفظ بيانات المصطلحات""" - try: - # التأكد من وجود المجلد - os.makedirs(os.path.dirname(self.terminology_file), exist_ok=True) - - # حفظ البيانات - with open(self.terminology_file, 'w', encoding='utf-8') as f: - json.dump(st.session_state.terminology_data, f, ensure_ascii=False, indent=2) - - except Exception as e: - st.error(f"حدث خطأ أثناء حفظ البيانات: {str(e)}") - - -# تشغيل النموذج بشكل مستقل -def main(): - """تشغيل وحدة تخصيص وضبط نماذج الذكاء الاصطناعي بشكل مستقل""" - # تهيئة الواجهة - st.set_page_config( - page_title="تخصيص وضبط نماذج الذكاء الاصطناعي | WAHBi AI", - page_icon="🧠", - layout="wide", - initial_sidebar_state="expanded", - menu_items={ - 'Get Help': 'mailto:support@wahbi-ai.com', - 'Report a bug': 'mailto:support@wahbi-ai.com', - 'About': 'وحدة تخصيص وضبط نماذج الذكاء الاصطناعي للمصطلحات التعاقدية المتخصصة - جزء من نظام WAHBi AI لتحليل المناقصات' - } - ) - - # تهيئة وحدة الضبط - model_finetuning = ModelFinetuning() - - # عرض واجهة الوحدة - model_finetuning.render() - -# تشغيل النموذج عند استدعاء الملف مباشرة -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/modules/data_analysis/data_analysis_app.py b/modules/data_analysis/data_analysis_app.py deleted file mode 100644 index 5a2b96f949e1edf728f7ca63a1da3610d63cf575..0000000000000000000000000000000000000000 --- a/modules/data_analysis/data_analysis_app.py +++ /dev/null @@ -1,1022 +0,0 @@ -""" -وحدة تحليل البيانات - التطبيق الرئيسي -""" - -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 DataAnalysisApp: - """وحدة تحليل البيانات""" - - def __init__(self): - """تهيئة وحدة تحليل البيانات""" - - # تهيئة حالة الجلسة - if 'uploaded_data' not in st.session_state: - st.session_state.uploaded_data = None - - if 'data_sources' not in st.session_state: - st.session_state.data_sources = [ - { - 'id': 1, - 'name': 'بيانات المناقصات السابقة', - 'type': 'CSV', - 'rows': 250, - 'columns': 15, - 'last_updated': '2024-03-01', - 'description': 'بيانات المناقصات السابقة للشركة خلال الثلاث سنوات الماضية' - }, - { - 'id': 2, - 'name': 'بيانات المنافسين', - 'type': 'Excel', - 'rows': 120, - 'columns': 10, - 'last_updated': '2024-02-15', - 'description': 'بيانات المنافسين الرئيسيين في السوق وأسعارهم التنافسية' - }, - { - 'id': 3, - 'name': 'بيانات أسعار المواد', - 'type': 'CSV', - 'rows': 500, - 'columns': 8, - 'last_updated': '2024-03-10', - 'description': 'بيانات أسعار المواد الرئيسية المستخدمة في المشاريع' - }, - { - 'id': 4, - 'name': 'بيانات الموردين', - 'type': 'Excel', - 'rows': 80, - 'columns': 12, - 'last_updated': '2024-02-20', - 'description': 'بيانات الموردين الرئيسيين وأسعارهم وجودة منتجاتهم' - }, - { - 'id': 5, - 'name': 'بيانات المشاريع المنجزة', - 'type': 'CSV', - 'rows': 150, - 'columns': 20, - 'last_updated': '2024-03-15', - 'description': 'بيانات المشاريع المنجزة وتكاليفها الفعلية ومدة تنفيذها' - } - ] - - if 'sample_data' not in st.session_state: - # إنشاء بيانات افتراضية للمناقصات السابقة - 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 - } - - st.session_state.sample_data = { - "tenders": pd.DataFrame(tenders_data) - } - - # إنشاء بيانات أسعار المواد - n_materials = 30 - material_ids = [f"M-{i+1:03d}" for i in range(n_materials)] - material_names = [ - "خرسانة جاهزة", "حديد تسليح", "طابوق", "أسمنت", "رمل", "بحص", "خشب", "ألمنيوم", "زجاج", "دهان", - "سيراميك", "رخام", "جبس", "عازل مائي", "عازل حراري", "أنابيب PVC", "أسلاك كهربائية", "مفاتيح كهربائية", - "إنارة", "تكييف", "مصاعد", "أبواب خشبية", "أبواب حديدية", "نوافذ ألمنيوم", "نوافذ زجاجية", - "أرضيات خشبية", "أرضيات بلاط", "أرضيات رخام", "أرضيات سيراميك", "أرضيات بورسلين" - ] - material_units = np.random.choice(["م3", "طن", "م2", "كجم", "لتر", "قطعة", "متر"], n_materials) - material_prices_2021 = np.random.randint(50, 5000, n_materials) - material_prices_2022 = np.array([price * np.random.uniform(1.0, 1.2) for price in material_prices_2021]) - material_prices_2023 = np.array([price * np.random.uniform(1.0, 1.15) for price in material_prices_2022]) - material_prices_2024 = np.array([price * np.random.uniform(0.95, 1.1) for price in material_prices_2023]) - - # إنشاء DataFrame لأسعار المواد - materials_data = { - "رمز المادة": material_ids, - "اسم المادة": material_names, - "الوحدة": material_units, - "سعر 2021 (ريال)": material_prices_2021, - "سعر 2022 (ريال)": material_prices_2022, - "سعر 2023 (ريال)": material_prices_2023, - "سعر 2024 (ريال)": material_prices_2024, - "نسبة التغير 2021-2024 (%)": (material_prices_2024 - material_prices_2021) / material_prices_2021 * 100 - } - - st.session_state.sample_data["materials"] = pd.DataFrame(materials_data) - - # إنشاء بيانات المنافسين - 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 - } - - st.session_state.sample_data["competitors"] = pd.DataFrame(competitors_data) - - def render(self): - """عرض واجهة وحدة تحليل البيانات""" - - st.markdown("

وحدة تحليل البيانات

", unsafe_allow_html=True) - - tabs = st.tabs([ - "لوحة المعلومات", - "تحليل المناقصات", - "تحليل الأسعار", - "تحليل المنافسين", - "استيراد وتصدير البيانات" - ]) - - with tabs[0]: - self._render_dashboard_tab() - - with tabs[1]: - self._render_tenders_analysis_tab() - - with tabs[2]: - self._render_price_analysis_tab() - - with tabs[3]: - self._render_competitors_analysis_tab() - - with tabs[4]: - self._render_import_export_tab() - - 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="الميزانية = التكلفة" - ) - ) - - st.plotly_chart(fig, use_container_width=True) - - def _render_tenders_analysis_tab(self): - """عرض تبويب تحليل المناقصات""" - - st.markdown("### تحليل المناقصات") - - # استخراج البيانات - tenders_df = st.session_state.sample_data["tenders"] - - # عرض خيارات التصفية - st.markdown("#### خيارات التصفية") - - col1, col2, col3 = st.columns(3) - - with col1: - selected_status = st.multiselect( - "الحالة", - options=tenders_df["الحالة"].unique(), - default=tenders_df["الحالة"].unique() - ) - - with col2: - selected_types = st.multiselect( - "نوع المشروع", - options=tenders_df["نوع المشروع"].unique(), - default=tenders_df["نوع المشروع"].unique() - ) - - with col3: - selected_locations = st.multiselect( - "الموقع", - options=tenders_df["الموقع"].unique(), - default=tenders_df["الموقع"].unique() - ) - - # تطبيق التصفية - filtered_df = tenders_df[ - tenders_df["الحالة"].isin(selected_status) & - tenders_df["نوع المشروع"].isin(selected_types) & - tenders_df["الموقع"].isin(selected_locations) - ] - - # عرض البيانات المصفاة - st.markdown("#### البيانات المصفاة") - - st.dataframe(filtered_df, use_container_width=True, hide_index=True) - - # عرض إحصائيات البيانات المصفاة - st.markdown("#### إحصائيات البيانات المصفاة") - - col1, col2, col3, col4 = st.columns(4) - - with col1: - st.metric("عدد المناقصات", f"{len(filtered_df)}") - - with col2: - won_count = len(filtered_df[filtered_df["الحالة"] == "فائز"]) - win_rate = won_count / len(filtered_df) * 100 if len(filtered_df) > 0 else 0 - st.metric("معدل الفوز", f"{win_rate:.1f}%") - - with col3: - avg_profit_margin = filtered_df["هامش الربح (%)"].mean() - st.metric("متوسط هامش الربح", f"{avg_profit_margin:.1f}%") - - with col4: - total_profit = filtered_df["الربح (ريال)"].sum() - st.metric("إجمالي الربح", f"{total_profit:,.0f} ريال") - - # عرض تحليل هامش الربح حسب نوع المشروع - st.markdown("#### تحليل هامش الربح حسب نوع المشروع") - - profit_margin_by_type = filtered_df.groupby("نوع المشروع")["هامش الربح (%)"].mean().reset_index() - - fig = px.bar( - profit_margin_by_type, - x="نوع المشروع", - y="هامش الربح (%)", - title="متوسط هامش الربح حسب نوع المشروع", - color="نوع المشروع", - text_auto=".1f" - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض تحليل هامش الربح حسب الموقع - st.markdown("#### تحليل هامش الربح حسب الموقع") - - profit_margin_by_location = filtered_df.groupby("الموقع")["هامش الربح (%)"].mean().reset_index() - - fig = px.bar( - profit_margin_by_location, - x="الموقع", - y="هامش الربح (%)", - title="متوسط هامش الربح حسب الموقع", - color="الموقع", - text_auto=".1f" - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض تحليل معدل الفوز حسب نوع المشروع - st.markdown("#### تحليل معدل الفوز حسب نوع المشروع") - - # حساب معدل الفوز لكل نوع مشروع - win_rate_by_type = [] - - for project_type in filtered_df["نوع المشروع"].unique(): - type_df = filtered_df[filtered_df["نوع المشروع"] == project_type] - won_count = len(type_df[type_df["الحالة"] == "فائز"]) - total_count = len(type_df) - win_rate = won_count / total_count * 100 if total_count > 0 else 0 - win_rate_by_type.append({ - "نوع المشروع": project_type, - "معدل الفوز (%)": win_rate, - "عدد المناقصات": total_count - }) - - win_rate_by_type_df = pd.DataFrame(win_rate_by_type) - - fig = px.bar( - win_rate_by_type_df, - x="نوع المشروع", - y="معدل الفوز (%)", - title="معدل الفوز حسب نوع المشروع", - color="نوع المشروع", - text_auto=".1f", - hover_data=["عدد المناقصات"] - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض تحليل العلاقة بين حجم المشروع وهامش الربح - st.markdown("#### العلاقة بين حجم المشروع وهامش الربح") - - fig = px.scatter( - filtered_df, - x="الميزانية (ريال)", - y="هامش الربح (%)", - color="الحالة", - size="المساحة (م2)", - hover_name="رقم المناقصة", - hover_data=["نوع المشروع", "الموقع", "المدة (شهر)"], - title="العلاقة بين حجم المشروع وهامش الربح", - color_discrete_map={ - "فائز": "#2ecc71", - "خاسر": "#e74c3c", - "قيد التنفيذ": "#3498db", - "منجز": "#f39c12" - } - ) - - # إضافة خط الاتجاه - fig.update_layout( - shapes=[ - dict( - type="line", - xref="x", - yref="y", - x0=filtered_df["الميزانية (ريال)"].min(), - y0=filtered_df["هامش الربح (%)"].mean(), - x1=filtered_df["الميزانية (ريال)"].max(), - y1=filtered_df["هامش الربح (%)"].mean(), - line=dict(color="gray", dash="dash") - ) - ] - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض تحليل العلاقة بين مدة المشروع وهامش الربح - st.markdown("#### العلاقة بين مدة المشروع وهامش الربح") - - fig = px.scatter( - filtered_df, - x="المدة (شهر)", - y="هامش الربح (%)", - color="الحالة", - size="الميزانية (ريال)", - hover_name="رقم المناقصة", - hover_data=["نوع المشروع", "الموقع", "المساحة (م2)"], - title="العلاقة بين مدة المشروع وهامش الربح", - color_discrete_map={ - "فائز": "#2ecc71", - "خاسر": "#e74c3c", - "قيد التنفيذ": "#3498db", - "منجز": "#f39c12" - } - ) - - st.plotly_chart(fig, use_container_width=True) - - def _render_price_analysis_tab(self): - """عرض تبويب تحليل الأسعار""" - - st.markdown("### تحليل الأسعار") - - # استخراج البيانات - materials_df = st.session_state.sample_data["materials"] - - # عرض بيانات أسعار المواد - st.markdown("#### بيانات أسعار المواد") - - st.dataframe(materials_df, use_container_width=True, hide_index=True) - - # عرض تطور أسعار المواد عبر السنوات - st.markdown("#### تطور أسعار المواد عبر السنوات") - - # اختيار المواد للعرض - selected_materials = st.multiselect( - "اختر المواد للعرض", - options=materials_df["اسم المادة"].unique(), - default=materials_df["اسم المادة"].unique()[:5] - ) - - if selected_materials: - # تحضير البيانات للرسم البياني - filtered_materials = materials_df[materials_df["اسم المادة"].isin(selected_materials)] - - # تحويل البيانات من العرض العريض إلى العرض الطويل - melted_df = pd.melt( - filtered_materials, - id_vars=["رمز المادة", "اسم المادة", "الوحدة"], - value_vars=["سعر 2021 (ريال)", "سعر 2022 (ريال)", "سعر 2023 (ريال)", "سعر 2024 (ريال)"], - var_name="السنة", - value_name="السعر (ريال)" - ) - - # استخراج السنة من اسم العمود - melted_df["السنة"] = melted_df["السنة"].str.extract(r"سعر (\d{4})") - - # رسم بياني لتطور الأسعار - fig = px.line( - melted_df, - x="السنة", - y="السعر (ريال)", - color="اسم المادة", - title="تطور أسعار المواد عبر السنوات", - markers=True, - hover_data=["الوحدة"] - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض نسبة التغير في أسعار المواد - st.markdown("#### نسبة التغير في أسعار المواد (2021-2024)") - - # ترتيب المواد حسب نسبة التغير - sorted_materials = materials_df.sort_values("نسبة التغير 2021-2024 (%)", ascending=False) - - fig = px.bar( - sorted_materials, - x="اسم المادة", - y="نسبة التغير 2021-2024 (%)", - title="نسبة التغير في أسعار المواد (2021-2024)", - color="نسبة التغير 2021-2024 (%)", - color_continuous_scale="RdYlGn_r", - text_auto=".1f" - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض توزيع أسعار المواد حسب الفئة - st.markdown("#### توزيع أسعار المواد حسب الفئة") - - # تصنيف المواد إلى فئات - materials_df["فئة المادة"] = materials_df["اسم المادة"].apply( - lambda x: "مواد إنشائية" if x in ["خرسانة جاهزة", "حديد تسليح", "طابوق", "أسمنت", "رمل", "بحص", "خشب"] - else "مواد تشطيب" if x in ["ألمنيوم", "زجاج", "دهان", "سيراميك", "رخام", "جبس", "عازل مائي", "عازل حراري"] - else "مواد كهربائية" if x in ["أنابيب PVC", "أسلاك كهربائية", "مفاتيح كهربائية", "إنارة"] - else "مواد ميكانيكية" if x in ["تكييف", "مصاعد"] - else "أبواب ونوافذ" if x in ["أبواب خشبية", "أبواب حديدية", "نوافذ ألمنيوم", "نوافذ زجاجية"] - else "أرضيات" if x in ["أرضيات خشبية", "أرضيات بلاط", "أرضيات رخام", "أرضيات سيراميك", "أرضيات بورسلين"] - else "أخرى" - ) - - # حساب متوسط نسبة التغير لكل فئة - category_change = materials_df.groupby("فئة المادة")["نسبة التغير 2021-2024 (%)"].mean().reset_index() - - fig = px.bar( - category_change, - x="فئة المادة", - y="نسبة التغير 2021-2024 (%)", - title="متوسط نسبة التغير في أسعار المواد حسب الفئة (2021-2024)", - color="فئة المادة", - text_auto=".1f" - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض تحليل تأثير تغير الأسعار على تكاليف المشاريع - st.markdown("#### تحليل تأثير تغير الأسعار على تكاليف المشاريع") - - # إنشاء بيانات افتراضية لتوزيع التكاليف - cost_distribution = { - "الفئة": [ - "مواد إنشائية", - "مواد تشطيب", - "مواد كهربائية", - "مواد ميكانيكية", - "أبواب ونوافذ", - "أرضيات", - "عمالة", - "معدات", - "نفقات عامة" - ], - "النسبة من التكلفة (%)": [30, 20, 10, 15, 5, 5, 10, 3, 2] - } - - cost_distribution_df = pd.DataFrame(cost_distribution) - - # حساب تأثير تغير الأسعار على التكاليف - impact_data = [] - - for index, row in cost_distribution_df.iterrows(): - category = row["الفئة"] - cost_percentage = row["النسبة من التكلفة (%)"] - - if category in category_change["فئة المادة"].values: - price_change = category_change[category_change["فئة المادة"] == category]["نسبة التغير 2021-2024 (%)"].values[0] - else: - # افتراض نسبة تغير للفئات غير المدرجة - price_change = 10 if category == "عمالة" else 5 - - impact = cost_percentage * price_change / 100 - - impact_data.append({ - "الفئة": category, - "النسبة من التكلفة (%)": cost_percentage, - "نسبة التغير في الأسعار (%)": price_change, - "التأثير على التكلفة الإجمالية (%)": impact - }) - - impact_df = pd.DataFrame(impact_data) - - # حساب إجمالي التأثير على التكلفة - total_impact = impact_df["التأثير على التكلفة الإجمالية (%)"].sum() - - st.metric("إجمالي التأثير على التكلفة", f"{total_impact:.1f}%") - - # عرض جدول التأثير - st.dataframe(impact_df, use_container_width=True, hide_index=True) - - # رسم بياني للتأثير على التكلفة - fig = px.bar( - impact_df, - x="الفئة", - y="التأثير على التكلفة الإجمالية (%)", - title="تأثير تغير الأسعار على التكلفة الإجمالية للمشاريع", - color="الفئة", - text_auto=".1f" - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض توصيات لإدارة تغير الأسعار - st.markdown("#### توصيات لإدارة تغير الأسعار") - - st.markdown(""" - 1. **التعاقد المسبق مع الموردين:** التعاقد المسبق مع الموردين لتثبيت الأسعار لفترة زمنية محددة. - 2. **تنويع مصادر التوريد:** تنويع مصادر التوريد لتقليل مخاطر ارتفاع الأسعار من مصدر واحد. - 3. **شراء المواد مقدماً:** شراء المواد الرئيسية مقدماً للمشاريع المستقبلية عندما تكون الأسعار منخفضة. - 4. **استخدام مواد بديلة:** استخدام مواد بديلة ذات جودة مماثلة وأسعار أقل. - 5. **تضمين بند تعديل الأسعار في العقود:** تضمين بند تعديل الأسعار في العقود لتغطية التغيرات الكبيرة في أسعار المواد. - 6. **تحسين كفاءة استخدام المواد:** تحسين كفاءة استخدام المواد لتقليل الهدر وتقليل التكاليف. - 7. **مراقبة اتجاهات الأسعار:** مراقبة اتجاهات الأسعار بشكل مستمر واتخاذ القرارات بناءً على التوقعات المستقبلية. - """) - - def _render_competitors_analysis_tab(self): - """عرض تبويب تحليل المنافسين""" - - st.markdown("### تحليل المنافسين") - - # استخراج البيانات - competitors_df = st.session_state.sample_data["competitors"] - - # عرض بيانات المنافسين - st.markdown("#### بيانات المنافسين") - - st.dataframe(competitors_df, use_container_width=True, hide_index=True) - - # عرض حصص السوق للمنافسين - st.markdown("#### حصص السوق للمنافسين") - - # ترتيب المنافسين حسب حصة السوق - sorted_competitors = competitors_df.sort_values("حصة السوق (%)", ascending=False) - - fig = px.pie( - sorted_competitors, - values="حصة السوق (%)", - names="اسم المنافس", - title="حصص السوق للمنافسين", - hover_data=["التخصص", "الحجم"] - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض معدلات الفوز للمنافسين - st.markdown("#### معدلات الفوز للمنافسين") - - # ترتيب المنافسين حسب معدل الفوز - sorted_by_win_rate = competitors_df.sort_values("معدل الفوز (%)", ascending=False) - - fig = px.bar( - sorted_by_win_rate, - x="اسم المنافس", - y="معدل الفوز (%)", - title="معدلات الفوز للمنافسين", - color="معدل الفوز (%)", - text_auto=".1f", - hover_data=["التخصص", "الحجم", "حصة السوق (%)"] - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض متوسط هوامش الربح للمنافسين - st.markdown("#### متوسط هوامش الربح للمنافسين") - - # ترتيب المنافسين حسب متوسط هامش الربح - sorted_by_margin = competitors_df.sort_values("متوسط هامش الربح (%)", ascending=False) - - fig = px.bar( - sorted_by_margin, - x="اسم المنافس", - y="متوسط هامش الربح (%)", - title="متوسط هوامش الربح للمنافسين", - color="متوسط هامش الربح (%)", - text_auto=".1f", - hover_data=["التخصص", "الحجم", "حصة السوق (%)"] - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض تحليل المنافسين حسب التخصص - st.markdown("#### تحليل المنافسين حسب التخصص") - - # حساب متوسط معدل الفوز وهامش الربح لكل تخصص - specialty_analysis = competitors_df.groupby("التخصص").agg({ - "معدل الفوز (%)": "mean", - "متوسط هامش الربح (%)": "mean", - "حصة السوق (%)": "sum" - }).reset_index() - - # عرض تحليل التخصصات - st.dataframe(specialty_analysis, use_container_width=True, hide_index=True) - - # رسم بياني للعلاقة بين معدل الفوز وهامش الربح حسب التخصص - fig = px.scatter( - specialty_analysis, - x="معدل الفوز (%)", - y="متوسط هامش الربح (%)", - size="حصة السوق (%)", - color="التخصص", - hover_name="التخصص", - title="العلاقة بين معدل الفوز وهامش الربح حسب التخصص", - text="التخصص" - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض تحليل المنافسين حسب الحجم - st.markdown("#### تحليل المنافسين حسب الحجم") - - # حساب متوسط معدل الفوز وهامش الربح لكل حجم - size_analysis = competitors_df.groupby("الحجم").agg({ - "معدل الفوز (%)": "mean", - "متوسط هامش الربح (%)": "mean", - "حصة السوق (%)": "sum" - }).reset_index() - - # عرض تحليل الأحجام - st.dataframe(size_analysis, use_container_width=True, hide_index=True) - - # رسم بياني للعلاقة بين معدل الفوز وهامش الربح حسب الحجم - fig = px.scatter( - size_analysis, - x="معدل الفوز (%)", - y="متوسط هامش الربح (%)", - size="حصة السوق (%)", - color="الحجم", - hover_name="الحجم", - title="العلاقة بين معدل الفوز وهامش الربح حسب الحجم", - text="الحجم" - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض تحليل نقاط القوة والضعف للمنافسين - st.markdown("#### تحليل نقاط القوة والضعف للمنافسين الرئيسيين") - - # اختيار المنافسين للتحليل - top_competitors = competitors_df.sort_values("حصة السوق (%)", ascending=False).head(3) - - for index, competitor in top_competitors.iterrows(): - with st.expander(f"{competitor['اسم المنافس']} - حصة السوق: {competitor['حصة السوق (%)']:.1f}%"): - st.markdown(f"**التخصص:** {competitor['التخصص']}") - st.markdown(f"**الحجم:** {competitor['الحجم']}") - st.markdown(f"**معدل الفوز:** {competitor['معدل الفوز (%)']:.1f}%") - st.markdown(f"**متوسط هامش الربح:** {competitor['متوسط هامش الربح (%)']:.1f}%") - - st.markdown("**نقاط القوة:**") - if competitor["الحجم"] == "كبيرة": - st.markdown("- قدرة مالية كبيرة") - st.markdown("- خبرة واسعة في المشاريع الكبيرة") - st.markdown("- سمعة قوية في السوق") - st.markdown("- شبكة علاقات واسعة") - elif competitor["الحجم"] == "متوسطة": - st.markdown("- مرونة في التعامل مع المشاريع") - st.markdown("- تكاليف تشغيلية أقل") - st.markdown("- تخصص في مجالات محددة") - st.markdown("- سرعة في اتخاذ القرارات") - else: - st.markdown("- مرونة عالية") - st.markdown("- تكاليف تشغيلية منخفضة") - st.markdown("- خدمة عملاء متميزة") - st.markdown("- تخصص دقيق في مجال محدد") - - st.markdown("**نقاط الضعف:**") - if competitor["الحجم"] == "كبيرة": - st.markdown("- بطء في اتخاذ القرارات") - st.markdown("- تكاليف تشغيلية عالية") - st.markdown("- أقل مرونة في التعامل مع التغييرات") - st.markdown("- تركيز على المشاريع الكبيرة فقط") - elif competitor["الحجم"] == "متوسطة": - st.markdown("- قدرة مالية محدودة مقارنة بالشركات الكبيرة") - st.markdown("- صعوبة في المنافسة على المشاريع الكبيرة") - st.markdown("- محدودية الموارد البشرية") - st.markdown("- صعوبة في الحصول على تمويل") - else: - st.markdown("- قدرة مالية محدودة جداً") - st.markdown("- صعوبة في المنافسة على المشاريع المتوسطة والكبيرة") - st.markdown("- محدودية الموارد البشرية والفنية") - st.markdown("- صعوبة في الحصول على تمويل") - - # عرض توصيات للتعامل مع المنافسين - st.markdown("#### توصيات للتعامل مع المنافسين") - - st.markdown(""" - 1. **التركيز على نقاط القوة:** التركيز على نقاط القوة الخاصة بالشركة والتي تميزها عن المنافسين. - 2. **استهداف شرائح سوقية محددة:** استهداف شرائح سوقية محددة والتركيز على تلبية احتياجاتها بشكل أفضل من المنافسين. - 3. **تطوير علاقات قوية مع العملاء:** تطوير علاقات قوية مع العملاء لضمان ولائهم وتكرار التعامل معهم. - 4. **الابتكار في الخدمات والحلول:** تقديم حلول مبتكرة وخدمات متميزة تلبي احتياجات العملاء بشكل أفضل من المنافسين. - 5. **تحسين الكفاءة التشغيلية:** تحسين الكفاءة التشغيلية لتقليل التكاليف وزيادة القدرة التنافسية. - 6. **بناء تحالفات استراتيجية:** بناء تحالفات استراتيجية مع شركات أخرى لتعزيز القدرة التنافسية. - 7. **مراقبة المنافسين باستمرار:** مراقبة المنافسين باستمرار وتحليل استراتيجياتهم وتحركاتهم في السوق. - """) - - def _render_import_export_tab(self): - """عرض تبويب استيراد وتصدير البيانات""" - - st.markdown("### استيراد وتصدير البيانات") - - # عرض مصادر البيانات الحالية - st.markdown("#### مصادر البيانات الحالية") - - # تحويل قائمة مصادر البيانات إلى DataFrame - sources_df = pd.DataFrame(st.session_state.data_sources) - - # عرض مصادر البيانات كجدول - st.dataframe( - sources_df, - column_config={ - "id": st.column_config.NumberColumn("الرقم"), - "name": st.column_config.TextColumn("اسم المصدر"), - "type": st.column_config.TextColumn("النوع"), - "rows": st.column_config.NumberColumn("عدد الصفوف"), - "columns": st.column_config.NumberColumn("عدد الأعمدة"), - "last_updated": st.column_config.DateColumn("تاريخ التحديث"), - "description": st.column_config.TextColumn("الوصف") - }, - use_container_width=True, - hide_index=True - ) - - # استيراد بيانات جديدة - st.markdown("#### استيراد بيانات جديدة") - - col1, col2 = st.columns(2) - - with col1: - data_type = st.selectbox( - "نوع البيانات", - ["بيانات المناقصات", "بيانات المنافسين", "بيانات أسعار المواد", "بيانات الموردين", "بيانات المشاريع", "أخرى"] - ) - - with col2: - file_format = st.selectbox( - "صيغة الملف", - ["CSV", "Excel", "JSON"] - ) - - uploaded_file = st.file_uploader(f"قم بتحميل ملف {file_format}", type=["csv", "xlsx", "json"]) - - if uploaded_file is not None: - if st.button("استيراد البيانات"): - # محاكاة استيراد البيانات - with st.spinner("جاري استيراد البيانات..."): - time.sleep(2) # محاكاة وقت المعالجة - - # تحديث قائمة مصادر البيانات - new_id = max([source["id"] for source in st.session_state.data_sources]) + 1 - - st.session_state.data_sources.append({ - "id": new_id, - "name": f"{data_type} - {uploaded_file.name}", - "type": file_format, - "rows": np.random.randint(50, 500), - "columns": np.random.randint(5, 20), - "last_updated": time.strftime("%Y-%m-%d"), - "description": f"بيانات تم استيرادها من ملف {uploaded_file.name}" - }) - - st.success("تم استيراد البيانات بنجاح!") - st.rerun() - - # تصدير البيانات - st.markdown("#### تصدير البيانات") - - col1, col2 = st.columns(2) - - with col1: - export_data_type = st.selectbox( - "نوع البيانات للتصدير", - ["بيانات المناقصات", "بيانات المنافسين", "بيانات أسعار المواد", "بيانات الموردين", "بيانات المشاريع", "تقرير تحليلي شامل"] - ) - - with col2: - export_format = st.selectbox( - "صيغة التصدير", - ["CSV", "Excel", "JSON", "PDF"] - ) - - if st.button("تصدير البيانات"): - # محاكاة تصدير البيانات - with st.spinner("جاري تصدير البيانات..."): - time.sleep(2) # محاكاة وقت المعالجة - st.success(f"تم تصدير {export_data_type} بصيغة {export_format} بنجاح!") - - # إنشاء رابط تنزيل وهمي - if export_data_type == "بيانات المناقصات": - df = st.session_state.sample_data["tenders"] - elif export_data_type == "بيانات المنافسين": - df = st.session_state.sample_data["competitors"] - elif export_data_type == "بيانات أسعار المواد": - df = st.session_state.sample_data["materials"] - else: - # إنشاء DataFrame وهمي للأنواع الأخرى - df = pd.DataFrame({ - "البيان": ["بيان 1", "بيان 2", "بيان 3"], - "القيمة": [100, 200, 300] - }) - - # تحويل DataFrame إلى CSV - csv = df.to_csv(index=False) - b64 = base64.b64encode(csv.encode()).decode() - href = f'انقر هنا لتنزيل الملف' - st.markdown(href, unsafe_allow_html=True) - - # تكامل البيانات مع الوحدات الأخرى - st.markdown("#### تكامل البيانات مع الوحدات الأخرى") - - st.markdown(""" - يمكن تكامل البيانات مع الوحدات الأخرى في النظام من خلال: - - 1. **إرسال البيانات إلى وحدة التسعير:** إرسال بيانات أسعار المواد وبيانات المناقصات السابقة إلى وحدة التسعير لتحسين دقة التسعير. - 2. **إرسال البيانات إلى وحدة تحليل المخاطر:** إرسال بيانات المناقصات السابقة وبيانات المنافسين إلى وحدة تحليل المخاطر لتحسين تقييم المخاطر. - 3. **إرسال البيانات إلى وحدة الذكاء الاصطناعي:** إرسال البيانات إلى وحدة الذكاء الاصطناعي لتدريب النماذج وتحسين دقة التنبؤات. - 4. **إرسال البيانات إلى وحدة إدارة المشاريع:** إرسال بيانات المشاريع المنجزة إلى وحدة إدارة المشاريع لتحسين تخطيط وإدارة المشاريع المستقبلية. - 5. **إرسال البيانات إلى وحدة التقارير:** إرسال البيانات إلى وحدة التقارير لإنشاء تقارير تحليلية شاملة. - """) - - col1, col2, col3 = st.columns(3) - - with col1: - if st.button("إرسال إلى وحدة التسعير"): - st.success("تم إرسال البيانات إلى وحدة التسعير بنجاح!") - - with col2: - if st.button("إرسال إلى وحدة تحليل المخاطر"): - st.success("تم إرسال البيانات إلى وحدة تحليل المخاطر بنجاح!") - - with col3: - if st.button("إرسال إلى وحدة الذكاء الاصطناعي"): - st.success("تم إرسال البيانات إلى وحدة الذكاء الاصطناعي بنجاح!") diff --git a/modules/document_analysis/analyzer.py b/modules/document_analysis/analyzer.py deleted file mode 100644 index d8c7cc50380e00e391877451bf35983f08505b77..0000000000000000000000000000000000000000 --- a/modules/document_analysis/analyzer.py +++ /dev/null @@ -1,281 +0,0 @@ -""" -وحدة تحليل المستندات لنظام إدارة المناقصات - 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_analysis_app.py b/modules/document_analysis/document_analysis_app.py deleted file mode 100644 index 5712d48d76796357fcbaa732a82472ef8a911c13..0000000000000000000000000000000000000000 --- a/modules/document_analysis/document_analysis_app.py +++ /dev/null @@ -1,1114 +0,0 @@ -# -*- 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 - -try: - # استيراد مكتبات Docling و MLX VLM - from docling_core.types.doc import ImageRefMode - from docling_core.types.doc.document import DocTagsDocument, DoclingDocument - from mlx_vlm import load, generate - from mlx_vlm.prompt_utils import apply_chat_template - from mlx_vlm.utils import load_config, stream_generate - docling_available = True -except ImportError: - docling_available = False - logging.warning("لم يتم العثور على مكتبات Docling و MLX VLM. بعض الوظائف قد لا تعمل.") - -try: - # استيراد مكتبة pdf2image للتعامل مع ملفات PDF - from pdf2image import convert_from_path - pdf_conversion_available = True -except ImportError: - pdf_conversion_available = False - logging.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.") - -# إعداد المسار للوحدات النمطية -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: - # الطريقة 1: استيراد نسبي مباشر - from .services.text_extractor import TextExtractor - from .services.item_extractor import ItemExtractor - from .services.document_parser import DocumentParser -except ImportError: - try: - # الطريقة 2: استيراد مطلق - from modules.document_analysis.services.text_extractor import TextExtractor - from modules.document_analysis.services.item_extractor import ItemExtractor - from modules.document_analysis.services.document_parser import DocumentParser - except ImportError: - # الطريقة 3: تعريف الفئات مباشرة كحل مؤقت - logging.warning("لا يمكن استيراد خدمات تحليل المستندات. استخدام التعريفات المؤقتة.") - - class TextExtractor: - def __init__(self, config=None): - self.config = config or {} - - def extract_from_pdf(self, file_path): - return "نص مستخرج مؤقت من PDF" - - def extract_from_docx(self, file_path): - return "نص مستخرج مؤقت من DOCX" - - def extract_from_image(self, file_path): - return "نص مستخرج مؤقت من صورة" - - 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): - return [{"عنوان": "جدول مؤقت", "بيانات": []}] - - def extract(self, file_path): - return [ - {"بند": "بند مؤقت 1", "قيمة": 1000}, - {"بند": "بند مؤقت 2", "قيمة": 2000}, - {"بند": "بند مؤقت 3", "قيمة": 3000} - ] - - class DocumentParser: - def __init__(self, config=None): - self.config = config or {} - - def parse_document(self, file_path): - return {"نوع": "مستند مؤقت", "محتوى": "محتوى مؤقت"} - - def parse(self, file_path): - return { - "نوع المستند": "مستند مؤقت", - "عدد الصفحات": 5, - "تاريخ التحليل": "2025-03-24", - "درجة الثقة": "80%", - "ملاحظات": "تحليل مؤقت للمستند" - } - - -class DoclingAnalyzer: - """ - فئة لتحليل المستندات باستخدام نماذج Docling و MLX VLM - """ - def __init__(self): - self.model = None - self.processor = None - self.config = None - self.docling_available = False - - try: - # تحميل النموذج - import os - from mlx_vlm import load, generate - from mlx_vlm.utils import load_config - - model_path = "ds4sd/SmolDocling-256M-preview-mlx-bf16" - self.model, self.processor = load(model_path) - self.config = load_config(model_path) - self.docling_available = True - except Exception as e: - print(f"خطأ في تحميل نموذج Docling: {str(e)}") - self.docling_available = False - - def is_available(self): - """التحقق من توفر نماذج Docling""" - return self.docling_available and self.model is not None - - def analyze_image(self, image_path=None, image_url=None, image_bytes=None, prompt="Convert this page to docling."): - """ - تحليل صورة باستخدام نموذج Docling - - المعلمات: - image_path (str): مسار الصورة المحلية (اختياري) - image_url (str): رابط الصورة (اختياري) - image_bytes (bytes): بيانات الصورة (اختياري) - prompt (str): التوجيه للنموذج - - العوائد: - dict: نتائج التحليل متضمنة النص والعلامات والمستند - """ - if not self.is_available(): - return { - "error": "Docling غير متوفر. يرجى تثبيت المكتبات المطلوبة." - } - - try: - from io import BytesIO - from pathlib import Path - from urllib.parse import urlparse - import requests - from PIL import Image - from docling_core.types.doc import ImageRefMode - from docling_core.types.doc.document import DocTagsDocument, DoclingDocument - from mlx_vlm.prompt_utils import apply_chat_template - from mlx_vlm.utils import stream_generate, load_image - - # تحميل الصورة - pil_image = None - image_source = None - - if image_url: - try: - response = requests.get(image_url, stream=True, timeout=10) - response.raise_for_status() - pil_image = Image.open(BytesIO(response.content)) - image_source = image_url - except Exception as e: - return {"error": f"فشل في تحميل الصورة من الرابط: {str(e)}"} - elif image_path: - try: - # التأكد من وجود الملف - if not Path(image_path).exists(): - return {"error": f"ملف الصورة غير موجود: {image_path}"} - pil_image = Image.open(image_path) - image_source = image_path - except Exception as e: - return {"error": f"فشل في فتح ملف الصورة: {str(e)}"} - elif image_bytes: - try: - pil_image = Image.open(BytesIO(image_bytes)) - # حفظ الصورة مؤقتا للتحليل - temp_path = "/tmp/temp_image.jpg" - pil_image.save(temp_path) - image_source = temp_path - except Exception as e: - return {"error": f"فشل في معالجة بيانات الصورة: {str(e)}"} - else: - return {"error": "يجب توفير مصدر للصورة (مسار، رابط، أو بيانات)"} - - # تطبيق قالب المحادثة - formatted_prompt = apply_chat_template(self.processor, self.config, prompt, num_images=1) - - # إنشاء النتيجة - output = "" - - # تمرير مسار الصورة أو عنوان URL الفعلي - try: - for token in stream_generate( - self.model, self.processor, formatted_prompt, [image_source], - max_tokens=4096, verbose=False - ): - output += token.text - if "" in token.text: - break - except Exception as e: - return {"error": f"فشل في تحليل الصورة: {str(e)}"} - - # إنشاء مستند Docling - try: - doctags_doc = DocTagsDocument.from_doctags_and_image_pairs([output], [pil_image]) - doc = DoclingDocument(name="AnalyzedDocument") - doc.load_from_doctags(doctags_doc) - - # إرجاع النتائج - return { - "doctags": output, - "markdown": doc.export_to_markdown(), - "document": doc, - "image": pil_image - } - except Exception as e: - return {"error": f"فشل في إنشاء مستند Docling: {str(e)}"} - - except Exception as e: - return {"error": f"حدث خطأ غير متوقع: {str(e)}"} - - def export_to_html(self, doc, output_path="./output.html", show_in_browser=False): - """ - تصدير المستند إلى HTML - - المعلمات: - doc (DoclingDocument): مستند Docling - output_path (str): مسار ملف الإخراج - show_in_browser (bool): عرض الملف في المتصفح - - العوائد: - str: مسار ملف HTML المولد - """ - if not self.is_available(): - return None - - try: - from pathlib import Path - from docling_core.types.doc import ImageRefMode - - # إنشاء مسار الإخراج - out_path = Path(output_path) - # التأكد من وجود المجلد - out_path.parent.mkdir(exist_ok=True, parents=True) - - doc.save_as_html(out_path, image_mode=ImageRefMode.EMBEDDED) - - # فتح في المتصفح إذا تم طلب ذلك - if show_in_browser: - import webbrowser - webbrowser.open(f"file:///{str(out_path.resolve())}") - - return str(out_path) - except Exception as e: - print(f"خطأ في تصدير المستند إلى HTML: {str(e)}") - return None - - -class ClaudeAnalyzer: - """ - فئة لتحليل المستندات باستخدام Claude.ai API - """ - def __init__(self): - """تهيئة محلل Claude""" - 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 analyze_document(self, file_path, model_name="claude-3-7-sonnet", prompt=None): - """ - تحليل مستند باستخدام Claude AI - - المعلمات: - file_path: مسار الملف المراد تحليله - model_name: اسم نموذج Claude المراد استخدامه - prompt: التوجيه المخصص للتحليل (اختياري) - - العوائد: - dict: نتائج التحليل - """ - try: - # الحصول على مفتاح API - api_key = self.get_api_key() - - # تحديد التوجيه المناسب إذا لم يتم توفيره - if prompt is None: - _, ext = os.path.splitext(file_path) - ext = ext.lower() - - if ext == '.pdf': - prompt = "قم بتحليل هذه الصورة المستخرجة من مستند PDF واستخراج المعلومات الرئيسية مثل العناوين، الفقرات، الجداول، والنقاط المهمة." - elif ext in ('.doc', '.docx'): - prompt = "قم بتحليل هذه الصورة المستخرجة من مستند Word واستخراج المعلومات الرئيسية والخلاصة." - elif ext in ('.jpg', '.jpeg', '.png', '.gif', '.webp'): - prompt = "قم بوصف وتحليل محتوى هذه الصورة بالتفصيل، مع ذكر العناصر المهمة والنصوص والبيانات الموجودة فيها." - else: - prompt = "قم بتحليل محتوى هذا الملف واستخراج المعلومات المفيدة منه." - - # التحقق من نوع الملف وتحويله إذا لزم الأمر - _, ext = os.path.splitext(file_path) - ext = ext.lower() - - processed_file_path = file_path - temp_files = [] # قائمة للملفات المؤقتة لحذفها لاحقاً - - # للملفات غير المدعومة مباشرة (مثل PDF) - if ext not in ('.jpg', '.jpeg', '.png', '.gif', '.webp'): - # إذا كان الملف PDF، حاول تحويله إلى صورة - if ext == '.pdf': - if not pdf_conversion_available: - return {"error": "لا يمكن تحويل ملف PDF إلى صورة. يرجى تثبيت مكتبة pdf2image."} - - try: - # تحويل الصفحة الأولى فقط - images = convert_from_path(file_path, first_page=1, last_page=1) - if images: - # حفظ الصورة بشكل مؤقت - temp_image_path = "/tmp/temp_pdf_image.jpg" - images[0].save(temp_image_path, 'JPEG') - processed_file_path = temp_image_path # استخدام مسار الصورة الجديد - temp_files.append(temp_image_path) - else: - return {"error": "فشل في تحويل ملف PDF إلى صورة"} - except Exception as e: - return {"error": f"فشل في تحويل ملف PDF إلى صورة: {str(e)}"} - else: - return {"error": f"نوع الملف {ext} غير مدعوم. Claude API يدعم فقط الصور (JPEG, PNG, GIF, WebP) أو PDF (يتم تحويله تلقائياً)."} - - # ضغط الصورة إذا كان حجمها كبيراً - try: - img = Image.open(processed_file_path) - - # تحقق من حجم الصورة وضغطها إذا كانت كبيرة - img_width, img_height = img.size - if img_width > 1500 or img_height > 1500: - # تحويل الصورة إلى حجم أصغر (1500×1500 بكسل كحد أقصى) - img.thumbnail((1500, 1500)) - - # حفظ الصورة المضغوطة في ملف مؤقت - compressed_image_path = "/tmp/compressed_image.jpg" - img.save(compressed_image_path, format="JPEG", quality=85) - - # إضافة الملف المؤقت إلى القائمة - if processed_file_path not in temp_files: - temp_files.append(compressed_image_path) - - processed_file_path = compressed_image_path - except Exception as e: - logging.warning(f"فشل في ضغط الصورة: {str(e)}. سيتم استخدام الصورة الأصلية.") - - # قراءة محتوى الملف المعالج - with open(processed_file_path, 'rb') as f: - file_content = f.read() - - # التحقق من حجم الملف (يجب أن يكون أقل من 20 ميجابايت) - file_size_mb = len(file_content) / (1024 * 1024) - if file_size_mb > 20: - # محاولة ضغط الصورة أكثر إذا كان حجمها أكبر من 20 ميجابايت - try: - img = Image.open(processed_file_path) - - # ضغط أكبر - حجم أصغر وجودة أقل - compressed_image_path = "/tmp/extra_compressed_image.jpg" - img.thumbnail((1000, 1000)) - img.save(compressed_image_path, format="JPEG", quality=70) - - # إضافة الملف المؤقت إلى القائمة - temp_files.append(compressed_image_path) - processed_file_path = compressed_image_path - - # قراءة الملف المضغوط - with open(processed_file_path, 'rb') as f: - file_content = f.read() - - # التحقق من الحجم مرة أخرى - file_size_mb = len(file_content) / (1024 * 1024) - if file_size_mb > 20: - # لا يزال الحجم كبيراً - for temp_file in temp_files: - try: - os.unlink(temp_file) - except: - pass - return {"error": f"حجم الملف كبير جدًا ({file_size_mb:.2f} ميجابايت) حتى بعد الضغط. يجب أن يكون أقل من 20 ميجابايت."} - except Exception as e: - for temp_file in temp_files: - try: - os.unlink(temp_file) - except: - pass - return {"error": f"الملف كبير جدًا ({file_size_mb:.2f} ميجابايت) ولا يمكن ضغطه. يجب أن يكون أقل من 20 ميجابايت."} - - # تحديد نوع الملف المعالج (بعد التحويل إذا تم) - file_type = self._get_file_type(processed_file_path) - - # تحويل المحتوى إلى Base64 - file_base64 = base64.b64encode(file_content).decode('utf-8') - - # إعداد البيانات للطلب - headers = { - "Content-Type": "application/json", - "x-api-key": api_key, - "anthropic-version": "2023-06-01" - } - - # التحقق من اسم النموذج وتصحيحه إذا لزم الأمر - valid_models = { - "claude-3-7-sonnet": "claude-3-7-sonnet-20250219", - "claude-3-5-haiku": "claude-3-5-haiku-20240307" - } - - if model_name in valid_models: - model_name = valid_models[model_name] - - # طباعة معلومات التصحيح - logging.debug(f"إرسال طلب إلى Claude API: {model_name}, نوع الملف: {file_type}") - - # تحضير payload للـ API - 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 مع محاولات إعادة - for attempt in range(3): # ثلاث محاولات كحد أقصى - try: - response = requests.post( - self.api_url, - headers=headers, - json=payload, - timeout=120 # زيادة مهلة الانتظار إلى دقيقتين - ) - - # إذا نجح الطلب، نخرج من الحلقة - if response.status_code == 200: - break - - # إذا كان الخطأ 502، ننتظر ونحاول مرة أخرى - if response.status_code == 502: - wait_time = (attempt + 1) * 5 # انتظار 5، 10، 15 ثانية - logging.warning(f"تم استلام خطأ 502. الانتظار {wait_time} ثانية قبل إعادة المحاولة.") - time.sleep(wait_time) - else: - # إذا كان الخطأ ليس 502، نخرج من الحلقة - break - - except requests.exceptions.RequestException as e: - logging.warning(f"فشل الطلب في المحاولة {attempt+1}: {str(e)}") - if attempt == 2: # آخر محاولة - # حذف الملفات المؤقتة - for temp_file in temp_files: - try: - os.unlink(temp_file) - except: - pass - return {"error": f"فشل الاتصال بعد عدة محاولات: {str(e)}"} - time.sleep((attempt + 1) * 5) # انتظار قبل إعادة المحاولة - - # حذف الملفات المؤقتة - for temp_file in temp_files: - try: - os.unlink(temp_file) - except: - pass - - # التحقق من نجاح الطلب - 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: - # حذف الملفات المؤقتة في حالة حدوث خطأ - for temp_file in temp_files: - try: - os.unlink(temp_file) - except: - pass - - logging.error(f"خطأ أثناء تحليل المستند: {str(e)}") - import traceback - stack_trace = traceback.format_exc() - return {"error": f"فشل في تحليل المستند: {str(e)}\n{stack_trace}"} - - def _get_file_type(self, file_path): - """تحديد نوع الملف من امتداده""" - _, ext = os.path.splitext(file_path) - ext = ext.lower() - - # Claude API يدعم فقط أنواع الصور التالية - if ext in ('.jpg', '.jpeg'): - return "image/jpeg" - elif ext == '.png': - return "image/png" - elif ext == '.gif': - return "image/gif" - elif ext == '.webp': - return "image/webp" - else: - # للملفات الأخرى، نعيد نوع صورة افتراضي - # هذا سيستخدم فقط إذا تم تحويل الملف إلى صورة أولاً - return "image/jpeg" - - def get_available_models(self): - """ - الحصول على قائمة بالنماذج المتاحة - - العوائد: - dict: قائمة بالنماذج مع وصفها - """ - return { - "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة", - "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية" - } - - 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) - - -class DocumentAnalysisApp: - def __init__(self): - # إنشاء كائنات الخدمات - self.text_extractor = TextExtractor() - self.item_extractor = ItemExtractor() - self.document_parser = DocumentParser() - - # إنشاء محلل Docling - self.docling_analyzer = DoclingAnalyzer() - - # إنشاء محلل Claude - self.claude_analyzer = ClaudeAnalyzer() - - def render(self): - """العرض الرئيسي للتطبيق""" - st.title("تحليل المستندات") - st.write("اختر ملفًا لتحليله واستخرج البيانات المطلوبة.") - - # إنشاء علامات تبويب للأنواع المختلفة من التحليل - tabs = st.tabs(["تحليل عام", "تحليل Docling", "تحليل Claude AI"]) - - with tabs[0]: - self._render_general_analysis() - - with tabs[1]: - self._render_docling_analysis() - - with tabs[2]: - self._render_claude_analysis() - - def _render_general_analysis(self): - """عرض واجهة التحليل العام""" - uploaded_file = st.file_uploader("ارفع ملف PDF أو DOCX", type=["pdf", "docx"], key="general_uploader") - - if uploaded_file: - with st.spinner("جاري تحليل المستند..."): - file_path = f"/tmp/{uploaded_file.name}" - with open(file_path, "wb") as f: - f.write(uploaded_file.read()) - - # تحديد نوع الملف من امتداده - _, ext = os.path.splitext(file_path) - ext = ext.lower() - - # استخراج النص حسب نوع الملف - if ext == '.pdf': - extracted_text = self.text_extractor.extract_from_pdf(file_path) - elif ext in ('.doc', '.docx'): - extracted_text = self.text_extractor.extract_from_docx(file_path) - else: - extracted_text = "نوع ملف غير مدعوم للنص" - - # عرض النص المستخرج - st.subheader("النص المستخرج:") - st.text_area("النص", extracted_text, height=300) - - # استخراج البنود - extracted_items = self.item_extractor.extract(file_path) - if extracted_items: - st.subheader("البنود المستخرجة:") - st.dataframe(extracted_items) - - # تحليل المستند - parsed_data = self.document_parser.parse(file_path) - st.subheader("تحليل المستند:") - st.json(parsed_data) - - def _render_docling_analysis(self): - """عرض واجهة تحليل Docling""" - import streamlit as st - from tempfile import NamedTemporaryFile - - if not self.docling_analyzer.is_available(): - st.warning("مكتبات Docling و MLX VLM غير متوفرة. يرجى تثبيت الحزم المطلوبة.") - st.code(""" - # يرجى تثبيت الحزم التالية: - pip install docling-core mlx-vlm pillow>=10.3.0 transformers>=4.49.0 tqdm>=4.66.2 - """) - return - - st.subheader("تحليل الصور والمستندات باستخدام Docling") - - # اختيار مصدر الصورة - source_option = st.radio("اختر مصدر الصورة:", ["رفع صورة", "رابط صورة"]) - - image_path = None - image_url = None - image_data = None - - if source_option == "رفع صورة": - uploaded_image = st.file_uploader("ارفع صورة", type=["jpg", "jpeg", "png"], key="docling_uploader") - if uploaded_image: - # حفظ الصورة المرفوعة إلى ملف مؤقت - image_data = uploaded_image.read() - - # عرض الصورة المرفوعة - st.image(image_data, caption="الصورة المرفوعة", width=400) - - # إنشاء ملف مؤقت لحفظ الصورة - with NamedTemporaryFile(delete=False, suffix=f".{uploaded_image.name.split('.')[-1]}") as temp_file: - temp_file.write(image_data) - image_path = temp_file.name - else: - image_url = st.text_input("أدخل رابط الصورة:") - if image_url: - try: - # عرض الصورة من الرابط - st.image(image_url, caption="الصورة من الرابط", width=400) - except Exception as e: - st.error(f"خطأ في تحميل الصورة: {str(e)}") - - # توجيه للنموذج - prompt = st.text_input("توجيه للنموذج:", value="Convert this page to docling.") - - # زر التحليل - if st.button("تحليل الصورة"): - if image_path or image_url: - with st.spinner("جاري تحليل الصورة..."): - # تحليل الصورة - results = self.docling_analyzer.analyze_image( - image_path=image_path, - image_url=image_url, - image_bytes=None, # نستخدم الملف المؤقت بدلاً من البيانات المباشرة - prompt=prompt - ) - - if "error" in results: - st.error(results["error"]) - else: - # عرض النتائج - with st.expander("علامات DocTags", expanded=True): - st.code(results["doctags"], language="xml") - - with st.expander("Markdown", expanded=True): - st.code(results["markdown"], language="markdown") - - # تصدير إلى HTML - if st.button("تصدير إلى HTML"): - html_path = self.docling_analyzer.export_to_html( - results["document"], - show_in_browser=True - ) - if html_path: - st.success(f"تم تصدير المستند إلى: {html_path}") - else: - st.error("فشل تصدير المستند إلى HTML") - - # حذف الملف المؤقت بعد الانتهاء - if image_path and os.path.exists(image_path) and image_data: - try: - os.unlink(image_path) - except: - pass - else: - st.warning("يرجى اختيار صورة للتحليل أولاً.") - - def _render_claude_analysis(self): - """عرض واجهة تحليل Claude AI مع توسعة البيانات المعروضة""" - import time - - st.subheader("تحليل المستندات باستخدام Claude AI") - - col1, col2 = st.columns([2, 1]) - - with col1: - # إضافة اختيار النموذج - claude_models = { - "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة", - "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية" - } - - selected_model = st.radio( - "اختر نموذج Claude", - options=list(claude_models.keys()), - format_func=lambda x: claude_models[x], - horizontal=True - ) - - with col2: - # إضافة شرح بسيط للنموذج - if selected_model == "claude-3-7-sonnet": - st.info("نموذج Claude 3.7 Sonnet هو أحدث نموذج ذكي يقدم تحليلاً متعمقاً للمستندات مع دقة عالية") - else: - st.info("نموذج Claude 3.5 Haiku أسرع في التحليل ومناسب للمهام البسيطة والاستخدام اليومي") - - # تخصيص التوجيه مع اقتراحات للتوجيهات المخصصة - st.subheader("تخصيص التحليل") - - prompt_templates = { - "تحليل عام": "قم بتحليل هذا المستند واستخراج جميع المعلومات المهمة.", - "استخراج البيانات الأساسية": "استخرج كافة البيانات الأساسية من هذا المستند بما في ذلك الأسماء والتواريخ والأرقام والمبالغ المالية.", - "تلخيص المستند": "قم بتلخيص هذا المستند بشكل مفصل مع التركيز على النقاط الرئيسية.", - "تحليل العقود": "حلل هذا العقد واستخرج الأطراف والالتزامات والشروط والتواريخ المهمة.", - "تحليل فواتير": "استخرج كافة المعلومات من هذه الفاتورة بما في ذلك المورد والعميل وتفاصيل المنتجات والأسعار والمبالغ الإجمالية." - } - - prompt_type = st.selectbox( - "اختر نوع التوجيه", - options=list(prompt_templates.keys()), - index=0 - ) - - default_prompt = prompt_templates[prompt_type] - - custom_prompt = st.text_area( - "تخصيص التوجيه للتحليل", - value=default_prompt, - height=100 - ) - - # خيارات متقدمة - with st.expander("خيارات متقدمة"): - extraction_format = st.selectbox( - "تنسيق استخراج البيانات", - ["عام", "جداول", "قائمة", "هيكل منظم"], - index=0 - ) - - detail_level = st.slider( - "مستوى التفاصيل", - min_value=1, - max_value=5, - value=3, - help="1: ملخص موجز، 5: تحليل تفصيلي كامل" - ) - - # تحديث التوجيه بناء على الخيارات المتقدمة - if extraction_format != "عام" or detail_level != 3: - custom_prompt += f"\n\nاستخدم تنسيق {extraction_format} مع مستوى تفاصيل {detail_level}/5." - - # رفع الملف - uploaded_file = st.file_uploader( - "ارفع ملفًا للتحليل", - type=["pdf", "jpg", "jpeg", "png"], - key="claude_uploader", - help="يدعم ملفات PDF والصور. سيتم تحويل PDF إلى صور لمعالجتها." - ) - - # التحقق من وجود مفتاح API - api_available = True - try: - self.claude_analyzer.get_api_key() - except ValueError: - api_available = False - st.warning("مفتاح API لـ Claude غير متوفر. يرجى التأكد من تعيين متغير البيئة 'anthropic'.") - - # زر التحليل - analyze_col1, analyze_col2 = st.columns([1, 3]) - - with analyze_col1: - analyze_button = st.button( - "تحليل المستند", - key="analyze_claude_btn", - use_container_width=True, - disabled=not (uploaded_file and api_available) - ) - - with analyze_col2: - if not uploaded_file: - st.info("يرجى رفع ملف للتحليل") - - # إجراء التحليل - if uploaded_file and api_available and analyze_button: - # عرض شريط التقدم - progress_bar = st.progress(0, text="جاري تجهيز الملف...") - - with st.spinner(f"جاري التحليل باستخدام {claude_models[selected_model].split('-')[0]}..."): - # حفظ الملف المرفوع إلى ملف مؤقت - temp_path = f"/tmp/{uploaded_file.name}" - with open(temp_path, "wb") as f: - f.write(uploaded_file.getbuffer()) - - # تحديث شريط التقدم - progress_bar.progress(25, text="جاري معالجة الملف...") - - try: - # تحليل المستند - progress_bar.progress(40, text="جاري إرسال الطلب إلى Claude AI...") - - results = self.claude_analyzer.analyze_document( - temp_path, - model_name=selected_model, - prompt=custom_prompt - ) - - progress_bar.progress(90, text="جاري معالجة النتائج...") - - if "error" in results: - st.error(results["error"]) - else: - progress_bar.progress(100, text="اكتمل التحليل!") - - # عرض النتائج بشكل منظم - st.success(f"تم التحليل بنجاح باستخدام {results.get('model', selected_model)}!") - - # إضافة علامات تبويب فرعية للنتائج - result_tabs = st.tabs(["التحليل الكامل", "بيانات مستخرجة", "معلومات إضافية"]) - - with result_tabs[0]: - # عرض النتائج الكاملة - st.markdown("## نتائج التحليل") - st.markdown(results["content"]) - - with result_tabs[1]: - # محاولة استخراج بيانات منظمة من النتائج - st.markdown("## البيانات المستخرجة") - - # تقسيم النتائج إلى أقسام - content_parts = results["content"].split("\n\n") - - # استخراج العناوين والبيانات الهامة - headings = [] - key_values = {} - - for part in content_parts: - # تحديد العناوين - if part.startswith("#") or part.startswith("##") or part.startswith("###"): - headings.append(part.strip()) - continue - - # محاولة استخراج أزواج المفتاح/القيمة - if ":" in part and len(part.split(":")) == 2: - key, value = part.split(":") - key_values[key.strip()] = value.strip() - - # عرض العناوين - if headings: - st.markdown("### العناوين الرئيسية") - for heading in headings[:5]: # عرض أهم 5 عناوين - st.markdown(f"- {heading}") - - if len(headings) > 5: - with st.expander(f"عرض {len(headings) - 5} عناوين إضافية"): - for heading in headings[5:]: - st.markdown(f"- {heading}") - - # عرض البيانات الهامة - if key_values: - st.markdown("### بيانات هامة") - - # تحويل البيانات إلى DataFrame - import pandas as pd - df = pd.DataFrame([key_values.values()], columns=key_values.keys()) - st.dataframe(df.T) - - # البحث عن الجداول في النص - if "| ------ |" in results["content"] or "\n|" in results["content"]: - st.markdown("### جداول مستخرجة") - # استخراج الجداول من النص Markdown - table_parts = [] - in_table = False - current_table = [] - - for line in results["content"].split("\n"): - if line.startswith("|") and "-|-" in line.replace(" ", ""): - in_table = True - current_table.append(line) - elif in_table and line.startswith("|"): - current_table.append(line) - elif in_table and not line.startswith("|") and line.strip(): - in_table = False - table_parts.append("\n".join(current_table)) - current_table = [] - - # إضافة الجدول الأخير إذا كان هناك - if current_table: - table_parts.append("\n".join(current_table)) - - # عرض الجداول - for i, table in enumerate(table_parts): - st.markdown(f"#### جدول {i+1}") - st.markdown(table) - - # إذا لم يتم العثور على أي بيانات منظمة - if not headings and not key_values and not ("| ------ |" in results["content"] or "\n|" in results["content"]): - st.info("لم يتم العثور على بيانات منظمة في النتائج. يمكنك تعديل التوجيه لطلب تنسيق أكثر هيكلية.") - - with result_tabs[2]: - # عرض معلومات إضافية - st.markdown("## معلومات عن التحليل") - - # عرض معلومات الاستخدام - col1, col2 = st.columns(2) - - with col1: - st.markdown("### معلومات النموذج") - st.markdown(f"**النموذج المستخدم**: {results.get('model', selected_model)}") - st.markdown(f"**تاريخ التحليل**: {time.strftime('%Y-%m-%d %H:%M:%S')}") - - with col2: - st.markdown("### إحصائيات الاستخدام") - - if "usage" in results: - usage = results["usage"] - st.markdown(f"**توكنز المدخلات**: {usage.get('input_tokens', 'غير متوفر')}") - st.markdown(f"**توكنز الإخراج**: {usage.get('output_tokens', 'غير متوفر')}") - st.markdown(f"**إجمالي التوكنز**: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0)}") - else: - st.info("معلومات الاستخدام غير متوفرة") - - # إضافة خيارات التصدير - st.markdown("### تصدير النتائج") - - export_col1, export_col2 = st.columns(2) - - with export_col1: - # تصدير كنص - st.download_button( - label="تحميل النتائج كملف نصي", - data=results["content"], - file_name=f"claude_analysis_{uploaded_file.name.split('.')[0]}.txt", - mime="text/plain" - ) - - with export_col2: - # تصدير كـ Markdown - st.download_button( - label="تحميل النتائج كملف Markdown", - data=results["content"], - file_name=f"claude_analysis_{uploaded_file.name.split('.')[0]}.md", - mime="text/markdown" - ) - finally: - # حذف الملف المؤقت - try: - os.unlink(temp_path) - except: - pass - - def analyze_document(self, file_path): - """ - تحليل مستند وإرجاع نتائج التحليل - - المعلمات: - file_path (str): مسار المستند المراد تحليله - - العوائد: - dict: نتائج تحليل المستند - """ - # تحديد نوع المستند من امتداد الملف - _, ext = os.path.splitext(file_path) - ext = ext.lower() - - # تحليل المستند حسب نوعه - if ext == '.pdf': - text = self.text_extractor.extract_from_pdf(file_path) - elif ext in ('.doc', '.docx'): - text = self.text_extractor.extract_from_docx(file_path) - elif ext in ('.jpg', '.jpeg', '.png'): - # استخدام محلل Docling للصور إذا كان متاحًا - if self.docling_analyzer.is_available(): - docling_results = self.docling_analyzer.analyze_image(image_path=file_path) - if "error" not in docling_results: - return { - "نص": docling_results["markdown"], - "doctags": docling_results["doctags"], - "معلومات": { - "نوع المستند": "صورة", - "تحليل": "تم تحليله باستخدام Docling" - } - } - - # استخدام المحلل العادي إذا كان Docling غير متاح - text = self.text_extractor.extract_from_image(file_path) - else: - raise ValueError(f"نوع المستند غير مدعوم: {ext}") - - # تحليل المستند - document = self.document_parser.parse_document(file_path) - - # استخراج العناصر المنظمة - tables = self.item_extractor.extract_tables(document) - - # إرجاع نتائج التحليل - return { - "نص": text, - "جداول": tables, - "معلومات": document - } - - def analyze_with_claude(self, file_path, model_name="claude-3-7-sonnet", prompt=None): - """ - تحليل مستند باستخدام Claude AI - - المعلمات: - file_path (str): مسار المستند المراد تحليله - model_name (str): اسم نموذج Claude المراد استخدامه - prompt (str): التوجيه المخصص للتحليل (اختياري) - - العوائد: - dict: نتائج التحليل - """ - # محاولة تحليل المستند باستخدام Claude - try: - # التحقق من وجود المفتاح - self.claude_analyzer.get_api_key() - - # تحليل المستند باستخدام Claude - return self.claude_analyzer.analyze_document( - file_path, - model_name=model_name, - prompt=prompt - ) - except Exception as e: - logging.error(f"خطأ في تحليل المستند باستخدام Claude: {str(e)}") - return {"error": f"فشل في تحليل المستند باستخدام Claude: {str(e)}"} - - -# تشغيل التطبيق -if __name__ == "__main__": - app = DocumentAnalysisApp() - app.render() \ No newline at end of file diff --git a/modules/document_analysis/document_app.py b/modules/document_analysis/document_app.py deleted file mode 100644 index b6d3741361dca8d1e38a5757ae3885b36ca141f6..0000000000000000000000000000000000000000 --- a/modules/document_analysis/document_app.py +++ /dev/null @@ -1,887 +0,0 @@ -""" -وحدة تحليل المستندات - التطبيق الرئيسي -""" - -# استيراد المكتبات القياسية -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 pandas as pd -import numpy as np -import matplotlib.pyplot as plt -import plotly.express as px -import plotly.graph_objects as go -import requests -from PIL import Image - -# محاولة استيراد خدمات تحليل المستندات -try: - from .services.text_extractor import TextExtractor - from .services.item_extractor import ItemExtractor - from .services.document_parser import DocumentParser -except ImportError: - try: - from modules.document_analysis.services.text_extractor import TextExtractor - from modules.document_analysis.services.item_extractor import ItemExtractor - from modules.document_analysis.services.document_parser import DocumentParser - except ImportError: - # تعريف فئات وهمية في حالة عدم وجود الخدمات - class TextExtractor: - def __init__(self, config=None): - self.config = config or {} - - def extract_from_pdf(self, file_path): - return "نص مستخرج مؤقت من PDF" - - def extract_from_docx(self, file_path): - return "نص مستخرج مؤقت من DOCX" - - def extract_from_image(self, file_path): - return "نص مستخرج مؤقت من صورة" - - 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): - return [{"عنوان": "جدول مؤقت", "بيانات": []}] - - def extract_items(self, document): - return [ - {"رقم البند": "A1", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", "الوحدة": "م3", "الكمية": 250.0}, - {"رقم البند": "A2", "وصف البند": "توريد وتركيب حديد التسليح للأساسات", "الوحدة": "طن", "الكمية": 25.0}, - {"رقم البند": "A3", "وصف البند": "أعمال العزل المائي للأساسات", "الوحدة": "م2", "الكمية": 500.0} - ] - - class DocumentParser: - def __init__(self, config=None): - self.config = config or {} - - def parse_document(self, file_path): - return { - "metadata": { - "title": "مستند مؤقت", - "author": "غير معروف", - "date": "2024-01-01", - "pages": 10 - }, - "content": "محتوى مؤقت للمستند", - "tables": [], - "items": [] - } - - def extract_metadata(self, file_path): - return { - "title": "مستند مؤقت", - "author": "غير معروف", - "date": "2024-01-01", - "pages": 10 - } - - -class DocumentAnalysisApp: - """وحدة تحليل المستندات""" - - def __init__(self): - """تهيئة وحدة تحليل المستندات""" - - # تهيئة خدمات تحليل المستندات - self.text_extractor = TextExtractor() - self.item_extractor = ItemExtractor() - self.document_parser = DocumentParser() - - # تهيئة حالة الجلسة - if 'analyzed_documents' not in st.session_state: - st.session_state.analyzed_documents = [] - - if 'extracted_items' not in st.session_state: - st.session_state.extracted_items = [] - - # إنشاء مجلد مؤقت للملفات - self.temp_dir = Path("temp_documents") - self.temp_dir.mkdir(exist_ok=True) - - def render(self): - """عرض واجهة وحدة تحليل المستندات""" - - st.markdown("

وحدة تحليل المستندات

", unsafe_allow_html=True) - - tabs = st.tabs([ - "تحليل المستندات", - "استخراج البنود والكميات", - "تحليل الصور والمخططات", - "مكتبة المستندات", - "الإعدادات" - ]) - - with tabs[0]: - self._render_document_analysis_tab() - - with tabs[1]: - self._render_item_extraction_tab() - - with tabs[2]: - self._render_image_analysis_tab() - - with tabs[3]: - self._render_document_library_tab() - - with tabs[4]: - self._render_settings_tab() - - def _render_document_analysis_tab(self): - """عرض تبويب تحليل المستندات""" - - st.markdown("### تحليل المستندات") - - # رفع المستند - uploaded_file = st.file_uploader("رفع مستند للتحليل", type=["pdf", "docx", "txt", "jpg", "jpeg", "png"], key="document_upload") - - if uploaded_file is not None: - # حفظ الملف مؤقتاً - file_path = self._save_uploaded_file(uploaded_file) - - if file_path: - st.success(f"تم رفع الملف: {uploaded_file.name}") - - # عرض معلومات الملف - file_info = self._get_file_info(file_path) - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("نوع الملف", file_info["type"]) - - with col2: - st.metric("حجم الملف", file_info["size"]) - - with col3: - if "pages" in file_info: - st.metric("عدد الصفحات", file_info["pages"]) - - # خيارات التحليل - analysis_options = st.multiselect( - "اختر خيارات التحليل", - [ - "استخراج النص", - "استخراج الجداول", - "استخراج البنود والكميات", - "استخراج المعلومات الرئيسية", - "تحليل هيكل المستند" - ], - default=["استخراج النص", "استخراج البنود والكميات"], - key="analysis_options" - ) - - # زر بدء التحليل - if st.button("بدء التحليل", key="start_analysis_button"): - with st.spinner("جاري تحليل المستند..."): - # محاكاة وقت التحليل - time.sleep(2) - - # تنفيذ التحليل المطلوب - analysis_results = {} - - if "استخراج النص" in analysis_options: - analysis_results["text"] = self.text_extractor.extract(file_path) - - if "استخراج الجداول" in analysis_options: - # محاكاة استخراج الجداول - tables = self.item_extractor.extract_tables(file_path) - analysis_results["tables"] = tables - - if "استخراج البنود والكميات" in analysis_options: - # محاكاة استخراج البنود - items = self.item_extractor.extract_items(file_path) - analysis_results["items"] = items - - # حفظ البنود المستخرجة في حالة الجلسة - st.session_state.extracted_items = items - - if "استخراج المعلومات الرئيسية" in analysis_options: - # محاكاة استخراج المعلومات الرئيسية - metadata = self.document_parser.extract_metadata(file_path) - analysis_results["metadata"] = metadata - - if "تحليل هيكل المستند" in analysis_options: - # محاكاة تحليل هيكل المستند - structure = { - "sections": [ - {"title": "مقدمة", "level": 1, "page": 1}, - {"title": "نطاق العمل", "level": 1, "page": 2}, - {"title": "المواصفات الفنية", "level": 1, "page": 3}, - {"title": "جدول الكميات", "level": 1, "page": 5}, - {"title": "الشروط الخاصة", "level": 1, "page": 7} - ] - } - analysis_results["structure"] = structure - - # حفظ نتائج التحليل في حالة الجلسة - st.session_state.analyzed_documents.append({ - "file_name": uploaded_file.name, - "file_path": str(file_path), - "analysis_options": analysis_options, - "results": analysis_results, - "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") - }) - - st.success("تم الانتهاء من تحليل المستند!") - - # عرض نتائج التحليل - self._display_analysis_results(analysis_results) - - # عرض سجل التحليلات السابقة - if st.session_state.analyzed_documents: - st.markdown("### سجل التحليلات السابقة") - - for i, doc in enumerate(reversed(st.session_state.analyzed_documents)): - with st.expander(f"{doc['file_name']} ({doc['timestamp']})"): - st.markdown(f"**خيارات التحليل:** {', '.join(doc['analysis_options'])}") - - # عرض نتائج التحليل - self._display_analysis_results(doc['results']) - - # أزرار العمليات - col1, col2 = st.columns(2) - - with col1: - if st.button("إرسال إلى وحدة التسعير", key=f"send_to_pricing_{i}"): - st.success("تم إرسال البيانات إلى وحدة التسعير بنجاح!") - - with col2: - if st.button("تصدير النتائج", key=f"export_results_{i}"): - st.success("تم تصدير النتائج بنجاح!") - - def _render_item_extraction_tab(self): - """عرض تبويب استخراج البنود والكميات""" - - st.markdown("### استخراج البنود والكميات") - - # التحقق من وجود بنود مستخرجة - if not st.session_state.extracted_items: - st.warning("لا توجد بنود مستخرجة. يرجى تحليل مستند أولاً.") - - # عرض بيانات افتراضية للتوضيح - st.markdown("### مثال توضيحي") - - # بيانات افتراضية - sample_items = [ - {"رقم البند": "A1", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", "الوحدة": "م3", "الكمية": 250.0}, - {"رقم البند": "A2", "وصف البند": "توريد وتركيب حديد التسليح للأساسات", "الوحدة": "طن", "الكمية": 25.0}, - {"رقم البند": "A3", "وصف البند": "أعمال العزل المائي للأساسات", "الوحدة": "م2", "الكمية": 500.0}, - {"رقم البند": "A4", "وصف البند": "أعمال الردم والدك للأساسات", "الوحدة": "م3", "الكمية": 300.0}, - {"رقم البند": "A5", "وصف البند": "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", "الوحدة": "م3", "الكمية": 120.0} - ] - - # عرض البنود كجدول - items_df = pd.DataFrame(sample_items) - st.dataframe(items_df, use_container_width=True, hide_index=True) - - # زر لاستخدام البيانات التوضيحية - if st.button("استخدام البيانات التوضيحية", key="use_sample_data_button"): - st.session_state.extracted_items = sample_items - st.success("تم استخدام البيانات التوضيحية!") - st.rerun() - else: - # عرض البنود المستخرجة - items_df = pd.DataFrame(st.session_state.extracted_items) - - # إضافة عمود سعر الوحدة والإجمالي إذا لم يكن موجوداً - if "سعر الوحدة" not in items_df.columns: - items_df["سعر الوحدة"] = 0.0 - - if "الإجمالي" not in items_df.columns: - items_df["الإجمالي"] = 0.0 - - # عرض البنود كجدول قابل للتعديل - st.markdown("### البنود المستخرجة") - edited_df = st.data_editor(items_df, use_container_width=True, hide_index=True, key="items_editor") - - # تحديث البنود المستخرجة بعد التعديل - st.session_state.extracted_items = edited_df.to_dict('records') - - # أزرار العمليات - col1, col2, col3 = st.columns(3) - - with col1: - if st.button("إرسال إلى وحدة التسعير", key="send_to_pricing_button"): - # محاكاة إرسال البيانات إلى وحدة التسعير - if 'current_pricing' not in st.session_state: - st.session_state.current_pricing = { - 'name': "مناقصة جديدة", - 'number': "T-" + time.strftime("%Y-%m-%d"), - 'client': "", - 'location': "", - 'method': "التسعير القياسي", - 'submission_date': None, - 'items': edited_df, - 'status': 'جديد', - 'created_at': time.strftime("%Y-%m-%d %H:%M:%S") - } - else: - st.session_state.current_pricing['items'] = edited_df - - st.success("تم إرسال البنود إلى وحدة التسعير بنجاح!") - - with col2: - if st.button("تصدير إلى Excel", key="export_to_excel_button"): - st.success("تم تصدير البنود إلى Excel بنجاح!") - - with col3: - if st.button("مسح البنود", key="clear_items_button"): - st.session_state.extracted_items = [] - st.warning("تم مسح البنود!") - st.rerun() - - # عرض إحصائيات البنود - st.markdown("### إحصائيات البنود") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("عدد البنود", len(edited_df)) - - with col2: - units_count = edited_df['الوحدة'].value_counts() - most_common_unit = units_count.index[0] if not units_count.empty else "غير متوفر" - st.metric("الوحدة الأكثر استخداماً", most_common_unit) - - with col3: - total_quantity = edited_df['الكمية'].sum() - st.metric("إجمالي الكميات", f"{total_quantity:,.2f}") - - # رسم بياني لتوزيع البنود حسب الوحدة - st.markdown("### توزيع البنود حسب الوحدة") - - units_df = pd.DataFrame(units_count).reset_index() - units_df.columns = ['الوحدة', 'العدد'] - - fig = px.pie( - units_df, - values='العدد', - names='الوحدة', - title='توزيع البنود حسب الوحدة', - hole=0.4 - ) - - st.plotly_chart(fig, use_container_width=True) - - def _render_image_analysis_tab(self): - """عرض تبويب تحليل الصور والمخططات""" - - st.markdown("### تحليل الصور والمخططات") - - # رفع الصورة - uploaded_image = st.file_uploader("رفع صورة أو مخطط للتحليل", type=["jpg", "jpeg", "png", "tif", "tiff"], key="image_upload") - - if uploaded_image is not None: - # عرض الصورة - image = Image.open(uploaded_image) - st.image(image, caption=uploaded_image.name, use_column_width=True) - - # خيارات التحليل - analysis_type = st.selectbox( - "نوع التحليل", - [ - "استخراج النص من الصورة", - "تحليل المخططات الهندسية", - "قياس المساحات والأبعاد", - "تحليل مخصص" - ], - key="image_analysis_type" - ) - - # زر بدء التحليل - if st.button("بدء التحليل", key="start_image_analysis_button"): - with st.spinner("جاري تحليل الصورة..."): - # محاكاة وقت التحليل - time.sleep(2) - - if analysis_type == "استخراج النص من الصورة": - # محاكاة استخراج النص - extracted_text = "نص مستخرج من الصورة (محاكاة):\n\n" - extracted_text += "مواصفات المشروع:\n" - extracted_text += "- مساحة الأرض: 1000 م2\n" - extracted_text += "- عدد الطوابق: 3\n" - extracted_text += "- ارتفاع المبنى: 12 م\n" - - st.markdown("### النص المستخرج من الصورة") - st.text_area("النص المستخرج", extracted_text, height=200) - - elif analysis_type == "تحليل المخططات الهندسية": - # محاكاة تحليل المخططات - st.markdown("### نتائج تحليل المخطط الهندسي") - - # محاكاة رسم تخطيطي للمخطط - fig, ax = plt.subplots(figsize=(10, 6)) - ax.imshow(image) - - # إضافة تعليقات توضيحية - ax.annotate('غرفة المعيشة', xy=(100, 100), xytext=(150, 50), - arrowprops=dict(facecolor='red', shrink=0.05)) - - ax.annotate('المطبخ', xy=(300, 150), xytext=(350, 100), - arrowprops=dict(facecolor='blue', shrink=0.05)) - - ax.annotate('غرفة النوم', xy=(200, 300), xytext=(250, 350), - arrowprops=dict(facecolor='green', shrink=0.05)) - - st.pyplot(fig) - - # عرض معلومات المخطط - st.markdown("### معلومات المخطط") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("المساحة الإجمالية", "150 م2") - - with col2: - st.metric("عدد الغرف", "3") - - with col3: - st.metric("عدد الحمامات", "2") - - elif analysis_type == "قياس المساحات والأبعاد": - # محاكاة قياس المساحات - st.markdown("### نتائج قياس المساحات والأبعاد") - - # محاكاة رسم تخطيطي للمساحات - fig, ax = plt.subplots(figsize=(10, 6)) - ax.imshow(image) - - # إضافة قياسات - ax.plot([50, 250], [50, 50], 'r-', linewidth=2) - ax.text(150, 40, '10 م', color='red', fontsize=12, ha='center') - - ax.plot([50, 50], [50, 250], 'b-', linewidth=2) - ax.text(40, 150, '8 م', color='blue', fontsize=12, va='center', rotation=90) - - st.pyplot(fig) - - # عرض جدول القياسات - measurements = pd.DataFrame({ - 'العنصر': ['الطول', 'العرض', 'المساحة', 'المحيط'], - 'القيمة': ['10 م', '8 م', '80 م2', '36 م'] - }) - - st.dataframe(measurements, use_container_width=True, hide_index=True) - - else: # تحليل مخصص - st.markdown("### نتائج التحليل المخصص") - st.info("تم تحليل الصورة بنجاح. يمكنك تخصيص التحليل حسب احتياجاتك.") - - # خيارات التصدير - col1, col2 = st.columns(2) - - with col1: - if st.button("تصدير نتائج التحليل", key="export_image_analysis_button"): - st.success("تم تصدير نتائج التحليل بنجاح!") - - with col2: - if st.button("إرسال إلى وحدة التسعير", key="send_image_to_pricing_button"): - st.success("تم إرسال نتائج التحليل إلى وحدة التسعير بنجاح!") - - def _render_document_library_tab(self): - """عرض تبويب مكتبة المستندات""" - - st.markdown("### مكتبة المستندات") - - # بيانات افتراضية للمستندات - if 'document_library' not in st.session_state: - st.session_state.document_library = [ - { - "id": 1, - "name": "كراسة شروط مشروع توسعة مستشفى الملك فهد", - "type": "PDF", - "size": "5.2 MB", - "pages": 120, - "upload_date": "2024-01-15", - "category": "كراسات الشروط", - "tags": ["صحي", "مستشفى", "توسعة"] - }, - { - "id": 2, - "name": "جدول كميات صيانة محطات المياه", - "type": "Excel", - "size": "1.8 MB", - "pages": None, - "upload_date": "2024-02-10", - "category": "جداول الكميات", - "tags": ["مياه", "صيانة", "محطات"] - }, - { - "id": 3, - "name": "مخططات إنشاء مدرسة ثانوية", - "type": "PDF", - "size": "12.5 MB", - "pages": 45, - "upload_date": "2024-02-25", - "category": "مخططات", - "tags": ["تعليم", "مدرسة", "إنشاء"] - }, - { - "id": 4, - "name": "عقد إنشاء طريق دائري", - "type": "Word", - "size": "0.9 MB", - "pages": 28, - "upload_date": "2024-03-05", - "category": "عقود", - "tags": ["طرق", "إنشاء", "دائري"] - }, - { - "id": 5, - "name": "تقرير فني لمشروع تطوير شبكة مياه", - "type": "PDF", - "size": "3.7 MB", - "pages": 65, - "upload_date": "2024-03-15", - "category": "تقارير فنية", - "tags": ["مياه", "شبكة", "تطوير"] - } - ] - - # البحث في المكتبة - search_query = st.text_input("البحث في المكتبة", key="library_search") - - col1, col2, col3 = st.columns(3) - - with col1: - category_filter = st.selectbox( - "تصفية حسب الفئة", - ["الكل", "كراسات الشروط", "جداول الكميات", "مخططات", "عقود", "تقارير فنية"], - key="category_filter" - ) - - with col2: - type_filter = st.selectbox( - "تصفية حسب النوع", - ["الكل", "PDF", "Word", "Excel", "Image"], - key="type_filter" - ) - - with col3: - sort_by = st.selectbox( - "ترتيب حسب", - ["تاريخ الرفع (الأحدث أولاً)", "تاريخ الرفع (الأقدم أولاً)", "الاسم (أ-ي)", "الاسم (ي-أ)", "الحجم (الأكبر أولاً)", "الحجم (الأصغر أولاً)"], - key="sort_by" - ) - - # تطبيق التصفية والبحث - filtered_documents = st.session_state.document_library.copy() - - # تطبيق البحث - if search_query: - filtered_documents = [doc for doc in filtered_documents if search_query.lower() in doc["name"].lower() or - any(search_query.lower() in tag.lower() for tag in doc["tags"])] - - # تطبيق تصفية الفئة - if category_filter != "الكل": - filtered_documents = [doc for doc in filtered_documents if doc["category"] == category_filter] - - # تطبيق تصفية النوع - if type_filter != "الكل": - filtered_documents = [doc for doc in filtered_documents if doc["type"] == type_filter] - - # تطبيق الترتيب - if sort_by == "تاريخ الرفع (الأحدث أولاً)": - filtered_documents.sort(key=lambda x: x["upload_date"], reverse=True) - elif sort_by == "تاريخ الرفع (الأقدم أولاً)": - filtered_documents.sort(key=lambda x: x["upload_date"]) - elif sort_by == "الاسم (أ-ي)": - filtered_documents.sort(key=lambda x: x["name"]) - elif sort_by == "الاسم (ي-أ)": - filtered_documents.sort(key=lambda x: x["name"], reverse=True) - elif sort_by == "الحجم (الأكبر أولاً)": - filtered_documents.sort(key=lambda x: float(x["size"].split()[0]), reverse=True) - elif sort_by == "الحجم (الأصغر أولاً)": - filtered_documents.sort(key=lambda x: float(x["size"].split()[0])) - - # عرض المستندات - st.markdown(f"### المستندات ({len(filtered_documents)})") - - if not filtered_documents: - st.info("لا توجد مستندات تطابق معايير البحث.") - else: - # عرض المستندات كبطاقات - for i, doc in enumerate(filtered_documents): - with st.container(): - col1, col2, col3 = st.columns([3, 1, 1]) - - with col1: - st.markdown(f"**{doc['name']}**") - st.markdown(f"الفئة: {doc['category']} | النوع: {doc['type']} | الحجم: {doc['size']} | تاريخ الرفع: {doc['upload_date']}") - st.markdown(f"الوسوم: {', '.join(doc['tags'])}") - - with col2: - if st.button("عرض", key=f"view_doc_{i}"): - st.session_state.selected_document = doc - st.success(f"جاري عرض المستند: {doc['name']}") - - with col3: - if st.button("تحليل", key=f"analyze_doc_{i}"): - st.session_state.selected_document = doc - st.success(f"جاري تحليل المستند: {doc['name']}") - - st.markdown("---") - - # رفع مستند جديد - st.markdown("### رفع مستند جديد") - - uploaded_file = st.file_uploader("اختر ملفاً للرفع", type=["pdf", "docx", "xlsx", "jpg", "jpeg", "png"], key="library_upload") - - if uploaded_file is not None: - col1, col2 = st.columns(2) - - with col1: - doc_category = st.selectbox( - "فئة المستند", - ["كراسات الشروط", "جداول الكميات", "مخططات", "عقود", "تقارير فنية", "أخرى"], - key="doc_category" - ) - - with col2: - doc_tags = st.text_input("الوسوم (مفصولة بفواصل)", key="doc_tags") - - if st.button("رفع المستند", key="upload_to_library_button"): - # محاكاة رفع المستند - new_doc = { - "id": len(st.session_state.document_library) + 1, - "name": uploaded_file.name, - "type": uploaded_file.name.split(".")[-1].upper(), - "size": f"{uploaded_file.size / (1024 * 1024):.1f} MB", - "pages": None, - "upload_date": time.strftime("%Y-%m-%d"), - "category": doc_category, - "tags": [tag.strip() for tag in doc_tags.split(",") if tag.strip()] - } - - st.session_state.document_library.append(new_doc) - st.success(f"تم رفع المستند: {uploaded_file.name}") - st.rerun() - - def _render_settings_tab(self): - """عرض تبويب الإعدادات""" - - st.markdown("### إعدادات تحليل المستندات") - - # إعدادات استخراج النص - with st.expander("إعدادات استخراج النص", expanded=True): - st.markdown("#### إعدادات استخراج النص") - - ocr_engine = st.selectbox( - "محرك التعرف الضوئي على النصوص", - ["Tesseract OCR", "Google Cloud Vision", "Amazon Textract", "Microsoft Azure OCR"], - index=0, - key="ocr_engine" - ) - - language = st.selectbox( - "لغة المستندات", - ["العربية", "الإنجليزية", "العربية والإنجليزية"], - index=0, - key="ocr_language" - ) - - dpi = st.slider( - "دقة المسح (DPI)", - min_value=100, - max_value=600, - value=300, - step=50, - key="ocr_dpi" - ) - - if st.button("حفظ إعدادات استخراج النص", key="save_ocr_settings"): - st.success("تم حفظ إعدادات استخراج النص بنجاح!") - - # إعدادات استخراج البنود - with st.expander("إعدادات استخراج البنود", expanded=True): - st.markdown("#### إعدادات استخراج البنود") - - extraction_method = st.selectbox( - "طريقة استخراج البنود", - ["تحليل الجداول", "تحليل النص", "الذكاء الاصطناعي", "مزيج"], - index=3, - key="extraction_method" - ) - - auto_detect_units = st.checkbox( - "اكتشاف الوحدات تلقائياً", - value=True, - key="auto_detect_units" - ) - - normalize_quantities = st.checkbox( - "توحيد صيغة الكميات", - value=True, - key="normalize_quantities" - ) - - if st.button("حفظ إعدادات استخراج البنود", key="save_extraction_settings"): - st.success("تم حفظ إعدادات استخراج البنود بنجاح!") - - # إعدادات تحليل الصور - with st.expander("إعدادات تحليل الصور", expanded=True): - st.markdown("#### إعدادات تحليل الصور") - - image_analysis_engine = st.selectbox( - "محرك تحليل الصور", - ["OpenCV", "Google Cloud Vision", "Amazon Rekognition", "Microsoft Azure Computer Vision"], - index=0, - key="image_analysis_engine" - ) - - image_resolution = st.slider( - "دقة تحليل الصور", - min_value=1, - max_value=10, - value=5, - key="image_resolution" - ) - - if st.button("حفظ إعدادات تحليل الصور", key="save_image_analysis_settings"): - st.success("تم حفظ إعدادات تحليل الصور بنجاح!") - - # إعدادات متقدمة - with st.expander("إعدادات متقدمة", expanded=False): - st.markdown("#### إعدادات متقدمة") - - temp_files_retention = st.slider( - "مدة الاحتفاظ بالملفات المؤقتة (أيام)", - min_value=1, - max_value=30, - value=7, - key="temp_files_retention" - ) - - max_file_size = st.slider( - "الحد الأقصى لحجم الملف (ميجابايت)", - min_value=5, - max_value=100, - value=50, - key="max_file_size" - ) - - parallel_processing = st.checkbox( - "تفعيل المعالجة المتوازية", - value=True, - key="parallel_processing" - ) - - if st.button("حفظ الإعدادات المتقدمة", key="save_advanced_settings"): - st.success("تم حفظ الإعدادات المتقدمة بنجاح!") - - def _save_uploaded_file(self, uploaded_file): - """حفظ الملف المرفوع في مجلد مؤقت""" - try: - file_path = self.temp_dir / uploaded_file.name - with open(file_path, "wb") as f: - f.write(uploaded_file.getbuffer()) - return file_path - except Exception as e: - st.error(f"حدث خطأ أثناء حفظ الملف: {str(e)}") - return None - - def _get_file_info(self, file_path): - """الحصول على معلومات الملف""" - file_info = { - "type": file_path.suffix[1:].upper(), - "size": f"{file_path.stat().st_size / (1024 * 1024):.2f} MB" - } - - # محاولة الحصول على عدد الصفحات للملفات المدعومة - if file_path.suffix.lower() == ".pdf": - # محاكاة عدد الصفحات - file_info["pages"] = 10 - - return file_info - - def _display_analysis_results(self, results): - """عرض نتائج التحليل""" - - if not results: - st.info("لا توجد نتائج للعرض.") - return - - # عرض النص المستخرج - if "text" in results: - with st.expander("النص المستخرج", expanded=False): - st.text_area("النص", results["text"], height=200) - - # عرض الجداول المستخرجة - if "tables" in results and results["tables"]: - with st.expander("الجداول المستخرجة", expanded=True): - for i, table in enumerate(results["tables"]): - st.markdown(f"**جدول {i+1}: {table.get('عنوان', 'بدون عنوان')}**") - - if "بيانات" in table and table["بيانات"]: - # محاولة عرض البيانات كجدول - try: - df = pd.DataFrame(table["بيانات"]) - st.dataframe(df, use_container_width=True, hide_index=True) - except Exception: - st.text(str(table["بيانات"])) - else: - st.info("لا توجد بيانات في هذا الجدول.") - - # عرض البنود المستخرجة - if "items" in results and results["items"]: - with st.expander("البنود المستخرجة", expanded=True): - items_df = pd.DataFrame(results["items"]) - st.dataframe(items_df, use_container_width=True, hide_index=True) - - # زر لإرسال البنود إلى وحدة التسعير - if st.button("إرسال البنود إلى وحدة التسعير", key="send_extracted_items_button"): - st.session_state.extracted_items = results["items"] - st.success("تم إرسال البنود المستخرجة إلى وحدة التسعير!") - - # عرض المعلومات الرئيسية - if "metadata" in results: - with st.expander("المعلومات الرئيسية", expanded=True): - metadata = results["metadata"] - - col1, col2 = st.columns(2) - - with col1: - st.markdown(f"**عنوان المستند:** {metadata.get('title', 'غير متوفر')}") - st.markdown(f"**المؤلف:** {metadata.get('author', 'غير متوفر')}") - - with col2: - st.markdown(f"**التاريخ:** {metadata.get('date', 'غير متوفر')}") - st.markdown(f"**عدد الصفحات:** {metadata.get('pages', 'غير متوفر')}") - - # عرض هيكل المستند - if "structure" in results and "sections" in results["structure"]: - with st.expander("هيكل المستند", expanded=False): - sections = results["structure"]["sections"] - - for section in sections: - indent = " " * (section["level"] * 4) - st.markdown(f"{indent}• **{section['title']}** (صفحة {section['page']})", unsafe_allow_html=True) diff --git a/modules/document_analysis/services/__init__.py b/modules/document_analysis/services/__init__.py deleted file mode 100644 index 42eb7368d3ae75ba2c3fae7fb957915352f2f178..0000000000000000000000000000000000000000 --- a/modules/document_analysis/services/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -حزمة خدمات تحليل المستندات - -توفر هذه الحزمة الأدوات والخدمات اللازمة لتحليل المستندات بمختلف أنواعها -واستخراج النصوص والبيانات المنظمة منها. -""" - -# استيراد الفئات الرئيسية -from .text_extractor import TextExtractor -from .item_extractor import ItemExtractor -from .document_parser import DocumentParser - -# تحديد الفئات التي يمكن استيرادها عند استخدام from services import * -__all__ = [ - 'TextExtractor', - 'ItemExtractor', - 'DocumentParser', -] - -# معلومات الإصدار -__version__ = '0.1.0' -__author__ = 'فريق تطوير تحليل المستندات' \ No newline at end of file diff --git a/modules/document_analysis/services/document_parser.py b/modules/document_analysis/services/document_parser.py deleted file mode 100644 index 7239cda8f37c3765bf7bdb762706d980163eac35..0000000000000000000000000000000000000000 --- a/modules/document_analysis/services/document_parser.py +++ /dev/null @@ -1,219 +0,0 @@ -# -*- coding: utf-8 -*- -""" -خدمة تحليل المستندات - -هذا الملف يحتوي على الفئة المسؤولة عن تحليل المستندات واستخراج المعلومات الهيكلية منها. -""" - -import os -import logging -import datetime - -class DocumentParser: - """فئة تحليل المستندات واستخراج المعلومات منها""" - - def __init__(self, config=None): - """ - تهيئة محلل المستندات - - المعلمات: - config (dict): إعدادات محلل المستندات - """ - self.config = config or {} - self.logger = logging.getLogger(__name__) - - def parse(self, file_path): - """ - تحليل المستند واستخراج المعلومات منه - - المعلمات: - file_path (str): مسار الملف - - العوائد: - dict: معلومات المستند المستخرجة - """ - self.logger.info(f"جاري تحليل المستند: {file_path}") - - try: - # في البيئة الحقيقية، استخدم تحليل متقدم للمستند - # محاكاة التحليل للعرض - file_name = os.path.basename(file_path) - file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0 - - # تحديد نوع الملف - _, ext = os.path.splitext(file_path) - ext = ext.lower() - - # تحديد نوع المستند - document_type = self._get_document_type(ext) - - # محاكاة معلومات المستند - current_date = datetime.datetime.now().strftime("%Y-%m-%d") - - result = { - "اسم الملف": file_name, - "حجم الملف": f"{file_size / 1024:.2f} كيلوبايت", - "نوع الملف": document_type, - "تاريخ التحليل": current_date, - "تقدير عدد الصفحات": self._estimate_pages(file_size), - "نتائج التحليل": { - "نوع المستند": self._classify_document(file_name), - "درجة الثقة": "85%", - "الأقسام الرئيسية": self._get_main_sections(), - "الكلمات الرئيسية": self._get_main_keywords(), - "الشروط الهامة": self._get_important_terms() - } - } - - return result - except Exception as e: - self.logger.error(f"خطأ في تحليل المستند: {str(e)}") - return {"خطأ": f"حدث خطأ أثناء تحليل المستند: {str(e)}"} - - def parse_document(self, file_path): - """ - تحليل المستند واستخراج المعلومات الأساسية منه - - المعلمات: - file_path (str): مسار الملف - - العوائد: - dict: معلومات المستند الأساسية - """ - self.logger.info(f"جاري تحليل المستند الأساسي: {file_path}") - - # في البيئة الحقيقية، استخدم تحليل متقدم للمستند - # محاكاة التحليل للعرض - file_name = os.path.basename(file_path) - - return { - "نوع": self._classify_document(file_name), - "محتوى": "محتوى المستند...", - "هيكل": { - "عنوان": "عنوان المستند", - "أقسام": ["قسم 1", "قسم 2", "قسم 3"] - } - } - - def _get_document_type(self, ext): - """ - تحديد نوع المستند من امتداد الملف - - المعلمات: - ext (str): امتداد الملف - - العوائد: - str: نوع المستند - """ - document_types = { - '.pdf': 'مستند PDF', - '.doc': 'مستند Word', - '.docx': 'مستند Word', - '.jpg': 'صورة JPEG', - '.jpeg': 'صورة JPEG', - '.png': 'صورة PNG', - '.xlsx': 'جدول Excel', - '.xls': 'جدول Excel', - '.txt': 'ملف نصي' - } - - return document_types.get(ext, 'نوع ملف غير معروف') - - def _estimate_pages(self, file_size): - """ - تقدير عدد صفحات المستند بناءً على حجمه - - المعلمات: - file_size (int): حجم الملف بالبايت - - العوائد: - int: تقدير عدد الصفحات - """ - # تقدير بسيط: كل 50 كيلوبايت تقريباً صفحة واحدة - # هذا تقدير بسيط جداً ويختلف حسب نوع المستند ومحتواه - return max(1, int(file_size / (50 * 1024))) - - def _classify_document(self, file_name): - """ - تصنيف نوع المستند بناءً على اسمه - - المعلمات: - file_name (str): اسم الملف - - العوائد: - str: تصنيف المستند - """ - file_name_lower = file_name.lower() - - if 'عقد' in file_name_lower or 'contract' in file_name_lower: - return "عقد" - elif 'مناقصة' in file_name_lower or 'tender' in file_name_lower: - return "مستند مناقصة" - elif 'تقرير' in file_name_lower or 'report' in file_name_lower: - return "تقرير" - elif 'فاتورة' in file_name_lower or 'invoice' in file_name_lower: - return "فاتورة" - elif 'عرض' in file_name_lower or 'proposal' in file_name_lower: - return "عرض سعر" - elif 'مواصفات' in file_name_lower or 'spec' in file_name_lower: - return "مواصفات فنية" - elif 'كراسة' in file_name_lower or 'شروط' in file_name_lower: - return "كراسة شروط" - else: - return "مستند عام" - - def _get_main_sections(self): - """ - الحصول على قائمة الأقسام الرئيسية التقديرية للمستند - - العوائد: - list: قائمة الأقسام الرئيسية - """ - # محاكاة قائمة الأقسام - return [ - "مقدمة", - "نطاق العمل", - "المواصفات الفنية", - "جدول الكميات", - "الشروط والأحكام", - "الجدول الزمني", - "المتطلبات الخاصة" - ] - - def _get_main_keywords(self): - """ - الحصول على قائمة الكلمات الرئيسية التقديرية للمستند - - العوائد: - list: قائمة الكلمات الرئيسية - """ - # محاكاة قائمة الكلمات الرئيسية - return [ - "مناقصة", - "بناء", - "تشييد", - "تسليم مفتاح", - "مواصفات فنية", - "جدول كميات", - "ضمان", - "غرامة تأخير", - "دفعة مقدمة", - "محتوى محلي" - ] - - def _get_important_terms(self): - """ - الحصول على قائمة الشروط الهامة التقديرية للمستند - - العوائد: - list: قائمة الشروط الهامة - """ - # محاكاة قائمة الشروط الهامة - return [ - "مدة تنفيذ المشروع: 18 شهر", - "غرامة التأخير: 0.5% أسبوعياً بحد أقصى 10%", - "الدفعة المقدمة: 10%", - "الضمان النهائي: 5% لمدة سنة", - "شروط الدفع: دفعات شهرية حسب نسبة الإنجاز", - "المحتوى المحلي: 70% كحد أدنى" - ] \ No newline at end of file diff --git a/modules/document_analysis/services/item_extractor.py b/modules/document_analysis/services/item_extractor.py deleted file mode 100644 index d9b18ea6467356b5254cd2583fe097157867e0fe..0000000000000000000000000000000000000000 --- a/modules/document_analysis/services/item_extractor.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -""" -خدمة استخراج البنود من المستندات - -هذا الملف يحتوي على الفئة المسؤولة عن استخراج البنود والجداول من المستندات. -""" - -import os -import logging - -class ItemExtractor: - """فئة استخراج البنود من المستندات""" - - def __init__(self, config=None): - """ - تهيئة مستخرج البنود - - المعلمات: - config (dict): إعدادات مستخرج البنود - """ - self.config = config or {} - self.logger = logging.getLogger(__name__) - - def extract(self, file_path): - """ - استخراج البنود من ملف - - المعلمات: - file_path (str): مسار الملف - - العوائد: - list: قائمة البنود المستخرجة - """ - self.logger.info(f"جاري استخراج البنود من الملف: {file_path}") - - try: - # في البيئة الحقيقية، استخدم تحليل متقدم للمستند - # محاكاة الاستخراج للعرض - file_name = os.path.basename(file_path) - - # تحديد نوع الملف - _, ext = os.path.splitext(file_path) - ext = ext.lower() - - if ext == '.pdf': - return self._extract_items_from_pdf(file_path) - elif ext in ('.doc', '.docx'): - return self._extract_items_from_docx(file_path) - else: - return [{"بند": "نوع الملف غير مدعوم", "قيمة": 0}] - except Exception as e: - self.logger.error(f"خطأ في استخراج البنود: {str(e)}") - return [{"بند": "حدث خطأ أثناء الاستخراج", "قيمة": 0, "خطأ": str(e)}] - - def _extract_items_from_pdf(self, file_path): - """ - استخراج البنود من ملف PDF - - المعلمات: - file_path (str): مسار ملف PDF - - العوائد: - list: قائمة البنود المستخرجة - """ - # في البيئة الحقيقية، استخدم تحليل متقدم للمستند - # محاكاة الاستخراج للعرض - return [ - {"بند": "أعمال الخرسانة", "وحدة": "م3", "كمية": 250, "سعر الوحدة": 1200, "الإجمالي": 300000}, - {"بند": "أعمال التشطيبات", "وحدة": "م2", "كمية": 1500, "سعر الوحدة": 350, "الإجمالي": 525000}, - {"بند": "أعمال الكهرباء", "وحدة": "نقطة", "كمية": 120, "سعر الوحدة": 200, "الإجمالي": 24000}, - {"بند": "أعمال السباكة", "وحدة": "نقطة", "كمية": 75, "سعر الوحدة": 300, "الإجمالي": 22500}, - {"بند": "أعمال الألمنيوم", "وحدة": "م2", "كمية": 85, "سعر الوحدة": 1500, "الإجمالي": 127500} - ] - - def _extract_items_from_docx(self, file_path): - """ - استخراج البنود من ملف Word - - المعلمات: - file_path (str): مسار ملف Word - - العوائد: - list: قائمة البنود المستخرجة - """ - # في البيئة الحقيقية، استخدم تحليل متقدم للمستند - # محاكاة الاستخراج للعرض - return [ - {"بند": "استشارات هندسية", "وحدة": "ساعة", "كمية": 120, "سعر الوحدة": 500, "الإجمالي": 60000}, - {"بند": "تصميم معماري", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 100, "الإجمالي": 180000}, - {"بند": "تصميم إنشائي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 80, "الإجمالي": 144000}, - {"بند": "تصميم كهربائي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 40, "الإجمالي": 72000}, - {"بند": "تصميم ميكانيكي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 40, "الإجمالي": 72000} - ] - - def extract_tables(self, document): - """ - استخراج الجداول من مستند - - المعلمات: - document (dict): المستند المحلل - - العوائد: - list: قائمة الجداول المستخرجة - """ - self.logger.info("جاري استخراج الجداول من المستند") - - try: - # في البيئة الحقيقية، استخدم تحليل متقدم للمستند - # محاكاة الاستخراج للعرض - return [ - { - "عنوان": "جدول البنود والتكاليف", - "بيانات": [ - {"بند": "أعمال الخرسانة", "وحدة": "م3", "كمية": 250, "سعر الوحدة": 1200, "الإجمالي": 300000}, - {"بند": "أعمال التشطيبات", "وحدة": "م2", "كمية": 1500, "سعر الوحدة": 350, "الإجمالي": 525000}, - {"بند": "أعمال الكهرباء", "وحدة": "نقطة", "كمية": 120, "سعر الوحدة": 200, "الإجمالي": 24000}, - {"بند": "أعمال السباكة", "وحدة": "نقطة", "كمية": 75, "سعر الوحدة": 300, "الإجمالي": 22500}, - {"بند": "أعمال الألمنيوم", "وحدة": "م2", "كمية": 85, "سعر الوحدة": 1500, "الإجمالي": 127500} - ] - }, - { - "عنوان": "جدول المعلومات العامة", - "بيانات": [ - {"اسم المشروع": "مبنى سكني", "المالك": "شركة الإسكان", "الموقع": "الرياض", "المساحة": "2500 م2"}, - {"اسم المشروع": "مبنى تجاري", "المالك": "شركة التطوير", "الموقع": "جدة", "المساحة": "3500 م2"} - ] - } - ] - except Exception as e: - self.logger.error(f"خطأ في استخراج الجداول: {str(e)}") - return [{"عنوان": "حدث خطأ أثناء الاستخراج", "بيانات": []}] \ No newline at end of file diff --git a/modules/document_analysis/services/text_extractor.py b/modules/document_analysis/services/text_extractor.py deleted file mode 100644 index 078fd4c490b1b2170ab1b9767bed978d885eeb27..0000000000000000000000000000000000000000 --- a/modules/document_analysis/services/text_extractor.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- -""" -خدمة استخراج النص من المستندات - -هذا الملف يحتوي على الفئة المسؤولة عن استخراج النص من أنواع مختلفة من المستندات. -""" - -import os -import logging - -class TextExtractor: - """فئة استخراج النص من المستندات""" - - def __init__(self, config=None): - """ - تهيئة مستخرج النص - - المعلمات: - config (dict): إعدادات مستخرج النص - """ - self.config = config or {} - self.logger = logging.getLogger(__name__) - - def extract(self, file_path): - """ - استخراج النص من ملف بناءً على نوع الملف - - المعلمات: - file_path (str): مسار الملف - - العوائد: - str: النص المستخرج - """ - _, 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: - self.logger.warning(f"نوع ملف غير مدعوم: {ext}") - return f"نوع ملف غير مدعوم: {ext}" - - def extract_from_pdf(self, file_path): - """ - استخراج النص من ملف PDF - - المعلمات: - file_path (str): مسار ملف PDF - - العوائد: - str: النص المستخرج - """ - self.logger.info(f"جاري استخراج النص من ملف PDF: {file_path}") - - try: - # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل PyPDF2 أو pdfplumber - # محاكاة الاستخراج للعرض - return f"هذا نص مستخرج من ملف PDF: {os.path.basename(file_path)}\n\nيتم استخراج النص من الملف باستخدام مكتبة مناسبة في البيئة الحقيقية." - except Exception as e: - self.logger.error(f"خطأ في استخراج النص من PDF: {str(e)}") - return f"حدث خطأ أثناء استخراج النص: {str(e)}" - - def extract_from_docx(self, file_path): - """ - استخراج النص من ملف Word - - المعلمات: - file_path (str): مسار ملف Word - - العوائد: - str: النص المستخرج - """ - self.logger.info(f"جاري استخراج النص من ملف Word: {file_path}") - - try: - # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل python-docx - # محاكاة الاستخراج للعرض - return f"هذا نص مستخرج من ملف Word: {os.path.basename(file_path)}\n\nيتم استخراج النص من الملف باستخدام مكتبة مناسبة في البيئة الحقيقية." - except Exception as e: - self.logger.error(f"خطأ في استخراج النص من Word: {str(e)}") - return f"حدث خطأ أثناء استخراج النص: {str(e)}" - - def extract_from_image(self, file_path): - """ - استخراج النص من ملف صورة باستخدام OCR - - المعلمات: - file_path (str): مسار ملف الصورة - - العوائد: - str: النص المستخرج - """ - self.logger.info(f"جاري استخراج النص من ملف صورة: {file_path}") - - try: - # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل pytesseract - # محاكاة الاستخراج للعرض - return f"هذا نص مستخرج من ملف صورة: {os.path.basename(file_path)}\n\nيتم استخراج النص من الصورة باستخدام تقنية OCR في البيئة الحقيقية." - except Exception as e: - self.logger.error(f"خطأ في استخراج النص من الصورة: {str(e)}") - return f"حدث خطأ أثناء استخراج النص: {str(e)}" \ No newline at end of file diff --git a/modules/document_comparison/__init__.py b/modules/document_comparison/__init__.py deleted file mode 100644 index f79b7f4986453a0c9a965ca015ff0b39d5b63f6f..0000000000000000000000000000000000000000 --- a/modules/document_comparison/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -""" -وحدة مقارنة المستندات المتقدمة -""" \ No newline at end of file diff --git a/modules/document_comparison/comparison_app.py b/modules/document_comparison/comparison_app.py deleted file mode 100644 index 6b51f3c3975cda417d15fa9c3a514ad0c0cd272b..0000000000000000000000000000000000000000 --- a/modules/document_comparison/comparison_app.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -تطبيق مقارنة المستندات المتقدمة -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات مقارنة المستندات -from modules.document_comparison.document_comparator import DocumentComparator - - -class DocumentComparisonApp: - """تطبيق مقارنة المستندات المتقدمة""" - - def __init__(self): - """تهيئة تطبيق مقارنة المستندات""" - self.comparator = DocumentComparator() - - def render(self): - """عرض واجهة المستخدم الرئيسية للتطبيق""" - self.comparator.render() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="مقارنة المستندات المتقدمة | WAHBi AI", - page_icon="📄", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = DocumentComparisonApp() - app.render() \ No newline at end of file diff --git a/modules/document_comparison/document_comparator.py b/modules/document_comparison/document_comparator.py deleted file mode 100644 index 229fceca3ec16033b80e1e7ff64d54bf0500d4d4..0000000000000000000000000000000000000000 --- a/modules/document_comparison/document_comparator.py +++ /dev/null @@ -1,1503 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة مقارنة المستندات المتقدمة لتحليل الفروقات بين نسخ المستندات -""" - -import os -import sys -import json -import re -import difflib -import Levenshtein -from datetime import datetime -import numpy as np -import pandas as pd -import streamlit as st -import plotly.express as px -import plotly.graph_objects as go -from collections import Counter -from nltk.tokenize import sent_tokenize, word_tokenize -from rouge_score import rouge_scorer -from PyPDF2 import PdfReader -import io - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد المكونات المساعدة -from utils.helpers import create_directory_if_not_exists, format_time, get_user_info - - -class DocumentComparator: - """فئة مقارنة المستندات المتقدمة""" - - def __init__(self): - """تهيئة مقارن المستندات""" - self.comparison_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'document_comparison') - create_directory_if_not_exists(self.comparison_dir) - - # تهيئة NLTK وتنزيل حزمة punkt إذا لم تكن موجودة - self._initialize_nltk() - - # إعداد مقيم ROUGE لمقارنة النصوص - self.rouge_scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=False) - - def _initialize_nltk(self): - """تهيئة مكتبة NLTK وتنزيل الحزم المطلوبة""" - try: - # استيراد nltk - import nltk - - # قائمة بالحزم المطلوبة - required_packages = ['punkt', 'stopwords', 'wordnet'] - for package in required_packages: - try: - # محاولة استخدام الحزمة أولاً، وإذا فشلت يتم تنزيلها - nltk.data.find(f'tokenizers/{package}') - except LookupError: - print(f"تنزيل حزمة NLTK: {package}") - nltk.download(package, quiet=True) - - # محاولة استخدام sent_tokenize للتحقق من وجود حزمة punkt - from nltk.tokenize import sent_tokenize - sent_tokenize("This is a test sentence.") - except LookupError: - # تنزيل حزمة punkt تلقائيًا إذا لم تكن موجودة - import nltk - nltk.download('punkt', quiet=True) - # طباعة رسالة تأكيد التنزيل - st.info("تم تنزيل حزمة NLTK punkt بنجاح للاستخدام في مقارنة المستندات.") - - def _preprocess_text(self, text): - """معالجة النص قبل التحليل""" - # إزالة الأرقام والرموز الخاصة والمسافات الزائدة - text = re.sub(r'\s+', ' ', text) - text = text.strip() - return text - - def _segment_text(self, text): - """تقسيم النص إلى فقرات وجمل""" - # تقسيم النص إلى فقرات - paragraphs = [p.strip() for p in text.split('\n') if p.strip()] - - # تقسيم كل فقرة إلى جمل - sentences = [] - for paragraph in paragraphs: - paragraph_sentences = sent_tokenize(paragraph) - sentences.extend(paragraph_sentences) - - return paragraphs, sentences - - def _calculate_similarity(self, text1, text2): - """حساب نسبة التشابه بين نصين""" - # حساب نسبة التشابه باستخدام مقياس Levenshtein - ratio = Levenshtein.ratio(text1, text2) - - # حساب درجات ROUGE - rouge_scores = self.rouge_scorer.score(text1, text2) - - # حساب متوسط نقاط Rouge - rouge1_f1 = rouge_scores['rouge1'].fmeasure - rouge2_f1 = rouge_scores['rouge2'].fmeasure - rougeL_f1 = rouge_scores['rougeL'].fmeasure - avg_rouge = (rouge1_f1 + rouge2_f1 + rougeL_f1) / 3 - - # دمج النقاط للحصول على نتيجة نهائية - combined_score = (ratio + avg_rouge) / 2 - - return { - 'levenshtein_ratio': ratio, - 'rouge1_f1': rouge1_f1, - 'rouge2_f1': rouge2_f1, - 'rougeL_f1': rougeL_f1, - 'avg_rouge': avg_rouge, - 'combined_score': combined_score - } - - def _extract_text_from_pdf(self, pdf_file): - """استخراج النص من ملف PDF""" - text = "" - try: - # قراءة ملف PDF - pdf_reader = PdfReader(pdf_file) - - # استخراج النص من كل صفحة - for page in pdf_reader.pages: - text += page.extract_text() + "\n" - except Exception as e: - st.error(f"خطأ في قراءة ملف PDF: {e}") - - return text - - def get_document_diff(self, text1, text2, title1="المستند الأول", title2="المستند الثاني"): - """حساب الفروقات بين نصين""" - if not text1 or not text2: - return { - "title1": title1, - "title2": title2, - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "similarity": 0, - "similarity_score": 0, - "text_diffs": [], - "summary": "أحد المستندات فارغ، لا يمكن إجراء المقارنة." - } - - # معالجة النصوص - preprocessed_text1 = self._preprocess_text(text1) - preprocessed_text2 = self._preprocess_text(text2) - - # حساب نسبة التشابه الإجمالية - similarity_metrics = self._calculate_similarity(preprocessed_text1, preprocessed_text2) - similarity_score = similarity_metrics['combined_score'] - similarity_percentage = int(similarity_score * 100) - - # تقسيم النصوص إلى فقرات وجمل - paragraphs1, sentences1 = self._segment_text(text1) - paragraphs2, sentences2 = self._segment_text(text2) - - # تحديد الفروقات بين الجمل باستخدام difflib - differ = difflib.Differ() - sentence_diffs = [] - - # مصفوفة التشابه بين الجمل - similarity_matrix = np.zeros((len(sentences1), len(sentences2))) - for i, s1 in enumerate(sentences1): - for j, s2 in enumerate(sentences2): - similarity_matrix[i, j] = Levenshtein.ratio(s1, s2) - - # تحديد أفضل مطابقة لكل جملة - matched_sentences2 = set() # تتبع الجمل المطابقة في المستند الثاني - - for i, s1 in enumerate(sentences1): - if len(s1.split()) < 3: # تجاهل الجمل القصيرة جداً - continue - - best_match_idx = -1 - best_match_score = 0.7 # عتبة التشابه - - for j, s2 in enumerate(sentences2): - if j in matched_sentences2: - continue # تجاهل الجمل التي تم مطابقتها بالفعل - - if len(s2.split()) < 3: # تجاهل الجمل القصيرة جداً - continue - - score = similarity_matrix[i, j] - if score > best_match_score and score > 0.7: - best_match_score = score - best_match_idx = j - - if best_match_idx != -1: - # وجدنا تطابق، تحديد الفروقات باستخدام difflib - s2 = sentences2[best_match_idx] - diff = list(differ.compare(s1.split(), s2.split())) - - # تحويل مخرجات difflib إلى تنسيق أسهل للاستخدام - formatted_diff = [] - for token in diff: - if token.startswith(' '): # متطابق - formatted_diff.append({'text': token[2:], 'status': 'same'}) - elif token.startswith('- '): # حذف - formatted_diff.append({'text': token[2:], 'status': 'removed'}) - elif token.startswith('+ '): # إضافة - formatted_diff.append({'text': token[2:], 'status': 'added'}) - - sentence_diffs.append({ - 'doc1_idx': i, - 'doc2_idx': best_match_idx, - 'doc1_text': s1, - 'doc2_text': s2, - 'similarity': best_match_score, - 'diff': formatted_diff - }) - - matched_sentences2.add(best_match_idx) - else: - # لم نجد تطابق، هذه الجملة غير موجودة في المستند الثاني - sentence_diffs.append({ - 'doc1_idx': i, - 'doc2_idx': -1, - 'doc1_text': s1, - 'doc2_text': "", - 'similarity': 0, - 'diff': [{'text': word, 'status': 'removed'} for word in s1.split()] - }) - - # تحديد الجمل الجديدة في المستند الثاني - for j, s2 in enumerate(sentences2): - if j not in matched_sentences2 and len(s2.split()) >= 3: - sentence_diffs.append({ - 'doc1_idx': -1, - 'doc2_idx': j, - 'doc1_text': "", - 'doc2_text': s2, - 'similarity': 0, - 'diff': [{'text': word, 'status': 'added'} for word in s2.split()] - }) - - # ترتيب الفروقات حسب الموقع في المستند الأول - sentence_diffs.sort(key=lambda x: (x['doc1_idx'] if x['doc1_idx'] != -1 else float('inf'), x['doc2_idx'] if x['doc2_idx'] != -1 else float('inf'))) - - # تحديد الفقرات المضافة والمحذوفة - paragraph_diffs = [] - matched_paragraphs2 = set() - - for i, p1 in enumerate(paragraphs1): - if len(p1.split()) < 5: # تجاهل الفقرات القصيرة جداً - continue - - best_match_idx = -1 - best_match_score = 0.6 # عتبة التشابه - - for j, p2 in enumerate(paragraphs2): - if j in matched_paragraphs2: - continue - - if len(p2.split()) < 5: - continue - - score = Levenshtein.ratio(p1, p2) - if score > best_match_score: - best_match_score = score - best_match_idx = j - - if best_match_idx != -1: - # وجدنا تطابق - p2 = paragraphs2[best_match_idx] - paragraph_diffs.append({ - 'doc1_idx': i, - 'doc2_idx': best_match_idx, - 'doc1_text': p1, - 'doc2_text': p2, - 'similarity': best_match_score, - 'status': 'modified' if best_match_score < 0.9 else 'same' - }) - - matched_paragraphs2.add(best_match_idx) - else: - # لم نجد تطابق، هذه الفقرة غير موجودة في المستند الثاني - paragraph_diffs.append({ - 'doc1_idx': i, - 'doc2_idx': -1, - 'doc1_text': p1, - 'doc2_text': "", - 'similarity': 0, - 'status': 'removed' - }) - - # تحديد الفقرات الجديدة في المستند الثاني - for j, p2 in enumerate(paragraphs2): - if j not in matched_paragraphs2 and len(p2.split()) >= 5: - paragraph_diffs.append({ - 'doc1_idx': -1, - 'doc2_idx': j, - 'doc1_text': "", - 'doc2_text': p2, - 'similarity': 0, - 'status': 'added' - }) - - # ترتيب الفروقات حسب الموقع - paragraph_diffs.sort(key=lambda x: (x['doc1_idx'] if x['doc1_idx'] != -1 else float('inf'), x['doc2_idx'] if x['doc2_idx'] != -1 else float('inf'))) - - # تحليل الفروقات للحصول على إحصائيات - total_paragraphs = len(paragraphs1) + len(paragraphs2) - removed_paragraphs = sum(1 for p in paragraph_diffs if p['status'] == 'removed') - added_paragraphs = sum(1 for p in paragraph_diffs if p['status'] == 'added') - modified_paragraphs = sum(1 for p in paragraph_diffs if p['status'] == 'modified') - - # تحليل الكلمات المضافة، المحذوفة والمتغيرة - added_words = [] - removed_words = [] - modified_contexts = [] - - for diff in sentence_diffs: - for token in diff['diff']: - if token['status'] == 'added': - added_words.append(token['text']) - elif token['status'] == 'removed': - removed_words.append(token['text']) - - # جمع السياقات المتغيرة للتحليل - if diff['doc1_idx'] != -1 and diff['doc2_idx'] != -1 and diff['similarity'] < 0.9: - modified_contexts.append({ - 'doc1_text': diff['doc1_text'], - 'doc2_text': diff['doc2_text'], - 'similarity': diff['similarity'] - }) - - # إنشاء التقرير النهائي - comparison_report = { - "title1": title1, - "title2": title2, - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "similarity": similarity_percentage, - "similarity_metrics": similarity_metrics, - "sentence_diffs": sentence_diffs, - "paragraph_diffs": paragraph_diffs, - "statistics": { - "doc1_paragraphs": len(paragraphs1), - "doc2_paragraphs": len(paragraphs2), - "doc1_sentences": len(sentences1), - "doc2_sentences": len(sentences2), - "removed_paragraphs": removed_paragraphs, - "added_paragraphs": added_paragraphs, - "modified_paragraphs": modified_paragraphs, - "removed_words_count": len(removed_words), - "added_words_count": len(added_words), - "top_removed_words": Counter(removed_words).most_common(10), - "top_added_words": Counter(added_words).most_common(10) - }, - "modified_contexts": modified_contexts[:10], # أهم 10 سياقات متغيرة - "summary": self._generate_comparison_summary( - similarity_percentage, - len(paragraphs1), - len(paragraphs2), - removed_paragraphs, - added_paragraphs, - modified_paragraphs, - len(removed_words), - len(added_words) - ) - } - - # حفظ تقرير المقارنة - self._save_comparison_report(comparison_report, title1, title2) - - return comparison_report - - def _generate_comparison_summary(self, similarity, p1_count, p2_count, removed_p, added_p, modified_p, removed_w, added_w): - """إنشاء ملخص للمقارنة بين المستندين""" - if similarity >= 90: - similarity_description = "متطابقة بشكل كبير" - elif similarity >= 70: - similarity_description = "متشابهة" - elif similarity >= 50: - similarity_description = "متشابهة جزئياً" - else: - similarity_description = "مختلفة" - - summary = f"المستندان {similarity_description} بنسبة {similarity}%. " - - # وصف التغييرات في الفقرات - if removed_p > 0 or added_p > 0 or modified_p > 0: - changes = [] - if removed_p > 0: - changes.append(f"تم حذف {removed_p} فقرة") - if added_p > 0: - changes.append(f"تم إضافة {added_p} فقرة") - if modified_p > 0: - changes.append(f"تم تعديل {modified_p} فقرة") - - summary += "التغييرات تشمل: " + "، ".join(changes) + ". " - - # وصف التغييرات في الكلمات - if removed_w > 0 or added_w > 0: - word_changes = [] - if removed_w > 0: - word_changes.append(f"تم حذف {removed_w} كلمة") - if added_w > 0: - word_changes.append(f"تم إضافة {added_w} كلمة") - - summary += "على مستوى الكلمات: " + "، ".join(word_changes) + "." - - return summary - - def _save_comparison_report(self, report, title1, title2): - """حفظ تقرير المقارنة""" - # إنشاء اسم ملف فريد - timestamp = datetime.now().strftime("%Y%m%d%H%M%S") - filename = f"compare_{title1.replace(' ', '_')}_{title2.replace(' ', '_')}_{timestamp}.json" - file_path = os.path.join(self.comparison_dir, filename) - - try: - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(report, f, ensure_ascii=False, indent=2) - except Exception as e: - print(f"خطأ في حفظ تقرير المقارنة: {e}") - - def load_comparison_report(self, filename): - """تحميل تقرير مقارنة محفوظ""" - file_path = os.path.join(self.comparison_dir, filename) - - if not os.path.exists(file_path): - return None - - try: - with open(file_path, 'r', encoding='utf-8') as f: - report = json.load(f) - return report - except Exception as e: - print(f"خطأ في تحميل تقرير المقارنة: {e}") - return None - - def get_comparison_reports(self): - """الحصول على قائمة تقارير المقارنة المحفوظة""" - reports = [] - - for filename in os.listdir(self.comparison_dir): - if filename.startswith("compare_") and filename.endswith(".json"): - file_path = os.path.join(self.comparison_dir, filename) - try: - with open(file_path, 'r', encoding='utf-8') as f: - report = json.load(f) - reports.append({ - "filename": filename, - "title1": report.get("title1", "مستند 1"), - "title2": report.get("title2", "مستند 2"), - "timestamp": report.get("timestamp", ""), - "similarity": report.get("similarity", 0) - }) - except Exception as e: - print(f"خطأ في قراءة تقرير المقارنة {filename}: {e}") - - # ترتيب التقارير حسب التاريخ (الأحدث أولاً) - reports.sort(key=lambda x: x["timestamp"], reverse=True) - - return reports - - def extract_key_differences(self, comparison_report): - """استخراج الاختلافات الرئيسية من تقرير المقارنة""" - if not comparison_report or "paragraph_diffs" not in comparison_report: - return [] - - key_differences = [] - - # استخراج الفقرات المضافة - added_paragraphs = [p for p in comparison_report["paragraph_diffs"] if p["status"] == "added"] - if added_paragraphs: - key_differences.append({ - "type": "added_paragraphs", - "label": "فقرات مضافة", - "count": len(added_paragraphs), - "items": [p["doc2_text"] for p in added_paragraphs] - }) - - # استخراج الفقرات المحذوفة - removed_paragraphs = [p for p in comparison_report["paragraph_diffs"] if p["status"] == "removed"] - if removed_paragraphs: - key_differences.append({ - "type": "removed_paragraphs", - "label": "فقرات محذوفة", - "count": len(removed_paragraphs), - "items": [p["doc1_text"] for p in removed_paragraphs] - }) - - # استخراج الفقرات المعدلة - modified_paragraphs = [p for p in comparison_report["paragraph_diffs"] if p["status"] == "modified"] - if modified_paragraphs: - modified_items = [] - for p in modified_paragraphs: - modified_items.append({ - "doc1_text": p["doc1_text"], - "doc2_text": p["doc2_text"], - "similarity": p["similarity"] - }) - - key_differences.append({ - "type": "modified_paragraphs", - "label": "فقرات معدلة", - "count": len(modified_paragraphs), - "items": modified_items - }) - - # استخراج الكلمات الرئيسية المضافة والمحذوفة - if "statistics" in comparison_report: - stats = comparison_report["statistics"] - - if "top_added_words" in stats and stats["top_added_words"]: - key_differences.append({ - "type": "added_words", - "label": "الكلمات المضافة الأكثر تكراراً", - "count": stats["added_words_count"], - "items": stats["top_added_words"] - }) - - if "top_removed_words" in stats and stats["top_removed_words"]: - key_differences.append({ - "type": "removed_words", - "label": "الكلمات المحذوفة الأكثر تكراراً", - "count": stats["removed_words_count"], - "items": stats["top_removed_words"] - }) - - return key_differences - - def analyze_legal_changes(self, comparison_report): - """تحليل التغييرات القانونية في المستندات""" - if not comparison_report: - return [] - - # قائمة المصطلحات القانونية الهامة للبحث عنها - legal_terms = { - "payment": ["دفع", "سداد", "مستحقات", "مقابل", "رسوم", "تكلفة", "مبلغ", "أتعاب"], - "deadlines": ["ميعاد", "موعد", "تاريخ", "أجل", "مدة", "فترة", "مهلة"], - "liability": ["مسؤولية", "التزام", "تحمل", "تعويض", "ضمان", "كفالة"], - "termination": ["إنهاء", "فسخ", "إلغاء", "إيقاف", "إنهاء العلاقة"], - "dispute": ["نزاع", "خلاف", "منازعة", "اعتراض", "تحكيم", "قضاء", "محكمة"], - "penalties": ["غرامة", "عقوبة", "شرط جزائي", "جزاء", "تعويض"], - "conditions": ["شرط", "بند", "حالة", "اشتراط", "متطلب"], - "rights": ["حق", "صلاحية", "امتياز", "منفعة", "ملكية", "تصرف"], - "obligations": ["التزام", "واجب", "تعهد", "إلزام", "لازم"] - } - - # البحث عن التغييرات المتعلقة بالمصطلحات القانونية - legal_changes = [] - - if "sentence_diffs" in comparison_report: - for category, terms in legal_terms.items(): - category_changes = [] - - for diff in comparison_report["sentence_diffs"]: - # فحص فقط الجمل المعدلة (المتطابقة جزئياً) - if diff["doc1_idx"] != -1 and diff["doc2_idx"] != -1 and diff["similarity"] < 0.9: - # فحص ما إذا كانت الجملة تحتوي على أي من المصطلحات القانونية - contains_term = False - for term in terms: - if term in diff["doc1_text"].lower() or term in diff["doc2_text"].lower(): - contains_term = True - break - - if contains_term: - category_changes.append({ - "doc1_text": diff["doc1_text"], - "doc2_text": diff["doc2_text"], - "similarity": diff["similarity"] - }) - - if category_changes: - legal_category_name = { - "payment": "الدفع والمستحقات المالية", - "deadlines": "المواعيد والفترات الزمنية", - "liability": "المسؤولية والالتزامات", - "termination": "إنهاء العقد أو فسخه", - "dispute": "النزاعات والخلافات", - "penalties": "الغرامات والعقوبات", - "conditions": "الشروط والبنود", - "rights": "الحقوق والصلاحيات", - "obligations": "الالتزامات والواجبات" - } - - legal_changes.append({ - "category": category, - "label": legal_category_name.get(category, category), - "count": len(category_changes), - "changes": category_changes - }) - - # ترتيب التغييرات حسب الأهمية (عدد التغييرات) - legal_changes.sort(key=lambda x: x["count"], reverse=True) - - return legal_changes - - def analyze_price_changes(self, text1, text2): - """تحليل التغييرات في الأسعار بين نسختي المستند""" - # البحث عن الأرقام متبوعة بعملة أو تعبيرات تدل على المبالغ - price_pattern = r'(\d{1,3}(?:,\d{3})*(?:\.\d+)?)\s*(?:ريال|دولار|يورو|جنيه|درهم|دينار|SAR|USD|EUR|SR|$|€|£)' - amount_pattern = r'مبلغ[\s\w]*?(\d{1,3}(?:,\d{3})*(?:\.\d+)?)' - - # استخراج الأسعار من كل نص - prices1 = re.findall(price_pattern, text1) - prices1.extend(re.findall(amount_pattern, text1)) - prices1 = [p.replace(',', '') for p in prices1] - prices1 = [float(p) for p in prices1 if p] - - prices2 = re.findall(price_pattern, text2) - prices2.extend(re.findall(amount_pattern, text2)) - prices2 = [p.replace(',', '') for p in prices2] - prices2 = [float(p) for p in prices2 if p] - - # تحليل التغييرات - price_diff = { - "doc1_prices_count": len(prices1), - "doc2_prices_count": len(prices2), - "doc1_total": sum(prices1) if prices1 else 0, - "doc2_total": sum(prices2) if prices2 else 0, - "doc1_average": sum(prices1) / len(prices1) if prices1 else 0, - "doc2_average": sum(prices2) / len(prices2) if prices2 else 0, - "doc1_min": min(prices1) if prices1 else 0, - "doc2_min": min(prices2) if prices2 else 0, - "doc1_max": max(prices1) if prices1 else 0, - "doc2_max": max(prices2) if prices2 else 0 - } - - # حساب التغيير في إجمالي الأسعار - if price_diff["doc1_total"] > 0: - price_diff["total_change_percentage"] = ((price_diff["doc2_total"] - price_diff["doc1_total"]) / price_diff["doc1_total"]) * 100 - else: - price_diff["total_change_percentage"] = 0 - - return price_diff - - def analyze_date_changes(self, text1, text2): - """تحليل التغييرات في التواريخ بين نسختي المستند""" - # البحث عن التواريخ بالصيغ المختلفة - date_patterns = [ - r'\d{1,2}/\d{1,2}/\d{2,4}', # DD/MM/YYYY or MM/DD/YYYY - r'\d{1,2}-\d{1,2}-\d{2,4}', # DD-MM-YYYY or MM-DD-YYYY - r'\d{2,4}/\d{1,2}/\d{1,2}', # YYYY/MM/DD - r'\d{2,4}-\d{1,2}-\d{1,2}', # YYYY-MM-DD - r'\d{1,2}\s+(?:يناير|فبراير|مارس|أبريل|مايو|يونيو|يوليو|أغسطس|سبتمبر|أكتوبر|نوفمبر|ديسمبر)\s+\d{2,4}' # DD شهر YYYY - ] - - dates1 = [] - dates2 = [] - - for pattern in date_patterns: - dates1.extend(re.findall(pattern, text1)) - dates2.extend(re.findall(pattern, text2)) - - # إنشاء تقرير التغييرات في التواريخ - date_changes = { - "doc1_dates_count": len(dates1), - "doc2_dates_count": len(dates2), - "doc1_dates": dates1[:10], # أول 10 تواريخ فقط - "doc2_dates": dates2[:10], - "common_dates": list(set(dates1).intersection(set(dates2))), - "removed_dates": list(set(dates1) - set(dates2)), - "added_dates": list(set(dates2) - set(dates1)) - } - - return date_changes - - def render_document_comparison(self, text1, text2, title1="المستند الأول", title2="المستند الثاني"): - """عرض مقارنة المستندات بالواجهة التفاعلية""" - st.markdown("

مقارنة المستندات المتقدمة

", unsafe_allow_html=True) - - if not text1 or not text2: - st.warning("يرجى توفير نصوص المستندين للمقارنة") - return - - with st.spinner("جاري تحليل ومقارنة المستندين..."): - # إجراء المقارنة - comparison_report = self.get_document_diff(text1, text2, title1, title2) - - # تحليل التغييرات القانونية - legal_changes = self.analyze_legal_changes(comparison_report) - - # تحليل التغييرات في الأسعار والتواريخ - price_changes = self.analyze_price_changes(text1, text2) - date_changes = self.analyze_date_changes(text1, text2) - - # عرض ملخص المقارنة - st.markdown("

ملخص المقارنة

", unsafe_allow_html=True) - - col1, col2, col3 = st.columns([1, 1, 1]) - - with col1: - similarity = comparison_report["similarity"] - color = "#00b894" if similarity >= 80 else "#fdcb6e" if similarity >= 50 else "#d63031" - - st.markdown(f""" -
-
نسبة التشابه الإجمالية
-
{similarity}%
-
تم تحليل {comparison_report["statistics"]["doc1_paragraphs"]} فقرة في {title1} و {comparison_report["statistics"]["doc2_paragraphs"]} فقرة في {title2}
-
- """, unsafe_allow_html=True) - - with col2: - st.markdown(f""" -
-
ملخص التغييرات
-
-
- فقرات محذوفة: - {comparison_report["statistics"]["removed_paragraphs"]} -
-
- فقرات مضافة: - {comparison_report["statistics"]["added_paragraphs"]} -
-
- فقرات معدلة: - {comparison_report["statistics"]["modified_paragraphs"]} -
-
-
- """, unsafe_allow_html=True) - - with col3: - st.markdown(f""" -
-
تغييرات الكلمات
-
-
- كلمات محذوفة: - {comparison_report["statistics"]["removed_words_count"]} -
-
- كلمات مضافة: - {comparison_report["statistics"]["added_words_count"]} -
-
-
- """, unsafe_allow_html=True) - - # عرض ملخص نصي - st.markdown(f""" -
- {comparison_report["summary"]} -
- """, unsafe_allow_html=True) - - # عرض تحليل التغييرات القانونية - st.markdown("

تحليل التغييرات القانونية

", unsafe_allow_html=True) - - if legal_changes: - tabs = st.tabs([change["label"] for change in legal_changes]) - - for i, tab in enumerate(tabs): - with tab: - st.markdown(f"**عدد التغييرات: {legal_changes[i]['count']}**") - - for j, change in enumerate(legal_changes[i]["changes"]): - col1, col2 = st.columns(2) - with col1: - st.markdown(f"**{title1}:**") - st.markdown(f"
{change['doc1_text']}
", unsafe_allow_html=True) - with col2: - st.markdown(f"**{title2}:**") - st.markdown(f"
{change['doc2_text']}
", unsafe_allow_html=True) - - if j < len(legal_changes[i]["changes"]) - 1: - st.markdown("---") - else: - st.info("لم يتم اكتشاف تغييرات قانونية هامة بين المستندين.") - - # عرض الرسوم البيانية للتغييرات - st.markdown("

رسوم بيانية للتغييرات

", unsafe_allow_html=True) - - col1, col2 = st.columns(2) - - with col1: - # رسم بياني لتوزيع أنواع التغييرات في الفقرات - stats = comparison_report["statistics"] - fig = px.pie( - names=["فقرات متطابقة", "فقرات معدلة", "فقرات محذوفة", "فقرات مضافة"], - values=[ - stats["doc1_paragraphs"] - stats["removed_paragraphs"] - stats["modified_paragraphs"], - stats["modified_paragraphs"], - stats["removed_paragraphs"], - stats["added_paragraphs"] - ], - title="توزيع التغييرات في الفقرات", - color_discrete_sequence=["#00b894", "#fdcb6e", "#d63031", "#0984e3"] - ) - - fig.update_layout( - font=dict(family="Arial, sans-serif", size=14), - height=350 - ) - - st.plotly_chart(fig, use_container_width=True) - - with col2: - # رسم بياني للكلمات المضافة والمحذوفة الأكثر تكراراً - words_data = [] - - for word, count in comparison_report["statistics"]["top_removed_words"]: - if len(word) > 1: # تجاهل الأحرف المفردة - words_data.append({"word": word, "count": count, "type": "محذوفة"}) - - for word, count in comparison_report["statistics"]["top_added_words"]: - if len(word) > 1: # تجاهل الأحرف المفردة - words_data.append({"word": word, "count": count, "type": "مضافة"}) - - if words_data: - words_df = pd.DataFrame(words_data) - - fig = px.bar( - words_df, - x="word", - y="count", - color="type", - title="الكلمات المضافة والمحذوفة الأكثر تكراراً", - labels={"word": "الكلمة", "count": "عدد المرات", "type": "النوع"}, - color_discrete_map={"محذوفة": "#d63031", "مضافة": "#0984e3"} - ) - - fig.update_layout( - font=dict(family="Arial, sans-serif", size=14), - height=350 - ) - - st.plotly_chart(fig, use_container_width=True) - else: - st.info("لا توجد بيانات كافية للكلمات المضافة والمحذوفة.") - - # عرض تحليل الأسعار والتواريخ - col1, col2 = st.columns(2) - - with col1: - st.markdown("

تحليل التغييرات في الأسعار

", unsafe_allow_html=True) - - if price_changes["doc1_prices_count"] > 0 or price_changes["doc2_prices_count"] > 0: - price_change_direction = "زيادة" if price_changes["total_change_percentage"] > 0 else "نقص" - price_change_color = "#d63031" if price_changes["total_change_percentage"] > 0 else "#00b894" - - st.markdown(f""" -
-
تغيير في إجمالي الأسعار بنسبة {abs(price_changes['total_change_percentage']):.2f}% ({price_change_direction})
-
-
-
-
{title1}
-
{title2}
-
-
-
عدد الأسعار:
-
{price_changes['doc1_prices_count']}
-
{price_changes['doc2_prices_count']}
-
-
-
الإجمالي:
-
{price_changes['doc1_total']:,.2f}
-
{price_changes['doc2_total']:,.2f}
-
-
-
المتوسط:
-
{price_changes['doc1_average']:,.2f}
-
{price_changes['doc2_average']:,.2f}
-
-
-
الحد الأدنى:
-
{price_changes['doc1_min']:,.2f}
-
{price_changes['doc2_min']:,.2f}
-
-
-
الحد الأقصى:
-
{price_changes['doc1_max']:,.2f}
-
{price_changes['doc2_max']:,.2f}
-
-
-
- """, unsafe_allow_html=True) - - # رسم بياني للأسعار - if price_changes["doc1_prices_count"] > 0 and price_changes["doc2_prices_count"] > 0: - price_chart_data = [ - {"document": title1, "metric": "الإجمالي", "value": price_changes["doc1_total"]}, - {"document": title2, "metric": "الإجمالي", "value": price_changes["doc2_total"]}, - {"document": title1, "metric": "المتوسط", "value": price_changes["doc1_average"]}, - {"document": title2, "metric": "المتوسط", "value": price_changes["doc2_average"]}, - {"document": title1, "metric": "الحد الأقصى", "value": price_changes["doc1_max"]}, - {"document": title2, "metric": "الحد الأقصى", "value": price_changes["doc2_max"]} - ] - - price_df = pd.DataFrame(price_chart_data) - - fig = px.bar( - price_df, - x="metric", - y="value", - color="document", - barmode="group", - title="مقارنة الأسعار بين المستندين", - color_discrete_map={title1: "#0984e3", title2: "#00b894"} - ) - - fig.update_layout( - font=dict(family="Arial, sans-serif", size=14), - height=350 - ) - - st.plotly_chart(fig, use_container_width=True) - else: - st.info("لم يتم اكتشاف أي أسعار في المستندين.") - - with col2: - st.markdown("

تحليل التغييرات في التواريخ

", unsafe_allow_html=True) - - if date_changes["doc1_dates_count"] > 0 or date_changes["doc2_dates_count"] > 0: - st.markdown(f""" -
-
تم اكتشاف {date_changes['doc1_dates_count']} تاريخ في {title1} و {date_changes['doc2_dates_count']} تاريخ في {title2}
-
-
- تواريخ مشتركة: - {len(date_changes['common_dates'])} -
-
- تواريخ محذوفة: - {len(date_changes['removed_dates'])} -
-
- تواريخ مضافة: - {len(date_changes['added_dates'])} -
-
-
- """, unsafe_allow_html=True) - - # عرض التواريخ المحذوفة والمضافة - if date_changes["removed_dates"]: - st.markdown("**التواريخ المحذوفة:**") - for date in date_changes["removed_dates"][:10]: # عرض أول 10 فقط إذا كان هناك الكثير - st.markdown(f"
{date}
", unsafe_allow_html=True) - - if date_changes["added_dates"]: - st.markdown("**التواريخ المضافة:**") - for date in date_changes["added_dates"][:10]: # عرض أول 10 فقط - st.markdown(f"
{date}
", unsafe_allow_html=True) - - # رسم بياني للتواريخ - date_chart_data = [ - {"category": "تواريخ مشتركة", "count": len(date_changes["common_dates"])}, - {"category": "تواريخ محذوفة", "count": len(date_changes["removed_dates"])}, - {"category": "تواريخ مضافة", "count": len(date_changes["added_dates"])} - ] - - date_df = pd.DataFrame(date_chart_data) - - fig = px.bar( - date_df, - x="category", - y="count", - title="توزيع التغييرات في التواريخ", - color="category", - color_discrete_map={ - "تواريخ مشتركة": "#00b894", - "تواريخ محذوفة": "#d63031", - "تواريخ مضافة": "#0984e3" - } - ) - - fig.update_layout( - font=dict(family="Arial, sans-serif", size=14), - height=350 - ) - - st.plotly_chart(fig, use_container_width=True) - else: - st.info("لم يتم اكتشاف أي تواريخ في المستندين.") - - # عرض العرض المرئي للتغييرات بين المستندين - st.markdown("

العرض المرئي للتغييرات

", unsafe_allow_html=True) - - # إضافة خيار لتصفية الفروقات - st.markdown("#### تصفية الفروقات حسب النوع") - col1, col2, col3 = st.columns(3) - - with col1: - show_added = st.checkbox("عرض الإضافات", value=True) - with col2: - show_removed = st.checkbox("عرض الحذف", value=True) - with col3: - show_modified = st.checkbox("عرض التعديلات", value=True) - - # تحديد الفروقات للعرض - filtered_diffs = [] - - for diff in comparison_report["paragraph_diffs"]: - if diff["status"] == "added" and show_added: - filtered_diffs.append(diff) - elif diff["status"] == "removed" and show_removed: - filtered_diffs.append(diff) - elif diff["status"] == "modified" and show_modified: - filtered_diffs.append(diff) - - # عرض الفروقات - if filtered_diffs: - for diff in filtered_diffs: - if diff["status"] == "added": - st.markdown(f""" -
-
-
فقرة مضافة في {title2}
-
-
- {diff["doc2_text"]} -
-
- """, unsafe_allow_html=True) - - elif diff["status"] == "removed": - st.markdown(f""" -
-
-
فقرة محذوفة من {title1}
-
-
- {diff["doc1_text"]} -
-
- """, unsafe_allow_html=True) - - elif diff["status"] == "modified": - similarity_percentage = int(diff["similarity"] * 100) - - st.markdown(f""" -
-
-
فقرة معدلة (نسبة التشابه: {similarity_percentage}%)
-
-
-
-
{title1}:
- {diff["doc1_text"]} -
-
-
{title2}:
- {diff["doc2_text"]} -
-
-
- """, unsafe_allow_html=True) - else: - st.info("لا توجد فروقات تطابق معايير التصفية المحددة.") - - # إضافة CSS للتنسيق - st.markdown(""" - - """, unsafe_allow_html=True) - - def render_advanced_comparison_tools(self): - """عرض أدوات المقارنة المتقدمة""" - st.markdown("

أدوات مقارنة المستندات المتقدمة

", unsafe_allow_html=True) - - st.markdown(""" -
- استخدم هذه الأدوات لمقارنة مستندات العقود بشكل متقدم، واكتشاف التغييرات والفروقات بين نسخ المستندات المختلفة، - مع تحليل التغييرات القانونية والمالية والتواريخ. -
- """, unsafe_allow_html=True) - - # إنشاء علامات التبويب للأدوات المختلفة - tabs = st.tabs([ - "مقارنة نصية مباشرة", - "مقارنة ملفات PDF", - "عرض تقارير المقارنة السابقة" - ]) - - with tabs[0]: - st.markdown("### مقارنة نصية مباشرة") - - col1, col2 = st.columns(2) - - with col1: - title1 = st.text_input("عنوان المستند الأول", key="text_title1") - text1 = st.text_area("نص المستند الأول", height=300, key="text_input1") - - with col2: - title2 = st.text_input("عنوان المستند الثاني", key="text_title2") - text2 = st.text_area("نص المستند الثاني", height=300, key="text_input2") - - if st.button("قارن النصوص", key="compare_text_btn"): - if text1 and text2: - self.render_document_comparison( - text1, - text2, - title1 or "المستند الأول", - title2 or "المستند الثاني" - ) - else: - st.warning("يرجى إدخال نص المستندين للمقارنة") - - with tabs[1]: - st.markdown("### مقارنة ملفات PDF") - - col1, col2 = st.columns(2) - - with col1: - title1_pdf = st.text_input("عنوان المستند الأول", key="pdf_title1") - uploaded_file1 = st.file_uploader("تحميل المستند الأول (PDF)", type=["pdf"], key="pdf_upload1") - - with col2: - title2_pdf = st.text_input("عنوان المستند الثاني", key="pdf_title2") - uploaded_file2 = st.file_uploader("تحميل المستند الثاني (PDF)", type=["pdf"], key="pdf_upload2") - - if st.button("قارن ملفات PDF", key="compare_pdf_btn"): - if uploaded_file1 is not None and uploaded_file2 is not None: - with st.spinner("جاري استخراج النصوص من ملفات PDF..."): - text1_pdf = self._extract_text_from_pdf(uploaded_file1) - text2_pdf = self._extract_text_from_pdf(uploaded_file2) - - if text1_pdf and text2_pdf: - self.render_document_comparison( - text1_pdf, - text2_pdf, - title1_pdf or uploaded_file1.name, - title2_pdf or uploaded_file2.name - ) - else: - st.error("تعذر استخراج النص من ملفات PDF. يرجى التأكد من أن الملفات تحتوي على نصوص قابلة للاستخراج.") - else: - st.warning("يرجى تحميل ملفي PDF للمقارنة") - - with tabs[2]: - st.markdown("### تقارير المقارنة السابقة") - - # الحصول على تقارير المقارنة المحفوظة - reports = self.get_comparison_reports() - - if reports: - # عرض التقارير في جدول - report_data = [] - for report in reports: - report_data.append({ - "التاريخ": report["timestamp"], - "المستند الأول": report["title1"], - "المستند الثاني": report["title2"], - "نسبة التشابه": f"{report['similarity']}%", - "الملف": report["filename"] - }) - - report_df = pd.DataFrame(report_data) - st.dataframe(report_df) - - # اختيار تقرير لعرضه - selected_report = st.selectbox( - "اختر تقريراً لعرضه", - options=[f"{r['title1']} و {r['title2']} ({r['timestamp']})" for r in reports], - format_func=lambda x: x - ) - - report_index = next((i for i, r in enumerate(reports) if f"{r['title1']} و {r['title2']} ({r['timestamp']})" == selected_report), None) - - if report_index is not None and st.button("عرض التقرير المحدد"): - selected_filename = reports[report_index]["filename"] - report_data = self.load_comparison_report(selected_filename) - - if report_data: - st.success(f"تم تحميل تقرير المقارنة بنجاح") - - # عرض ملخص التقرير - st.markdown(f"### ملخص تقرير المقارنة") - st.markdown(f"**نسبة التشابه:** {report_data['similarity']}%") - st.markdown(f"**تاريخ المقارنة:** {report_data['timestamp']}") - st.markdown(f"**ملخص التغييرات:** {report_data['summary']}") - - # استخراج الاختلافات الرئيسية - key_differences = self.extract_key_differences(report_data) - - if key_differences: - st.markdown("### الاختلافات الرئيسية") - - for diff in key_differences: - st.markdown(f"#### {diff['label']} ({diff['count']})") - - if diff["type"] == "added_paragraphs": - for item in diff["items"][:5]: # عرض أول 5 فقط - st.markdown(f"
{item}
", unsafe_allow_html=True) - - elif diff["type"] == "removed_paragraphs": - for item in diff["items"][:5]: - st.markdown(f"
{item}
", unsafe_allow_html=True) - - elif diff["type"] == "modified_paragraphs": - for item in diff["items"][:3]: - col1, col2 = st.columns(2) - with col1: - st.markdown(f"**{report_data['title1']}:**") - st.markdown(f"
{item['doc1_text']}
", unsafe_allow_html=True) - with col2: - st.markdown(f"**{report_data['title2']}:**") - st.markdown(f"
{item['doc2_text']}
", unsafe_allow_html=True) - - elif diff["type"] in ["added_words", "removed_words"]: - # عرض الكلمات في شكل جدول - word_data = [] - for word, count in diff["items"]: - if len(word) > 1: # تجاهل الأحرف المفردة - word_data.append({"الكلمة": word, "عدد المرات": count}) - - if word_data: - word_df = pd.DataFrame(word_data) - st.dataframe(word_df) - - # تحليل التغييرات القانونية - legal_changes = self.analyze_legal_changes(report_data) - - if legal_changes: - st.markdown("### تحليل التغييرات القانونية") - - for change in legal_changes[:3]: # عرض أهم 3 فئات فقط - st.markdown(f"#### {change['label']} ({change['count']})") - - for item in change["changes"][:2]: # عرض أول مثالين فقط - col1, col2 = st.columns(2) - with col1: - st.markdown(f"**{report_data['title1']}:**") - st.markdown(f"
{item['doc1_text']}
", unsafe_allow_html=True) - with col2: - st.markdown(f"**{report_data['title2']}:**") - st.markdown(f"
{item['doc2_text']}
", unsafe_allow_html=True) - else: - st.error("تعذر تحميل تقرير المقارنة") - else: - st.info("لا توجد تقارير مقارنة محفوظة") - - # إضافة CSS للتنسيق - st.markdown(""" - - """, unsafe_allow_html=True) - - def render(self): - """عرض واجهة المستخدم الرئيسية للتطبيق""" - self.render_advanced_comparison_tools() \ No newline at end of file diff --git a/modules/document_comparison/document_comparison_app.py b/modules/document_comparison/document_comparison_app.py deleted file mode 100644 index 9921479e4b89392338e1c3486f5dd5e1f99a6a24..0000000000000000000000000000000000000000 --- a/modules/document_comparison/document_comparison_app.py +++ /dev/null @@ -1,1003 +0,0 @@ -""" -وحدة مقارنة المستندات - نظام تحليل المناقصات -""" - -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/README.md b/modules/maps/README.md deleted file mode 100644 index 4698548083da0a4ae7f3e011b2a6b26222364955..0000000000000000000000000000000000000000 --- a/modules/maps/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد - -## نظرة عامة -تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد، مما يساعد في تقييم طبيعة الموقع بشكل أفضل قبل البدء في العمل. - -## الميزات الرئيسية - -### الخريطة التفاعلية -- عرض جميع مواقع المشاريع على خريطة تفاعلية -- إمكانية البحث عن المواقع وتصفيتها -- تجميع المواقع القريبة (Clustering) -- عرض خرائط حرارية لتوزيع المشاريع -- أدوات قياس المسافة والمساحة - -### عرض التضاريس ثلاثي الأبعاد -- عرض تضاريس موقع المشروع بشكل ثلاثي الأبعاد -- التحكم في نطاق العرض ومقياس الارتفاع -- تحليل الارتفاعات وعرض المقطع الجانبي -- إمكانية تدوير وتكبير العرض للرؤية من زوايا مختلفة - -### تحليل المواقع -- عرض توزيع المشاريع حسب المدينة والحالة -- تحليل المسافات بين المشاريع -- عرض المشاريع القريبة من مشروع محدد -- رسوم بيانية توضيحية للتوزيع الجغرافي - -### إدارة المواقع -- إضافة مواقع جديدة -- تحرير وحذف المواقع الموجودة -- استيراد وتصدير بيانات المواقع بصيغ متعددة (CSV, JSON, GeoJSON) - -## المتطلبات الفنية -- Streamlit -- Folium -- PyDeck -- Pandas -- NumPy -- Plotly -- streamlit-folium - -## المطورون -فريق تطوير نظام WAHBI AI لتحليل العقود والمناقصات - شركة شبه الجزيرة للمقاولات - -## تاريخ الإصدار -مارس 2025 \ No newline at end of file diff --git a/modules/maps/__init__.py b/modules/maps/__init__.py deleted file mode 100644 index 03d5e034693e76c23713942b30b72703bfdfc710..0000000000000000000000000000000000000000 --- a/modules/maps/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# ملف تهيئة وحدة الخرائط \ No newline at end of file diff --git a/modules/maps/interactive_map.py b/modules/maps/interactive_map.py deleted file mode 100644 index 653d7d6c110ba1b8b0f15dcfb0858161ff8a78bc..0000000000000000000000000000000000000000 --- a/modules/maps/interactive_map.py +++ /dev/null @@ -1,1671 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد -تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np -import pydeck as pdk -import folium -from folium.plugins import MarkerCluster, HeatMap, MeasureControl -from streamlit_folium import folium_static -import requests -import json -import random -from typing import List, Dict, Any, Tuple, Optional -import tempfile -import base64 -from PIL import Image -from io import BytesIO - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات واجهة المستخدم -from utils.components.header import render_header -from utils.components.credits import render_credits -from utils.helpers import format_number, format_currency, styled_button - - -class InteractiveMap: - """فئة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد""" - - def __init__(self): - """تهيئة وحدة الخريطة التفاعلية""" - # تهيئة مجلدات حفظ البيانات - self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/maps")) - os.makedirs(self.data_dir, exist_ok=True) - - # مفاتيح API لخدمات الخرائط - self.mapbox_token = os.environ.get("MAPBOX_TOKEN", "") - self.opentopodata_api = "https://api.opentopodata.org/v1/srtm30m" - - # تهيئة حالة الجلسة - if 'project_locations' not in st.session_state: - st.session_state.project_locations = [] - - if 'selected_location' not in st.session_state: - st.session_state.selected_location = None - - if 'terrain_data' not in st.session_state: - st.session_state.terrain_data = None - - # بيانات اختبارية للمشاريع (سيتم استبدالها بالبيانات الفعلية من قاعدة البيانات) - self._initialize_sample_projects() - - def render(self): - """عرض واجهة وحدة الخريطة التفاعلية""" - # عرض الشعار والعنوان الرئيسي - render_header("خريطة مواقع المشاريع التفاعلية") - - # تبويبات الوحدة - tabs = st.tabs([ - "الخريطة التفاعلية", - "عرض التضاريس ثلاثي الأبعاد", - "تحليل المواقع", - "إدارة المواقع" - ]) - - # تبويب الخريطة التفاعلية - with tabs[0]: - self._render_interactive_map() - - # تبويب عرض التضاريس ثلاثي الأبعاد - with tabs[1]: - self._render_3d_terrain() - - # تبويب تحليل المواقع - with tabs[2]: - self._render_location_analysis() - - # تبويب إدارة المواقع - with tabs[3]: - self._render_location_management() - - # عرض حقوق النشر - render_credits() - - def _render_interactive_map(self): - """عرض الخريطة التفاعلية""" - st.markdown(""" -
-

🗺️ الخريطة التفاعلية لمواقع المشاريع

-

خريطة تفاعلية تعرض مواقع جميع المشاريع مع إمكانية التكبير والتصغير والتحرك.

-

يمكنك النقر على أي موقع للحصول على تفاصيل المشروع.

-
- """, unsafe_allow_html=True) - - # مربع البحث - search_query = st.text_input("البحث عن موقع أو مشروع", key="map_search") - - # أزرار تحكم للخريطة - col1, col2, col3, col4 = st.columns(4) - - with col1: - map_style = st.selectbox( - "نمط الخريطة", - options=["OpenStreetMap", "Stamen Terrain", "Stamen Toner", "CartoDB Positron"], - key="map_style" - ) - - with col2: - cluster_markers = st.checkbox("تجميع المواقع القريبة", value=True, key="cluster_markers") - - with col3: - show_heatmap = st.checkbox("عرض خريطة حرارية للمواقع", value=False, key="show_heatmap") - - with col4: - show_measurements = st.checkbox("أدوات القياس", value=False, key="show_measurements") - - # إنشاء الخريطة - if len(st.session_state.project_locations) > 0: - # بيانات النقاط على الخريطة - locations = [] - - # تصفية المشاريع حسب البحث - filtered_projects = st.session_state.project_locations - if search_query: - filtered_projects = [ - p for p in filtered_projects - if search_query.lower() in p.get("name", "").lower() or - search_query.lower() in p.get("description", "").lower() or - search_query.lower() in p.get("city", "").lower() - ] - - # عرض عدد النتائج - if search_query: - st.markdown(f"عدد النتائج: {len(filtered_projects)}") - - # تحضير البيانات للخريطة - heat_data = [] - for project in filtered_projects: - locations.append({ - "lat": project.get("latitude"), - "lon": project.get("longitude"), - "name": project.get("name"), - "description": project.get("description"), - "city": project.get("city"), - "status": project.get("status"), - "project_id": project.get("project_id") - }) - heat_data.append([project.get("latitude"), project.get("longitude"), 1]) - - # تعيين نقطة المركز والتكبير - if filtered_projects: - center_lat = sum(p.get("latitude", 0) for p in filtered_projects) / len(filtered_projects) - center_lon = sum(p.get("longitude", 0) for p in filtered_projects) / len(filtered_projects) - zoom_level = 6 # مستوى التكبير الافتراضي - else: - # مركز المملكة العربية السعودية - center_lat = 24.7136 - center_lon = 46.6753 - zoom_level = 5 - - # تحديد الإسناد (attribution) بناءً على نمط الخريطة - attribution = None - if map_style == "OpenStreetMap": - attribution = '© OpenStreetMap contributors' - elif map_style.startswith("Stamen"): - attribution = 'Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.' - elif map_style == "CartoDB Positron": - attribution = '© OpenStreetMap contributors, © CartoDB' - - # إنشاء الخريطة - m = folium.Map( - location=[center_lat, center_lon], - zoom_start=zoom_level, - tiles=map_style, - attr=attribution # إضافة سمة الإسناد - ) - - # إضافة أدوات القياس إذا تم اختيارها - if show_measurements: - MeasureControl(position='topright', primary_length_unit='kilometers').add_to(m) - - # إضافة النقاط إلى الخريطة - if cluster_markers: - # إنشاء مجموعة تجميع - marker_cluster = MarkerCluster(name="تجميع المشاريع").add_to(m) - - # إضافة النقاط إلى المجموعة - for location in locations: - # إنشاء النافذة المنبثقة - popup_html = f""" -
-

{location['name']}

-

الوصف: {location['description']}

-

المدينة: {location['city']}

-

الحالة: {location['status']}

-

الإحداثيات: {location['lat']:.6f}, {location['lon']:.6f}

- -
- """ - - # تحديد لون العلامة حسب حالة المشروع - icon_color = 'green' - if location['status'] == 'قيد التنفيذ': - icon_color = 'orange' - elif location['status'] == 'متوقف': - icon_color = 'red' - elif location['status'] == 'مكتمل': - icon_color = 'blue' - - # إضافة العلامة - folium.Marker( - location=[location['lat'], location['lon']], - popup=folium.Popup(popup_html, max_width=300), - tooltip=location['name'], - icon=folium.Icon(color=icon_color, icon='info-sign') - ).add_to(marker_cluster) - else: - # إضافة النقاط مباشرة إلى الخريطة - for location in locations: - # إنشاء النافذة المنبثقة - popup_html = f""" -
-

{location['name']}

-

الوصف: {location['description']}

-

المدينة: {location['city']}

-

الحالة: {location['status']}

-

الإحداثيات: {location['lat']:.6f}, {location['lon']:.6f}

- -
- """ - - # تحديد لون العلامة حسب حالة المشروع - icon_color = 'green' - if location['status'] == 'قيد التنفيذ': - icon_color = 'orange' - elif location['status'] == 'متوقف': - icon_color = 'red' - elif location['status'] == 'مكتمل': - icon_color = 'blue' - - # إضافة العلامة - folium.Marker( - location=[location['lat'], location['lon']], - popup=folium.Popup(popup_html, max_width=300), - tooltip=location['name'], - icon=folium.Icon(color=icon_color, icon='info-sign') - ).add_to(m) - - # إضافة خريطة حرارية إذا تم اختيارها - if show_heatmap and heat_data: - HeatMap(heat_data, radius=15).add_to(m) - - # إضافة طبقات متنوعة للخريطة - folium.TileLayer('OpenStreetMap').add_to(m) - folium.TileLayer('Stamen Terrain').add_to(m) - folium.TileLayer('Stamen Toner').add_to(m) - folium.TileLayer('CartoDB positron').add_to(m) - folium.TileLayer('CartoDB dark_matter').add_to(m) - - # إضافة أدوات التحكم بالطبقات - folium.LayerControl().add_to(m) - - # عرض الخريطة - st_map = folium_static(m, width=1000, height=600) - - # التفاعل مع النقر على الخريطة (سيتم تنفيذه عندما يتم دعم التفاعل) - # حاليًا لا يوجد دعم مباشر للتفاعل مع الخريطة في Streamlit - - # عرض بيانات المشاريع في جدول - st.markdown("### قائمة المشاريع على الخريطة") - - projects_df = pd.DataFrame(filtered_projects) - - # إعادة تسمية الأعمدة بالعربية - renamed_columns = { - "name": "اسم المشروع", - "city": "المدينة", - "status": "الحالة", - "description": "الوصف", - "project_id": "معرف المشروع", - "latitude": "خط العرض", - "longitude": "خط الطول" - } - - # تحديد الأعمدة للعرض - display_columns = ["name", "city", "status", "project_id"] - - # إنشاء جدول للعرض - display_df = projects_df[display_columns].rename(columns=renamed_columns) - - # عرض الجدول - st.dataframe(display_df, width=1000, height=400) - - # زر لاختيار مشروع لعرض التضاريس - selected_project_id = st.selectbox( - "اختر مشروعًا لعرض التضاريس ثلاثية الأبعاد", - options=projects_df["project_id"].tolist(), - format_func=lambda x: next((p["name"] for p in filtered_projects if p["project_id"] == x), x), - key="select_project_for_terrain" - ) - - # زر عرض التضاريس - if styled_button("عرض التضاريس", key="show_terrain_btn", type="primary", icon="🏔️"): - # العثور على المشروع المحدد - selected_project = next((p for p in filtered_projects if p["project_id"] == selected_project_id), None) - - if selected_project: - # تخزين الموقع المحدد في حالة الجلسة - st.session_state.selected_location = { - "latitude": selected_project["latitude"], - "longitude": selected_project["longitude"], - "name": selected_project["name"], - "project_id": selected_project["project_id"] - } - - # جلب بيانات التضاريس - try: - terrain_data = self._fetch_terrain_data( - selected_project["latitude"], - selected_project["longitude"] - ) - - # تخزين بيانات التضاريس في حالة الجلسة - st.session_state.terrain_data = terrain_data - - # الانتقال إلى تبويب عرض التضاريس - st.rerun() - except Exception as e: - st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}") - else: - st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.") - - def _render_3d_terrain(self): - """عرض التضاريس ثلاثي الأبعاد""" - st.markdown(""" -
-

🏔️ عرض التضاريس ثلاثي الأبعاد

-

عرض تفاعلي ثلاثي الأبعاد لتضاريس موقع المشروع المحدد.

-

يمكنك تدوير وتكبير العرض للحصول على رؤية أفضل للموقع.

-
- """, unsafe_allow_html=True) - - # التحقق من وجود موقع محدد - if st.session_state.selected_location is None: - st.info("يرجى اختيار موقع من تبويب 'الخريطة التفاعلية' أولاً.") - - # بديل: السماح بإدخال الإحداثيات يدوياً - st.markdown("### إدخال الإحداثيات يدوياً") - - col1, col2 = st.columns(2) - - with col1: - manual_lat = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="manual_lat") - - with col2: - manual_lon = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="manual_lon") - - if styled_button("عرض التضاريس", key="manual_terrain_btn", type="primary", icon="🏔️"): - try: - # جلب بيانات التضاريس - terrain_data = self._fetch_terrain_data(manual_lat, manual_lon) - - # تخزين بيانات التضاريس والموقع في حالة الجلسة - st.session_state.terrain_data = terrain_data - st.session_state.selected_location = { - "latitude": manual_lat, - "longitude": manual_lon, - "name": f"الموقع المخصص ({manual_lat:.4f}, {manual_lon:.4f})", - "project_id": "custom" - } - - # إعادة تشغيل التطبيق لتحديث العرض - st.rerun() - except Exception as e: - st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}") - - # عرض خريطة لتحديد الموقع - st.markdown("### حدد موقعًا على الخريطة") - m = folium.Map( - location=[24.7136, 46.6753], - zoom_start=6, - attr='© OpenStreetMap contributors' - ) - folium_static(m, width=1000, height=500) - - st.info("ملاحظة: لا يمكن تحديد موقع على الخريطة مباشرة في هذا الإصدار. يرجى إدخال الإحداثيات يدوياً أو اختيار مشروع من القائمة.") - - return - - # عرض معلومات الموقع المحدد - st.markdown(f"### تضاريس موقع: {st.session_state.selected_location['name']}") - st.markdown(f"الإحداثيات: {st.session_state.selected_location['latitude']:.6f}, {st.session_state.selected_location['longitude']:.6f}") - - # التحقق من وجود بيانات التضاريس - if st.session_state.terrain_data is None: - st.warning("لا توجد بيانات تضاريس متاحة لهذا الموقع. جاري جلب البيانات...") - - try: - # جلب بيانات التضاريس - terrain_data = self._fetch_terrain_data( - st.session_state.selected_location["latitude"], - st.session_state.selected_location["longitude"] - ) - - # تخزين بيانات التضاريس في حالة الجلسة - st.session_state.terrain_data = terrain_data - - # إعادة تشغيل التطبيق لتحديث العرض - st.rerun() - except Exception as e: - st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}") - return - - # عرض الخريطة ثنائية الأبعاد للموقع - st.markdown("### خريطة الموقع") - - # إنشاء خريطة صغيرة للموقع - mini_map = folium.Map( - location=[st.session_state.selected_location["latitude"], st.session_state.selected_location["longitude"]], - zoom_start=10, - attr='© OpenStreetMap contributors' - ) - folium.Marker(location=[st.session_state.selected_location["latitude"], st.session_state.selected_location["longitude"]], tooltip="الموقع المحدد").add_to(mini_map) - folium_static(mini_map, width=700, height=300) - - # عرض بيانات التضاريس - st.markdown("### نموذج التضاريس ثلاثي الأبعاد") - - # تحويل بيانات التضاريس إلى DataFrame - df = pd.DataFrame(st.session_state.terrain_data) - - # اختيار نظام ألوان - color_schemes = { - "Viridis": "Viridis", - "أخضر إلى بني": "Greens", - "أزرق إلى أحمر": "RdBu", - "أرجواني إلى أخضر": "PuGn", - "نظام الارتفاعات": "Terrain" - } - - color_scheme = st.selectbox( - "نظام الألوان", - options=list(color_schemes.keys()), - index=4, - key="3d_color_scheme" - ) - - # خيارات العرض - col1, col2, col3 = st.columns(3) - - with col1: - exaggeration = st.slider("تضخيم الارتفاع", 1, 50, 15, key="terrain_exaggeration") - - with col2: - radius = st.slider("نطاق العرض (كم)", 1, 20, 5, key="terrain_radius") - - with col3: - resolution = st.slider("دقة العرض", 10, 100, 50, key="terrain_resolution") - - if not df.empty and len(df) > 1: - # إعادة جلب البيانات إذا تغير النطاق - current_lat = st.session_state.selected_location["latitude"] - current_lon = st.session_state.selected_location["longitude"] - current_radius = radius - - # جلب بيانات جديدة إذا تغير النطاق - if styled_button("تحديث النطاق", key="update_radius_btn"): - try: - # جلب بيانات التضاريس - terrain_data = self._fetch_terrain_data( - current_lat, - current_lon, - radius_km=current_radius - ) - - # تخزين بيانات التضاريس في حالة الجلسة - st.session_state.terrain_data = terrain_data - - # إعادة تشغيل التطبيق لتحديث العرض - st.rerun() - except Exception as e: - st.error(f"حدث خطأ أثناء تحديث بيانات التضاريس: {str(e)}") - - # تحويل البيانات إلى تنسيق مناسب لـ PyDeck - x = df["longitude"].values - y = df["latitude"].values - z = df["elevation"].values * exaggeration # تضخيم الارتفاع - - # تطبيع الارتفاعات للحصول على ألوان مناسبة - normalized_elevation = (z - z.min()) / (z.max() - z.min() if z.max() != z.min() else 1) - - # الحصول على نظام الألوان - cmap = self._get_color_map(color_schemes[color_scheme]) - - # إنشاء عمود الألوان - df["color"] = [ - cmap(ne) if ne <= 1.0 else cmap(1.0) - for ne in normalized_elevation - ] - - # تهيئة عرض PyDeck - view_state = pdk.ViewState( - latitude=current_lat, - longitude=current_lon, - zoom=10, - pitch=45, - bearing=0 - ) - - # إنشاء طبقة التضاريس - terrain_layer = pdk.Layer( - "ColumnLayer", - data=df, - get_position=["longitude", "latitude"], - get_elevation="elevation * " + str(exaggeration), - get_fill_color="color", - get_radius=resolution, - pickable=True, - auto_highlight=True, - elevation_scale=1, - elevation_range=[0, 1000], - coverage=1, - ) - - # إضافة طبقة لعلامة الموقع المحدد - marker_df = pd.DataFrame({ - "latitude": [current_lat], - "longitude": [current_lon], - "size": [400] - }) - - marker_layer = pdk.Layer( - "ScatterplotLayer", - data=marker_df, - get_position=["longitude", "latitude"], - get_radius="size", - get_fill_color=[255, 0, 0, 200], - pickable=True, - ) - - # تهيئة العرض - r = pdk.Deck( - layers=[terrain_layer, marker_layer], - initial_view_state=view_state, - map_style="mapbox://styles/mapbox/satellite-v9", - tooltip={ - "html": "ارتفاع: {elevation} متر
إحداثيات: {latitude:.6f}, {longitude:.6f}", - "style": { - "backgroundColor": "steelblue", - "color": "white", - "direction": "rtl", - "text-align": "right" - } - } - ) - - # عرض نموذج التضاريس - st.pydeck_chart(r) - - # إضافة معلومات إضافية - st.markdown("### معلومات الارتفاع") - - # حساب الإحصاءات - min_elevation = df["elevation"].min() - max_elevation = df["elevation"].max() - avg_elevation = df["elevation"].mean() - - # عرض الإحصاءات - stat_col1, stat_col2, stat_col3 = st.columns(3) - - with stat_col1: - st.metric("أدنى ارتفاع", f"{min_elevation:.1f} متر") - - with stat_col2: - st.metric("متوسط الارتفاع", f"{avg_elevation:.1f} متر") - - with stat_col3: - st.metric("أعلى ارتفاع", f"{max_elevation:.1f} متر") - - # زر لتصدير البيانات - if styled_button("تصدير بيانات التضاريس", key="export_terrain_btn", type="secondary", icon="📊"): - # تحويل البيانات إلى CSV - csv = df.to_csv(index=False) - - # إنشاء رابط تنزيل - b64 = base64.b64encode(csv.encode()).decode() - href = f'تنزيل البيانات (CSV)' - st.markdown(href, unsafe_allow_html=True) - else: - st.error("لا توجد بيانات كافية لعرض نموذج التضاريس. حاول اختيار موقع آخر أو زيادة النطاق.") - - def _render_location_analysis(self): - """عرض تحليل المواقع""" - st.markdown(""" -
-

📊 تحليل موقع المشروع

-

تحليل متقدم لموقع المشروع وتضاريسه والظروف المحيطة.

-

يمكنك تحليل الارتفاعات والمسافات وقياس التكاليف المرتبطة بالموقع.

-
- """, unsafe_allow_html=True) - - # التحقق من وجود مواقع - if len(st.session_state.project_locations) == 0: - st.warning("لا توجد مواقع مشاريع متاحة للتحليل. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.") - return - - # اختيار موقع أو موقعين للتحليل - analysis_type = st.radio( - "نوع التحليل", - options=["تحليل موقع واحد", "مقارنة موقعين"], - key="location_analysis_type", - horizontal=True - ) - - # تحويل المواقع إلى DataFrame - projects_df = pd.DataFrame(st.session_state.project_locations) - - if analysis_type == "تحليل موقع واحد": - # اختيار موقع للتحليل - selected_project_id = st.selectbox( - "اختر موقع المشروع للتحليل", - options=projects_df["project_id"].tolist(), - format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x), - key="analysis_project" - ) - - # العثور على المشروع المحدد - selected_project = next((p for p in st.session_state.project_locations if p["project_id"] == selected_project_id), None) - - if selected_project: - # عرض معلومات المشروع - st.markdown(f"### تحليل موقع: {selected_project['name']}") - - # عرض خريطة الموقع - st.markdown("#### موقع المشروع") - - # إنشاء خريطة صغيرة للموقع - m2 = folium.Map( - location=[selected_project["latitude"], selected_project["longitude"]], - zoom_start=10, - attr='© OpenStreetMap contributors' - ) - folium.Marker(location=[selected_project["latitude"], selected_project["longitude"]], tooltip=selected_project["name"]).add_to(m2) - - # إضافة دائرة بنصف قطر محدد - analysis_radius = st.slider("نطاق التحليل (كم)", 1, 50, 10, key="analysis_radius") - folium.Circle( - location=[selected_project["latitude"], selected_project["longitude"]], - radius=analysis_radius * 1000, # تحويل إلى أمتار - color="red", - fill=True, - fill_opacity=0.2 - ).add_to(m2) - - folium_static(m2, width=700, height=400) - - # تحليل الموقع - st.markdown("#### عوامل الموقع") - - # تحليل اعتباري للموقع (يمكن استبداله بتحليل حقيقي من خدمات مثل Google Places API) - - # عوامل افتراضية - ستتغير هذه باستخدام بيانات حقيقية - factors = { - "قرب المدينة": random.uniform(0.4, 1.0), - "توفر المياه": random.uniform(0.3, 0.9), - "سهولة الوصول": random.uniform(0.5, 1.0), - "الظروف الجوية": random.uniform(0.6, 1.0), - "التضاريس": random.uniform(0.3, 0.8), - "توفر العمالة": random.uniform(0.5, 0.9), - "البنية التحتية": random.uniform(0.4, 0.9), - "المخاطر البيئية": random.uniform(0.3, 0.7) - } - - # مخطط شريطي للعوامل - factors_df = pd.DataFrame({ - "العامل": list(factors.keys()), - "التقييم": list(factors.values()) - }) - - # الترتيب تنازلياً - factors_df = factors_df.sort_values(by="التقييم", ascending=False) - - # عرض الرسم البياني - st.bar_chart(factors_df.set_index("العامل")) - - # تقييم إجمالي للموقع - overall_score = sum(factors.values()) / len(factors) - - # عرض التقييم الإجمالي - st.markdown(f"#### التقييم الإجمالي للموقع: {overall_score:.2f}/1.0") - - # مؤشر تقدم للتقييم - st.progress(overall_score) - - # تصنيف التقييم - if overall_score >= 0.8: - rating = "ممتاز" - color = "green" - elif overall_score >= 0.6: - rating = "جيد" - color = "blue" - elif overall_score >= 0.4: - rating = "مقبول" - color = "orange" - else: - rating = "ضعيف" - color = "red" - - st.markdown(f"

تصنيف الموقع: {rating}

", unsafe_allow_html=True) - - # توصيات للموقع - st.markdown("#### توصيات الموقع") - - recommendations = [ - "تحسين طرق الوصول للموقع لزيادة كفاءة نقل المواد والمعدات.", - "إجراء دراسة جيوتقنية مفصلة للتضاريس قبل البدء في أعمال الحفر.", - "التأكد من توفر مصادر المياه الكافية لاحتياجات المشروع.", - "التنسيق مع السلطات المحلية لتسهيل توصيل الخدمات للموقع.", - "وضع خطة للتعامل مع الظروف الجوية المتقلبة في المنطقة." - ] - - for rec in recommendations: - st.markdown(f"- {rec}") - - # المرافق القريبة - st.markdown("#### المرافق القريبة (تمثيل افتراضي)") - - # بيانات افتراضية للمرافق القريبة - nearby_facilities = { - "مستشفى": random.uniform(5, 30), - "مدرسة": random.uniform(2, 15), - "محطة وقود": random.uniform(2, 20), - "مركز تسوق": random.uniform(3, 25), - "مكتب حكومي": random.uniform(7, 35), - "مطار": random.uniform(15, 100), - "ميناء": random.uniform(20, 150) - } - - # عرض المرافق في جدول - facilities_df = pd.DataFrame({ - "المرفق": list(nearby_facilities.keys()), - "المسافة (كم)": list(nearby_facilities.values()) - }) - - # ترتيب حسب المسافة - facilities_df = facilities_df.sort_values(by="المسافة (كم)") - - # عرض الجدول - st.dataframe(facilities_df, width=700) - - # تقرير تكلفة الموقع - st.markdown("#### تقديرات تكلفة الموقع") - - # بنود التكلفة الافتراضية - cost_items = { - "تكلفة تسوية الأرض": random.uniform(50000, 200000), - "تكلفة البنية التحتية": random.uniform(100000, 500000), - "تكلفة النقل الإضافية": random.uniform(30000, 150000), - "تكلفة الحماية من المخاطر البيئية": random.uniform(20000, 100000), - "تكلفة توصيل الخدمات": random.uniform(40000, 200000) - } - - # عرض بنود التكلفة - st.markdown("##### بنود التكلفة") - - for item, cost in cost_items.items(): - st.markdown(f"- {item}: {format_currency(cost)} ريال") - - # إجمالي التكلفة - total_cost = sum(cost_items.values()) - st.markdown(f"##### إجمالي تكلفة الموقع: {format_currency(total_cost)} ريال") - - # خيارات تحسين الموقع - st.markdown("#### خيارات تحسين الموقع") - - improvement_options = [ - {"name": "تسوية الأرض وإزالة العوائق", "cost": 75000, "impact": 0.15}, - {"name": "تحسين طرق الوصول", "cost": 120000, "impact": 0.2}, - {"name": "بناء نظام صرف للمياه", "cost": 90000, "impact": 0.18}, - {"name": "تعزيز البنية التحتية", "cost": 180000, "impact": 0.25}, - {"name": "نظام حماية من العوامل الجوية", "cost": 60000, "impact": 0.12} - ] - - # عرض خيارات التحسين - st.markdown("اختر خيارات التحسين لتقييم التأثير والتكلفة:") - - selected_improvements = [] - for i, option in enumerate(improvement_options): - if st.checkbox(f"{option['name']} - {format_currency(option['cost'])} ريال", key=f"imp_{i}"): - selected_improvements.append(option) - - if selected_improvements: - # حساب التأثير والتكلفة الإجمالية - total_impact = sum(imp["impact"] for imp in selected_improvements) - total_improvement_cost = sum(imp["cost"] for imp in selected_improvements) - - # عرض النتائج - st.markdown(f"##### تحسين التقييم المتوقع: +{total_impact:.2f}") - new_score = min(1.0, overall_score + total_impact) - st.markdown(f"##### التقييم الجديد المتوقع: {new_score:.2f}/1.0") - st.progress(new_score) - - # تصنيف التقييم الجديد - if new_score >= 0.8: - new_rating = "ممتاز" - new_color = "green" - elif new_score >= 0.6: - new_rating = "جيد" - new_color = "blue" - elif new_score >= 0.4: - new_rating = "مقبول" - new_color = "orange" - else: - new_rating = "ضعيف" - new_color = "red" - - st.markdown(f"
التصنيف الجديد المتوقع: {new_rating}
", unsafe_allow_html=True) - - # عرض التكلفة الإجمالية - st.markdown(f"##### تكلفة التحسينات: {format_currency(total_improvement_cost)} ريال") - else: - st.error("لم يتم العثور على المشروع المحدد.") - else: # مقارنة موقعين - # اختيار موقعين للمقارنة - col1, col2 = st.columns(2) - - with col1: - project_id_1 = st.selectbox( - "الموقع الأول", - options=projects_df["project_id"].tolist(), - format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x), - key="compare_project_1" - ) - - with col2: - # استبعاد الموقع الأول من الخيارات - remaining_options = [pid for pid in projects_df["project_id"].tolist() if pid != project_id_1] - - if remaining_options: - project_id_2 = st.selectbox( - "الموقع الثاني", - options=remaining_options, - format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x), - key="compare_project_2" - ) - else: - st.warning("يجب أن يكون هناك موقعان على الأقل للمقارنة.") - return - - # العثور على المشروعين المحددين - project_1 = next((p for p in st.session_state.project_locations if p["project_id"] == project_id_1), None) - project_2 = next((p for p in st.session_state.project_locations if p["project_id"] == project_id_2), None) - - if project_1 and project_2: - # عرض عنوان المقارنة - st.markdown(f"### مقارنة بين موقعي {project_1['name']} و {project_2['name']}") - - # عرض خريطة توضح الموقعين - st.markdown("#### الموقعان على الخريطة") - - # حساب المركز والزوم المناسب - center_lat = (project_1["latitude"] + project_2["latitude"]) / 2 - center_lon = (project_1["longitude"] + project_2["longitude"]) / 2 - - # حساب المسافة بين الموقعين - distance = self._calculate_distance( - project_1["latitude"], project_1["longitude"], - project_2["latitude"], project_2["longitude"] - ) - - # تحديد مستوى التكبير حسب المسافة - zoom_level = 12 if distance < 10 else (10 if distance < 50 else 8) - - # إنشاء الخريطة - compare_map = folium.Map( - location=[center_lat, center_lon], - zoom_start=zoom_level, - attr='© OpenStreetMap contributors' - ) - - # إضافة العلامات للموقعين - folium.Marker( - location=[project_1["latitude"], project_1["longitude"]], - tooltip=project_1["name"], - icon=folium.Icon(color="blue", icon="info-sign") - ).add_to(compare_map) - - folium.Marker( - location=[project_2["latitude"], project_2["longitude"]], - tooltip=project_2["name"], - icon=folium.Icon(color="red", icon="info-sign") - ).add_to(compare_map) - - # إضافة خط يربط بين الموقعين - folium.PolyLine( - locations=[ - [project_1["latitude"], project_1["longitude"]], - [project_2["latitude"], project_2["longitude"]] - ], - color="green", - weight=3, - opacity=0.7, - tooltip=f"المسافة: {distance:.2f} كم" - ).add_to(compare_map) - - # عرض الخريطة - folium_static(compare_map, width=800, height=500) - - # عرض المسافة بين الموقعين - st.markdown(f"#### المسافة بين الموقعين: {distance:.2f} كيلومتر") - - # مقارنة معلومات الموقعين - st.markdown("#### مقارنة المعلومات الأساسية") - - # إنشاء جدول المقارنة - comparison_data = { - "المعلومات": ["المدينة", "الحالة", "خط العرض", "خط الطول", "الوصف"], - project_1["name"]: [ - project_1.get("city", ""), - project_1.get("status", ""), - f"{project_1['latitude']:.6f}", - f"{project_1['longitude']:.6f}", - project_1.get("description", "") - ], - project_2["name"]: [ - project_2.get("city", ""), - project_2.get("status", ""), - f"{project_2['latitude']:.6f}", - f"{project_2['longitude']:.6f}", - project_2.get("description", "") - ] - } - - comparison_df = pd.DataFrame(comparison_data) - st.dataframe(comparison_df, width=800) - - # مقارنة العوامل البيئية والمكانية - st.markdown("#### مقارنة العوامل") - - # بيانات افتراضية للعوامل - ستتغير هذه باستخدام بيانات حقيقية - factors_comparison = { - "العامل": ["قرب المدينة", "توفر المياه", "سهولة الوصول", "الظروف الجوية", "التضاريس", "توفر العمالة", "البنية التحتية", "المخاطر البيئية"], - project_1["name"]: [random.uniform(0.4, 1.0) for _ in range(8)], - project_2["name"]: [random.uniform(0.4, 1.0) for _ in range(8)] - } - - # تحويل إلى DataFrame - factors_df = pd.DataFrame(factors_comparison) - - # رسم بياني شريطي للمقارنة - st.bar_chart(factors_df.set_index("العامل")) - - # حساب إجمالي التقييم لكل موقع - project_1_score = sum(factors_comparison[project_1["name"]]) / len(factors_comparison[project_1["name"]]) - project_2_score = sum(factors_comparison[project_2["name"]]) / len(factors_comparison[project_2["name"]]) - - # عرض التقييم الإجمالي - col1, col2 = st.columns(2) - - with col1: - st.markdown(f"##### تقييم {project_1['name']}: {project_1_score:.2f}/1.0") - st.progress(project_1_score) - - with col2: - st.markdown(f"##### تقييم {project_2['name']}: {project_2_score:.2f}/1.0") - st.progress(project_2_score) - - # تحديد الموقع المفضل - preferred_site = project_1["name"] if project_1_score > project_2_score else project_2["name"] - score_diff = abs(project_1_score - project_2_score) - - if score_diff < 0.1: - recommendation = "الموقعان متقاربان في التقييم ويمكن اعتبارهما متكافئين." - color = "blue" - else: - recommendation = f"الموقع الأفضل هو: {preferred_site}" - color = "green" - - st.markdown(f"

{recommendation}

", unsafe_allow_html=True) - - # تحليل التكلفة - st.markdown("#### مقارنة تقديرات التكلفة") - - # بنود التكلفة الافتراضية - cost_items = ["تسوية الأرض", "البنية التحتية", "النقل", "الحماية من المخاطر", "توصيل الخدمات"] - - site_1_costs = [random.uniform(50000, 200000) for _ in range(len(cost_items))] - site_2_costs = [random.uniform(50000, 200000) for _ in range(len(cost_items))] - - # إنشاء DataFrame للتكاليف - cost_df = pd.DataFrame({ - "بند التكلفة": cost_items, - f"{project_1['name']} (ريال)": site_1_costs, - f"{project_2['name']} (ريال)": site_2_costs - }) - - # عرض جدول التكاليف - st.dataframe(cost_df, width=800) - - # حساب إجمالي التكلفة لكل موقع - total_cost_1 = sum(site_1_costs) - total_cost_2 = sum(site_2_costs) - - # عرض إجمالي التكلفة - col1, col2 = st.columns(2) - - with col1: - st.metric( - f"إجمالي تكلفة {project_1['name']}", - f"{format_currency(total_cost_1)} ريال" - ) - - with col2: - st.metric( - f"إجمالي تكلفة {project_2['name']}", - f"{format_currency(total_cost_2)} ريال", - f"{format_currency(total_cost_2 - total_cost_1)}" - ) - - # تحليل إضافي للمقارنة - st.markdown("#### ملخص المقارنة") - - comparison_summary = f""" - بناءً على التحليل المقدم، يمكن استخلاص الملاحظات التالية: - - 1. **المسافة بين الموقعين:** {distance:.2f} كيلومتر. - 2. **التقييم:** {project_1['name']} بتقييم {project_1_score:.2f}/1.0، و{project_2['name']} بتقييم {project_2_score:.2f}/1.0. - 3. **التكلفة:** {project_1['name']} بتكلفة {format_currency(total_cost_1)} ريال، و{project_2['name']} بتكلفة {format_currency(total_cost_2)} ريال. - - بالنظر إلى العوامل أعلاه، فإن الموقع **{preferred_site}** هو الخيار الأفضل من حيث التوازن بين التقييم والتكلفة. - """ - - st.markdown(comparison_summary) - else: - st.error("لم يتم العثور على أحد المشروعين المحددين.") - - def _render_location_management(self): - """عرض إدارة المواقع""" - st.markdown(""" -
-

📍 إدارة مواقع المشاريع

-

إضافة وتعديل مواقع المشاريع وتصدير واستيراد البيانات.

-

يمكنك إدخال مواقع المشاريع الجديدة وتعديل المواقع الموجودة وحذفها.

-
- """, unsafe_allow_html=True) - - # تبويبات فرعية للإدارة - subtabs = st.tabs([ - "إضافة موقع جديد", - "تحرير المواقع", - "استيراد/تصدير المواقع" - ]) - - # تبويب إضافة موقع جديد - with subtabs[0]: - self._render_add_location() - - # تبويب تحرير المواقع - with subtabs[1]: - self._render_edit_locations() - - # تبويب استيراد/تصدير المواقع - with subtabs[2]: - self._render_import_export_locations() - - def _render_add_location(self): - """عرض نموذج إضافة موقع جديد""" - st.markdown("### إضافة موقع مشروع جديد") - - # نموذج إضافة موقع جديد - with st.form(key="add_location_form"): - # معلومات أساسية - project_name = st.text_input("اسم المشروع", key="new_project_name") - project_description = st.text_area("وصف المشروع", key="new_project_description") - - # معلومات الموقع - col1, col2 = st.columns(2) - - with col1: - city = st.text_input("المدينة", key="new_city") - status = st.selectbox( - "حالة المشروع", - options=["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"], - key="new_status" - ) - - with col2: - latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="new_latitude") - longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="new_longitude") - - # عرض الموقع على خريطة صغيرة - mini_map = folium.Map( - location=[latitude, longitude], - zoom_start=10, - attr='© OpenStreetMap contributors' - ) - folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map) - folium_static(mini_map, width=700, height=300) - - # زر الإضافة - submit_button = st.form_submit_button("إضافة الموقع") - - # معالجة النموذج عند الإرسال - if submit_button: - if not project_name: - st.error("يرجى إدخال اسم المشروع.") - else: - # إنشاء معرف فريد للمشروع - project_id = f"PRJ{len(st.session_state.project_locations) + 1:03d}" - - # إضافة المشروع الجديد - new_project = { - "project_id": project_id, - "name": project_name, - "description": project_description, - "city": city, - "status": status, - "latitude": latitude, - "longitude": longitude - } - - # إضافة المشروع إلى القائمة - st.session_state.project_locations.append(new_project) - - # حفظ البيانات - self._save_locations_data() - - # عرض رسالة نجاح - st.success(f"تمت إضافة موقع المشروع '{project_name}' بنجاح.") - - # إعادة تحميل الصفحة - st.rerun() - - def _render_edit_locations(self): - """عرض واجهة تحرير المواقع الموجودة""" - st.markdown("### تحرير مواقع المشاريع") - - if len(st.session_state.project_locations) == 0: - st.warning("لا توجد مواقع مشاريع للتحرير. يرجى إضافة مواقع أولاً.") - return - - # عرض قائمة المشاريع - projects_df = pd.DataFrame(st.session_state.project_locations) - - # إعادة تسمية الأعمدة بالعربية - renamed_columns = { - "name": "اسم المشروع", - "city": "المدينة", - "status": "الحالة", - "description": "الوصف", - "project_id": "معرف المشروع", - "latitude": "خط العرض", - "longitude": "خط الطول" - } - - # تحديد الأعمدة للعرض - display_columns = ["project_id", "name", "city", "status"] - - # إنشاء جدول للعرض - display_df = projects_df[display_columns].rename(columns=renamed_columns) - - # عرض الجدول - st.dataframe(display_df, width=800, height=300) - - # اختيار مشروع للتحرير - selected_project_id = st.selectbox( - "اختر مشروعًا للتحرير", - options=projects_df["project_id"].tolist(), - format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x), - key="edit_project_id" - ) - - # العثور على المشروع المحدد - selected_project_index = next((i for i, p in enumerate(st.session_state.project_locations) if p["project_id"] == selected_project_id), None) - - if selected_project_index is not None: - selected_project = st.session_state.project_locations[selected_project_index] - - # نموذج تحرير المشروع - with st.form(key="edit_location_form"): - st.markdown(f"### تحرير مشروع: {selected_project['name']}") - - # معلومات أساسية - project_name = st.text_input("اسم المشروع", value=selected_project["name"], key="edit_project_name") - project_description = st.text_area("وصف المشروع", value=selected_project.get("description", ""), key="edit_project_description") - - # معلومات الموقع - col1, col2 = st.columns(2) - - with col1: - city = st.text_input("المدينة", value=selected_project.get("city", ""), key="edit_city") - status = st.selectbox( - "حالة المشروع", - options=["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"], - index=["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"].index(selected_project.get("status", "مخطط")), - key="edit_status" - ) - - with col2: - latitude = st.number_input("خط العرض", value=selected_project["latitude"], step=0.0001, format="%.6f", key="edit_latitude") - longitude = st.number_input("خط الطول", value=selected_project["longitude"], step=0.0001, format="%.6f", key="edit_longitude") - - # عرض الموقع على خريطة صغيرة - mini_map = folium.Map( - location=[latitude, longitude], - zoom_start=10, - attr='© OpenStreetMap contributors' - ) - folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map) - folium_static(mini_map, width=700, height=300) - - # أزرار الإجراءات - col1, col2 = st.columns(2) - - with col1: - update_button = st.form_submit_button("تحديث المعلومات") - - with col2: - delete_button = st.form_submit_button("حذف المشروع", type="secondary") - - # معالجة تحديث المعلومات - if update_button: - if not project_name: - st.error("لا يمكن ترك اسم المشروع فارغًا.") - else: - # تحديث معلومات المشروع - st.session_state.project_locations[selected_project_index] = { - "project_id": selected_project["project_id"], - "name": project_name, - "description": project_description, - "city": city, - "status": status, - "latitude": latitude, - "longitude": longitude - } - - # حفظ البيانات - self._save_locations_data() - - # عرض رسالة نجاح - st.success(f"تم تحديث معلومات المشروع '{project_name}' بنجاح.") - - # إعادة تحميل الصفحة - st.rerun() - - # معالجة حذف المشروع - if delete_button: - # نافذة تأكيد الحذف - st.warning(f"هل أنت متأكد من رغبتك في حذف المشروع '{selected_project['name']}'؟") - - confirm_col1, confirm_col2 = st.columns(2) - - with confirm_col1: - if st.button("نعم، حذف المشروع", key="confirm_delete"): - # حذف المشروع - st.session_state.project_locations.pop(selected_project_index) - - # حفظ البيانات - self._save_locations_data() - - # عرض رسالة نجاح - st.success(f"تم حذف المشروع '{selected_project['name']}' بنجاح.") - - # إعادة تحميل الصفحة - st.rerun() - - with confirm_col2: - if st.button("لا، إلغاء الحذف", key="cancel_delete"): - st.rerun() - else: - st.error("لم يتم العثور على المشروع المحدد.") - - def _render_import_export_locations(self): - """عرض واجهة استيراد وتصدير المواقع""" - st.markdown("### استيراد وتصدير مواقع المشاريع") - - # تبويبات فرعية للاستيراد والتصدير - export_tab, import_tab = st.tabs(["تصدير المواقع", "استيراد المواقع"]) - - # تبويب تصدير المواقع - with export_tab: - st.markdown("#### تصدير مواقع المشاريع") - - if len(st.session_state.project_locations) == 0: - st.warning("لا توجد مواقع مشاريع للتصدير.") - else: - # اختيار تنسيق التصدير - export_format = st.radio( - "اختر تنسيق التصدير", - options=["CSV", "Excel", "JSON"], - horizontal=True, - key="export_format" - ) - - # زر التصدير - if styled_button("تصدير المواقع", key="export_btn", type="primary", icon="📤"): - # تصدير البيانات - exported_data = self._export_locations(export_format.lower()) - - if exported_data: - # تحديد نوع الملف ومعلومات التنزيل - if export_format == "CSV": - mime_type = "text/csv" - file_ext = "csv" - elif export_format == "Excel": - mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - file_ext = "xlsx" - else: # JSON - mime_type = "application/json" - file_ext = "json" - - # إنشاء رابط التنزيل - b64 = base64.b64encode(exported_data).decode() - href = f'تنزيل ملف {export_format}' - st.markdown(href, unsafe_allow_html=True) - - # عرض معاينة البيانات - if export_format == "CSV": - st.markdown("#### معاينة البيانات المصدرة") - st.text(exported_data.decode("utf-8")) - elif export_format == "JSON": - st.markdown("#### معاينة البيانات المصدرة") - st.json(json.loads(exported_data.decode("utf-8"))) - - # تبويب استيراد المواقع - with import_tab: - st.markdown("#### استيراد مواقع المشاريع") - - # اختيار تنسيق الاستيراد - import_format = st.radio( - "اختر تنسيق الاستيراد", - options=["CSV", "Excel", "JSON"], - horizontal=True, - key="import_format" - ) - - # تحميل الملف - uploaded_file = st.file_uploader(f"تحميل ملف {import_format}", type=[import_format.lower()]) - - if uploaded_file: - # معاينة الملف - st.markdown("#### معاينة الملف المحمل") - - if import_format == "CSV": - df = pd.read_csv(uploaded_file) - st.dataframe(df) - elif import_format == "Excel": - df = pd.read_excel(uploaded_file) - st.dataframe(df) - else: # JSON - json_data = json.load(uploaded_file) - st.json(json_data) - - # خيارات الاستيراد - import_mode = st.radio( - "طريقة الاستيراد", - options=["إضافة إلى المواقع الحالية", "استبدال جميع المواقع"], - key="import_mode" - ) - - # زر الاستيراد - if styled_button("استيراد المواقع", key="import_btn", type="primary", icon="📥"): - # إعادة قراءة الملف (قد يكون تم استنفاد التدفق) - uploaded_file.seek(0) - - try: - # استيراد البيانات - imported_count = self._import_locations(uploaded_file, import_format.lower()) - - if import_mode == "استبدال جميع المواقع": - st.success(f"تم استبدال جميع المواقع بنجاح. عدد المواقع الجديدة: {imported_count}") - else: - st.success(f"تمت إضافة {imported_count} مواقع جديدة بنجاح.") - - # إعادة تحميل الصفحة - st.rerun() - except Exception as e: - st.error(f"حدث خطأ أثناء استيراد البيانات: {str(e)}") - - def _fetch_terrain_data(self, latitude, longitude, radius_km=5): - """جلب بيانات التضاريس من واجهة برمجة التطبيقات""" - # حساب نطاق الإحداثيات - # 1 درجة تقريبًا = 111 كم - delta = radius_km / 111.0 - - # إنشاء شبكة نقاط - lat_min, lat_max = latitude - delta, latitude + delta - lon_min, lon_max = longitude - delta, longitude + delta - - # عدد نقاط الشبكة - grid_size = 20 - - # إنشاء شبكة إحداثيات - lats = np.linspace(lat_min, lat_max, grid_size) - lons = np.linspace(lon_min, lon_max, grid_size) - - # تهيئة مصفوفة النتائج - results = [] - - # بناء سلسلة الإحداثيات للطلب - locations = [] - for lat in lats: - for lon in lons: - locations.append(f"{lat:.6f},{lon:.6f}") - - # تقسيم الطلبات إلى مجموعات (واجهة البرمجة تقبل 100 نقطة كحد أقصى) - batch_size = 100 - for i in range(0, len(locations), batch_size): - batch = locations[i:i+batch_size] - - # محاولة استخدام خدمة OpenTopoData - try: - url = f"{self.opentopodata_api}?locations={'|'.join(batch)}" - response = requests.get(url) - - if response.status_code == 200: - data = response.json() - if "results" in data: - for result in data["results"]: - if "elevation" in result: - results.append({ - "latitude": result["location"]["lat"], - "longitude": result["location"]["lng"], - "elevation": result["elevation"] - }) - else: - # استخدام بيانات افتراضية في حالة فشل الطلب - st.warning(f"فشل جلب بيانات التضاريس من الخدمة (رمز الحالة: {response.status_code}). استخدام بيانات افتراضية.") - - # إنشاء بيانات افتراضية - for j, loc in enumerate(batch): - lat, lon = map(float, loc.split(",")) - # حساب ارتفاع افتراضي بناءً على المسافة من المركز - dist = self._calculate_distance(latitude, longitude, lat, lon) - # إضافة ضوضاء عشوائية للحصول على تضاريس أكثر واقعية - noise = np.sin(lat * 10) * np.cos(lon * 10) * 50 - elevation = 500 - dist * 100 + noise - - results.append({ - "latitude": lat, - "longitude": lon, - "elevation": max(0, elevation) - }) - except Exception as e: - st.warning(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}. استخدام بيانات افتراضية.") - - # إنشاء بيانات افتراضية - for j, loc in enumerate(batch): - lat, lon = map(float, loc.split(",")) - # حساب ارتفاع افتراضي بناءً على المسافة من المركز - dist = self._calculate_distance(latitude, longitude, lat, lon) - # إضافة ضوضاء عشوائية للحصول على تضاريس أكثر واقعية - noise = np.sin(lat * 10) * np.cos(lon * 10) * 50 - elevation = 500 - dist * 100 + noise - - results.append({ - "latitude": lat, - "longitude": lon, - "elevation": max(0, elevation) - }) - - return results - - def _calculate_distance(self, lat1, lon1, lat2, lon2): - """حساب المسافة بين نقطتين بالكيلومترات باستخدام صيغة هافرساين""" - from math import radians, sin, cos, sqrt, atan2 - - # تحويل الإحداثيات إلى راديان - lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2]) - - # صيغة هافرساين - dlon = lon2 - lon1 - dlat = lat2 - lat1 - a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 - c = 2 * atan2(sqrt(a), sqrt(1-a)) - distance = 6371 * c # نصف قطر الأرض بالكيلومترات - - return distance - - def _get_color_map(self, scheme): - """الحصول على خريطة الألوان حسب النظام المختار""" - import matplotlib.cm as cm - import matplotlib.colors as colors - - # الحصول على خريطة الألوان - colormap = cm.get_cmap(scheme) - - # إرجاع دالة لتطبيق خريطة الألوان - return lambda x: colors.rgb2hex(colormap(x)) - - def _export_locations(self, format): - """تصدير مواقع المشاريع إلى ملف""" - try: - # تحويل البيانات إلى DataFrame - df = pd.DataFrame(st.session_state.project_locations) - - # تصدير البيانات حسب التنسيق المطلوب - if format == "csv": - csv_data = df.to_csv(index=False).encode("utf-8") - return csv_data - elif format == "excel": - # إنشاء ملف إكسل مؤقت - with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as temp: - df.to_excel(temp.name, index=False, engine="xlsxwriter") - temp.flush() - - # قراءة الملف كبيانات ثنائية - with open(temp.name, "rb") as f: - excel_data = f.read() - - # حذف الملف المؤقت - os.unlink(temp.name) - - return excel_data - elif format == "json": - json_data = json.dumps(st.session_state.project_locations, ensure_ascii=False, indent=4).encode("utf-8") - return json_data - else: - st.error(f"تنسيق غير مدعوم: {format}") - return None - except Exception as e: - st.error(f"حدث خطأ أثناء تصدير البيانات: {str(e)}") - return None - - def _import_locations(self, uploaded_file, format): - """استيراد مواقع المشاريع من ملف""" - try: - imported_data = [] - - # تحميل البيانات حسب التنسيق - if format == "csv": - df = pd.read_csv(uploaded_file) - imported_data = df.to_dict("records") - elif format == "excel": - df = pd.read_excel(uploaded_file) - imported_data = df.to_dict("records") - elif format == "json": - imported_data = json.load(uploaded_file) - else: - raise ValueError(f"تنسيق غير مدعوم: {format}") - - # التحقق من صحة البيانات - required_fields = ["project_id", "name", "latitude", "longitude"] - - for item in imported_data: - missing_fields = [field for field in required_fields if field not in item] - - if missing_fields: - raise ValueError(f"الحقول المطلوبة مفقودة: {', '.join(missing_fields)}") - - # تحديث البيانات - if "import_mode" in st.session_state and st.session_state.import_mode == "استبدال جميع المواقع": - # استبدال جميع البيانات - st.session_state.project_locations = imported_data - else: - # إضافة البيانات الجديدة فقط - existing_ids = {p["project_id"] for p in st.session_state.project_locations} - new_items = [item for item in imported_data if item["project_id"] not in existing_ids] - st.session_state.project_locations.extend(new_items) - imported_data = new_items - - # حفظ البيانات - self._save_locations_data() - - return len(imported_data) - except Exception as e: - raise Exception(f"حدث خطأ أثناء استيراد البيانات: {str(e)}") - - def _save_locations_data(self): - """حفظ بيانات المواقع""" - try: - # إنشاء مسار الملف - file_path = os.path.join(self.data_dir, "project_locations.json") - - # حفظ البيانات كملف JSON - with open(file_path, "w", encoding="utf-8") as f: - json.dump(st.session_state.project_locations, f, ensure_ascii=False, indent=4) - except Exception as e: - st.error(f"حدث خطأ أثناء حفظ بيانات المواقع: {str(e)}") - - def _load_locations_data(self): - """تحميل بيانات المواقع""" - try: - # إنشاء مسار الملف - file_path = os.path.join(self.data_dir, "project_locations.json") - - # التحقق من وجود الملف - if os.path.exists(file_path): - # تحميل البيانات من ملف JSON - with open(file_path, "r", encoding="utf-8") as f: - st.session_state.project_locations = json.load(f) - else: - # تهيئة بيانات اختبارية - self._initialize_sample_projects() - except Exception as e: - st.error(f"حدث خطأ أثناء تحميل بيانات المواقع: {str(e)}") - # تهيئة بيانات اختبارية - self._initialize_sample_projects() - - def _initialize_sample_projects(self): - """تهيئة بيانات اختبارية للمشاريع""" - # قائمة بأسماء مدن المملكة العربية السعودية - saudi_cities = [ - {"name": "الرياض", "lat": 24.7136, "lon": 46.6753}, - {"name": "جدة", "lat": 21.4858, "lon": 39.1925}, - {"name": "مكة المكرمة", "lat": 21.3891, "lon": 39.8579}, - {"name": "المدينة المنورة", "lat": 24.5247, "lon": 39.5692}, - {"name": "الدمام", "lat": 26.4207, "lon": 50.0888}, - {"name": "الطائف", "lat": 21.2704, "lon": 40.4157}, - {"name": "تبوك", "lat": 28.3835, "lon": 36.5662}, - {"name": "بريدة", "lat": 26.3267, "lon": 43.9717}, - {"name": "الخبر", "lat": 26.2172, "lon": 50.1971}, - {"name": "أبها", "lat": 18.2164, "lon": 42.5053} - ] - - # قائمة بأنواع المشاريع - project_types = [ - "إنشاء مبنى سكني", - "تطوير طريق سريع", - "بناء جسر", - "إنشاء مدرسة", - "تطوير حديقة عامة", - "بناء مستشفى", - "إنشاء محطة تحلية مياه", - "تطوير مركز تجاري", - "بناء مصنع", - "توسعة مطار" - ] - - # قائمة بحالات المشاريع - project_statuses = ["مخطط", "قيد التنفيذ", "متوقف", "مكتمل"] - - # إنشاء مشاريع اختبارية - sample_projects = [] - - for i in range(10): - city = saudi_cities[i] - - # إضافة اختلاف عشوائي صغير للإحداثيات - lat_offset = random.uniform(-0.05, 0.05) - lon_offset = random.uniform(-0.05, 0.05) - - project = { - "project_id": f"PRJ{i+1:03d}", - "name": f"{project_types[i]} في {city['name']}", - "description": f"مشروع {project_types[i]} بمدينة {city['name']}. هذا وصف اختباري للمشروع يوضح تفاصيله وأهدافه ونطاق العمل.", - "city": city["name"], - "status": random.choice(project_statuses), - "latitude": city["lat"] + lat_offset, - "longitude": city["lon"] + lon_offset - } - - sample_projects.append(project) - - # حفظ المشاريع الاختبارية في حالة الجلسة - st.session_state.project_locations = sample_projects - - -if __name__ == "__main__": - """تشغيل وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد بشكل مستقل""" - interactive_map = InteractiveMap() - interactive_map.render() \ No newline at end of file diff --git a/modules/maps/interactive_map.py.bak b/modules/maps/interactive_map.py.bak deleted file mode 100644 index 9a161132b1327788cd1ec7b6e9c035ecf8f4aa00..0000000000000000000000000000000000000000 --- a/modules/maps/interactive_map.py.bak +++ /dev/null @@ -1,1647 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد -تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np -import pydeck as pdk -import folium -from folium.plugins import MarkerCluster, HeatMap, MeasureControl -from streamlit_folium import folium_static -import requests -import json -import random -from typing import List, Dict, Any, Tuple, Optional -import tempfile -import base64 -from PIL import Image -from io import BytesIO - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات واجهة المستخدم -from utils.components.header import render_header -from utils.components.credits import render_credits -from utils.helpers import format_number, format_currency, styled_button - - -class InteractiveMap: - """فئة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد""" - - def __init__(self): - """تهيئة وحدة الخريطة التفاعلية""" - # تهيئة مجلدات حفظ البيانات - self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/maps")) - os.makedirs(self.data_dir, exist_ok=True) - - # مفاتيح API لخدمات الخرائط - self.mapbox_token = os.environ.get("MAPBOX_TOKEN", "") - self.opentopodata_api = "https://api.opentopodata.org/v1/srtm30m" - - # تهيئة حالة الجلسة - if 'project_locations' not in st.session_state: - st.session_state.project_locations = [] - - if 'selected_location' not in st.session_state: - st.session_state.selected_location = None - - if 'terrain_data' not in st.session_state: - st.session_state.terrain_data = None - - # بيانات اختبارية للمشاريع (سيتم استبدالها بالبيانات الفعلية من قاعدة البيانات) - self._initialize_sample_projects() - - def render(self): - """عرض واجهة وحدة الخريطة التفاعلية""" - # عرض الشعار والعنوان الرئيسي - render_header("خريطة مواقع المشاريع التفاعلية") - - # تبويبات الوحدة - tabs = st.tabs([ - "الخريطة التفاعلية", - "عرض التضاريس ثلاثي الأبعاد", - "تحليل المواقع", - "إدارة المواقع" - ]) - - # تبويب الخريطة التفاعلية - with tabs[0]: - self._render_interactive_map() - - # تبويب عرض التضاريس ثلاثي الأبعاد - with tabs[1]: - self._render_3d_terrain() - - # تبويب تحليل المواقع - with tabs[2]: - self._render_location_analysis() - - # تبويب إدارة المواقع - with tabs[3]: - self._render_location_management() - - # عرض حقوق النشر - render_credits() - - def _render_interactive_map(self): - """عرض الخريطة التفاعلية""" - st.markdown(""" -
-

🗺️ الخريطة التفاعلية لمواقع المشاريع

-

خريطة تفاعلية تعرض مواقع جميع المشاريع مع إمكانية التكبير والتصغير والتحرك.

-

يمكنك النقر على أي موقع للحصول على تفاصيل المشروع.

-
- """, unsafe_allow_html=True) - - # مربع البحث - search_query = st.text_input("البحث عن موقع أو مشروع", key="map_search") - - # أزرار تحكم للخريطة - col1, col2, col3, col4 = st.columns(4) - - with col1: - map_style = st.selectbox( - "نمط الخريطة", - options=["OpenStreetMap", "Stamen Terrain", "Stamen Toner", "CartoDB Positron"], - key="map_style" - ) - - with col2: - cluster_markers = st.checkbox("تجميع المواقع القريبة", value=True, key="cluster_markers") - - with col3: - show_heatmap = st.checkbox("عرض خريطة حرارية للمواقع", value=False, key="show_heatmap") - - with col4: - show_measurements = st.checkbox("أدوات القياس", value=False, key="show_measurements") - - # إنشاء الخريطة - if len(st.session_state.project_locations) > 0: - # بيانات النقاط على الخريطة - locations = [] - - # تصفية المشاريع حسب البحث - filtered_projects = st.session_state.project_locations - if search_query: - filtered_projects = [ - p for p in filtered_projects - if search_query.lower() in p.get("name", "").lower() or - search_query.lower() in p.get("description", "").lower() or - search_query.lower() in p.get("city", "").lower() - ] - - # عرض عدد النتائج - if search_query: - st.markdown(f"عدد النتائج: {len(filtered_projects)}") - - # تحضير البيانات للخريطة - heat_data = [] - for project in filtered_projects: - locations.append({ - "lat": project.get("latitude"), - "lon": project.get("longitude"), - "name": project.get("name"), - "description": project.get("description"), - "city": project.get("city"), - "status": project.get("status"), - "project_id": project.get("project_id") - }) - heat_data.append([project.get("latitude"), project.get("longitude"), 1]) - - # تعيين نقطة المركز والتكبير - if filtered_projects: - center_lat = sum(p.get("latitude", 0) for p in filtered_projects) / len(filtered_projects) - center_lon = sum(p.get("longitude", 0) for p in filtered_projects) / len(filtered_projects) - zoom_level = 6 # مستوى التكبير الافتراضي - else: - # مركز المملكة العربية السعودية - center_lat = 24.7136 - center_lon = 46.6753 - zoom_level = 5 - - # تحديد الإسناد (attribution) بناءً على نمط الخريطة - attribution = None - if map_style == "OpenStreetMap": - attribution = '© OpenStreetMap contributors' - elif map_style.startswith("Stamen"): - attribution = 'Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.' - elif map_style == "CartoDB Positron": - attribution = '© OpenStreetMap contributors, © CartoDB' - - # إنشاء الخريطة - m = folium.Map( - location=[center_lat, center_lon], - zoom_start=zoom_level, - tiles=map_style, - attr=attribution # إضافة سمة الإسناد - ) - - # إضافة أدوات القياس إذا تم اختيارها - if show_measurements: - MeasureControl(position='topright', primary_length_unit='kilometers').add_to(m) - - # إضافة النقاط إلى الخريطة - if cluster_markers: - # إنشاء مجموعة تجميع - marker_cluster = MarkerCluster(name="تجميع المشاريع").add_to(m) - - # إضافة النقاط إلى المجموعة - for location in locations: - # إنشاء النافذة المنبثقة - popup_html = f""" -
-

{location['name']}

-

الوصف: {location['description']}

-

المدينة: {location['city']}

-

الحالة: {location['status']}

-

الإحداثيات: {location['lat']:.6f}, {location['lon']:.6f}

- -
- """ - - # تحديد لون العلامة حسب حالة المشروع - icon_color = 'green' - if location['status'] == 'قيد التنفيذ': - icon_color = 'orange' - elif location['status'] == 'متوقف': - icon_color = 'red' - elif location['status'] == 'مكتمل': - icon_color = 'blue' - - # إضافة العلامة - folium.Marker( - location=[location['lat'], location['lon']], - popup=folium.Popup(popup_html, max_width=300), - tooltip=location['name'], - icon=folium.Icon(color=icon_color, icon='info-sign') - ).add_to(marker_cluster) - else: - # إضافة النقاط مباشرة إلى الخريطة - for location in locations: - # إنشاء النافذة المنبثقة - popup_html = f""" -
-

{location['name']}

-

الوصف: {location['description']}

-

المدينة: {location['city']}

-

الحالة: {location['status']}

-

الإحداثيات: {location['lat']:.6f}, {location['lon']:.6f}

- -
- """ - - # تحديد لون العلامة حسب حالة المشروع - icon_color = 'green' - if location['status'] == 'قيد التنفيذ': - icon_color = 'orange' - elif location['status'] == 'متوقف': - icon_color = 'red' - elif location['status'] == 'مكتمل': - icon_color = 'blue' - - # إضافة العلامة - folium.Marker( - location=[location['lat'], location['lon']], - popup=folium.Popup(popup_html, max_width=300), - tooltip=location['name'], - icon=folium.Icon(color=icon_color, icon='info-sign') - ).add_to(m) - - # إضافة خريطة حرارية إذا تم اختيارها - if show_heatmap and heat_data: - HeatMap(heat_data, radius=15).add_to(m) - - # إضافة طبقات متنوعة للخريطة - folium.TileLayer('OpenStreetMap').add_to(m) - folium.TileLayer('Stamen Terrain').add_to(m) - folium.TileLayer('Stamen Toner').add_to(m) - folium.TileLayer('CartoDB positron').add_to(m) - folium.TileLayer('CartoDB dark_matter').add_to(m) - - # إضافة أدوات التحكم بالطبقات - folium.LayerControl().add_to(m) - - # عرض الخريطة - st_map = folium_static(m, width=1000, height=600) - - # التفاعل مع النقر على الخريطة (سيتم تنفيذه عندما يتم دعم التفاعل) - # حاليًا لا يوجد دعم مباشر للتفاعل مع الخريطة في Streamlit - - # عرض بيانات المشاريع في جدول - st.markdown("### قائمة المشاريع على الخريطة") - - projects_df = pd.DataFrame(filtered_projects) - - # إعادة تسمية الأعمدة بالعربية - renamed_columns = { - "name": "اسم المشروع", - "city": "المدينة", - "status": "الحالة", - "description": "الوصف", - "project_id": "معرف المشروع", - "latitude": "خط العرض", - "longitude": "خط الطول" - } - - # تحديد الأعمدة للعرض - display_columns = ["name", "city", "status", "project_id"] - - # إنشاء جدول للعرض - display_df = projects_df[display_columns].rename(columns=renamed_columns) - - # عرض الجدول - st.dataframe(display_df, width=1000, height=400) - - # زر لاختيار مشروع لعرض التضاريس - selected_project_id = st.selectbox( - "اختر مشروعًا لعرض التضاريس ثلاثية الأبعاد", - options=projects_df["project_id"].tolist(), - format_func=lambda x: next((p["name"] for p in filtered_projects if p["project_id"] == x), x), - key="select_project_for_terrain" - ) - - # زر عرض التضاريس - if styled_button("عرض التضاريس", key="show_terrain_btn", type="primary", icon="🏔️"): - # العثور على المشروع المحدد - selected_project = next((p for p in filtered_projects if p["project_id"] == selected_project_id), None) - - if selected_project: - # تخزين الموقع المحدد في حالة الجلسة - st.session_state.selected_location = { - "latitude": selected_project["latitude"], - "longitude": selected_project["longitude"], - "name": selected_project["name"], - "project_id": selected_project["project_id"] - } - - # جلب بيانات التضاريس - try: - terrain_data = self._fetch_terrain_data( - selected_project["latitude"], - selected_project["longitude"] - ) - - # تخزين بيانات التضاريس في حالة الجلسة - st.session_state.terrain_data = terrain_data - - # الانتقال إلى تبويب عرض التضاريس - st.experimental_rerun() - except Exception as e: - st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}") - else: - st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.") - - def _render_3d_terrain(self): - """عرض التضاريس ثلاثي الأبعاد""" - st.markdown(""" -
-

🏔️ عرض التضاريس ثلاثي الأبعاد

-

عرض تفاعلي ثلاثي الأبعاد لتضاريس موقع المشروع المحدد.

-

يمكنك تدوير وتكبير العرض للحصول على رؤية أفضل للموقع.

-
- """, unsafe_allow_html=True) - - # التحقق من وجود موقع محدد - if st.session_state.selected_location is None: - st.info("يرجى اختيار موقع من تبويب 'الخريطة التفاعلية' أولاً.") - - # بديل: السماح بإدخال الإحداثيات يدوياً - st.markdown("### إدخال الإحداثيات يدوياً") - - col1, col2 = st.columns(2) - - with col1: - manual_lat = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="manual_lat") - - with col2: - manual_lon = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="manual_lon") - - if styled_button("عرض التضاريس", key="manual_terrain_btn", type="primary", icon="🏔️"): - try: - # جلب بيانات التضاريس - terrain_data = self._fetch_terrain_data(manual_lat, manual_lon) - - # تخزين بيانات التضاريس والموقع في حالة الجلسة - st.session_state.terrain_data = terrain_data - st.session_state.selected_location = { - "latitude": manual_lat, - "longitude": manual_lon, - "name": "موقع مخصص", - "project_id": "custom" - } - - st.success("تم جلب بيانات التضاريس بنجاح!") - st.experimental_rerun() - except Exception as e: - st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}") - - return - - # عرض معلومات الموقع المحدد - location = st.session_state.selected_location - st.markdown(f"### عرض تضاريس موقع: {location['name']}") - st.markdown(f"**الإحداثيات:** {location['latitude']:.6f}, {location['longitude']:.6f}") - - # تجهيز بيانات التضاريس - if st.session_state.terrain_data is None: - # محاولة جلب بيانات التضاريس - try: - terrain_data = self._fetch_terrain_data( - location["latitude"], - location["longitude"] - ) - - # تخزين بيانات التضاريس في حالة الجلسة - st.session_state.terrain_data = terrain_data - except Exception as e: - st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}") - return - - # استخدام بيانات التضاريس المخزنة - terrain_data = st.session_state.terrain_data - - # عرض نطاق التضاريس وإعدادات الارتفاع - col1, col2, col3 = st.columns(3) - - with col1: - elevation_scale = st.slider( - "مقياس الارتفاع", - min_value=1, - max_value=50, - value=15, - key="elevation_scale" - ) - - with col2: - radius = st.slider( - "نطاق العرض (كم)", - min_value=1, - max_value=20, - value=5, - key="terrain_radius" - ) - - with col3: - color_scheme = st.selectbox( - "نظام الألوان", - options=["terrain", "elevation", "custom"], - key="color_scheme" - ) - - # إنشاء نموذج PyDeck للعرض ثلاثي الأبعاد - try: - # تحويل بيانات التضاريس إلى DataFrame - terrain_df = pd.DataFrame(terrain_data) - - # تعيين حجم الخلية بناءً على النطاق - cell_size = radius * 100 # تحويل الكيلومترات إلى أمتار وتقسيمها - - # إنشاء طبقة التضاريس - terrain_layer = pdk.Layer( - "TerrainLayer", - data=None, - elevation_decoder={ - "elevations": "elevation", - "bounds": terrain_df["bounds"].iloc[0] - }, - texture=None, - elevation_data=terrain_df["terrain"].iloc[0], - elevation_scale=elevation_scale, - color_map=self._get_color_map(color_scheme), - wireframe=True, - pickable=True - ) - - # إنشاء طبقة النقطة المركزية - point_layer = pdk.Layer( - "ScatterplotLayer", - data=[{ - "position": [location["longitude"], location["latitude"]], - "name": location["name"] - }], - get_position="position", - get_radius=100, - get_fill_color=[255, 0, 0, 200], - pickable=True - ) - - # إنشاء عرض PyDeck - INITIAL_VIEW_STATE = pdk.ViewState( - longitude=location["longitude"], - latitude=location["latitude"], - zoom=12, - max_zoom=20, - pitch=45, - bearing=0 - ) - - deck = pdk.Deck( - map_style="mapbox://styles/mapbox/satellite-v9", - initial_view_state=INITIAL_VIEW_STATE, - api_keys={"mapbox": self.mapbox_token} if self.mapbox_token else None, - layers=[terrain_layer, point_layer], - tooltip={ - "html": "{name}", - "style": { - "backgroundColor": "steelblue", - "color": "white" - } - } - ) - - # عرض النموذج ثلاثي الأبعاد - st.pydeck_chart(deck) - - # عرض تحليل التضاريس - if "elevation_stats" in terrain_df: - elevation_stats = terrain_df["elevation_stats"].iloc[0] - - st.markdown("### تحليل التضاريس") - - stats_col1, stats_col2, stats_col3, stats_col4 = st.columns(4) - - with stats_col1: - st.metric("أدنى ارتفاع", f"{elevation_stats['min']:.1f} م") - - with stats_col2: - st.metric("أعلى ارتفاع", f"{elevation_stats['max']:.1f} م") - - with stats_col3: - st.metric("متوسط الارتفاع", f"{elevation_stats['mean']:.1f} م") - - with stats_col4: - st.metric("فرق الارتفاع", f"{elevation_stats['range']:.1f} م") - - # عرض رسم بياني للارتفاعات - if "elevation_profile" in terrain_df: - elevation_profile = terrain_df["elevation_profile"].iloc[0] - - # إنشاء DataFrame للرسم البياني - profile_df = pd.DataFrame(elevation_profile) - - # عرض الرسم البياني - st.markdown("### مقطع الارتفاع") - - # استخدام Plotly Express - import plotly.express as px - - fig = px.line( - profile_df, - x="distance", - y="elevation", - title="مقطع الارتفاع عبر الموقع", - labels={"distance": "المسافة (كم)", "elevation": "الارتفاع (م)"} - ) - - fig.update_layout( - title_font_size=20, - font_family="Arial", - font_size=14, - height=400 - ) - - st.plotly_chart(fig, use_container_width=True) - - # أزرار التحكم الإضافية - col1, col2 = st.columns(2) - - with col1: - if styled_button("إعادة تحميل بيانات التضاريس", key="reload_terrain", type="primary", icon="🔄"): - # حذف بيانات التضاريس الحالية - st.session_state.terrain_data = None - st.experimental_rerun() - - with col2: - if styled_button("العودة للخريطة التفاعلية", key="back_to_map", type="secondary", icon="🗺️"): - # إعادة تعيين الموقع المحدد - st.session_state.selected_location = None - st.session_state.terrain_data = None - st.experimental_rerun() - - except Exception as e: - st.error(f"حدث خطأ أثناء عرض التضاريس ثلاثي الأبعاد: {str(e)}") - - def _render_location_analysis(self): - """عرض تحليل المواقع""" - st.markdown(""" -
-

📊 تحليل المواقع

-

تحليل لمواقع المشاريع وتوزيعها الجغرافي.

-

يمكنك عرض إحصائيات وتقارير متنوعة حول مواقع المشاريع.

-
- """, unsafe_allow_html=True) - - # التحقق من وجود مواقع - if not st.session_state.project_locations: - st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.") - return - - # تحويل بيانات المواقع إلى DataFrame - locations_df = pd.DataFrame(st.session_state.project_locations) - - # عرض توزيع المشاريع حسب المدينة - st.markdown("### توزيع المشاريع حسب المدينة") - - city_counts = locations_df["city"].value_counts().reset_index() - city_counts.columns = ["المدينة", "عدد المشاريع"] - - # عرض الرسم البياني - import plotly.express as px - - fig = px.bar( - city_counts, - x="المدينة", - y="عدد المشاريع", - title="توزيع المشاريع حسب المدينة", - color="عدد المشاريع", - color_continuous_scale="Viridis" - ) - - fig.update_layout( - title_font_size=20, - font_family="Arial", - font_size=14, - height=400 - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض توزيع المشاريع حسب الحالة - st.markdown("### توزيع المشاريع حسب الحالة") - - status_counts = locations_df["status"].value_counts().reset_index() - status_counts.columns = ["الحالة", "عدد المشاريع"] - - # عرض الرسم البياني - fig2 = px.pie( - status_counts, - values="عدد المشاريع", - names="الحالة", - title="توزيع المشاريع حسب الحالة", - color_discrete_sequence=px.colors.qualitative.Set3 - ) - - fig2.update_layout( - title_font_size=20, - font_family="Arial", - font_size=14, - height=400 - ) - - st.plotly_chart(fig2, use_container_width=True) - - # عرض تحليل المسافات بين المشاريع - st.markdown("### تحليل المسافات بين المشاريع") - - # حساب مصفوفة المسافات - if len(locations_df) > 1: - # اختيار مشروع كنقطة مرجعية - reference_project = st.selectbox( - "اختر مشروعًا كنقطة مرجعية", - options=locations_df["project_id"].tolist(), - format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x), - key="reference_project" - ) - - # العثور على المشروع المرجعي - ref_project_data = locations_df[locations_df["project_id"] == reference_project].iloc[0] - - # حساب المسافات - distances = [] - for _, project in locations_df.iterrows(): - if project["project_id"] != reference_project: - distance = self._calculate_distance( - ref_project_data["latitude"], ref_project_data["longitude"], - project["latitude"], project["longitude"] - ) - - distances.append({ - "project_id": project["project_id"], - "name": project["name"], - "city": project["city"], - "distance": distance - }) - - # تحويل البيانات إلى DataFrame - distances_df = pd.DataFrame(distances) - - # ترتيب المشاريع حسب المسافة - distances_df = distances_df.sort_values("distance") - - # عرض المسافات - st.markdown(f"المسافات من مشروع: **{ref_project_data['name']}**") - - # إعادة تسمية الأعمدة - distances_df = distances_df.rename(columns={ - "name": "اسم المشروع", - "city": "المدينة", - "distance": "المسافة (كم)" - }) - - # تنسيق المسافة - distances_df["المسافة (كم)"] = distances_df["المسافة (كم)"].round(2) - - # عرض الجدول - st.dataframe(distances_df[["اسم المشروع", "المدينة", "المسافة (كم)"]], width=800) - - # عرض رسم بياني للمسافات - fig3 = px.bar( - distances_df, - x="اسم المشروع", - y="المسافة (كم)", - title=f"المسافات من مشروع {ref_project_data['name']}", - color="المسافة (كم)", - color_continuous_scale="Viridis" - ) - - fig3.update_layout( - title_font_size=20, - font_family="Arial", - font_size=14, - height=400 - ) - - st.plotly_chart(fig3, use_container_width=True) - - # عرض المشاريع القريبة على خريطة - st.markdown("### المشاريع القريبة على الخريطة") - - # إنشاء الخريطة - m2 = folium.Map( - location=[ref_project_data["latitude"], ref_project_data["longitude"]], - zoom_start=8, - tiles="OpenStreetMap", - attr='© OpenStreetMap contributors' - ) - - # إضافة المشروع المرجعي - folium.Marker( - location=[ref_project_data["latitude"], ref_project_data["longitude"]], - popup=ref_project_data["name"], - tooltip=ref_project_data["name"], - icon=folium.Icon(color='red', icon='star') - ).add_to(m2) - - # إضافة الدوائر - folium.Circle( - location=[ref_project_data["latitude"], ref_project_data["longitude"]], - radius=50000, # 50 كم - color='red', - fill=True, - fill_opacity=0.1, - popup="50 كم" - ).add_to(m2) - - folium.Circle( - location=[ref_project_data["latitude"], ref_project_data["longitude"]], - radius=100000, # 100 كم - color='orange', - fill=True, - fill_opacity=0.1, - popup="100 كم" - ).add_to(m2) - - folium.Circle( - location=[ref_project_data["latitude"], ref_project_data["longitude"]], - radius=200000, # 200 كم - color='blue', - fill=True, - fill_opacity=0.1, - popup="200 كم" - ).add_to(m2) - - # إضافة المشاريع الأخرى - for _, project in distances_df.iterrows(): - project_data = locations_df[locations_df["project_id"] == project["project_id"]].iloc[0] - - folium.Marker( - location=[project_data["latitude"], project_data["longitude"]], - popup=f"{project_data['name']} - {project['المسافة (كم)']} كم", - tooltip=project_data["name"], - icon=folium.Icon(color='green', icon='info-sign') - ).add_to(m2) - - # إضافة خط للربط - folium.PolyLine( - locations=[ - [ref_project_data["latitude"], ref_project_data["longitude"]], - [project_data["latitude"], project_data["longitude"]] - ], - color='gray', - weight=2, - opacity=0.5, - popup=f"{project['المسافة (كم)']} كم" - ).add_to(m2) - - # عرض الخريطة - folium_static(m2, width=800, height=500) - else: - st.info("يجب وجود أكثر من مشروع واحد لحساب المسافات.") - - def _render_location_management(self): - """عرض إدارة المواقع""" - st.markdown(""" -
-

⚙️ إدارة المواقع

-

إضافة وتحرير وحذف مواقع المشاريع.

-

يمكنك إضافة مواقع جديدة أو تحديث المواقع الموجودة.

-
- """, unsafe_allow_html=True) - - # تبويبات إدارة المواقع - management_tabs = st.tabs(["إضافة موقع جديد", "تحرير المواقع الموجودة", "استيراد وتصدير المواقع"]) - - # تبويب إضافة موقع جديد - with management_tabs[0]: - self._render_add_location() - - # تبويب تحرير المواقع الموجودة - with management_tabs[1]: - self._render_edit_locations() - - # تبويب استيراد وتصدير المواقع - with management_tabs[2]: - self._render_import_export_locations() - - def _render_add_location(self): - """عرض نموذج إضافة موقع جديد""" - st.markdown("### إضافة موقع مشروع جديد") - - # البيانات الأساسية - project_name = st.text_input("اسم المشروع", key="new_project_name") - project_desc = st.text_area("وصف المشروع", key="new_project_desc") - - col1, col2 = st.columns(2) - - with col1: - project_city = st.text_input("المدينة", key="new_project_city") - project_status = st.selectbox( - "حالة المشروع", - options=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"], - key="new_project_status" - ) - - with col2: - project_id = st.text_input("معرف المشروع (اختياري)", key="new_project_id", placeholder="سيتم إنشاؤه تلقائيًا إذا تُرك فارغًا") - - # إدخال إحداثيات الموقع - st.markdown("#### إحداثيات الموقع") - location_method = st.radio( - "طريقة تحديد الموقع", - options=["إدخال يدوي", "اختيار من الخريطة"], - key="new_location_method" - ) - - # تحديد الموقع - if location_method == "إدخال يدوي": - loc_col1, loc_col2 = st.columns(2) - - with loc_col1: - latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="new_latitude") - - with loc_col2: - longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="new_longitude") - - # عرض الموقع على خريطة صغيرة - mini_map = folium.Map(location=[latitude, longitude], zoom_start=10) - folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map) - folium_static(mini_map, width=700, height=300) - else: - st.markdown("#### اختر الموقع من الخريطة") - st.info("انقر على الخريطة لتحديد الموقع.") - - # إنشاء خريطة - m = folium.Map(location=[24.7136, 46.6753], zoom_start=6) - - # إضافة محدد النقر - m.add_child(folium.ClickForMarker(popup="الموقع المحدد")) - - # عرض الخريطة - map_data = folium_static(m, width=700, height=400) - - # استخراج الإحداثيات المحددة (ليس مدعومًا حاليًا في Streamlit) - st.warning("ملاحظة: خاصية النقر على الخريطة غير مدعومة حاليًا في Streamlit. يرجى استخدام الإدخال اليدوي.") - - latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="map_latitude") - longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="map_longitude") - - # زر إضافة الموقع - if styled_button("إضافة الموقع", key="add_location", type="primary", icon="➕"): - if not project_name or not project_desc or not project_city: - st.error("يرجى تعبئة جميع الحقول المطلوبة.") - else: - # إنشاء معرف فريد للمشروع إذا لم يتم تحديده - if not project_id: - project_id = f"PRJ-{len(st.session_state.project_locations) + 1:04d}" - - # إنشاء كائن الموقع - new_location = { - "name": project_name, - "description": project_desc, - "city": project_city, - "status": project_status, - "latitude": latitude, - "longitude": longitude, - "project_id": project_id, - "created_at": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S"), - "updated_at": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S") - } - - # إضافة الموقع للقائمة - st.session_state.project_locations.append(new_location) - - # حفظ البيانات - self._save_locations_data() - - st.success(f"تمت إضافة موقع المشروع '{project_name}' بنجاح!") - st.balloons() - - def _render_edit_locations(self): - """عرض واجهة تحرير المواقع الموجودة""" - st.markdown("### تحرير أو حذف مواقع المشاريع") - - # التحقق من وجود مواقع - if not st.session_state.project_locations: - st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع أولاً.") - return - - # اختيار المشروع للتحرير - selected_project_id = st.selectbox( - "اختر مشروعًا للتحرير", - options=[p["project_id"] for p in st.session_state.project_locations], - format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x), - key="edit_project_select" - ) - - # العثور على المشروع المحدد - selected_project = next((p for p in st.session_state.project_locations if p["project_id"] == selected_project_id), None) - - if selected_project: - # عرض نموذج التحرير - st.markdown(f"### تحرير مشروع: {selected_project['name']}") - - # البيانات الأساسية - project_name = st.text_input("اسم المشروع", value=selected_project["name"], key="edit_project_name") - project_desc = st.text_area("وصف المشروع", value=selected_project["description"], key="edit_project_desc") - - col1, col2 = st.columns(2) - - with col1: - project_city = st.text_input("المدينة", value=selected_project["city"], key="edit_project_city") - project_status = st.selectbox( - "حالة المشروع", - options=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"], - index=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"].index(selected_project["status"]), - key="edit_project_status" - ) - - with col2: - st.text_input("معرف المشروع", value=selected_project["project_id"], disabled=True, key="edit_project_id") - - # إدخال إحداثيات الموقع - st.markdown("#### إحداثيات الموقع") - - # تحديد الموقع - loc_col1, loc_col2 = st.columns(2) - - with loc_col1: - latitude = st.number_input( - "خط العرض", - value=selected_project["latitude"], - step=0.0001, - format="%.6f", - key="edit_latitude" - ) - - with loc_col2: - longitude = st.number_input( - "خط الطول", - value=selected_project["longitude"], - step=0.0001, - format="%.6f", - key="edit_longitude" - ) - - # عرض الموقع على خريطة صغيرة - mini_map = folium.Map(location=[latitude, longitude], zoom_start=10) - folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map) - folium_static(mini_map, width=700, height=300) - - # أزرار الإجراءات - col1, col2 = st.columns(2) - - with col1: - if styled_button("حفظ التغييرات", key="save_location_changes", type="primary", icon="💾"): - if not project_name or not project_desc or not project_city: - st.error("يرجى تعبئة جميع الحقول المطلوبة.") - else: - # تحديث بيانات المشروع - selected_project["name"] = project_name - selected_project["description"] = project_desc - selected_project["city"] = project_city - selected_project["status"] = project_status - selected_project["latitude"] = latitude - selected_project["longitude"] = longitude - selected_project["updated_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S") - - # حفظ البيانات - self._save_locations_data() - - st.success(f"تم تحديث بيانات المشروع '{project_name}' بنجاح!") - st.experimental_rerun() - - with col2: - if styled_button("حذف المشروع", key="delete_location", type="danger", icon="🗑️"): - # تأكيد الحذف - st.warning(f"هل أنت متأكد من حذف المشروع '{selected_project['name']}'؟") - - confirm_col1, confirm_col2 = st.columns(2) - - with confirm_col1: - if styled_button("تأكيد الحذف", key="confirm_delete", type="danger", icon="✓"): - # إزالة المشروع من القائمة - st.session_state.project_locations.remove(selected_project) - - # حفظ البيانات - self._save_locations_data() - - st.success(f"تم حذف المشروع '{selected_project['name']}' بنجاح!") - st.experimental_rerun() - - with confirm_col2: - if styled_button("إلغاء", key="cancel_delete", type="secondary", icon="❌"): - st.experimental_rerun() - - def _render_import_export_locations(self): - """عرض واجهة استيراد وتصدير المواقع""" - st.markdown("### استيراد وتصدير مواقع المشاريع") - - col1, col2 = st.columns(2) - - with col1: - st.markdown("#### تصدير المواقع") - - export_format = st.selectbox( - "صيغة التصدير", - options=["CSV", "JSON", "GeoJSON"], - key="export_format" - ) - - if styled_button("تصدير المواقع", key="export_locations", type="primary", icon="📤"): - self._export_locations(export_format) - - with col2: - st.markdown("#### استيراد المواقع") - - import_format = st.selectbox( - "صيغة الاستيراد", - options=["CSV", "JSON", "GeoJSON"], - key="import_format" - ) - - uploaded_file = st.file_uploader( - "اختر ملف للاستيراد", - type=["csv", "json", "geojson"], - key="import_locations_file" - ) - - if uploaded_file is not None: - if styled_button("استيراد المواقع", key="import_locations", type="success", icon="📥"): - self._import_locations(uploaded_file, import_format) - - # عرض إحصائيات البيانات - st.markdown("### إحصائيات البيانات") - - stats_col1, stats_col2, stats_col3 = st.columns(3) - - with stats_col1: - st.metric("عدد المشاريع", len(st.session_state.project_locations)) - - with stats_col2: - cities = set(p["city"] for p in st.session_state.project_locations) - st.metric("عدد المدن", len(cities)) - - with stats_col3: - statuses = {} - for p in st.session_state.project_locations: - statuses[p["status"]] = statuses.get(p["status"], 0) + 1 - - status_str = ", ".join([f"{k}: {v}" for k, v in statuses.items()]) - st.metric("توزيع الحالات", status_str if statuses else "لا توجد بيانات") - - # خيارات متقدمة - with st.expander("خيارات متقدمة"): - if styled_button("حذف جميع المواقع", key="clear_locations", type="danger", icon="🗑️"): - # تأكيد الحذف - st.warning("هل أنت متأكد من حذف جميع مواقع المشاريع؟ لا يمكن التراجع عن هذا الإجراء.") - - confirm_col1, confirm_col2 = st.columns(2) - - with confirm_col1: - if styled_button("تأكيد الحذف", key="confirm_clear", type="danger", icon="✓"): - # مسح القائمة - st.session_state.project_locations = [] - - # حفظ البيانات - self._save_locations_data() - - st.success("تم حذف جميع مواقع المشاريع بنجاح!") - st.experimental_rerun() - - with confirm_col2: - if styled_button("إلغاء", key="cancel_clear", type="secondary", icon="❌"): - st.experimental_rerun() - - def _fetch_terrain_data(self, latitude, longitude, radius_km=5): - """جلب بيانات التضاريس من واجهة برمجة التطبيقات""" - try: - # تحديث حالة الجلسة - import plotly.express as px - - # تعيين الإحداثيات وحجم المنطقة - center_lat, center_lon = latitude, longitude - - # تحويل نصف القطر من كم إلى درجات (تقريبي) - radius_deg = radius_km / 111.0 # تقريب: 1 درجة = 111 كم - - # تحديد حدود المنطقة - min_lat = center_lat - radius_deg - max_lat = center_lat + radius_deg - min_lon = center_lon - radius_deg - max_lon = center_lon + radius_deg - - # إنشاء شبكة من النقاط - resolution = 50 # عدد النقاط في كل اتجاه - lats = np.linspace(min_lat, max_lat, resolution) - lons = np.linspace(min_lon, max_lon, resolution) - - # إنشاء مصفوفة للإحداثيات - grid_lats, grid_lons = np.meshgrid(lats, lons) - - # تحويل الشبكة إلى قائمة من النقاط - points = [] - for i in range(grid_lats.shape[0]): - for j in range(grid_lats.shape[1]): - points.append((grid_lats[i, j], grid_lons[i, j])) - - # تقسيم النقاط إلى مجموعات لتقليل عدد الطلبات - batch_size = 100 - batches = [points[i:i + batch_size] for i in range(0, len(points), batch_size)] - - # إنشاء بيانات التضاريس - elevation_data = np.zeros((len(lats), len(lons))) - - # محاكاة بيانات التضاريس (يمكن استبدالها بواجهة برمجة تطبيقات حقيقية) - for batch_idx, batch in enumerate(batches): - # في بيئة الإنتاج، سيتم استبدال هذا بطلب API حقيقي - # هنا نقوم بمحاكاة بيانات التضاريس لأغراض العرض - for point_idx, (lat, lon) in enumerate(batch): - # حساب المؤشر في مصفوفة الارتفاع - lat_idx = np.abs(lats - lat).argmin() - lon_idx = np.abs(lons - lon).argmin() - - # محاكاة الارتفاع (في بيئة الإنتاج سيكون هذا من واجهة برمجة التطبيقات) - # هنا نصنع تضاريس اصطناعية باستخدام دالة جيبية - dist_from_center = np.sqrt( - (lat - center_lat) ** 2 + (lon - center_lon) ** 2 - ) - - # إنشاء بعض التلال والوديان الاصطناعية - elevation = 500 + 200 * np.sin(dist_from_center * 100) + 100 * np.cos(lat * 30) + 150 * np.sin(lon * 40) - - # إضافة بعض الضوضاء العشوائية - elevation += np.random.normal(0, 30) - - # تخزين الارتفاع - elevation_data[lat_idx, lon_idx] = elevation - - # حساب إحصائيات الارتفاع - elevation_stats = { - "min": float(np.min(elevation_data)), - "max": float(np.max(elevation_data)), - "mean": float(np.mean(elevation_data)), - "range": float(np.max(elevation_data) - np.min(elevation_data)) - } - - # إنشاء مقطع ارتفاع من الشمال إلى الجنوب عبر المركز - center_lon_idx = np.abs(lons - center_lon).argmin() - ns_profile = [] - for i, lat in enumerate(lats): - ns_profile.append({ - "distance": (lat - min_lat) * 111.0, # تحويل الدرجات إلى كم - "elevation": float(elevation_data[i, center_lon_idx]) - }) - - # إنشاء مقطع ارتفاع من الشرق إلى الغرب عبر المركز - center_lat_idx = np.abs(lats - center_lat).argmin() - ew_profile = [] - for i, lon in enumerate(lons): - ew_profile.append({ - "distance": (lon - min_lon) * 111.0 * np.cos(np.radians(center_lat)), # تحويل الدرجات إلى كم مع تصحيح خط العرض - "elevation": float(elevation_data[center_lat_idx, i]) - }) - - # دمج المقاطع - elevation_profile = ns_profile + ew_profile - - # تحضير بيانات التضاريس للعرض ثلاثي الأبعاد - bounds = [min_lon, min_lat, max_lon, max_lat] - - # تحويل مصفوفة الارتفاع إلى تنسيق مناسب لـ PyDeck - terrain_array = elevation_data.astype(np.float32) - - # إنشاء كائن للتضاريس - terrain_data = [{ - "bounds": bounds, - "terrain": terrain_array.tolist(), - "elevation_stats": elevation_stats, - "elevation_profile": elevation_profile - }] - - return terrain_data - - except Exception as e: - st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}") - raise e - - def _calculate_distance(self, lat1, lon1, lat2, lon2): - """حساب المسافة بين نقطتين بالكيلومترات باستخدام صيغة هافرساين""" - import math - - # تحويل الإحداثيات إلى راديان - lat1 = math.radians(lat1) - lon1 = math.radians(lon1) - lat2 = math.radians(lat2) - lon2 = math.radians(lon2) - - # صيغة هافرساين - dlon = lon2 - lon1 - dlat = lat2 - lat1 - a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2 - c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) - distance = 6371 * c # نصف قطر الأرض بالكيلومترات - - return distance - - def _get_color_map(self, scheme): - """الحصول على خريطة الألوان حسب النظام المختار""" - if scheme == "terrain": - return [ - [0, (0, 50, 0)], - [0.1, (0, 100, 0)], - [0.25, (0, 150, 0)], - [0.4, (200, 170, 0)], - [0.6, (150, 100, 0)], - [0.8, (100, 50, 0)], - [1, (200, 200, 200)] - ] - elif scheme == "elevation": - return [ - [0, (0, 0, 100)], - [0.2, (0, 100, 150)], - [0.4, (0, 150, 50)], - [0.6, (150, 150, 0)], - [0.8, (150, 50, 0)], - [1, (100, 0, 0)] - ] - else: # custom - return [ - [0, (30, 100, 200)], - [0.3, (60, 170, 250)], - [0.5, (200, 220, 150)], - [0.7, (180, 120, 60)], - [0.9, (110, 60, 30)], - [1, (80, 30, 10)] - ] - - def _export_locations(self, format): - """تصدير مواقع المشاريع إلى ملف""" - try: - if not st.session_state.project_locations: - st.error("لا توجد مواقع مشاريع للتصدير.") - return - - if format == "CSV": - # تصدير إلى CSV - df = pd.DataFrame(st.session_state.project_locations) - - csv_data = df.to_csv(index=False) - - st.download_button( - label="تنزيل ملف CSV", - data=csv_data, - file_name="project_locations.csv", - mime="text/csv" - ) - - elif format == "JSON": - # تصدير إلى JSON - json_data = json.dumps(st.session_state.project_locations, ensure_ascii=False, indent=2) - - st.download_button( - label="تنزيل ملف JSON", - data=json_data, - file_name="project_locations.json", - mime="application/json" - ) - - elif format == "GeoJSON": - # تصدير إلى GeoJSON - features = [] - - for location in st.session_state.project_locations: - feature = { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [location["longitude"], location["latitude"]] - }, - "properties": { - "name": location["name"], - "description": location["description"], - "city": location["city"], - "status": location["status"], - "project_id": location["project_id"], - "created_at": location.get("created_at", ""), - "updated_at": location.get("updated_at", "") - } - } - - features.append(feature) - - geojson = { - "type": "FeatureCollection", - "features": features - } - - geojson_data = json.dumps(geojson, ensure_ascii=False, indent=2) - - st.download_button( - label="تنزيل ملف GeoJSON", - data=geojson_data, - file_name="project_locations.geojson", - mime="application/geo+json" - ) - - st.success(f"تم تصدير {len(st.session_state.project_locations)} موقع بنجاح!") - - except Exception as e: - st.error(f"حدث خطأ أثناء تصدير المواقع: {str(e)}") - - def _import_locations(self, uploaded_file, format): - """استيراد مواقع المشاريع من ملف""" - try: - if format == "CSV": - # استيراد من CSV - df = pd.read_csv(uploaded_file) - - # التحقق من وجود الأعمدة المطلوبة - required_columns = ["name", "latitude", "longitude"] - missing_columns = [col for col in required_columns if col not in df.columns] - - if missing_columns: - st.error(f"الملف لا يحتوي على الأعمدة التالية: {', '.join(missing_columns)}") - return - - # تحويل DataFrame إلى قائمة من القواميس - imported_locations = df.to_dict("records") - - elif format == "JSON": - # استيراد من JSON - imported_locations = json.loads(uploaded_file.read()) - - elif format == "GeoJSON": - # استيراد من GeoJSON - geojson = json.loads(uploaded_file.read()) - - # التحقق من صحة التنسيق - if "type" not in geojson or geojson["type"] != "FeatureCollection" or "features" not in geojson: - st.error("تنسيق GeoJSON غير صحيح.") - return - - # تحويل المميزات إلى مواقع - imported_locations = [] - - for feature in geojson["features"]: - if feature["type"] == "Feature" and feature["geometry"]["type"] == "Point": - coords = feature["geometry"]["coordinates"] - properties = feature["properties"] - - location = { - "name": properties.get("name", ""), - "description": properties.get("description", ""), - "city": properties.get("city", ""), - "status": properties.get("status", "جديد"), - "longitude": coords[0], - "latitude": coords[1], - "project_id": properties.get("project_id", f"PRJ-{len(imported_locations)+1:04d}"), - "created_at": properties.get("created_at", pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")), - "updated_at": properties.get("updated_at", pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")) - } - - imported_locations.append(location) - - # التحقق من وجود البيانات المطلوبة في الملف المستورد - valid_locations = [] - for location in imported_locations: - # التحقق من وجود الحقول المطلوبة - if "name" not in location or "latitude" not in location or "longitude" not in location: - continue - - # إضافة القيم الافتراضية إذا لم تكن موجودة - if "description" not in location: - location["description"] = "" - - if "city" not in location: - location["city"] = "" - - if "status" not in location: - location["status"] = "جديد" - - if "project_id" not in location: - location["project_id"] = f"PRJ-{len(valid_locations)+1:04d}" - - if "created_at" not in location: - location["created_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S") - - if "updated_at" not in location: - location["updated_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S") - - valid_locations.append(location) - - if not valid_locations: - st.error("لم يتم العثور على مواقع صالحة في الملف.") - return - - # سؤال المستخدم عن كيفية الاستيراد - import_mode = st.radio( - "كيفية الاستيراد", - options=["إضافة إلى المواقع الموجودة", "استبدال المواقع الموجودة"], - key="import_mode" - ) - - if styled_button("تأكيد الاستيراد", key="confirm_import", type="success", icon="✓"): - if import_mode == "إضافة إلى المواقع الموجودة": - # إضافة المواقع المستوردة إلى القائمة الحالية - st.session_state.project_locations.extend(valid_locations) - else: - # استبدال المواقع الموجودة بالمواقع المستوردة - st.session_state.project_locations = valid_locations - - # حفظ البيانات - self._save_locations_data() - - st.success(f"تم استيراد {len(valid_locations)} موقع بنجاح!") - st.experimental_rerun() - - except Exception as e: - st.error(f"حدث خطأ أثناء استيراد المواقع: {str(e)}") - - def _save_locations_data(self): - """حفظ بيانات المواقع""" - try: - # التأكد من وجود المجلد - os.makedirs(self.data_dir, exist_ok=True) - - # حفظ البيانات - locations_file = os.path.join(self.data_dir, "project_locations.json") - - with open(locations_file, 'w', encoding='utf-8') as f: - json.dump(st.session_state.project_locations, f, ensure_ascii=False, indent=2) - except Exception as e: - st.error(f"حدث خطأ أثناء حفظ بيانات المواقع: {str(e)}") - - def _load_locations_data(self): - """تحميل بيانات المواقع""" - try: - # التحقق من وجود الملف - locations_file = os.path.join(self.data_dir, "project_locations.json") - - if os.path.exists(locations_file): - with open(locations_file, 'r', encoding='utf-8') as f: - locations = json.load(f) - - # تحديث حالة الجلسة - st.session_state.project_locations = locations - except Exception as e: - st.error(f"حدث خطأ أثناء تحميل بيانات المواقع: {str(e)}") - - def _initialize_sample_projects(self): - """تهيئة بيانات اختبارية للمشاريع""" - # التحقق من وجود بيانات محفوظة - locations_file = os.path.join(self.data_dir, "project_locations.json") - - if os.path.exists(locations_file): - # تحميل البيانات المحفوظة - self._load_locations_data() - return - - # إنشاء بيانات اختبارية إذا لم تكن هناك بيانات محفوظة - sample_projects = [ - { - "name": "تطوير شبكة الطرق في منطقة الرياض", - "description": "مشروع تطوير وتوسعة شبكة الطرق الرئيسية في منطقة الرياض", - "city": "الرياض", - "status": "قيد التنفيذ", - "latitude": 24.7136, - "longitude": 46.6753, - "project_id": "PRJ-0001", - "created_at": "2025-01-15 10:30:00", - "updated_at": "2025-01-15 10:30:00" - }, - { - "name": "إنشاء سد وادي حنيفة", - "description": "مشروع إنشاء سد لحجز مياه الأمطار في وادي حنيفة", - "city": "الرياض", - "status": "جديد", - "latitude": 24.6748, - "longitude": 46.5831, - "project_id": "PRJ-0002", - "created_at": "2025-02-01 14:45:00", - "updated_at": "2025-02-01 14:45:00" - }, - { - "name": "تطوير ميناء جدة الإسلامي", - "description": "مشروع تطوير وتوسعة ميناء جدة الإسلامي لزيادة الطاقة الاستيعابية", - "city": "جدة", - "status": "قيد التنفيذ", - "latitude": 21.4858, - "longitude": 39.1925, - "project_id": "PRJ-0003", - "created_at": "2024-11-20 09:15:00", - "updated_at": "2024-11-20 09:15:00" - }, - { - "name": "إنشاء مطار الدمام الجديد", - "description": "مشروع إنشاء مطار جديد في مدينة الدمام لتلبية الطلب المتزايد", - "city": "الدمام", - "status": "متوقف", - "latitude": 26.4207, - "longitude": 50.0888, - "project_id": "PRJ-0004", - "created_at": "2024-10-05 11:30:00", - "updated_at": "2024-10-05 11:30:00" - }, - { - "name": "توسعة جامعة الملك فهد للبترول والمعادن", - "description": "مشروع توسعة مباني ومرافق جامعة الملك فهد للبترول والمعادن", - "city": "الظهران", - "status": "قيد التنفيذ", - "latitude": 26.3927, - "longitude": 50.1150, - "project_id": "PRJ-0005", - "created_at": "2025-01-10 08:00:00", - "updated_at": "2025-01-10 08:00:00" - }, - { - "name": "إنشاء محطة تحلية مياه القنفذة", - "description": "مشروع إنشاء محطة تحلية مياه جديدة في محافظة القنفذة", - "city": "القنفذة", - "status": "جديد", - "latitude": 19.1299, - "longitude": 41.0825, - "project_id": "PRJ-0006", - "created_at": "2025-02-20 15:20:00", - "updated_at": "2025-02-20 15:20:00" - }, - { - "name": "تطوير مجمع حكومي في حائل", - "description": "مشروع إنشاء وتطوير مجمع للدوائر الحكومية في مدينة حائل", - "city": "حائل", - "status": "مكتمل", - "latitude": 27.5114, - "longitude": 41.7208, - "project_id": "PRJ-0007", - "created_at": "2024-06-15 10:00:00", - "updated_at": "2024-12-10 14:30:00" - }, - { - "name": "إنشاء مستشفى الإحساء العام", - "description": "مشروع إنشاء مستشفى عام جديد في محافظة الإحساء بسعة 500 سرير", - "city": "الإحساء", - "status": "قيد التنفيذ", - "latitude": 25.3753, - "longitude": 49.5873, - "project_id": "PRJ-0008", - "created_at": "2024-09-01 09:45:00", - "updated_at": "2024-09-01 09:45:00" - }, - { - "name": "تطوير شبكة الصرف الصحي في أبها", - "description": "مشروع تطوير وتوسعة شبكة الصرف الصحي في مدينة أبها", - "city": "أبها", - "status": "جديد", - "latitude": 18.2164, - "longitude": 42.5053, - "project_id": "PRJ-0009", - "created_at": "2025-02-25 11:15:00", - "updated_at": "2025-02-25 11:15:00" - }, - { - "name": "إنشاء مدينة صناعية في سكاكا", - "description": "مشروع إنشاء مدينة صناعية جديدة في منطقة سكاكا", - "city": "سكاكا", - "status": "متوقف", - "latitude": 29.9720, - "longitude": 40.2006, - "project_id": "PRJ-0010", - "created_at": "2024-07-20 13:30:00", - "updated_at": "2024-07-20 13:30:00" - } - ] - - # تحديث حالة الجلسة - st.session_state.project_locations = sample_projects - - # حفظ البيانات - self._save_locations_data() - - -# فئة تحويل Folium إلى Streamlit -class folium_static: - """فئة لعرض خرائط Folium في Streamlit""" - - def __init__(self, fig, width=700, height=500): - """عرض خريطة Folium في Streamlit""" - import streamlit.components.v1 as components - - # تحويل خريطة Folium إلى HTML - fig_html = fig._repr_html_() - - # إنشاء مكون HTML مخصص - components.html(fig_html, width=width, height=height) - - -# تشغيل الوحدة بشكل مستقل -def main(): - """تشغيل وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد بشكل مستقل""" - # تهيئة الواجهة - st.set_page_config( - page_title="الخريطة التفاعلية | WAHBi AI", - page_icon="🗺️", - layout="wide", - initial_sidebar_state="expanded", - menu_items={ - 'Get Help': 'mailto:support@wahbi-ai.com', - 'Report a bug': 'mailto:support@wahbi-ai.com', - 'About': 'وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد - جزء من نظام WAHBi AI لتحليل المناقصات' - } - ) - - # تهيئة وحدة الخريطة التفاعلية - interactive_map = InteractiveMap() - - # عرض واجهة الوحدة - interactive_map.render() - -# تشغيل الوحدة عند استدعاء الملف مباشرة -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/modules/maps/maps_app.py b/modules/maps/maps_app.py deleted file mode 100644 index cbd8753a34662ccdf1af4454e3de51fb79e8457f..0000000000000000000000000000000000000000 --- a/modules/maps/maps_app.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة تطبيق الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات الخريطة التفاعلية -from modules.maps.interactive_map import InteractiveMap - - -class MapsApp: - """وحدة تطبيق الخريطة التفاعلية""" - - def __init__(self): - """تهيئة وحدة تطبيق الخريطة التفاعلية""" - self.interactive_map = InteractiveMap() - - def render(self): - """عرض واجهة وحدة تطبيق الخريطة التفاعلية""" - st.markdown("

وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد

", unsafe_allow_html=True) - - st.markdown(""" -
- تمكنك هذه الوحدة من عرض وإدارة مواقع المشاريع على خريطة تفاعلية، مع إمكانية عرض التضاريس بشكل ثلاثي الأبعاد. - يمكنك إضافة وتحرير مواقع المشاريع، وتحليل توزيعها الجغرافي، وعرض المعلومات الطبوغرافية للمواقع. -
- """, unsafe_allow_html=True) - - # عرض وحدة الخريطة التفاعلية - self.interactive_map.render() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="الخريطة التفاعلية | WAHBi AI", - page_icon="🗺️", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = MapsApp() - app.render() \ No newline at end of file diff --git a/modules/notifications/__init__.py b/modules/notifications/__init__.py deleted file mode 100644 index a177748231ffad32b4284b13461159d77057db3d..0000000000000000000000000000000000000000 --- a/modules/notifications/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# ملف تهيئة وحدة الإشعارات الذكية \ No newline at end of file diff --git a/modules/notifications/notifications_app.py b/modules/notifications/notifications_app.py deleted file mode 100644 index 16aae363ddc6a62d64c8d66ecb40d0999c009f57..0000000000000000000000000000000000000000 --- a/modules/notifications/notifications_app.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة تطبيق نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات الإشعارات الذكية -from modules.notifications.smart_notifications import SmartNotificationSystem - - -class NotificationsApp: - """وحدة تطبيق نظام الإشعارات الذكي""" - - def __init__(self): - """تهيئة وحدة تطبيق نظام الإشعارات الذكي""" - self.smart_notification_system = SmartNotificationSystem() - - def render(self): - """عرض واجهة وحدة تطبيق نظام الإشعارات الذكي""" - st.markdown("

نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات

", unsafe_allow_html=True) - - st.markdown(""" -
- يتيح لك نظام الإشعارات الذكي متابعة تحديثات المشاريع وتلقي تنبيهات مخصصة حسب أدوارك واهتماماتك. - يمكنك تخصيص إعدادات الإشعارات وجدولة التذكيرات للمواعيد النهائية والمهام. -
- """, unsafe_allow_html=True) - - # عرض نظام الإشعارات الذكي - self.smart_notification_system.render() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="نظام الإشعارات الذكي | WAHBi AI", - page_icon="🔔", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = NotificationsApp() - app.render() \ No newline at end of file diff --git a/modules/notifications/smart_notifications.py b/modules/notifications/smart_notifications.py deleted file mode 100644 index cb9cf6615e230c40ff913ae9e7d8e2b226f725d9..0000000000000000000000000000000000000000 --- a/modules/notifications/smart_notifications.py +++ /dev/null @@ -1,1237 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات -تتيح هذه الوحدة متابعة تحديثات المشاريع وإرسال تنبيهات ذكية مخصصة للمستخدمين بناءً على أدوارهم واهتماماتهم -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np -import json -import datetime -import time -import threading -import logging -from typing import List, Dict, Any, Tuple, Optional, Union - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات واجهة المستخدم -from utils.components.header import render_header -from utils.components.credits import render_credits -from utils.helpers import format_number, format_currency, styled_button - - -class SmartNotificationSystem: - """فئة نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات""" - - def __init__(self): - """تهيئة نظام الإشعارات الذكي""" - # تهيئة مجلدات حفظ البيانات - self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/notifications")) - os.makedirs(self.data_dir, exist_ok=True) - - # تهيئة قائمة الإشعارات - if 'notifications' not in st.session_state: - st.session_state.notifications = [] - - if 'unread_count' not in st.session_state: - st.session_state.unread_count = 0 - - if 'notification_channels' not in st.session_state: - st.session_state.notification_channels = { - "browser": True, - "email": False, - "sms": False, - "mobile_app": False - } - - if 'notification_preferences' not in st.session_state: - st.session_state.notification_preferences = { - "project_updates": True, - "document_analysis": True, - "deadline_reminders": True, - "risk_alerts": True, - "price_changes": True, - "team_mentions": True, - "system_updates": True - } - - # تحميل الإشعارات المحفوظة - self._load_notifications() - - # تسجيل الأحداث - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler(os.path.join(self.data_dir, "notifications.log")), - logging.StreamHandler() - ] - ) - self.logger = logging.getLogger("smart_notifications") - - def render(self): - """عرض واجهة نظام الإشعارات الذكي""" - render_header("نظام الإشعارات الذكي") - - # تبويبات الوحدة - tabs = st.tabs([ - "جميع الإشعارات", - "إشعارات غير مقروءة", - "إعدادات الإشعارات", - "جدولة الإشعارات", - "تقارير وإحصائيات" - ]) - - # تبويب جميع الإشعارات - with tabs[0]: - self._render_all_notifications() - - # تبويب الإشعارات غير المقروءة - with tabs[1]: - self._render_unread_notifications() - - # تبويب إعدادات الإشعارات - with tabs[2]: - self._render_notification_settings() - - # تبويب جدولة الإشعارات - with tabs[3]: - self._render_notification_scheduling() - - # تبويب تقارير وإحصائيات - with tabs[4]: - self._render_notification_analytics() - - # عرض حقوق النشر - render_credits() - - def _render_all_notifications(self): - """عرض جميع الإشعارات""" - st.markdown(""" -
-

🔔 جميع الإشعارات

-

عرض كافة الإشعارات والتنبيهات الخاصة بالمشاريع والنظام.

-
- """, unsafe_allow_html=True) - - # أزرار التحكم - col1, col2, col3 = st.columns([1, 1, 1]) - - with col1: - if styled_button("تحديث الإشعارات", key="refresh_notifications", type="primary", icon="🔄"): - self._load_notifications() - st.success("تم تحديث الإشعارات بنجاح") - - with col2: - if styled_button("تعليم الكل كمقروء", key="mark_all_read", type="secondary", icon="✓"): - self._mark_all_as_read() - st.success("تم تعليم جميع الإشعارات كمقروءة") - - with col3: - if styled_button("حذف جميع الإشعارات", key="clear_notifications", type="danger", icon="🗑️"): - confirmed = st.text_input("اكتب 'تأكيد' لحذف جميع الإشعارات", key="confirm_clear") - if confirmed == "تأكيد": - self._clear_all_notifications() - st.success("تم حذف جميع الإشعارات بنجاح") - - # فلترة الإشعارات - filter_col1, filter_col2 = st.columns(2) - - with filter_col1: - notification_type = st.multiselect( - "تصفية حسب النوع", - options=[ - "تحديث مشروع", "وثيقة جديدة", "تذكير موعد نهائي", - "تنبيه مخاطر", "تغيير سعر", "إشارة فريق العمل", "تحديث النظام" - ], - key="filter_notification_type" - ) - - with filter_col2: - date_range = st.date_input( - "نطاق التاريخ", - value=( - datetime.datetime.now() - datetime.timedelta(days=30), - datetime.datetime.now() - ), - key="filter_date_range" - ) - - # تصفية الإشعارات - filtered_notifications = self._filter_notifications( - notification_type=notification_type, - date_range=date_range - ) - - # عرض الإشعارات المصفاة - if filtered_notifications: - for notification in filtered_notifications: - self._render_notification_card(notification) - else: - st.info("لا توجد إشعارات متاحة") - - def _render_unread_notifications(self): - """عرض الإشعارات غير المقروءة""" - st.markdown(""" -
-

🔔 الإشعارات غير المقروءة

-

عرض الإشعارات والتنبيهات التي لم تتم قراءتها بعد.

-
- """, unsafe_allow_html=True) - - # أزرار التحكم - col1, col2 = st.columns(2) - - with col1: - if styled_button("تحديث الإشعارات", key="refresh_unread", type="primary", icon="🔄"): - self._load_notifications() - st.success("تم تحديث الإشعارات بنجاح") - - with col2: - if styled_button("تعليم الكل كمقروء", key="mark_unread_read", type="secondary", icon="✓"): - self._mark_all_as_read() - st.success("تم تعليم جميع الإشعارات كمقروءة") - - # فلترة الإشعارات غير المقروءة - unread_notifications = [n for n in st.session_state.notifications if not n.get("read", False)] - - # عرض الإشعارات غير المقروءة - if unread_notifications: - for notification in unread_notifications: - self._render_notification_card(notification, show_mark_button=True) - else: - st.success("لا توجد إشعارات غير مقروءة") - - def _render_notification_settings(self): - """عرض إعدادات الإشعارات""" - st.markdown(""" -
-

⚙️ إعدادات الإشعارات

-

تخصيص إعدادات وتفضيلات الإشعارات الخاصة بك.

-
- """, unsafe_allow_html=True) - - # قسم قنوات الإشعارات - st.markdown("### قنوات الإشعارات") - st.markdown("حدد الطرق التي ترغب في تلقي الإشعارات من خلالها.") - - channels_col1, channels_col2 = st.columns(2) - - with channels_col1: - st.session_state.notification_channels["browser"] = st.checkbox( - "إشعارات المتصفح", - value=st.session_state.notification_channels.get("browser", True), - key="channel_browser" - ) - - st.session_state.notification_channels["email"] = st.checkbox( - "البريد الإلكتروني", - value=st.session_state.notification_channels.get("email", False), - key="channel_email" - ) - - if st.session_state.notification_channels["email"]: - email = st.text_input( - "البريد الإلكتروني للإشعارات", - value=st.session_state.get("notification_email", ""), - key="notification_email" - ) - st.session_state.notification_email = email - - with channels_col2: - st.session_state.notification_channels["sms"] = st.checkbox( - "الرسائل النصية (SMS)", - value=st.session_state.notification_channels.get("sms", False), - key="channel_sms" - ) - - if st.session_state.notification_channels["sms"]: - phone = st.text_input( - "رقم الهاتف للإشعارات", - value=st.session_state.get("notification_phone", ""), - key="notification_phone" - ) - st.session_state.notification_phone = phone - - st.session_state.notification_channels["mobile_app"] = st.checkbox( - "تطبيق الهاتف المحمول", - value=st.session_state.notification_channels.get("mobile_app", False), - key="channel_mobile_app" - ) - - # قسم تفضيلات الإشعارات - st.markdown("### أنواع الإشعارات") - st.markdown("حدد أنواع الإشعارات التي ترغب في تلقيها.") - - prefs_col1, prefs_col2 = st.columns(2) - - with prefs_col1: - st.session_state.notification_preferences["project_updates"] = st.checkbox( - "تحديثات المشاريع", - value=st.session_state.notification_preferences.get("project_updates", True), - key="pref_project_updates" - ) - - st.session_state.notification_preferences["document_analysis"] = st.checkbox( - "تحليل المستندات", - value=st.session_state.notification_preferences.get("document_analysis", True), - key="pref_document_analysis" - ) - - st.session_state.notification_preferences["deadline_reminders"] = st.checkbox( - "تذكيرات المواعيد النهائية", - value=st.session_state.notification_preferences.get("deadline_reminders", True), - key="pref_deadline_reminders" - ) - - st.session_state.notification_preferences["risk_alerts"] = st.checkbox( - "تنبيهات المخاطر", - value=st.session_state.notification_preferences.get("risk_alerts", True), - key="pref_risk_alerts" - ) - - with prefs_col2: - st.session_state.notification_preferences["price_changes"] = st.checkbox( - "تغييرات الأسعار", - value=st.session_state.notification_preferences.get("price_changes", True), - key="pref_price_changes" - ) - - st.session_state.notification_preferences["team_mentions"] = st.checkbox( - "إشارات فريق العمل", - value=st.session_state.notification_preferences.get("team_mentions", True), - key="pref_team_mentions" - ) - - st.session_state.notification_preferences["system_updates"] = st.checkbox( - "تحديثات النظام", - value=st.session_state.notification_preferences.get("system_updates", True), - key="pref_system_updates" - ) - - # إعدادات التكرار - st.markdown("### إعدادات التكرار") - - frequency = st.radio( - "تكرار الإشعارات المتشابهة", - options=["فوري", "تجميع كل ساعة", "تجميع كل يوم", "مخصص"], - index=0, - key="notification_frequency" - ) - - if frequency == "مخصص": - custom_hours = st.number_input( - "التجميع كل (ساعات)", - min_value=1, - max_value=24, - value=4, - key="custom_frequency_hours" - ) - st.session_state.custom_frequency_hours = custom_hours - - # إعدادات متقدمة - with st.expander("إعدادات متقدمة"): - st.checkbox( - "عرض الإشعارات عند بدء تشغيل النظام", - value=True, - key="show_on_startup" - ) - - st.checkbox( - "الإشعارات الصوتية", - value=False, - key="audio_notifications" - ) - - st.checkbox( - "حفظ سجل الإشعارات", - value=True, - key="log_notifications" - ) - - # استدعاء القيمة من session_state إذا كانت موجودة أو استخدام القيمة الافتراضية - retention_days = st.slider( - "الاحتفاظ بالإشعارات (أيام)", - min_value=7, - max_value=365, - value=st.session_state.get("retention_days_value", 90), - key="retention_days" - ) - # حفظ القيمة في مفتاح آخر بعد تحديثها عن طريق المستخدم - if "retention_days_value" not in st.session_state: - st.session_state.retention_days_value = retention_days - - # زر حفظ الإعدادات - if styled_button("حفظ الإعدادات", key="save_notification_settings", type="primary", icon="💾"): - self._save_notification_settings() - st.success("تم حفظ إعدادات الإشعارات بنجاح") - - def _render_notification_scheduling(self): - """عرض واجهة جدولة الإشعارات""" - st.markdown(""" -
-

🕒 جدولة الإشعارات

-

إنشاء وإدارة الإشعارات المجدولة والتذكيرات الدورية.

-
- """, unsafe_allow_html=True) - - # إنشاء تذكير جديد - st.markdown("### إنشاء تذكير جديد") - - col1, col2 = st.columns(2) - - with col1: - reminder_name = st.text_input("عنوان التذكير", key="new_reminder_name") - reminder_desc = st.text_area("وصف التذكير", key="new_reminder_desc") - reminder_date = st.date_input("تاريخ التذكير", key="new_reminder_date") - reminder_time = st.time_input("وقت التذكير", key="new_reminder_time") - - with col2: - reminder_type = st.selectbox( - "نوع التذكير", - options=[ - "موعد نهائي للمناقصة", - "اجتماع مشروع", - "زيارة موقع", - "تسليم مستندات", - "دفعة مالية", - "مراجعة أداء", - "أخرى" - ], - key="new_reminder_type" - ) - - reminder_priority = st.select_slider( - "الأولوية", - options=["منخفضة", "متوسطة", "عالية", "حرجة"], - value="متوسطة", - key="new_reminder_priority" - ) - - reminder_repeat = st.selectbox( - "التكرار", - options=[ - "مرة واحدة", - "يومياً", - "أسبوعياً", - "شهرياً", - "سنوياً" - ], - key="new_reminder_repeat" - ) - - if reminder_type == "أخرى": - custom_type = st.text_input("حدد نوع التذكير", key="custom_reminder_type") - - # زر إضافة التذكير - if styled_button("إضافة التذكير", key="add_reminder", type="primary", icon="➕"): - if not reminder_name or not reminder_desc: - st.error("يرجى تعبئة حقول العنوان والوصف") - else: - self._add_scheduled_notification( - title=reminder_name, - message=reminder_desc, - notification_date=datetime.datetime.combine(reminder_date, reminder_time), - notification_type=reminder_type if reminder_type != "أخرى" else custom_type, - priority=reminder_priority, - repeat=reminder_repeat - ) - st.success("تم إضافة التذكير بنجاح") - - # عرض التذكيرات المجدولة - st.markdown("### التذكيرات المجدولة") - - # التحقق من وجود تذكيرات مجدولة - scheduled_notifications = self._get_scheduled_notifications() - - if scheduled_notifications: - # عرض التذكيرات في جدول - scheduled_df = pd.DataFrame(scheduled_notifications) - - # تنسيق البيانات للعرض - display_df = scheduled_df.copy() - display_df["التاريخ والوقت"] = display_df["notification_date"].apply(lambda x: x.strftime("%Y-%m-%d %H:%M")) - display_df["العنوان"] = display_df["title"] - display_df["النوع"] = display_df["notification_type"] - display_df["الأولوية"] = display_df["priority"] - display_df["التكرار"] = display_df["repeat"] - - # عرض الجدول - st.dataframe( - display_df[["العنوان", "النوع", "التاريخ والوقت", "الأولوية", "التكرار"]], - use_container_width=True - ) - - # عرض الإشعارات المجدولة كبطاقات - for notification in scheduled_notifications: - with st.expander(f"{notification['title']} - {notification['notification_date'].strftime('%Y-%m-%d %H:%M')}"): - notification_col1, notification_col2 = st.columns([3, 1]) - - with notification_col1: - st.markdown(f"**الوصف:** {notification['message']}") - st.markdown(f"**النوع:** {notification['notification_type']}") - st.markdown(f"**الأولوية:** {notification['priority']}") - st.markdown(f"**التكرار:** {notification['repeat']}") - - with notification_col2: - if styled_button("تعديل", key=f"edit_{notification['id']}", type="secondary", icon="✏️"): - # تنفيذ في المرحلة القادمة - st.info("ميزة التعديل قيد التطوير") - - if styled_button("حذف", key=f"delete_{notification['id']}", type="danger", icon="🗑️"): - self._delete_scheduled_notification(notification['id']) - st.rerun() - else: - st.info("لا توجد تذكيرات مجدولة") - - def _render_notification_analytics(self): - """عرض تقارير وإحصائيات الإشعارات""" - st.markdown(""" -
-

📊 تقارير وإحصائيات الإشعارات

-

تحليل وعرض إحصائيات الإشعارات والتنبيهات.

-
- """, unsafe_allow_html=True) - - # إحصائيات عامة - st.markdown("### إحصائيات عامة") - - # التحقق من وجود إشعارات - if st.session_state.notifications: - # إعداد البيانات - total_count = len(st.session_state.notifications) - read_count = len([n for n in st.session_state.notifications if n.get("read", False)]) - unread_count = total_count - read_count - - # تصنيف الإشعارات حسب النوع - notification_types = {} - for notification in st.session_state.notifications: - notification_type = notification.get("notification_type", "أخرى") - notification_types[notification_type] = notification_types.get(notification_type, 0) + 1 - - # عرض الإحصائيات - metric_col1, metric_col2, metric_col3 = st.columns(3) - - with metric_col1: - st.metric("إجمالي الإشعارات", total_count) - - with metric_col2: - st.metric("الإشعارات المقروءة", read_count, delta=f"{read_count/total_count*100:.1f}%" if total_count > 0 else "0%") - - with metric_col3: - st.metric("الإشعارات غير المقروءة", unread_count, delta=f"{unread_count/total_count*100:.1f}%" if total_count > 0 else "0%") - - # رسم بياني لتوزيع الإشعارات حسب النوع - st.markdown("### توزيع الإشعارات حسب النوع") - - # إنشاء DataFrame للرسم البياني - types_df = pd.DataFrame({ - "النوع": list(notification_types.keys()), - "العدد": list(notification_types.values()) - }) - - # رسم بياني دائري - import plotly.express as px - - fig = px.pie( - types_df, - values="العدد", - names="النوع", - title="توزيع الإشعارات حسب النوع", - color_discrete_sequence=px.colors.sequential.RdBu - ) - - fig.update_layout( - title_font_size=20, - font_family="Arial", - font_size=14, - height=400 - ) - - st.plotly_chart(fig, use_container_width=True) - - # رسم بياني لتوزيع الإشعارات حسب الوقت - st.markdown("### توزيع الإشعارات حسب الوقت") - - # تحويل التواريخ إلى DataFrame - dates = [ - n.get("timestamp", datetime.datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0) - for n in st.session_state.notifications - if "timestamp" in n - ] - - if dates: - date_counts = pd.Series(dates).value_counts().sort_index() - - # إنشاء DataFrame للرسم البياني - date_df = pd.DataFrame({ - "التاريخ": date_counts.index, - "العدد": date_counts.values - }) - - # رسم بياني خطي - fig2 = px.line( - date_df, - x="التاريخ", - y="العدد", - title="توزيع الإشعارات حسب التاريخ", - markers=True - ) - - fig2.update_layout( - title_font_size=20, - font_family="Arial", - font_size=14, - height=400 - ) - - st.plotly_chart(fig2, use_container_width=True) - - # تصدير البيانات - st.markdown("### تصدير بيانات الإشعارات") - - export_col1, export_col2 = st.columns(2) - - with export_col1: - if styled_button("تصدير CSV", key="export_csv", type="primary", icon="📄"): - # تحويل الإشعارات إلى DataFrame - export_df = pd.DataFrame(st.session_state.notifications) - - # تنسيق البيانات - if "timestamp" in export_df.columns: - export_df["timestamp"] = export_df["timestamp"].apply( - lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if isinstance(x, datetime.datetime) else str(x) - ) - - # تصدير إلى CSV - csv_data = export_df.to_csv(index=False) - - # تنزيل الملف - st.download_button( - label="تنزيل ملف CSV", - data=csv_data, - file_name=f"notifications_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", - mime="text/csv" - ) - - with export_col2: - if styled_button("تصدير JSON", key="export_json", type="primary", icon="📄"): - # تنسيق البيانات - export_data = [] - for notification in st.session_state.notifications: - export_item = notification.copy() - if "timestamp" in export_item and isinstance(export_item["timestamp"], datetime.datetime): - export_item["timestamp"] = export_item["timestamp"].strftime("%Y-%m-%d %H:%M:%S") - export_data.append(export_item) - - # تحويل إلى JSON - json_data = json.dumps(export_data, ensure_ascii=False, indent=2) - - # تنزيل الملف - st.download_button( - label="تنزيل ملف JSON", - data=json_data, - file_name=f"notifications_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json", - mime="application/json" - ) - else: - st.info("لا توجد بيانات كافية لعرض الرسم البياني") - else: - st.info("لا توجد إشعارات لعرض الإحصائيات") - - def _render_notification_card(self, notification, show_mark_button=False): - """عرض بطاقة إشعار""" - # تعيين نمط البطاقة حسب الأولوية والحالة - card_style = "notification-card" - if not notification.get("read", False): - card_style += " unread-notification" - - priority = notification.get("priority", "متوسطة") - if priority == "عالية" or priority == "حرجة": - card_style += " high-priority-notification" - - # تعيين الأيقونة حسب نوع الإشعار - icon_map = { - "تحديث مشروع": "🔄", - "وثيقة جديدة": "📄", - "تذكير موعد نهائي": "⏰", - "تنبيه مخاطر": "⚠️", - "تغيير سعر": "💰", - "إشارة فريق العمل": "👥", - "تحديث النظام": "🖥️" - } - - notification_type = notification.get("notification_type", "تحديث مشروع") - icon = icon_map.get(notification_type, "🔔") - - # تنسيق التاريخ - timestamp = notification.get("timestamp", datetime.datetime.now()) - if isinstance(timestamp, datetime.datetime): - time_str = timestamp.strftime("%Y-%m-%d %H:%M") - else: - time_str = str(timestamp) - - # إنشاء HTML للبطاقة - card_html = f""" -
-
- {icon} - {notification.get('title', 'إشعار جديد')} - {time_str} -
-
-

{notification.get('message', '')}

-
- -
- """ - - # عرض البطاقة - st.markdown(card_html, unsafe_allow_html=True) - - # إضافة أزرار التحكم - if show_mark_button: - col1, col2 = st.columns([1, 4]) - - with col1: - if styled_button("تعليم كمقروء", key=f"mark_read_{notification.get('id', '')}", type="secondary", icon="✓"): - self._mark_notification_as_read(notification.get('id', '')) - st.rerun() - - with col2: - if notification.get("link"): - if styled_button("عرض التفاصيل", key=f"view_details_{notification.get('id', '')}", type="primary", icon="🔍"): - # افتح الرابط المرتبط بالإشعار - # ملاحظة: هذا سيعمل بشكل مختلف حسب بيئة التشغيل - st.markdown(f"[عرض التفاصيل]({notification.get('link')})") - - def add_notification(self, title, message, notification_type="تحديث مشروع", priority="متوسطة", link=None): - """ - إضافة إشعار جديد - - المعلمات: - title: عنوان الإشعار - message: نص الإشعار - notification_type: نوع الإشعار - priority: أولوية الإشعار - link: رابط مرتبط بالإشعار (اختياري) - - الإرجاع: - معرف الإشعار الجديد - """ - # إنشاء معرف فريد للإشعار - notification_id = f"notif_{int(time.time())}_{len(st.session_state.notifications)}" - - # إنشاء كائن الإشعار - notification = { - "id": notification_id, - "title": title, - "message": message, - "notification_type": notification_type, - "priority": priority, - "read": False, - "timestamp": datetime.datetime.now(), - "link": link - } - - # إضافة الإشعار لقائمة الإشعارات - st.session_state.notifications.append(notification) - - # زيادة عداد الإشعارات غير المقروءة - st.session_state.unread_count += 1 - - # حفظ الإشعارات - self._save_notifications() - - # تسجيل الإشعار - self.logger.info( - f"تمت إضافة إشعار جديد: {title} ({notification_type})" - ) - - return notification_id - - def _mark_notification_as_read(self, notification_id): - """ - تعليم إشعار كمقروء - - المعلمات: - notification_id: معرف الإشعار - - الإرجاع: - قيمة بوليانية تشير إلى نجاح العملية - """ - # البحث عن الإشعار - for i, notification in enumerate(st.session_state.notifications): - if notification.get("id") == notification_id and not notification.get("read", False): - # تعليم الإشعار كمقروء - st.session_state.notifications[i]["read"] = True - - # تحديث عداد الإشعارات غير المقروءة - st.session_state.unread_count = max(0, st.session_state.unread_count - 1) - - # حفظ الإشعارات - self._save_notifications() - - return True - - return False - - def _mark_all_as_read(self): - """ - تعليم جميع الإشعارات كمقروءة - - الإرجاع: - عدد الإشعارات التي تم تعليمها - """ - count = 0 - - # تعليم جميع الإشعارات كمقروءة - for i, notification in enumerate(st.session_state.notifications): - if not notification.get("read", False): - st.session_state.notifications[i]["read"] = True - count += 1 - - # إعادة تعيين عداد الإشعارات غير المقروءة - st.session_state.unread_count = 0 - - # حفظ الإشعارات - self._save_notifications() - - return count - - def _clear_all_notifications(self): - """ - حذف جميع الإشعارات - - الإرجاع: - عدد الإشعارات التي تم حذفها - """ - count = len(st.session_state.notifications) - - # مسح قائمة الإشعارات - st.session_state.notifications = [] - - # إعادة تعيين عداد الإشعارات غير المقروءة - st.session_state.unread_count = 0 - - # حفظ الإشعارات - self._save_notifications() - - return count - - def _filter_notifications(self, notification_type=None, date_range=None): - """ - تصفية الإشعارات حسب النوع والتاريخ - - المعلمات: - notification_type: قائمة أنواع الإشعارات - date_range: نطاق تاريخ الإشعارات - - الإرجاع: - قائمة الإشعارات المصفاة - """ - filtered_notifications = st.session_state.notifications.copy() - - # تصفية حسب النوع - if notification_type and len(notification_type) > 0: - filtered_notifications = [ - n for n in filtered_notifications - if n.get("notification_type") in notification_type - ] - - # تصفية حسب نطاق التاريخ - if date_range and len(date_range) == 2: - start_date, end_date = date_range - - # تحويل التواريخ إلى datetime - start_date = datetime.datetime.combine(start_date, datetime.time.min) - end_date = datetime.datetime.combine(end_date, datetime.time.max) - - filtered_notifications = [ - n for n in filtered_notifications - if isinstance(n.get("timestamp"), datetime.datetime) and - start_date <= n.get("timestamp") <= end_date - ] - - return filtered_notifications - - def _add_scheduled_notification(self, title, message, notification_date, notification_type="تذكير", priority="متوسطة", repeat="مرة واحدة"): - """ - إضافة إشعار مجدول - - المعلمات: - title: عنوان الإشعار - message: نص الإشعار - notification_date: تاريخ ووقت الإشعار - notification_type: نوع الإشعار - priority: أولوية الإشعار - repeat: نمط تكرار الإشعار - - الإرجاع: - معرف الإشعار المجدول - """ - # إنشاء معرف فريد للإشعار المجدول - scheduled_id = f"sched_{int(time.time())}_{len(self._get_scheduled_notifications())}" - - # إنشاء كائن الإشعار المجدول - scheduled_notification = { - "id": scheduled_id, - "title": title, - "message": message, - "notification_date": notification_date, - "notification_type": notification_type, - "priority": priority, - "repeat": repeat, - "created_at": datetime.datetime.now(), - "last_triggered": None - } - - # إضافة الإشعار المجدول للقائمة - scheduled_notifications = self._get_scheduled_notifications() - scheduled_notifications.append(scheduled_notification) - - # حفظ الإشعارات المجدولة - self._save_scheduled_notifications(scheduled_notifications) - - # تسجيل الإشعار المجدول - self.logger.info( - f"تمت إضافة إشعار مجدول: {title} ({notification_date.strftime('%Y-%m-%d %H:%M')})" - ) - - return scheduled_id - - def _delete_scheduled_notification(self, notification_id): - """ - حذف إشعار مجدول - - المعلمات: - notification_id: معرف الإشعار المجدول - - الإرجاع: - قيمة بوليانية تشير إلى نجاح العملية - """ - scheduled_notifications = self._get_scheduled_notifications() - - # البحث عن الإشعار المجدول - for i, notification in enumerate(scheduled_notifications): - if notification.get("id") == notification_id: - # حذف الإشعار المجدول - del scheduled_notifications[i] - - # حفظ الإشعارات المجدولة - self._save_scheduled_notifications(scheduled_notifications) - - # تسجيل الحذف - self.logger.info( - f"تم حذف الإشعار المجدول: {notification_id}" - ) - - return True - - return False - - def _get_scheduled_notifications(self): - """ - الحصول على قائمة الإشعارات المجدولة - - الإرجاع: - قائمة الإشعارات المجدولة - """ - try: - # التحقق من وجود ملف الإشعارات المجدولة - scheduled_file = os.path.join(self.data_dir, "scheduled_notifications.json") - - if os.path.exists(scheduled_file): - with open(scheduled_file, 'r', encoding='utf-8') as f: - scheduled_data = json.load(f) - - # تحويل التواريخ من نصوص إلى كائنات datetime - for notification in scheduled_data: - if "notification_date" in notification: - notification["notification_date"] = datetime.datetime.fromisoformat(notification["notification_date"]) - - if "created_at" in notification: - notification["created_at"] = datetime.datetime.fromisoformat(notification["created_at"]) - - if "last_triggered" in notification and notification["last_triggered"]: - notification["last_triggered"] = datetime.datetime.fromisoformat(notification["last_triggered"]) - - return scheduled_data - - return [] - - except Exception as e: - self.logger.error(f"حدث خطأ أثناء قراءة الإشعارات المجدولة: {str(e)}") - return [] - - def _save_scheduled_notifications(self, scheduled_notifications): - """ - حفظ قائمة الإشعارات المجدولة - - المعلمات: - scheduled_notifications: قائمة الإشعارات المجدولة - """ - try: - # التأكد من وجود المجلد - os.makedirs(self.data_dir, exist_ok=True) - - # تحويل كائنات datetime إلى نصوص - scheduled_data = [] - - for notification in scheduled_notifications: - notification_copy = notification.copy() - - if "notification_date" in notification_copy and isinstance(notification_copy["notification_date"], datetime.datetime): - notification_copy["notification_date"] = notification_copy["notification_date"].isoformat() - - if "created_at" in notification_copy and isinstance(notification_copy["created_at"], datetime.datetime): - notification_copy["created_at"] = notification_copy["created_at"].isoformat() - - if "last_triggered" in notification_copy and isinstance(notification_copy["last_triggered"], datetime.datetime): - notification_copy["last_triggered"] = notification_copy["last_triggered"].isoformat() - - scheduled_data.append(notification_copy) - - # حفظ البيانات - scheduled_file = os.path.join(self.data_dir, "scheduled_notifications.json") - - with open(scheduled_file, 'w', encoding='utf-8') as f: - json.dump(scheduled_data, f, ensure_ascii=False, indent=2) - - except Exception as e: - self.logger.error(f"حدث خطأ أثناء حفظ الإشعارات المجدولة: {str(e)}") - - def _save_notification_settings(self): - """حفظ إعدادات الإشعارات""" - try: - # التأكد من وجود المجلد - os.makedirs(self.data_dir, exist_ok=True) - - # إعداد البيانات - settings_data = { - "notification_channels": st.session_state.notification_channels, - "notification_preferences": st.session_state.notification_preferences, - "notification_email": st.session_state.get("notification_email", ""), - "notification_phone": st.session_state.get("notification_phone", ""), - "notification_frequency": st.session_state.get("notification_frequency", "فوري"), - "custom_frequency_hours": st.session_state.get("custom_frequency_hours", 4), - "show_on_startup": st.session_state.get("show_on_startup", True), - "audio_notifications": st.session_state.get("audio_notifications", False), - "log_notifications": st.session_state.get("log_notifications", True), - "retention_days": st.session_state.get("retention_days", 90) - } - - # حفظ البيانات - settings_file = os.path.join(self.data_dir, "notification_settings.json") - - with open(settings_file, 'w', encoding='utf-8') as f: - json.dump(settings_data, f, ensure_ascii=False, indent=2) - - # تسجيل الحفظ - self.logger.info("تم حفظ إعدادات الإشعارات بنجاح") - - except Exception as e: - self.logger.error(f"حدث خطأ أثناء حفظ إعدادات الإشعارات: {str(e)}") - - def _load_notification_settings(self): - """تحميل إعدادات الإشعارات""" - try: - # التحقق من وجود ملف الإعدادات - settings_file = os.path.join(self.data_dir, "notification_settings.json") - - if os.path.exists(settings_file): - with open(settings_file, 'r', encoding='utf-8') as f: - settings_data = json.load(f) - - # تحديث حالة الجلسة - st.session_state.notification_channels = settings_data.get("notification_channels", {}) - st.session_state.notification_preferences = settings_data.get("notification_preferences", {}) - st.session_state.notification_email = settings_data.get("notification_email", "") - st.session_state.notification_phone = settings_data.get("notification_phone", "") - st.session_state.notification_frequency = settings_data.get("notification_frequency", "فوري") - st.session_state.custom_frequency_hours = settings_data.get("custom_frequency_hours", 4) - st.session_state.show_on_startup = settings_data.get("show_on_startup", True) - st.session_state.audio_notifications = settings_data.get("audio_notifications", False) - st.session_state.log_notifications = settings_data.get("log_notifications", True) - st.session_state.retention_days = settings_data.get("retention_days", 90) - - # تسجيل التحميل - self.logger.info("تم تحميل إعدادات الإشعارات بنجاح") - - except Exception as e: - self.logger.error(f"حدث خطأ أثناء تحميل إعدادات الإشعارات: {str(e)}") - - def _save_notifications(self): - """حفظ الإشعارات""" - try: - # التأكد من وجود المجلد - os.makedirs(self.data_dir, exist_ok=True) - - # تحويل كائنات datetime إلى نصوص - notifications_data = [] - - for notification in st.session_state.notifications: - notification_copy = notification.copy() - - if "timestamp" in notification_copy and isinstance(notification_copy["timestamp"], datetime.datetime): - notification_copy["timestamp"] = notification_copy["timestamp"].isoformat() - - notifications_data.append(notification_copy) - - # حفظ البيانات - notifications_file = os.path.join(self.data_dir, "notifications.json") - - with open(notifications_file, 'w', encoding='utf-8') as f: - json.dump(notifications_data, f, ensure_ascii=False, indent=2) - - except Exception as e: - self.logger.error(f"حدث خطأ أثناء حفظ الإشعارات: {str(e)}") - - def _load_notifications(self): - """تحميل الإشعارات""" - try: - # التحقق من وجود ملف الإشعارات - notifications_file = os.path.join(self.data_dir, "notifications.json") - - if os.path.exists(notifications_file): - with open(notifications_file, 'r', encoding='utf-8') as f: - notifications_data = json.load(f) - - # تحويل النصوص إلى كائنات datetime - for notification in notifications_data: - if "timestamp" in notification: - notification["timestamp"] = datetime.datetime.fromisoformat(notification["timestamp"]) - - # تحديث حالة الجلسة - st.session_state.notifications = notifications_data - - # حساب عدد الإشعارات غير المقروءة - st.session_state.unread_count = len([ - n for n in st.session_state.notifications - if not n.get("read", False) - ]) - - # تحميل إعدادات الإشعارات - self._load_notification_settings() - - # تسجيل التحميل - self.logger.info(f"تم تحميل {len(notifications_data)} إشعار بنجاح") - - except Exception as e: - self.logger.error(f"حدث خطأ أثناء تحميل الإشعارات: {str(e)}") - - def check_scheduled_notifications(self): - """ - التحقق من الإشعارات المجدولة وإطلاقها إذا حان وقتها - - الإرجاع: - عدد الإشعارات التي تم إطلاقها - """ - count = 0 - - # الحصول على الإشعارات المجدولة - scheduled_notifications = self._get_scheduled_notifications() - - # الوقت الحالي - now = datetime.datetime.now() - - # التحقق من كل إشعار مجدول - for notification in scheduled_notifications: - notification_date = notification.get("notification_date") - - if notification_date and notification_date <= now: - # إنشاء إشعار جديد - self.add_notification( - title=notification.get("title"), - message=notification.get("message"), - notification_type=notification.get("notification_type"), - priority=notification.get("priority") - ) - - # تحديث آخر مرة تم فيها إطلاق الإشعار - notification["last_triggered"] = now - - # التعامل مع التكرار - repeat = notification.get("repeat", "مرة واحدة") - - if repeat == "مرة واحدة": - # حذف الإشعار المجدول - self._delete_scheduled_notification(notification.get("id")) - else: - # حساب التاريخ التالي - if repeat == "يومياً": - new_date = notification_date + datetime.timedelta(days=1) - elif repeat == "أسبوعياً": - new_date = notification_date + datetime.timedelta(weeks=1) - elif repeat == "شهرياً": - # إضافة شهر (تقريبي) - new_month = notification_date.month + 1 - new_year = notification_date.year - - if new_month > 12: - new_month = 1 - new_year += 1 - - new_date = notification_date.replace(year=new_year, month=new_month) - elif repeat == "سنوياً": - new_date = notification_date.replace(year=notification_date.year + 1) - else: - # افتراضي: يومياً - new_date = notification_date + datetime.timedelta(days=1) - - # تحديث تاريخ الإشعار المجدول - notification["notification_date"] = new_date - - count += 1 - - # حفظ الإشعارات المجدولة إذا تم تغييرها - if count > 0: - self._save_scheduled_notifications(scheduled_notifications) - - return count - - -# تطبيق وحدة نظام الإشعارات الذكي -class NotificationsApp: - """وحدة تطبيق نظام الإشعارات الذكي""" - - def __init__(self): - """تهيئة وحدة تطبيق نظام الإشعارات الذكي""" - self.smart_notification_system = SmartNotificationSystem() - - def render(self): - """عرض واجهة وحدة تطبيق نظام الإشعارات الذكي""" - st.markdown("

نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات

", unsafe_allow_html=True) - - st.markdown(""" -
- يتيح لك نظام الإشعارات الذكي متابعة تحديثات المشاريع وتلقي تنبيهات مخصصة حسب أدوارك واهتماماتك. - يمكنك تخصيص إعدادات الإشعارات وجدولة التذكيرات للمواعيد النهائية والمهام. -
- """, unsafe_allow_html=True) - - # عرض نظام الإشعارات الذكي - self.smart_notification_system.render() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="نظام الإشعارات الذكي | WAHBi AI", - page_icon="🔔", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = NotificationsApp() - app.render() \ No newline at end of file diff --git a/modules/pricing/constants.py b/modules/pricing/constants.py deleted file mode 100644 index 47389637d89c60506bf252b975c1caf981e05798..0000000000000000000000000000000000000000 --- a/modules/pricing/constants.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -ثوابت وحدة التسعير -""" - -# أوزان المحتوى المحلي -LOCAL_CONTENT_WEIGHTS = { - 'منتجات_البناء': 1.5, # المنتجات الأساسية في البناء لها وزن أكبر - 'المنتجات_الإنشائية': 1.5, # المنتجات الإنشائية لها وزن أكبر - 'منتجات_التشطيب': 1.0, # منتجات التشطيب لها وزن عادي - 'الخدمات_الهندسية': 1.3, # الخدمات الهندسية لها وزن أكبر - 'الخدمات_الإدارية': 1.0, # الخدمات الإدارية لها وزن عادي - 'القوى_العاملة_الفنية': 1.2, # القوى العاملة الفنية لها وزن أكبر - 'القوى_العاملة_العادية': 1.0, # القوى العاملة العادية لها وزن عادي - 'القوى_العاملة_الإدارية': 0.8 # القوى العاملة الإدارية لها وزن أقل -} - -# فئات التكاليف -COST_CATEGORIES = { - 'مباشرة': [ - 'مواد', - 'عمالة', - 'معدات', - 'مقاولين من الباطن' - ], - 'غير_مباشرة': [ - 'إدارة المشروع', - 'ضمانات بنكية', - 'تأمينات', - 'مكاتب الموقع', - 'نقل وسكن', - 'مرافق', - 'أمن وسلامة' - ], - 'مصاريف_عامة': [ - 'مصاريف إدارية', - 'رواتب إدارية', - 'إيجارات', - 'اتصالات', - 'قرطاسية', - 'تسويق وعلاقات عامة' - ], - 'احتياطيات': [ - 'احتياطي مخاطر', - 'احتياطي تضخم', - 'احتياطي تغييرات' - ] -} - -# أنواع التسعير -PRICING_TYPES = { - 'قياسي': 'التسعير المتوازن لجميع البنود', - 'غير_متزن': 'تحميل بعض البنود بسعر أعلى وتخفيض بنود أخرى مع الحفاظ على نفس الإجمالي', - 'تنافسي': 'التسعير بناءً على أسعار المنافسين', - 'ربحية': 'التسعير بناءً على هامش الربح المستهدف' -} - -# أنواع استراتيجيات التسعير غير المتزن -UNBALANCED_PRICING_STRATEGIES = { - 'تحميل_أمامي': 'زيادة أسعار البنود المبكرة في المشروع', - 'تحميل_خلفي': 'زيادة أسعار البنود المتأخرة في المشروع', - 'تحميل_مؤكد': 'زيادة أسعار البنود المؤكدة التنفيذ', - 'تخفيض_متغير': 'تخفيض أسعار البنود المحتمل تغير كمياتها' -} - -# معلمات افتراضية للمشروع -DEFAULT_PROJECT_PARAMS = { - 'نسبة_المصاريف_العامة': 8.0, # 8% من التكاليف المباشرة - 'نسبة_الأرباح': 10.0, # 10% من التكاليف الكلية - 'نسبة_احتياطي_المخاطر': 5.0, # 5% من التكاليف المباشرة - 'نسبة_ضمان_ابتدائي': 2.0, # 2% من قيمة العطاء - 'نسبة_ضمان_نهائي': 5.0, # 5% من قيمة العطاء - 'نسبة_محتجزات': 10.0, # 10% من قيمة المستخلصات - 'نسبة_دفعة_مقدمة': 10.0 # 10% من قيمة العطاء -} - -# وحدات القياس -UNITS_OF_MEASURE = { - 'طولية': ['م.ط', 'متر طولي', 'م'], - 'مسطحة': ['م2', 'متر مربع'], - 'حجمية': ['م3', 'متر مكعب'], - 'وزن': ['كجم', 'طن', 'جم'], - 'عدد': ['عدد', 'وحدة', 'قطعة'], - 'زمن': ['يوم', 'ساعة', 'شهر'], - 'نقطة': ['نقطة', 'مخرج'] -} - -# نسب الزيادة في التكاليف -COST_INCREASE_FACTORS = { - 'تعقيد_مرتفع': 1.25, # زيادة 25% للأعمال المعقدة - 'تعقيد_متوسط': 1.15, # زيادة 15% للأعمال متوسطة التعقيد - 'منطقة_نائية': 1.2, # زيادة 20% للمناطق النائية - 'ظروف_جوية_قاسية': 1.15, # زيادة 15% للظروف الجوية القاسية - 'ظروف_الموقع_صعبة': 1.2, # زيادة 20% لظروف الموقع الصعبة - 'عاجل': 1.3 # زيادة 30% للأعمال العاجلة -} - -# أنواع المشاريع -PROJECT_TYPES = [ - 'سكني', - 'تجاري', - 'صناعي', - 'تعليمي', - 'صحي', - 'بنية تحتية', - 'طرق', - 'نقل', - 'طاقة', - 'مياه وصرف صحي', - 'اتصالات', - 'عسكري', - 'ترفيهي', - 'متعدد الاستخدام' -] \ No newline at end of file diff --git a/modules/pricing/construction_calculator.py b/modules/pricing/construction_calculator.py deleted file mode 100644 index 4423dbcd8aaab2cece0a4865df496eea39aa2667..0000000000000000000000000000000000000000 --- a/modules/pricing/construction_calculator.py +++ /dev/null @@ -1,787 +0,0 @@ -""" -حاسبة تكاليف البناء المتكاملة -تتضمن العناصر التالية: -- المواد الخام -- المعدات -- العمالة -- المصاريف الإدارية -- هامش الربح -""" - -import streamlit as st -import pandas as pd -import numpy as np -import plotly.express as px -import plotly.graph_objects as go - - -def render_construction_calculator(): - """ - عرض حاسبة تكاليف البناء المتكاملة - """ - # التأكد من وجود المتغيرات في حالة الجلسة - if 'materials_cost' not in st.session_state: - st.session_state.materials_cost = 0.0 - if 'equipment_cost' not in st.session_state: - st.session_state.equipment_cost = 0.0 - if 'labor_cost' not in st.session_state: - st.session_state.labor_cost = 0.0 - if 'admin_cost' not in st.session_state: - st.session_state.admin_cost = 0.0 - if 'profit_margin' not in st.session_state: - st.session_state.profit_margin = 15.0 - - st.markdown("

حاسبة تكاليف البناء المتكاملة

", unsafe_allow_html=True) - - # معلومات المشروع - st.markdown("

معلومات المشروع

", unsafe_allow_html=True) - - col1, col2 = st.columns(2) - - with col1: - project_name = st.text_input("اسم المشروع", "مشروع سكني") - project_location = st.text_input("موقع المشروع", "الرياض - حي النرجس") - - with col2: - project_area = st.number_input("المساحة الإجمالية (م²)", min_value=1, value=500) - project_type = st.selectbox( - "نوع المشروع", - options=[ - "سكني", "تجاري", "صناعي", "إداري", "صحي", "تعليمي", - "بنية تحتية", "طرق", "جسور", "أخرى" - ] - ) - - # التبويبات الرئيسية للحاسبة - tabs = st.tabs([ - "المواد الخام", "المعدات", "العمالة", "المصاريف الإدارية", "هامش الربح", "التقرير النهائي" - ]) - - # تعريف المتغيرات العامة - if "materials_cost" not in st.session_state: - st.session_state.materials_cost = 0.0 - if "equipment_cost" not in st.session_state: - st.session_state.equipment_cost = 0.0 - if "labor_cost" not in st.session_state: - st.session_state.labor_cost = 0.0 - if "admin_cost" not in st.session_state: - st.session_state.admin_cost = 0.0 - if "profit_margin" not in st.session_state: - st.session_state.profit_margin = 10.0 - if "materials" not in st.session_state: - st.session_state.materials = [] - if "equipment" not in st.session_state: - st.session_state.equipment = [] - if "labor" not in st.session_state: - st.session_state.labor = [] - if "admin_expenses" not in st.session_state: - st.session_state.admin_expenses = [] - - # تبويب المواد الخام - with tabs[0]: - render_materials_tab() - - # تبويب المعدات - with tabs[1]: - render_equipment_tab() - - # تبويب العمالة - with tabs[2]: - render_labor_tab() - - # تبويب المصاريف الإدارية - with tabs[3]: - render_admin_tab() - - # تبويب هامش الربح - with tabs[4]: - render_profit_tab() - - # تبويب التقرير النهائي - with tabs[5]: - render_final_report(project_name, project_location, project_area, project_type) - - -def render_materials_tab(): - """ - عرض تبويب المواد الخام - """ - st.markdown("

تكاليف المواد الخام

", unsafe_allow_html=True) - - # إضافة مادة جديدة - st.markdown("

إضافة مادة جديدة

", unsafe_allow_html=True) - - col1, col2, col3, col4 = st.columns(4) - - with col1: - material_name = st.text_input("اسم المادة", key="new_material_name") - with col2: - material_quantity = st.number_input("الكمية", min_value=0.0, step=0.1, key="new_material_quantity") - with col3: - material_unit = st.selectbox( - "الوحدة", - options=["م²", "م³", "طن", "كجم", "لتر", "قطعة", "لفة", "كيس", "أخرى"], - key="new_material_unit" - ) - with col4: - material_price = st.number_input("السعر للوحدة (ريال)", min_value=0.0, step=0.01, key="new_material_price") - - if st.button("إضافة مادة", key="add_material_btn"): - total_price = material_quantity * material_price - new_material = { - "name": material_name, - "quantity": material_quantity, - "unit": material_unit, - "price": material_price, - "total": total_price - } - st.session_state.materials.append(new_material) - st.success(f"تمت إضافة {material_name} بنجاح!") - - # عرض قائمة المواد المضافة - if st.session_state.materials: - st.markdown("

قائمة المواد المضافة

", unsafe_allow_html=True) - - materials_df = pd.DataFrame(st.session_state.materials) - materials_df.columns = ["اسم المادة", "الكمية", "الوحدة", "السعر للوحدة", "التكلفة الإجمالية"] - st.dataframe(materials_df) - - total_materials_cost = sum(item["total"] for item in st.session_state.materials) - st.session_state.materials_cost = total_materials_cost - - st.markdown(f"

إجمالي تكلفة المواد: {total_materials_cost:,.2f} ريال

", unsafe_allow_html=True) - - # رسم بياني للمواد حسب التكلفة - if len(st.session_state.materials) > 1: - st.markdown("

توزيع تكاليف المواد

", unsafe_allow_html=True) - - fig = px.pie( - materials_df, - values="التكلفة الإجمالية", - names="اسم المادة", - title="توزيع تكاليف المواد", - color_discrete_sequence=px.colors.sequential.Teal, - hole=0.4 - ) - fig.update_layout( - font=dict(family="Almarai, Arial", size=14), - margin=dict(t=50, b=50, l=20, r=20) - ) - st.plotly_chart(fig, use_container_width=True) - - st.markdown("---") - - # استيراد بيانات المواد من ملف - st.markdown("

استيراد بيانات المواد من ملف

", unsafe_allow_html=True) - uploaded_file = st.file_uploader("اختر ملف Excel أو CSV", type=["xlsx", "csv"], key="materials_upload") - - if uploaded_file is not None: - if uploaded_file.name.endswith('.csv'): - df = pd.read_csv(uploaded_file) - else: - df = pd.read_excel(uploaded_file) - - st.success("تم استيراد البيانات بنجاح!") - st.dataframe(df) - - if st.button("إضافة المواد من الملف"): - try: - # تحويل أسماء الأعمدة للمطابقة مع النظام - column_mapping = { - "المادة": "name", - "اسم المادة": "name", - "الكمية": "quantity", - "الوحدة": "unit", - "السعر": "price", - "سعر الوحدة": "price" - } - - mapped_df = df.rename(columns=column_mapping) - - # حساب التكلفة الإجمالية لكل مادة - for _, row in mapped_df.iterrows(): - total_price = row["quantity"] * row["price"] - new_material = { - "name": row["name"], - "quantity": row["quantity"], - "unit": row["unit"], - "price": row["price"], - "total": total_price - } - st.session_state.materials.append(new_material) - - st.success("تمت إضافة جميع المواد من الملف بنجاح!") - - except Exception as e: - st.error(f"حدث خطأ: {str(e)}") - st.error("تأكد من أن الملف يحتوي على الأعمدة المطلوبة: اسم المادة، الكمية، الوحدة، السعر للوحدة") - - -def render_equipment_tab(): - """ - عرض تبويب المعدات - """ - st.markdown("

تكاليف المعدات

", unsafe_allow_html=True) - - # إضافة معدة جديدة - st.markdown("

إضافة معدة جديدة

", unsafe_allow_html=True) - - col1, col2, col3 = st.columns(3) - - with col1: - equipment_name = st.text_input("اسم المعدة", key="new_equipment_name") - with col2: - rental_type = st.selectbox( - "نوع الإيجار", - options=["يومي", "أسبوعي", "شهري", "سنوي", "مملوكة (استهلاك)"], - key="rental_type" - ) - with col3: - usage_period = st.number_input(f"مدة الاستخدام ({rental_type})", min_value=1, value=1, key="usage_period") - - col4, col5, col6 = st.columns(3) - - with col4: - equipment_rate = st.number_input(f"سعر الإيجار لكل ({rental_type}) (ريال)", min_value=0.0, step=0.01, key="equipment_rate") - with col5: - fuel_cost = st.number_input("تكلفة الوقود اليومية (ريال)", min_value=0.0, step=0.01, key="fuel_cost") - with col6: - operator_cost = st.number_input("تكلفة المشغل اليومية (ريال)", min_value=0.0, step=0.01, key="operator_cost") - - # حساب إجمالي التكلفة - rental_days = { - "يومي": 1, - "أسبوعي": 7, - "شهري": 30, - "سنوي": 365, - "مملوكة (استهلاك)": 1 - } - - total_days = usage_period * rental_days[rental_type] - total_equipment_cost = equipment_rate * usage_period - total_fuel_cost = fuel_cost * total_days - total_operator_cost = operator_cost * total_days - total_cost = total_equipment_cost + total_fuel_cost + total_operator_cost - - if st.button("إضافة معدة", key="add_equipment_btn"): - new_equipment = { - "name": equipment_name, - "rental_type": rental_type, - "usage_period": usage_period, - "equipment_rate": equipment_rate, - "fuel_cost": fuel_cost, - "operator_cost": operator_cost, - "total": total_cost - } - st.session_state.equipment.append(new_equipment) - st.success(f"تمت إضافة {equipment_name} بنجاح!") - - # عرض تفاصيل الحساب - st.markdown("
", unsafe_allow_html=True) - st.markdown(f"

عدد أيام الاستخدام الإجمالية: {total_days} يوم

", unsafe_allow_html=True) - st.markdown(f"

تكلفة إيجار المعدة: {total_equipment_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

تكلفة الوقود: {total_fuel_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

تكلفة المشغل: {total_operator_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

التكلفة الإجمالية للمعدة: {total_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown("
", unsafe_allow_html=True) - - # عرض قائمة المعدات المضافة - if st.session_state.equipment: - st.markdown("

قائمة المعدات المضافة

", unsafe_allow_html=True) - - equipment_data = [] - for item in st.session_state.equipment: - equipment_data.append({ - "اسم المعدة": item["name"], - "نوع الإيجار": item["rental_type"], - "مدة الاستخدام": item["usage_period"], - "إيجار الوحدة": item["equipment_rate"], - "تكلفة الوقود": item["fuel_cost"], - "تكلفة المشغل": item["operator_cost"], - "التكلفة الإجمالية": item["total"] - }) - - equipment_df = pd.DataFrame(equipment_data) - st.dataframe(equipment_df) - - total_equipment_cost = sum(item["total"] for item in st.session_state.equipment) - st.session_state.equipment_cost = total_equipment_cost - - st.markdown(f"

إجمالي تكلفة المعدات: {total_equipment_cost:,.2f} ريال

", unsafe_allow_html=True) - - # رسم بياني للمعدات حسب التكلفة - if len(st.session_state.equipment) > 1: - st.markdown("

توزيع تكاليف المعدات

", unsafe_allow_html=True) - - fig = go.Figure() - - fig.add_trace(go.Bar( - x=[item["اسم المعدة"] for item in equipment_data], - y=[item["التكلفة الإجمالية"] for item in equipment_data], - name="التكلفة الإجمالية", - marker_color="teal" - )) - - fig.update_layout( - title="تكاليف المعدات", - xaxis_title="المعدة", - yaxis_title="التكلفة (ريال)", - font=dict(family="Almarai, Arial", size=14), - margin=dict(t=50, b=50, l=20, r=20) - ) - - st.plotly_chart(fig, use_container_width=True) - - -def render_labor_tab(): - """ - عرض تبويب العمالة - """ - st.markdown("

تكاليف العمالة

", unsafe_allow_html=True) - - # إضافة عامل أو مجموعة عمال - st.markdown("

إضافة عمالة جديدة

", unsafe_allow_html=True) - - col1, col2, col3 = st.columns(3) - - with col1: - labor_type = st.text_input("نوع العمالة", key="new_labor_type") - with col2: - labor_count = st.number_input("العدد", min_value=1, value=1, key="new_labor_count") - with col3: - payment_type = st.selectbox( - "نوع الدفع", - options=["يومي", "أسبوعي", "شهري", "بالقطعة"], - key="new_payment_type" - ) - - col4, col5, col6 = st.columns(3) - - with col4: - wage_rate = st.number_input(f"الأجرة ({payment_type}) (ريال)", min_value=0.0, step=0.01, key="new_wage_rate") - with col5: - work_period = st.number_input(f"مدة العمل ({payment_type})", min_value=1, value=30, key="new_work_period") - with col6: - benefits_percent = st.slider("نسبة البدلات والتأمين (%)", min_value=0, max_value=50, value=15, key="new_benefits_percent") - - # حساب إجمالي التكلفة - days_factor = { - "يومي": 1, - "أسبوعي": 7, - "شهري": 30, - "بالقطعة": 1 - } - - monthly_days = work_period * days_factor[payment_type] / 30 # تحويل الأيام إلى شهور - - if payment_type == "بالقطعة": - total_labor_cost = labor_count * wage_rate * work_period - else: - # حساب الراتب الشهري - monthly_wage = wage_rate * 30 / days_factor[payment_type] - # حساب تكلفة البدلات والتأمين - benefits_cost = monthly_wage * (benefits_percent / 100) - # إجمالي التكلفة الشهرية - monthly_total_cost = monthly_wage + benefits_cost - # إجمالي التكلفة - total_labor_cost = labor_count * monthly_total_cost * monthly_days - - if st.button("إضافة عمالة"): - new_labor = { - "type": labor_type, - "count": labor_count, - "payment_type": payment_type, - "wage_rate": wage_rate, - "work_period": work_period, - "benefits_percent": benefits_percent, - "total": total_labor_cost - } - st.session_state.labor.append(new_labor) - st.success(f"تمت إضافة {labor_type} بنجاح!") - - # عرض تفاصيل الحساب - st.markdown("
", unsafe_allow_html=True) - if payment_type != "بالقطعة": - monthly_wage = wage_rate * 30 / days_factor[payment_type] - benefits_cost = monthly_wage * (benefits_percent / 100) - monthly_total_cost = monthly_wage + benefits_cost - - st.markdown(f"

الراتب الشهري للعامل: {monthly_wage:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

تكلفة البدلات والتأمين الشهرية: {benefits_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

إجمالي التكلفة الشهرية للعامل: {monthly_total_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

مدة العمل بالشهور: {monthly_days:.2f} شهر

", unsafe_allow_html=True) - else: - st.markdown(f"

سعر القطعة: {wage_rate:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

عدد القطع: {work_period}

", unsafe_allow_html=True) - - st.markdown(f"

عدد العمال: {labor_count}

", unsafe_allow_html=True) - st.markdown(f"

التكلفة الإجمالية للعمالة: {total_labor_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown("
", unsafe_allow_html=True) - - # عرض قائمة العمالة المضافة - if st.session_state.labor: - st.markdown("

قائمة العمالة المضافة

", unsafe_allow_html=True) - - labor_data = [] - for item in st.session_state.labor: - labor_data.append({ - "نوع العمالة": item["type"], - "العدد": item["count"], - "نوع الدفع": item["payment_type"], - "معدل الأجرة": item["wage_rate"], - "مدة العمل": item["work_period"], - "نسبة البدلات": f"{item['benefits_percent']}%", - "التكلفة الإجمالية": item["total"] - }) - - labor_df = pd.DataFrame(labor_data) - st.dataframe(labor_df) - - total_labor_cost = sum(item["total"] for item in st.session_state.labor) - st.session_state.labor_cost = total_labor_cost - - st.markdown(f"

إجمالي تكلفة العمالة: {total_labor_cost:,.2f} ريال

", unsafe_allow_html=True) - - # رسم بياني للعمالة حسب التكلفة - if len(st.session_state.labor) > 1: - st.markdown("

توزيع تكاليف العمالة

", unsafe_allow_html=True) - - fig = px.bar( - labor_df, - x="نوع العمالة", - y="التكلفة الإجمالية", - color="العدد", - title="توزيع تكاليف العمالة", - color_continuous_scale=px.colors.sequential.Teal - ) - fig.update_layout( - font=dict(family="Almarai, Arial", size=14), - margin=dict(t=50, b=50, l=20, r=20) - ) - st.plotly_chart(fig, use_container_width=True) - - -def render_admin_tab(): - """ - عرض تبويب المصاريف الإدارية - """ - st.markdown("

المصاريف الإدارية والعمومية

", unsafe_allow_html=True) - - # إضافة مصروف جديد - st.markdown("

إضافة مصروف جديد

", unsafe_allow_html=True) - - col1, col2, col3 = st.columns(3) - - with col1: - expense_name = st.text_input("اسم المصروف", key="new_expense_name") - with col2: - expense_type = st.selectbox( - "نوع المصروف", - options=[ - "رواتب إدارية", "إيجارات", "مكتبية", "سفر", "تأمين", - "استشارات", "رسوم حكومية", "منافع", "أخرى" - ], - key="new_expense_type" - ) - with col3: - expense_amount = st.number_input("المبلغ (ريال)", min_value=0.0, step=100.0, key="new_expense_amount") - - if st.button("إضافة مصروف"): - new_expense = { - "name": expense_name, - "type": expense_type, - "amount": expense_amount - } - st.session_state.admin_expenses.append(new_expense) - st.success(f"تمت إضافة {expense_name} بنجاح!") - - # عرض قائمة المصاريف المضافة - if st.session_state.admin_expenses: - st.markdown("

قائمة المصاريف الإدارية

", unsafe_allow_html=True) - - admin_data = [] - for item in st.session_state.admin_expenses: - admin_data.append({ - "اسم المصروف": item["name"], - "نوع المصروف": item["type"], - "المبلغ": item["amount"] - }) - - admin_df = pd.DataFrame(admin_data) - st.dataframe(admin_df) - - total_admin_cost = sum(item["amount"] for item in st.session_state.admin_expenses) - st.session_state.admin_cost = total_admin_cost - - st.markdown(f"

إجمالي المصاريف الإدارية: {total_admin_cost:,.2f} ريال

", unsafe_allow_html=True) - - # رسم بياني للمصاريف حسب النوع - if len(st.session_state.admin_expenses) > 1: - st.markdown("

توزيع المصاريف الإدارية حسب النوع

", unsafe_allow_html=True) - - # تجميع المصاريف حسب النوع - expense_by_type = admin_df.groupby("نوع المصروف")["المبلغ"].sum().reset_index() - - fig = px.pie( - expense_by_type, - values="المبلغ", - names="نوع المصروف", - title="توزيع المصاريف الإدارية", - color_discrete_sequence=px.colors.sequential.Teal, - hole=0.4 - ) - fig.update_layout( - font=dict(family="Almarai, Arial", size=14), - margin=dict(t=50, b=50, l=20, r=20) - ) - st.plotly_chart(fig, use_container_width=True) - - # نسبة المصاريف الإدارية - st.markdown("

احتساب المصاريف الإدارية بالنسبة المئوية

", unsafe_allow_html=True) - - # حساب التكاليف المباشرة - direct_costs = st.session_state.materials_cost + st.session_state.equipment_cost + st.session_state.labor_cost - - col1, col2 = st.columns(2) - - with col1: - admin_percent = st.slider("نسبة المصاريف الإدارية من التكاليف المباشرة (%)", min_value=0, max_value=30, value=10, key="admin_percent") - - with col2: - calculated_admin_cost = direct_costs * (admin_percent / 100) - st.markdown(f"

المصاريف الإدارية بالنسبة: {calculated_admin_cost:,.2f} ريال

", unsafe_allow_html=True) - - if st.button("استخدام النسبة المئوية للمصاريف الإدارية"): - st.session_state.admin_cost = calculated_admin_cost - st.success("تم تحديث إجمالي المصاريف الإدارية بناء على النسبة المئوية!") - - -def render_profit_tab(): - """ - عرض تبويب هامش الربح - """ - st.markdown("

هامش الربح

", unsafe_allow_html=True) - - # حساب التكاليف المباشرة والإجمالية - direct_costs = st.session_state.materials_cost + st.session_state.equipment_cost + st.session_state.labor_cost - total_costs = direct_costs + st.session_state.admin_cost - - # عرض ملخص التكاليف - st.markdown("
", unsafe_allow_html=True) - st.markdown("

ملخص التكاليف

", unsafe_allow_html=True) - st.markdown(f"

إجمالي تكلفة المواد: {st.session_state.materials_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

إجمالي تكلفة المعدات: {st.session_state.equipment_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

إجمالي تكلفة العمالة: {st.session_state.labor_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

إجمالي التكاليف المباشرة: {direct_costs:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

إجمالي المصاريف الإدارية: {st.session_state.admin_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

إجمالي التكاليف: {total_costs:,.2f} ريال

", unsafe_allow_html=True) - st.markdown("
", unsafe_allow_html=True) - - # تحديد هامش الربح - st.markdown("

تحديد هامش الربح

", unsafe_allow_html=True) - - col1, col2 = st.columns(2) - - with col1: - profit_margin = st.slider("نسبة هامش الربح (%)", min_value=0, max_value=30, value=int(st.session_state.profit_margin), key="profit_margin_slider") - st.session_state.profit_margin = profit_margin - - with col2: - profit_amount = total_costs * (profit_margin / 100) - st.markdown(f"

قيمة هامش الربح: {profit_amount:,.2f} ريال

", unsafe_allow_html=True) - - # إجمالي قيمة العرض - total_price = total_costs + profit_amount - st.markdown("
", unsafe_allow_html=True) - st.markdown(f"

إجمالي قيمة العرض: {total_price:,.2f} ريال

", unsafe_allow_html=True) - st.markdown("
", unsafe_allow_html=True) - - # تحليل الحساسية لهامش الربح - st.markdown("

تحليل حساسية هامش الربح

", unsafe_allow_html=True) - - sensitivity_data = [] - for margin in range(5, 31, 5): - profit = total_costs * (margin / 100) - total = total_costs + profit - sensitivity_data.append({ - "نسبة الربح": f"{margin}%", - "قيمة الربح": profit, - "إجمالي العرض": total - }) - - sensitivity_df = pd.DataFrame(sensitivity_data) - - # رسم بياني لتحليل الحساسية - fig = go.Figure() - - fig.add_trace(go.Bar( - x=[item["نسبة الربح"] for item in sensitivity_data], - y=[item["قيمة الربح"] for item in sensitivity_data], - name="قيمة الربح", - marker_color="rgba(14, 165, 165, 0.7)" - )) - - fig.add_trace(go.Scatter( - x=[item["نسبة الربح"] for item in sensitivity_data], - y=[item["إجمالي العرض"] for item in sensitivity_data], - name="إجمالي العرض", - mode="lines+markers", - marker=dict(size=8, color="rgba(255, 154, 60, 1.0)"), - line=dict(width=3, color="rgba(255, 154, 60, 0.7)") - )) - - fig.update_layout( - title="تحليل حساسية هامش الربح", - xaxis_title="نسبة الربح", - yaxis_title="القيمة (ريال)", - font=dict(family="Almarai, Arial", size=14), - margin=dict(t=50, b=50, l=20, r=20), - hovermode="x unified" - ) - - st.plotly_chart(fig, use_container_width=True) - - # جدول تحليل الحساسية - st.dataframe(sensitivity_df) - - -def render_final_report(project_name, project_location, project_area, project_type): - """ - عرض التقرير النهائي للتكاليف - """ - st.markdown("

التقرير النهائي لتكاليف المشروع

", unsafe_allow_html=True) - - # التأكد من وجود المتغيرات المطلوبة في حالة الجلسة وضمان أن لديهم قيم صحيحة - required_fields = { - 'materials_cost': 0.0, - 'equipment_cost': 0.0, - 'labor_cost': 0.0, - 'admin_cost': 0.0, - 'profit_margin': 15.0, - 'materials': [], - 'equipment': [], - 'labor': [], - 'admin_expenses': [] - } - - # مرور على كافة الحقول المطلوبة للتأكد من وجودها - for field, default_value in required_fields.items(): - if field not in st.session_state: - st.session_state[field] = default_value - - # التحقق من أن القيم العددية صالحة (غير None وليست NaN) - if field in ['materials_cost', 'equipment_cost', 'labor_cost', 'admin_cost', 'profit_margin']: - # إذا كانت القيمة None أو NaN، استخدم القيمة الافتراضية - if st.session_state[field] is None or pd.isna(st.session_state[field]): - st.session_state[field] = default_value - - # حساب التكاليف المباشرة والإجمالية - direct_costs = st.session_state.materials_cost + st.session_state.equipment_cost + st.session_state.labor_cost - total_costs = direct_costs + st.session_state.admin_cost - profit_amount = total_costs * (st.session_state.profit_margin / 100) - total_price = total_costs + profit_amount - - # معلومات المشروع - st.markdown("
", unsafe_allow_html=True) - st.markdown("

معلومات المشروع

", unsafe_allow_html=True) - col1, col2 = st.columns(2) - - with col1: - st.markdown(f"

اسم المشروع: {project_name}

", unsafe_allow_html=True) - st.markdown(f"

نوع المشروع: {project_type}

", unsafe_allow_html=True) - - with col2: - st.markdown(f"

موقع المشروع: {project_location}

", unsafe_allow_html=True) - st.markdown(f"

المساحة الإجمالية: {project_area} م²

", unsafe_allow_html=True) - - st.markdown("
", unsafe_allow_html=True) - - # ملخص التكاليف - st.markdown("
", unsafe_allow_html=True) - st.markdown("

ملخص التكاليف

", unsafe_allow_html=True) - - col1, col2 = st.columns(2) - - with col1: - st.markdown(f"

تكلفة المواد: {st.session_state.materials_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

تكلفة المعدات: {st.session_state.equipment_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

تكلفة العمالة: {st.session_state.labor_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

إجمالي التكاليف المباشرة: {direct_costs:,.2f} ريال

", unsafe_allow_html=True) - - with col2: - st.markdown(f"

المصاريف الإدارية: {st.session_state.admin_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

إجمالي التكاليف: {total_costs:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

هامش الربح ({st.session_state.profit_margin}%): {profit_amount:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

إجمالي قيمة العرض: {total_price:,.2f} ريال

", unsafe_allow_html=True) - - st.markdown("
", unsafe_allow_html=True) - - # عرض التفاصيل بالمتر المربع - if project_area > 0: - per_sqm_cost = total_price / project_area - st.markdown("
", unsafe_allow_html=True) - st.markdown("

تكلفة المتر المربع

", unsafe_allow_html=True) - st.markdown(f"

تكلفة المتر المربع الإجمالية: {per_sqm_cost:,.2f} ريال/م²

", unsafe_allow_html=True) - st.markdown("
", unsafe_allow_html=True) - else: - st.markdown("
", unsafe_allow_html=True) - st.markdown("

تكلفة المتر المربع

", unsafe_allow_html=True) - st.markdown("

يرجى إدخال مساحة صحيحة للمشروع لحساب تكلفة المتر المربع

", unsafe_allow_html=True) - st.markdown("
", unsafe_allow_html=True) - - # رسم بياني لتوزيع التكاليف - st.markdown("

توزيع التكاليف

", unsafe_allow_html=True) - - # تجنب القسمة على صفر - if total_price > 0: - cost_distribution = [ - {"النوع": "المواد", "القيمة": st.session_state.materials_cost, "النسبة": st.session_state.materials_cost / total_price * 100}, - {"النوع": "المعدات", "القيمة": st.session_state.equipment_cost, "النسبة": st.session_state.equipment_cost / total_price * 100}, - {"النوع": "العمالة", "القيمة": st.session_state.labor_cost, "النسبة": st.session_state.labor_cost / total_price * 100}, - {"النوع": "المصاريف الإدارية", "القيمة": st.session_state.admin_cost, "النسبة": st.session_state.admin_cost / total_price * 100}, - {"النوع": "هامش الربح", "القيمة": profit_amount, "النسبة": profit_amount / total_price * 100} - ] - else: - # إذا كان المجموع صفر، اجعل جميع النسب المئوية صفر - cost_distribution = [ - {"النوع": "المواد", "القيمة": st.session_state.materials_cost, "النسبة": 0}, - {"النوع": "المعدات", "القيمة": st.session_state.equipment_cost, "النسبة": 0}, - {"النوع": "العمالة", "القيمة": st.session_state.labor_cost, "النسبة": 0}, - {"النوع": "المصاريف الإدارية", "القيمة": st.session_state.admin_cost, "النسبة": 0}, - {"النوع": "هامش الربح", "القيمة": profit_amount, "النسبة": 0} - ] - - cost_df = pd.DataFrame(cost_distribution) - - fig = px.pie( - cost_df, - values="القيمة", - names="النوع", - title="توزيع التكاليف والأرباح", - color_discrete_sequence=px.colors.sequential.Teal, - hole=0.4 - ) - - fig.update_traces(textposition='inside', textinfo='percent+label') - - fig.update_layout( - annotations=[dict(text=f"{total_price:,.0f} ريال", x=0.5, y=0.5, font_size=14, showarrow=False)], - font=dict(family="Almarai, Arial", size=14), - margin=dict(t=50, b=50, l=20, r=20) - ) - - st.plotly_chart(fig, use_container_width=True) - - # جدول توزيع التكاليف - st.dataframe(cost_df) - - # زر لإنشاء تقرير PDF - col1, col2 = st.columns(2) - - with col1: - if st.button("تصدير التقرير إلى PDF"): - st.success("تم تصدير التقرير بنجاح!") - - with col2: - if st.button("حفظ التقرير في قاعدة البيانات"): - st.success("تم حفظ التقرير في قاعدة البيانات بنجاح!") \ No newline at end of file diff --git a/modules/pricing/exceptions.py b/modules/pricing/exceptions.py deleted file mode 100644 index 92784731deaa73438f8b193f68f2e9d20ddb0389..0000000000000000000000000000000000000000 --- a/modules/pricing/exceptions.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -استثناءات وحدة التسعير -""" - -class PricingError(Exception): - """استثناء أساسي لأخطاء التسعير""" - pass - - -class LocalContentCalculationError(PricingError): - """استثناء لأخطاء حساب المحتوى المحلي""" - pass - - -class PriceEstimationError(PricingError): - """استثناء لأخطاء تقدير الأسعار""" - pass - - -class ResourceNotFoundError(PricingError): - """استثناء لعدم وجود المورد المطلوب""" - pass - - -class InvalidInputError(PricingError): - """استثناء للمدخلات غير الصالحة""" - pass - - -class ModelLoadingError(PricingError): - """استثناء لأخطاء تحميل النموذج""" - pass - - -class DataProcessingError(PricingError): - """استثناء لأخطاء معالجة البيانات""" - pass - - -class UnbalancedPricingError(PricingError): - """استثناء لأخطاء التسعير غير المتزن""" - pass \ No newline at end of file diff --git a/modules/pricing/price_analysis_component.py b/modules/pricing/price_analysis_component.py deleted file mode 100644 index 13c76fb38b3d616490b18d561f10c42bb39e2587..0000000000000000000000000000000000000000 --- a/modules/pricing/price_analysis_component.py +++ /dev/null @@ -1,932 +0,0 @@ -import streamlit as st -import pandas as pd -import numpy as np -from datetime import datetime -import time - -class PriceAnalysisComponent: - """مكون تحليل الأسعار للبنود""" - - def __init__(self): - """تهيئة مكون تحليل الأسعار""" - # تهيئة قائمة الوحدات المتاحة - self.unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"] - - # تهيئة فئات التكاليف - self.cost_categories = [ - "مواد", - "عمالة", - "معدات", - "مقاولي الباطن", - "مصاريف عامة", - "أرباح" - ] - - # تهيئة قائمة البنود وتحليل أسعارها - if 'items_price_analysis' not in st.session_state: - st.session_state.items_price_analysis = {} - - def render(self): - """عرض واجهة تحليل الأسعار""" - st.markdown("

تحليل أسعار البنود

", unsafe_allow_html=True) - - # التحقق من وجود بنود في التسعير الحالي - if 'current_pricing' not in st.session_state or 'items' not in st.session_state.current_pricing: - st.warning("ليس هناك بنود للتحليل. يرجى إنشاء تسعير أولاً.") - return - - # الحصول على البنود من التسعير الحالي - items = st.session_state.current_pricing['items'].copy() - - # عرض قائمة البنود - st.markdown("### قائمة البنود") - st.dataframe(items[['رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي']], - use_container_width=True, hide_index=True) - - # اختيار البند لتحليل السعر - selected_item_id = st.selectbox( - "اختر البند لتحليل السعر", - options=items['رقم البند'].tolist(), - format_func=lambda x: f"{x}: {items[items['رقم البند'] == x]['وصف البند'].values[0][:50]}..." - ) - - if selected_item_id: - # الحصول على البند المحدد - selected_item = items[items['رقم البند'] == selected_item_id].iloc[0] - - # عرض تفاصيل البند المختار - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("رقم البند", selected_item['رقم البند']) - - with col2: - st.metric("الكمية", f"{selected_item['الكمية']} {selected_item['الوحدة']}") - - with col3: - st.metric("سعر الوحدة", f"{selected_item['سعر الوحدة']:,.2f} ريال") - - st.markdown(f"**وصف البند**: {selected_item['وصف البند']}") - - # إنشاء أو تحديث تحليل السعر للبند المحدد - if selected_item_id not in st.session_state.items_price_analysis: - # إنشاء تحليل سعر افتراضي - self._create_default_price_analysis(selected_item_id, selected_item) - - # عرض وتحرير تحليل السعر - self._render_price_analysis_editor(selected_item_id, selected_item) - - def _create_default_price_analysis(self, item_id, item): - """إنشاء تحليل سعر افتراضي للبند""" - # إنشاء قائمة مكونات تحليل السعر - components = pd.DataFrame(columns=[ - 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي' - ]) - - # إضافة مكونات افتراضية بناءً على نوع البند - is_concrete = 'خرسان' in item['وصف البند'] - is_steel = 'حديد' in item['وصف البند'] or 'تسليح' in item['وصف البند'] - is_bricks = 'بلوك' in item['وصف البند'] or 'طوب' in item['وصف البند'] - is_paint = 'دهان' in item['وصف البند'] or 'طلاء' in item['وصف البند'] - is_insulation = 'عزل' in item['وصف البند'] - - # إضافة المكونات بناءً على نوع البند - if is_concrete: - # مكونات الخرسانة - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'], - 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1], - 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'], - 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150], - 'الإجمالي': [175, 40, 96, 400, 500, 100, 150] - }) - components = pd.concat([components, default_components], ignore_index=True) - - elif is_steel: - # مكونات الحديد - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'], - 'الكمية': [1000, 10, 1, 1, 1], - 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'], - 'سعر الوحدة': [4.5, 50, 300, 200, 300], - 'الإجمالي': [4500, 500, 300, 200, 300] - }) - components = pd.concat([components, default_components], ignore_index=True) - - elif is_bricks: - # مكونات البلوك - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'], - 'الكمية': [12.5, 0.02, 1, 1, 1], - 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'], - 'سعر الوحدة': [8, 500, 80, 15, 20], - 'الإجمالي': [100, 10, 80, 15, 20] - }) - components = pd.concat([components, default_components], ignore_index=True) - - elif is_paint: - # مكونات الدهانات - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'], - 'الكمية': [0.4, 0.1, 1, 1, 1], - 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'], - 'سعر الوحدة': [80, 20, 35, 5, 10], - 'الإجمالي': [32, 2, 35, 5, 10] - }) - components = pd.concat([components, default_components], ignore_index=True) - - elif is_insulation: - # مكونات العزل - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'], - 'الكمية': [1.1, 0.2, 1, 1, 1], - 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'], - 'سعر الوحدة': [60, 30, 25, 10, 15], - 'الإجمالي': [66, 6, 25, 10, 15] - }) - components = pd.concat([components, default_components], ignore_index=True) - - else: - # مكونات عامة افتراضية - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['مواد أساسية', 'عمالة', 'معدات ومعد مساعدة', 'مصاريف عامة', 'أرباح'], - 'الكمية': [1, 1, 1, 1, 1], - 'الوحدة': [item['الوحدة'], 'وحدة', 'وحدة', 'وحدة', 'وحدة'], - 'سعر الوحدة': [ - item['سعر الوحدة'] * 0.6, - item['سعر الوحدة'] * 0.2, - item['سعر الوحدة'] * 0.1, - item['سعر الوحدة'] * 0.05, - item['سعر الوحدة'] * 0.05 - ], - 'الإجمالي': [ - item['سعر الوحدة'] * 0.6, - item['سعر الوحدة'] * 0.2, - item['سعر الوحدة'] * 0.1, - item['سعر الوحدة'] * 0.05, - item['سعر الوحدة'] * 0.05 - ] - }) - components = pd.concat([components, default_components], ignore_index=True) - - # حفظ تحليل السعر للبند - st.session_state.items_price_analysis[item_id] = components - - def _render_price_analysis_editor(self, item_id, item): - """عرض محرر تحليل السعر للبند""" - st.markdown("### تحليل السعر") - - # الحصول على مكونات تحليل السعر - components = st.session_state.items_price_analysis[item_id] - - # عرض تحليل السعر في محرر بيانات - st.markdown("#### مكونات السعر") - - edited_components = st.data_editor( - components, - use_container_width=True, - hide_index=True, - num_rows="dynamic", - column_config={ - 'نوع التكلفة': st.column_config.SelectboxColumn( - 'نوع التكلفة', - help='فئة التكلفة', - options=self.cost_categories - ), - 'الوحدة': st.column_config.SelectboxColumn( - 'الوحدة', - help='وحدة القياس', - options=self.unit_options + ["وحدة", "ساعة", "يوم"] - ), - 'الكمية': st.column_config.NumberColumn( - 'الكمية', - help='الكمية', - min_value=0.0, - format="%.2f" - ), - 'سعر الوحدة': st.column_config.NumberColumn( - 'سعر الوحدة', - help='سعر الوحدة', - min_value=0.0, - format="%.2f" - ), - 'الإجمالي': st.column_config.NumberColumn( - 'الإجمالي', - help='الإجمالي', - min_value=0.0, - format="%.2f" - ) - } - ) - - # إعادة حساب الإجمالي لكل مكون - edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة'] - - # حفظ التعديلات - st.session_state.items_price_analysis[item_id] = edited_components - - # حساب إجمالي تحليل السعر - total_analysis_price = edited_components['الإجمالي'].sum() - unit_price_from_analysis = total_analysis_price / item['الكمية'] if item['الكمية'] > 0 else 0 - - # عرض ملخص تحليل السعر - st.markdown("#### ملخص تحليل السعر") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال") - - with col2: - st.metric("سعر الوحدة من التحليل", f"{unit_price_from_analysis:,.2f} ريال") - - with col3: - # المقارنة مع السعر الأصلي - diff = unit_price_from_analysis - item['سعر الوحدة'] - st.metric( - "الفرق عن السعر الأصلي", - f"{diff:,.2f} ريال", - delta=f"{(diff/item['سعر الوحدة']*100) if item['سعر الوحدة'] > 0 else 0:.1f}%" - ) - - # تحليل توزيع التكاليف حسب الفئة - cost_by_category = edited_components.groupby('نوع التكلفة')['الإجمالي'].sum().reset_index() - - # عرض مخطط توزيع التكاليف - st.markdown("#### توزيع التكاليف حسب الفئة") - - # عرض توزيع التكاليف في جدول - distribution_df = pd.DataFrame({ - 'نوع التكلفة': cost_by_category['نوع التكلفة'], - 'القيمة': cost_by_category['الإجمالي'], - 'النسبة المئوية': (cost_by_category['الإجمالي'] / total_analysis_price * 100).round(2) - }) - - st.dataframe( - distribution_df, - use_container_width=True, - hide_index=True, - column_config={ - 'القيمة': st.column_config.NumberColumn( - 'القيمة', - help='القيمة', - format="%.2f" - ), - 'النسبة المئوية': st.column_config.ProgressColumn( - 'النسبة المئوية', - help='النسبة المئوية', - format="%.2f%%", - min_value=0, - max_value=100 - ) - } - ) - - # أزرار الإجراءات - col1, col2, col3 = st.columns(3) - - with col1: - if st.button("تحديث سعر البند", use_container_width=True): - # تحديث سعر البند بناءً على تحليل السعر - items = st.session_state.current_pricing['items'].copy() - item_index = items[items['رقم البند'] == item_id].index[0] - - # تحديث سعر الوحدة والإجمالي - items.at[item_index, 'سعر الوحدة'] = unit_price_from_analysis - items.at[item_index, 'الإجمالي'] = unit_price_from_analysis * items.at[item_index, 'الكمية'] - - # حفظ التعديلات في التسعير الحالي - st.session_state.current_pricing['items'] = items - - st.success(f"تم تحديث سعر البند بناءً على تحليل السعر: {unit_price_from_analysis:,.2f} ريال") - time.sleep(0.5) - st.rerun() - - with col2: - if st.button("تصدير تحليل السعر", use_container_width=True): - st.success("تم إرسال تحليل السعر للتصدير بنجاح!") - - with col3: - if st.button("مسح تحليل السعر", use_container_width=True): - # حذف تحليل السعر للبند - if item_id in st.session_state.items_price_analysis: - del st.session_state.items_price_analysis[item_id] - - st.warning("تم مسح تحليل السعر للبند") - time.sleep(0.5) - st.rerun() - - def add_to_pricing_app(self, pricing_app): - """إضافة مكون تحليل الأسعار إلى تطبيق التسعير""" - # إضافة تبويب جديد - if not hasattr(pricing_app, 'tabs'): - pricing_app.tabs = [] - - if len(pricing_app.tabs) == 4: # إذا كان هناك 4 تبويبات فقط - pricing_app.tabs.append("تحليل أسعار البنود") - - # إضافة دالة العرض - pricing_app._render_price_analysis_tab = self.render - - -def render_integrated_item_input(): - """عرض واجهة إدخال البنود مع تحليل السعر المتكامل""" - - # ضبط CSS لتحسين ظهور الواجهة العربية - st.markdown(""" - - """, unsafe_allow_html=True) - - # تهيئة قائمة الوحدات المتاحة - unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"] - - # تهيئة فئات التكاليف - cost_categories = [ - "مواد", - "عمالة", - "معدات", - "مقاولي الباطن", - "مصاريف عامة", - "أرباح" - ] - - # إنشاء جدول البنود اذا لم يكن موجوداً - if 'manual_items' not in st.session_state: - manual_items = pd.DataFrame(columns=[ - 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي' - ]) - - # إضافة بضعة صفوف افتراضية - default_items = pd.DataFrame({ - 'رقم البند': ["A1", "A2", "A3", "A4", "A5"], - 'وصف البند': [ - "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", - "توريد وتركيب حديد التسليح للأساسات", - "أعمال العزل المائي للأساسات", - "أعمال الردم والدك للأساسات", - "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة" - ], - 'الوحدة': ["م3", "طن", "م2", "م3", "م3"], - 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0], - 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0], - 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0] - }) - - manual_items = pd.concat([manual_items, default_items]) - st.session_state.manual_items = manual_items - - # إنشاء جدول تحليل الأسعار اذا لم يكن موجوداً - if 'items_price_analysis' not in st.session_state: - st.session_state.items_price_analysis = {} - - # عرض واجهة إدخال البنود - st.markdown("### إدخال تفاصيل البنود مع تحليل الأسعار") - - # عرض البنود الحالية كجدول للعرض - st.markdown("### جدول البنود الحالية") - st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True) - - # التبويبات لإضافة بند جديد أو تعديل بند - tabs = st.tabs(["إضافة بند جديد", "تعديل بند حالي"]) - - with tabs[0]: # إضافة بند جديد - st.markdown("### إضافة بند جديد مع تحليل السعر") - - col1, col2 = st.columns(2) - - with col1: - new_id = st.text_input("رقم البند", value=f"A{len(st.session_state.manual_items)+1}", key="new_id") - new_desc = st.text_area("وصف البند", value="", key="new_desc") - - with col2: - new_unit = st.selectbox("الوحدة", options=unit_options, key="new_unit") - new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f", key="new_qty") - - # إنشاء تحليل السعر للبند الجديد - st.markdown('
', unsafe_allow_html=True) - st.markdown("#### تحليل سعر البند") - - # التعرف التلقائي على نوع البند من الوصف - is_concrete = False - is_steel = False - is_bricks = False - is_paint = False - is_insulation = False - - if new_desc: - is_concrete = 'خرسان' in new_desc - is_steel = 'حديد' in new_desc or 'تسليح' in new_desc - is_bricks = 'بلوك' in new_desc or 'طوب' in new_desc - is_paint = 'دهان' in new_desc or 'طلاء' in new_desc - is_insulation = 'عزل' in new_desc - - # تلميح للمستخدم عن التعرف التلقائي - if any([is_concrete, is_steel, is_bricks, is_paint, is_insulation]): - detected_type = "" - if is_concrete: - detected_type = "أعمال خرسانة" - elif is_steel: - detected_type = "أعمال حديد" - elif is_bricks: - detected_type = "أعمال بلوك" - elif is_paint: - detected_type = "أعمال دهانات" - elif is_insulation: - detected_type = "أعمال عزل" - - st.info(f"تم التعرف تلقائياً على نوع البند: {detected_type}") - - # إنشاء مصفوفة فارغة لمكونات البند - if 'new_components' not in st.session_state: - # إنشاء DataFrame فارغ - new_components = pd.DataFrame(columns=[ - 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي' - ]) - - # إضافة مكونات افتراضية بناءً على نوع البند - if is_concrete: - # مكونات الخرسانة - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'], - 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1], - 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'], - 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150], - 'الإجمالي': [175, 40, 96, 400, 500, 100, 150] - }) - new_components = pd.concat([new_components, default_components], ignore_index=True) - - elif is_steel: - # مكونات الحديد - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'], - 'الكمية': [1000, 10, 1, 1, 1], - 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'], - 'سعر الوحدة': [4.5, 50, 300, 200, 300], - 'الإجمالي': [4500, 500, 300, 200, 300] - }) - new_components = pd.concat([new_components, default_components], ignore_index=True) - - elif is_bricks: - # مكونات البلوك - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'], - 'الكمية': [12.5, 0.02, 1, 1, 1], - 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'], - 'سعر الوحدة': [8, 500, 80, 15, 20], - 'الإجمالي': [100, 10, 80, 15, 20] - }) - new_components = pd.concat([new_components, default_components], ignore_index=True) - - elif is_paint: - # مكونات الدهانات - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'], - 'الكمية': [0.4, 0.1, 1, 1, 1], - 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'], - 'سعر الوحدة': [80, 20, 35, 5, 10], - 'الإجمالي': [32, 2, 35, 5, 10] - }) - new_components = pd.concat([new_components, default_components], ignore_index=True) - - elif is_insulation: - # مكونات العزل - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'], - 'الكمية': [1.1, 0.2, 1, 1, 1], - 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'], - 'سعر الوحدة': [60, 30, 25, 10, 15], - 'الإجمالي': [66, 6, 25, 10, 15] - }) - new_components = pd.concat([new_components, default_components], ignore_index=True) - - else: - # مكونات عامة افتراضية - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['مواد أساسية', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'], - 'الكمية': [1, 1, 1, 1, 1], - 'الوحدة': [new_unit if new_unit else 'وحدة', 'وحدة', 'وحدة', 'وحدة', 'وحدة'], - 'سعر الوحدة': [100, 50, 30, 20, 20], - 'الإجمالي': [100, 50, 30, 20, 20] - }) - new_components = pd.concat([new_components, default_components], ignore_index=True) - - st.session_state.new_components = new_components - - # عرض وتحرير مكونات تحليل السعر - edited_components = st.data_editor( - st.session_state.new_components, - use_container_width=True, - hide_index=True, - num_rows="dynamic", - column_config={ - 'نوع التكلفة': st.column_config.SelectboxColumn( - 'نوع التكلفة', - help='فئة التكلفة', - options=cost_categories - ), - 'الوحدة': st.column_config.SelectboxColumn( - 'الوحدة', - help='وحدة القياس', - options=unit_options + ["وحدة", "ساعة", "يوم"] - ), - 'الكمية': st.column_config.NumberColumn( - 'الكمية', - help='الكمية', - min_value=0.0, - format="%.2f" - ), - 'سعر الوحدة': st.column_config.NumberColumn( - 'سعر الوحدة', - help='سعر الوحدة', - min_value=0.0, - format="%.2f" - ), - 'الإجمالي': st.column_config.NumberColumn( - 'الإجمالي', - help='الإجمالي', - min_value=0.0, - format="%.2f" - ) - } - ) - - # إعادة حساب الإجمالي لكل مكون - edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة'] - - # حفظ التعديلات - st.session_state.new_components = edited_components - - # حساب إجمالي تحليل السعر - total_analysis_price = edited_components['الإجمالي'].sum() - unit_price_from_analysis = total_analysis_price / new_qty if new_qty > 0 else 0 - - # عرض ملخص تحليل السعر - st.markdown("#### ملخص تحليل السعر") - - col1, col2 = st.columns(2) - - with col1: - st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال") - - with col2: - st.metric("سعر الوحدة المحسوب", f"{unit_price_from_analysis:,.2f} ريال") - - st.markdown('
', unsafe_allow_html=True) - - # استخدام السعر المحسوب - use_calculated_price = st.checkbox("استخدام السعر المحسوب من التحليل", value=True) - - # تحديد سعر الوحدة النهائي - if use_calculated_price and new_qty > 0: - new_price = unit_price_from_analysis - else: - new_price = st.number_input("سعر الوحدة", value=unit_price_from_analysis if new_qty > 0 else 0.0, min_value=0.0, format="%.2f", key="new_price") - - # حساب الإجمالي - new_total = new_qty * new_price - st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال") - - # مقارنة السعر المدخل مع السعر المحسوب - if not use_calculated_price and new_qty > 0 and unit_price_from_analysis > 0: - price_diff = new_price - unit_price_from_analysis - diff_percentage = (price_diff / unit_price_from_analysis) * 100 - - if abs(diff_percentage) > 5: # إذا كان الفرق أكبر من 5% - if diff_percentage > 0: - st.warning(f"السعر المدخل أعلى من السعر المحسوب بنسبة {diff_percentage:.2f}%") - else: - st.warning(f"السعر المدخل أقل من السعر المحسوب بنسبة {abs(diff_percentage):.2f}%") - - # زر إضافة البند - if st.button("إضافة البند"): - # التحقق من صحة البيانات - if new_id and new_desc and new_qty > 0: - # إنشاء صف جديد - new_row = pd.DataFrame({ - 'رقم البند': [new_id], - 'وصف البند': [new_desc], - 'الوحدة': [new_unit], - 'الكمية': [float(new_qty)], - 'سعر الوحدة': [float(new_price)], - 'الإجمالي': [float(new_total)] - }) - - # إضافة الصف إلى DataFrame - st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True) - - # حفظ تحليل سعر البند - st.session_state.items_price_analysis[new_id] = st.session_state.new_components.copy() - - # إعادة تهيئة مكونات البند الجديد - if 'new_components' in st.session_state: - del st.session_state.new_components - - st.success("تم إضافة البند وتحليل السعر بنجاح!") - time.sleep(0.5) - st.rerun() - else: - st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.") - - with tabs[1]: # تعديل بند حالي - st.markdown("### تعديل بند حالي مع تحليل السعر") - - # اختيار البند للتعديل - edit_item_id = st.selectbox( - "اختر البند للتعديل", - options=st.session_state.manual_items['رقم البند'].tolist(), - format_func=lambda x: f"{x}: {st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == x]['وصف البند'].values[0][:30]}..." - ) - - if edit_item_id: - # الحصول على مؤشر الصف للبند المحدد - idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == edit_item_id].index[0] - row = st.session_state.manual_items.loc[idx] - - # إنشاء نموذج تعديل البند - col1, col2 = st.columns(2) - - with col1: - edited_id = st.text_input("رقم البند (تعديل)", value=row['رقم البند'], key="edit_id") - edited_desc = st.text_area("وصف البند (تعديل)", value=row['وصف البند'], key="edit_desc") - - with col2: - edited_unit = st.selectbox( - "الوحدة (تعديل)", - options=unit_options, - index=unit_options.index(row['الوحدة']) if row['الوحدة'] in unit_options else 0, - key="edit_unit" - ) - edited_qty = st.number_input("الكمية (تعديل)", value=float(row['الكمية']), min_value=0.0, format="%.2f", key="edit_qty") - - # إنشاء أو تحرير تحليل السعر للبند - st.markdown('
', unsafe_allow_html=True) - st.markdown("#### تحليل سعر البند") - - # التحقق مما إذا كان البند له تحليل سعر محفوظ - if edit_item_id in st.session_state.items_price_analysis: - # استخدام تحليل السعر المحفوظ - components = st.session_state.items_price_analysis[edit_item_id] - else: - # إنشاء تحليل سعر افتراضي - components = pd.DataFrame(columns=[ - 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي' - ]) - - # فحص نوع البند من الوصف - is_concrete = 'خرسان' in row['وصف البند'] - is_steel = 'حديد' in row['وصف البند'] or 'تسليح' in row['وصف البند'] - is_bricks = 'بلوك' in row['وصف البند'] or 'طوب' in row['وصف البند'] - is_paint = 'دهان' in row['وصف البند'] or 'طلاء' in row['وصف البند'] - is_insulation = 'عزل' in row['وصف البند'] - - # إضافة مكونات افتراضية بناءً على نوع البند - if is_concrete: - # مكونات الخرسانة - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'], - 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1], - 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'], - 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150], - 'الإجمالي': [175, 40, 96, 400, 500, 100, 150] - }) - components = pd.concat([components, default_components], ignore_index=True) - - elif is_steel: - # مكونات الحديد - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'], - 'الكمية': [1000, 10, 1, 1, 1], - 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'], - 'سعر الوحدة': [4.5, 50, 300, 200, 300], - 'الإجمالي': [4500, 500, 300, 200, 300] - }) - components = pd.concat([components, default_components], ignore_index=True) - - elif is_bricks: - # مكونات البلوك - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'], - 'الكمية': [12.5, 0.02, 1, 1, 1], - 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'], - 'سعر الوحدة': [8, 500, 80, 15, 20], - 'الإجمالي': [100, 10, 80, 15, 20] - }) - components = pd.concat([components, default_components], ignore_index=True) - - elif is_paint: - # مكونات الدهانات - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'], - 'الكمية': [0.4, 0.1, 1, 1, 1], - 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'], - 'سعر الوحدة': [80, 20, 35, 5, 10], - 'الإجمالي': [32, 2, 35, 5, 10] - }) - components = pd.concat([components, default_components], ignore_index=True) - - elif is_insulation: - # مكونات العزل - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'], - 'الكمية': [1.1, 0.2, 1, 1, 1], - 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'], - 'سعر الوحدة': [60, 30, 25, 10, 15], - 'الإجمالي': [66, 6, 25, 10, 15] - }) - components = pd.concat([components, default_components], ignore_index=True) - - else: - # مكونات عامة افتراضية - default_components = pd.DataFrame({ - 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'], - 'الوصف': ['مواد أساسية', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'], - 'الكمية': [1, 1, 1, 1, 1], - 'الوحدة': [row['الوحدة'], 'وحدة', 'وحدة', 'وحدة', 'وحدة'], - 'سعر الوحدة': [ - row['سعر الوحدة'] * 0.6, - row['سعر الوحدة'] * 0.2, - row['سعر الوحدة'] * 0.1, - row['سعر الوحدة'] * 0.05, - row['سعر الوحدة'] * 0.05 - ], - 'الإجمالي': [ - row['سعر الوحدة'] * 0.6, - row['سعر الوحدة'] * 0.2, - row['سعر الوحدة'] * 0.1, - row['سعر الوحدة'] * 0.05, - row['سعر الوحدة'] * 0.05 - ] - }) - components = pd.concat([components, default_components], ignore_index=True) - - # حفظ تحليل السعر - st.session_state.items_price_analysis[edit_item_id] = components - - # عرض وتحرير مكونات تحليل السعر - edited_components = st.data_editor( - components, - use_container_width=True, - hide_index=True, - num_rows="dynamic", - column_config={ - 'نوع التكلفة': st.column_config.SelectboxColumn( - 'نوع التكلفة', - help='فئة التكلفة', - options=cost_categories - ), - 'الوحدة': st.column_config.SelectboxColumn( - 'الوحدة', - help='وحدة القياس', - options=unit_options + ["وحدة", "ساعة", "يوم"] - ), - 'الكمية': st.column_config.NumberColumn( - 'الكمية', - help='الكمية', - min_value=0.0, - format="%.2f" - ), - 'سعر الوحدة': st.column_config.NumberColumn( - 'سعر الوحدة', - help='سعر الوحدة', - min_value=0.0, - format="%.2f" - ), - 'الإجمالي': st.column_config.NumberColumn( - 'الإجمالي', - help='الإجمالي', - min_value=0.0, - format="%.2f" - ) - } - ) - - # إعادة حساب الإجمالي لكل مكون - edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة'] - - # حفظ التعديلات - st.session_state.items_price_analysis[edit_item_id] = edited_components - - # حساب إجمالي تحليل السعر - total_analysis_price = edited_components['الإجمالي'].sum() - unit_price_from_analysis = total_analysis_price / edited_qty if edited_qty > 0 else 0 - - # عرض ملخص تحليل السعر - st.markdown("#### ملخص تحليل السعر") - - col1, col2 = st.columns(2) - - with col1: - st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال") - - with col2: - st.metric("سعر الوحدة المحسوب", f"{unit_price_from_analysis:,.2f} ريال") - - st.markdown('
', unsafe_allow_html=True) - - # استخدام السعر المحسوب - use_calculated_price = st.checkbox("استخدام السعر المحسوب من التحليل", value=True, key="use_calc_edit") - - # تحديد سعر الوحدة النهائي - if use_calculated_price and edited_qty > 0: - edited_price = unit_price_from_analysis - else: - edited_price = st.number_input( - "سعر الوحدة (تعديل)", - value=unit_price_from_analysis if edited_qty > 0 and unit_price_from_analysis > 0 else float(row['سعر الوحدة']), - min_value=0.0, - format="%.2f", - key="edit_price" - ) - - # حساب الإجمالي - edited_total = edited_qty * edited_price - st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال") - - # مقارنة السعر المدخل مع السعر المحسوب - if not use_calculated_price and edited_qty > 0 and unit_price_from_analysis > 0: - price_diff = edited_price - unit_price_from_analysis - diff_percentage = (price_diff / unit_price_from_analysis) * 100 - - if abs(diff_percentage) > 5: # إذا كان الفرق أكبر من 5% - if diff_percentage > 0: - st.warning(f"السعر المدخل أعلى من السعر المحسوب بنسبة {diff_percentage:.2f}%") - else: - st.warning(f"السعر المدخل أقل من السعر المحسوب بنسبة {abs(diff_percentage):.2f}%") - - # أزرار الإجراءات - col1, col2, col3 = st.columns(3) - - with col1: - if st.button("حفظ التعديلات", use_container_width=True): - # التحقق من صحة البيانات - if edited_id and edited_desc and edited_qty > 0: - # التحقق من تغيير رقم البند - if edited_id != edit_item_id: - # نقل تحليل السعر إلى الرقم الجديد - st.session_state.items_price_analysis[edited_id] = st.session_state.items_price_analysis.pop(edit_item_id) - - # تحديث البند - st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id - st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc - st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit - st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty - st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price - st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total - - st.success("تم تحديث البند وتحليل السعر بنجاح!") - time.sleep(0.5) - st.rerun() - else: - st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.") - - with col2: - if st.button("استعادة القيم الأصلية", use_container_width=True): - # إعادة تحميل الصفحة لاستعادة القيم الأصلية - st.rerun() - - with col3: - if st.button("حذف هذا البند", use_container_width=True): - # حذف تحليل السعر للبند - if edit_item_id in st.session_state.items_price_analysis: - del st.session_state.items_price_analysis[edit_item_id] - - # حذف البند - st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True) - - st.warning("تم حذف البند وتحليل السعر!") - time.sleep(0.5) - st.rerun() diff --git a/modules/pricing/price_analyzer.py b/modules/pricing/price_analyzer.py deleted file mode 100644 index ecdbf1a08b00585ac39627249913839a0d5b5a65..0000000000000000000000000000000000000000 --- a/modules/pricing/price_analyzer.py +++ /dev/null @@ -1,1695 +0,0 @@ -""" -محلل الأسعار لنظام إدارة المناقصات -""" - -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 deleted file mode 100644 index b2d598c7d72429103bfbd7a5906634951498a2ab..0000000000000000000000000000000000000000 --- a/modules/pricing/pricing_app.py +++ /dev/null @@ -1,4358 +0,0 @@ -""" -تطبيق وحدة التسعير المتكاملة -""" - -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 random -import os -import time -import io - -# ملاحظة: نحن لا نستخدم st.set_page_config هنا لأنه يجب أن يكون في ملف app.py الرئيسي فقط - -# تحسين المظهر العام باستخدام CSS -st.markdown(""" - -""", unsafe_allow_html=True) - -# تحسين شكل الأزرار بشكل متقدم -st.markdown(""" - -""", unsafe_allow_html=True) - -# وظيفة مساعدة لإنشاء أزرار بتنسيقات مختلفة -def styled_button(label, key, type="primary", on_click=None, args=None, full_width=False, icon=None): - """ - إنشاء زر بتنسيق معين - :param label: نص الزر - :param key: مفتاح الزر الفريد - :param type: نوع التنسيق ('primary', 'secondary', 'success', 'warning', 'danger', 'info', 'glass', 'flat') - :param on_click: الدالة التي سيتم تنفيذها عند النقر - :param args: معاملات الدالة - :param full_width: هل يأخذ الزر العرض كاملاً - :param icon: أيقونة لعرضها قبل النص (emoji) - :return: زر مُنسّق - """ - # استخدام مكونات Streamlit فقط بدون HTML - with st.container(): - # إنشاء مساحة تعرض الزر فقط - col1 = st.columns([1]) - - # إضافة الأيقونة للنص إذا تم تزويدها - display_label = f"{icon} {label}" if icon else label - - # إنشاء الزر مباشرة باستخدام Streamlit - clicked = col1[0].button( - display_label, - key=key, - on_click=on_click, - args=args, - use_container_width=full_width - ) - - return clicked - -# وظيفة لإنشاء أزرار أيقونات صغيرة -def icon_button(icon, key, type="primary", on_click=None, args=None, tooltip=""): - """ - إنشاء زر أيقونة صغير - :param icon: الأيقونة (emoji) - :param key: مفتاح الزر الفريد - :param type: نوع التنسيق - :param on_click: الدالة التي سيتم تنفيذها عند النقر - :param args: معاملات الدالة - :param tooltip: تلميح عند تمرير المؤشر فوق الزر - :return: زر أيقونة - """ - # استخدام مكونات Streamlit فقط - with st.container(): - # إضافة تلميح باستخدام Streamlit - if tooltip: - st.caption(tooltip) - - # إنشاء زر الأيقونة - clicked = st.button( - icon, - key=key, - on_click=on_click, - args=args, - help=tooltip # استخدام خاصية help لعرض التلميح - ) - - return clicked - -from modules.pricing.services.standard_pricing import StandardPricing -from modules.pricing.services.unbalanced_pricing import UnbalancedPricing -from modules.pricing.services.local_content_calculator import LocalContentCalculator -from modules.pricing.services.price_prediction import PricePrediction -from modules.pricing.services.construction_cost_calculator import ConstructionCostCalculator -from modules.pricing.services.construction_templates import ConstructionTemplates -from modules.pricing.services.templates_catalog.templates_catalog import TemplatesCatalog -from utils.excel_handler import export_to_excel -from utils.pdf_handler import export_pricing_to_pdf, export_pricing_with_analysis_to_pdf -from utils.helpers import format_number, format_currency, create_directory_if_not_exists - - -class PricingApp: - """وحدة التسعير المتكاملة""" - - def __init__(self): - """تهيئة وحدة التسعير المتكاملة""" - self.pricing_methods = [ - "التسعير القياسي", - "التسعير غير المتزن", - "التسعير التنافسي", - "التسعير الموجه بالربحية" - ] - - # تهيئة خدمات التسعير - self.standard_pricing = StandardPricing() - self.unbalanced_pricing = UnbalancedPricing() - self.local_content = LocalContentCalculator() - self.price_prediction = PricePrediction() - self.construction_calculator = ConstructionCostCalculator() - self.construction_templates = ConstructionTemplates() - self.templates_catalog = TemplatesCatalog(self.construction_templates) - - def render(self): - """عرض واجهة وحدة التسعير""" - - # استخدام مكونات Streamlit مباشرة بدلاً من HTML - st.title("وحدة التسعير المتكاملة") - - tabs = st.tabs([ - "إنشاء تسعير جديد", - "تحليل سعر البند", - "نموذج التسعير الشامل", - "التسعير غير المتزن", - "المحتوى المحلي", - "حاسبة تكاليف البناء", - "كتالوج البنود النموذجية", - "الأدوات المساعدة" - ]) - - with tabs[0]: - self._render_new_pricing_tab() - - with tabs[1]: - self._render_item_analysis_tab() - - with tabs[2]: - self._render_comprehensive_pricing_tab() - - with tabs[3]: - self._render_unbalanced_pricing_tab() - - with tabs[4]: - self._render_local_content_tab() - - with tabs[5]: - self._render_construction_calculator_tab() - - with tabs[6]: - self._render_templates_catalog_tab() - - with tabs[7]: - self._render_utilities_tab() - - def _render_templates_catalog_tab(self): - """عرض تبويب كتالوج البنود النموذجية""" - - st.markdown("### كتالوج البنود النموذجية") - - # شرح كتالوج البنود النموذجية - with st.expander("دليل استخدام كتالوج البنود النموذجية", expanded=False): - st.markdown(""" - **كتالوج البنود النموذجية** هو مكتبة شاملة من البنود الجاهزة لمختلف أنواع الأعمال الإنشائية (خرسانة، حديد، عزل، تشطيبات، إلخ). - - ### مميزات الكتالوج: - - تفاصيل دقيقة للمواد والعمالة والمعدات المطلوبة لكل بند. - - تحليل تكلفة تفصيلي يمكن استخدامه مباشرة في عروض الأسعار. - - ربط مباشر مع حاسبة تكاليف البناء وحاسبة الأسعار. - - ### كيفية الاستخدام: - - استخدام البنود النموذجية مباشرة في مشاريعك. - - تعديل البنود النموذجية لتناسب متطلبات المشروع. - - إضافة بنود جديدة إلى الكتالوج للاستخدام المستقبلي. - """) - - # عرض الكتالوج باستخدام مكون TemplatesCatalog - self.templates_catalog.render() - - def _render_item_analysis_tab(self): - """عرض تبويب تحليل سعر البند""" - - st.markdown("### تحليل سعر البند") - - # التحقق من وجود تسعير حالي - if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: - st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") - return - - # اختيار البند للتحليل - if 'current_pricing' in st.session_state and st.session_state.current_pricing is not None: - items = st.session_state.current_pricing['items'] - item_options = items['رقم البند'].tolist() - selected_item = st.selectbox("اختر البند للتحليل", item_options, key="item_analysis_selector") - - if selected_item: - item_data = items[items['رقم البند'] == selected_item].iloc[0] - - st.markdown(f"### تحليل البند: {selected_item}") - st.markdown(f"**وصف البند**: {item_data['وصف البند']}") - st.markdown(f"**الوحدة**: {item_data['الوحدة']}") - st.markdown(f"**الكمية**: {item_data['الكمية']}") - st.markdown(f"**سعر الوحدة**: {item_data['سعر الوحدة']:,.2f} ريال") - - # تحليل مكونات السعر - st.markdown("### تحليل مكونات السعر") - - # عناصر التكلفة الافتراضية - cost_components = { - 'المواد': 0.6, # 60% من التكلفة - 'العمالة': 0.25, # 25% من التكلفة - 'المعدات': 0.1, # 10% من التكلفة - 'نفقات عامة': 0.05 # 5% من التكلفة - } - - # حساب تكلفة كل عنصر - unit_price = item_data['سعر الوحدة'] - component_values = {k: v * unit_price for k, v in cost_components.items()} - - # عرض مكونات التكلفة في جدول - components_df = pd.DataFrame({ - 'العنصر': component_values.keys(), - 'نسبة من التكلفة': [f"{v*100:.1f}%" for v in cost_components.values()], - 'القيمة (ريال)': [f"{v:,.2f}" for v in component_values.values()] - }) - - st.table(components_df) - - # رسم بياني لمكونات التكلفة - fig = px.pie( - names=list(component_values.keys()), - values=list(component_values.values()), - title='توزيع مكونات التكلفة' - ) - - st.plotly_chart(fig) - - # تحليل تاريخي للأسعار - st.markdown("### تحليل تاريخي للأسعار") - - # بيانات تاريخية افتراضية - historical_data = { - 'التاريخ': ['2020-01', '2020-07', '2021-01', '2021-07', '2022-01', '2022-07', '2023-01', '2023-07'], - 'السعر': [ - unit_price * 0.7, - unit_price * 0.75, - unit_price * 0.8, - unit_price * 0.85, - unit_price * 0.9, - unit_price * 0.95, - unit_price, - unit_price * 1.05 - ] - } - - hist_df = pd.DataFrame(historical_data) - - # رسم بياني للتحليل التاريخي - fig = px.line( - hist_df, - x='التاريخ', - y='السعر', - title='تطور سعر الوحدة عبر الزمن', - markers=True - ) - - st.plotly_chart(fig) - - # المقارنة مع الأسعار المرجعية - st.markdown("### المقارنة مع الأسعار المرجعية") - - # بيانات مرجعية افتراضية - reference_data = { - 'المصدر': ['قاعدة البيانات الداخلية', 'دليل الأسعار الاسترشادي', 'متوسط أسعار السوق', 'أسعار المشاريع المماثلة'], - 'السعر المرجعي': [ - unit_price * 0.95, - unit_price * 1.05, - unit_price * 1.1, - unit_price * 0.9 - ] - } - - ref_df = pd.DataFrame(reference_data) - ref_df['الفرق عن السعر الحالي'] = ref_df['السعر المرجعي'] - unit_price - ref_df['نسبة الفرق'] = (ref_df['الفرق عن السعر الحالي'] / unit_price * 100).round(2).astype(str) + '%' - - st.table(ref_df) - - def _render_new_pricing_tab(self): - """عرض تبويب إنشاء تسعير جديد""" - - st.markdown("### إنشاء تسعير جديد") - - col1, col2 = st.columns(2) - - with col1: - tender_name = st.text_input("اسم المناقصة", key="tender_name_input") - client = st.text_input("الجهة المالكة", key="client_input") - pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods, key="pricing_method_selector") - - with col2: - tender_number = st.text_input("رقم المناقصة", key="tender_number_input") - location = st.text_input("الموقع", key="location_input") - submission_date = st.date_input("تاريخ التقديم", key="submission_date_input") - - # خيارات بيانات البنود - st.markdown("### بيانات البنود") - - data_source = st.radio( - "مصدر بيانات البنود", - ["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات", "استيراد من وحدة المشاريع"], - key="data_source_radio" - ) - - if data_source == "إدخال يدوي": - # ضبط CSS لتحسين ظهور الواجهة العربية - st.markdown(""" - - """, unsafe_allow_html=True) - - # تهيئة قائمة الوحدات المتاحة - unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"] - - # إنشاء بيانات افتراضية إذا لم تكن موجودة - if 'manual_items' not in st.session_state: - # إنشاء DataFrame فارغ - manual_items = pd.DataFrame(columns=[ - 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي' - ]) - - # إضافة بضعة صفوف افتراضية - default_items = pd.DataFrame({ - 'رقم البند': ["A1", "A2", "A3", "A4", "A5"], - 'وصف البند': [ - "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", - "توريد وتركيب حديد التسليح للأساسات", - "أعمال العزل المائي للأساسات", - "أعمال الردم والدك للأساسات", - "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة" - ], - 'الوحدة': ["م3", "طن", "م2", "م3", "م3"], - 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0], - 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0], - 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0] - }) - - manual_items = pd.concat([manual_items, default_items]) - st.session_state.manual_items = manual_items - - # عرض واجهة إدخال البنود - st.markdown("### إدخال تفاصيل البنود") - - # التحقق من استخدام طريقة الإدخال البسيطة - use_simple_input = st.checkbox("استخدام طريقة الإدخال البسيطة", value=True, key="use_simple_input_checkbox") - - if use_simple_input: - # عرض البنود الحالية كجدول للعرض فقط - st.markdown("### جدول البنود الحالية") - st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True) - - # إضافة بند جديد - st.markdown("### إضافة بند جديد") - col1, col2 = st.columns(2) - - with col1: - new_id = st.text_input("رقم البند", value=f"A{len(st.session_state.manual_items)+1}", key="new_item_id") - new_desc = st.text_area("وصف البند", value="", key="new_item_description") - - with col2: - new_unit = st.selectbox("الوحدة", options=unit_options, key="new_item_unit") - new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f", key="new_item_qty") - new_price = st.number_input("سعر الوحدة", value=0.0, min_value=0.0, format="%.2f", key="new_item_price") - - new_total = new_qty * new_price - st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال") - - if st.button("إضافة البند", key="add_item_button"): - # التحقق من صحة البيانات - if new_id and new_desc and new_qty > 0: - # إنشاء صف جديد - new_row = pd.DataFrame({ - 'رقم البند': [new_id], - 'وصف البند': [new_desc], - 'الوحدة': [new_unit], - 'الكمية': [float(new_qty)], - 'سعر الوحدة': [float(new_price)], - 'الإجمالي': [float(new_total)] - }) - - # إضافة الصف إلى DataFrame - st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True) - st.success("تم إضافة البند بنجاح!") - st.rerun() - else: - st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.") - - # تعديل البنود الحالية - st.markdown("### تعديل البنود الحالية") - - # تحديد البند المراد تعديله - item_to_edit = st.selectbox( - "اختر البند للتعديل", - options=st.session_state.manual_items['رقم البند'].tolist(), - format_func=lambda x: f"{x}: {st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == x]['وصف البند'].values[0][:30]}...", - key="item_to_edit_selector" - ) - - if item_to_edit: - # الحصول على مؤشر الصف للبند المحدد - idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == item_to_edit].index[0] - row = st.session_state.manual_items.loc[idx] - - # إنشاء نموذج تعديل - col1, col2 = st.columns(2) - - with col1: - edited_id = st.text_input("رقم البند (تعديل)", value=row['رقم البند'], key="edit_id") - edited_desc = st.text_area("وصف البند (تعديل)", value=row['وصف البند'], key="edit_desc") - - with col2: - edited_unit = st.selectbox( - "الوحدة (تعديل)", - options=unit_options, - index=unit_options.index(row['الوحدة']) if row['الوحدة'] in unit_options else 0, - key="edit_unit" - ) - edited_qty = st.number_input("الكمية (تعديل)", value=float(row['الكمية']), min_value=0.0, format="%.2f", key="edit_qty") - edited_price = st.number_input("سعر الوحدة (تعديل)", value=float(row['سعر الوحدة']), min_value=0.0, format="%.2f", key="edit_price") - - edited_total = edited_qty * edited_price - st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال") - - col1, col2 = st.columns(2) - with col1: - if st.button("حفظ التعديلات", key="save_edit_button"): - # تحديث البند - st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id - st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc - st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit - st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty - st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price - st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total - - st.success("تم تحديث البند بنجاح!") - st.rerun() - - with col2: - if st.button("حذف هذا البند", key="delete_item_button"): - st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True) - st.warning("تم حذف البند!") - st.rerun() - - # المجموع الكلي - total = st.session_state.manual_items['الإجمالي'].sum() - st.metric("المجموع الكلي", f"{total:,.2f} ريال") - - # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية - edited_items = st.session_state.manual_items.copy() - - else: - # عرض رسالة توضح أن طريقة الإدخال البسيطة هي الأفضل - st.warning("لتجنب مشاكل عدم التوافق في أنواع البيانات، يُفضل استخدام طريقة الإدخال البسيطة.") - - # محاولة استخدام المحرر القياسي مع معالجة الأخطاء - try: - # تحويل البيانات إلى الأنواع المناسبة - for col in st.session_state.manual_items.columns: - if col in ['رقم البند', 'وصف البند', 'الوحدة']: - st.session_state.manual_items[col] = st.session_state.manual_items[col].astype(str) - - # عرض المحرر (للقراءة فقط) - st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True) - - # إنشاء نظام تعديل منفصل - st.markdown("### تعديل أسعار الوحدات") - - for idx, row in st.session_state.manual_items.iterrows(): - col1, col2 = st.columns([3, 1]) - - with col1: - st.text(f"{row['رقم البند']}: {row['وصف البند'][:50]}") - - with col2: - price = st.number_input( - f"سعر الوحدة ({row['الوحدة']})", - value=float(row['سعر الوحدة']), - min_value=0.0, - key=f"price_{idx}" - ) - - # تحديث السعر والإجمالي - st.session_state.manual_items.at[idx, 'سعر الوحدة'] = price - st.session_state.manual_items.at[idx, 'الإجمالي'] = price * row['الكمية'] - - # المجموع الكلي - total = st.session_state.manual_items['الإجمالي'].sum() - st.metric("المجموع الكلي", f"{total:,.2f} ريال") - - # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية - edited_items = st.session_state.manual_items.copy() - - except Exception as e: - st.error(f"حدث خطأ: {str(e)}") - st.info("يرجى استخدام طريقة الإدخال البسيطة لتجنب هذه المشكلة.") - - elif data_source == "استيراد من Excel": - uploaded_file = st.file_uploader("رفع ملف Excel", type=["xlsx", "xls"]) - - if uploaded_file is not None: - st.success("تم رفع الملف بنجاح") - # محاكاة قراءة الملف - st.markdown("### معاينة البيانات المستوردة") - - # إنشاء بيانات افتراضية - import_items = pd.DataFrame({ - 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"], - 'وصف البند': [ - "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", - "توريد وتركيب حديد التسليح للأساسات", - "أعمال العزل المائي للأساسات", - "أعمال الردم والدك للأساسات", - "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", - "توريد وتركيب حديد التسليح للأعمدة", - "أعمال البلوك للجدران" - ], - 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"], - 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0], - 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }) - - st.dataframe(import_items) - - if st.button("استيراد البيانات", key="import_excel_button"): - st.session_state.manual_items = import_items.copy() - st.session_state.manual_items_modified = True - st.success("تم استيراد البيانات بنجاح!") - st.rerun() - - elif data_source == "استيراد من وحدة تحليل المستندات": - available_documents = [ - "كراسة شروط مشروع توسعة مستشفى الملك فهد", - "جدول كميات صيانة محطات المياه", - "مخططات إنشاء مدرسة ثانوية" - ] - - selected_doc = st.selectbox("اختر المستند", available_documents, key="document_selector") - - if styled_button("استيراد البيانات من تحليل المستند", key="import_doc_analysis_button", type="info", icon="📄", full_width=True): - # محاكاة استيراد البيانات - with st.spinner("جاري استيراد البيانات..."): - time.sleep(2) - - # إنشاء بيانات افتراضية - doc_items = pd.DataFrame({ - 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"], - 'وصف البند': [ - "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", - "توريد وتركيب حديد التسليح للأساسات", - "أعمال العزل المائي للأساسات", - "أعمال الردم والدك للأساسات", - "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", - "توريد وتركيب حديد التسليح للأعمدة", - "أعمال البلوك للجدران" - ], - 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"], - 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0], - 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }) - - st.session_state.manual_items = doc_items.copy() - st.success("تم استيراد البيانات من تحليل المستند بنجاح!") - st.dataframe(doc_items) - - elif data_source == "استيراد من وحدة المشاريع": - # قائمة المشاريع المتاحة للاستيراد منها - available_projects = [ - "مشروع تطوير طريق الملك عبدالعزيز", - "مشروع إنشاء محطة تحلية المياه بالجبيل", - "مشروع توسعة مستشفى الملك فهد", - "مشروع إنشاء مجمع سكني بالرياض" - ] - - selected_project = st.selectbox("اختر المشروع", available_projects, key="project_selector") - - if styled_button("استيراد البيانات من المشروع", key="import_project_data_button", type="primary", icon="🏗️", full_width=True): - # محاكاة استيراد البيانات من المشروع - with st.spinner("جاري استيراد بيانات المشروع..."): - time.sleep(1.5) - - # إنشاء بيانات مشروع افتراضية بناءً على المشروع المختار - if selected_project == "مشروع تطوير طريق الملك عبدالعزيز": - project_items = pd.DataFrame({ - 'رقم البند': ["R1", "R2", "R3", "R4", "R5"], - 'وصف البند': [ - "أعمال الحفر والردم للطريق", - "توريد وتنفيذ طبقة الأساس", - "طبقة الأسفلت الأولى", - "طبقة الأسفلت النهائية", - "أعمال الدهانات والعلامات" - ], - 'الوحدة': ["م3", "م2", "م2", "م2", "م.ط"], - 'الكمية': [5000.0, 8000.0, 8000.0, 8000.0, 4000.0], - 'سعر الوحدة': [45.0, 120.0, 85.0, 95.0, 25.0], - 'الإجمالي': [225000.0, 960000.0, 680000.0, 760000.0, 100000.0] - }) - elif selected_project == "مشروع إنشاء محطة تحلية المياه بالجبيل": - project_items = pd.DataFrame({ - 'رقم البند': ["W1", "W2", "W3", "W4", "W5"], - 'وصف البند': [ - "أعمال الخرسانة المسلحة للخزانات", - "توريد وتركيب معدات التحلية", - "أعمال التمديدات والأنابيب", - "تشطيبات المباني الإدارية", - "أعمال الكهرباء والتحكم" - ], - 'الوحدة': ["م3", "قطعة", "م.ط", "م2", "مقطوعية"], - 'الكمية': [1800.0, 12.0, 5000.0, 1200.0, 1.0], - 'سعر الوحدة': [1200.0, 250000.0, 850.0, 750.0, 1500000.0], - 'الإجمالي': [2160000.0, 3000000.0, 4250000.0, 900000.0, 1500000.0] - }) - elif selected_project == "مشروع توسعة مستشفى الملك فهد": - project_items = pd.DataFrame({ - 'رقم البند': ["H1", "H2", "H3", "H4", "H5", "H6"], - 'وصف البند': [ - "أعمال الهيكل الخرساني", - "أعمال البناء والجدران", - "التشطيبات الداخلية", - "الأنظمة الكهربائية والميكانيكية", - "تجهيزات طبية", - "أعمال الموقع العام" - ], - 'الوحدة': ["م3", "م2", "م2", "غرفة", "قطعة", "م2"], - 'الكمية': [2800.0, 4500.0, 7500.0, 120.0, 45.0, 3000.0], - 'سعر الوحدة': [1500.0, 650.0, 1200.0, 35000.0, 80000.0, 350.0], - 'الإجمالي': [4200000.0, 2925000.0, 9000000.0, 4200000.0, 3600000.0, 1050000.0] - }) - else: # مشروع إنشاء مجمع سكني بالرياض - project_items = pd.DataFrame({ - 'رقم البند': ["B1", "B2", "B3", "B4", "B5", "B6", "B7"], - 'وصف البند': [ - "الهياكل الخرسانية للفلل", - "الجدران والقواطع الداخلية", - "التشطيبات الخارجية", - "التشطيبات الداخلية", - "أعمال الكهرباء والسباكة", - "الملحقات والحدائق", - "البنية التحتية للموقع" - ], - 'الوحدة': ["م3", "م2", "م2", "م2", "فيلا", "م2", "مقطوعية"], - 'الكمية': [3500.0, 12000.0, 8000.0, 18000.0, 25.0, 5000.0, 1.0], - 'سعر الوحدة': [1350.0, 450.0, 380.0, 750.0, 85000.0, 280.0, 2500000.0], - 'الإجمالي': [4725000.0, 5400000.0, 3040000.0, 13500000.0, 2125000.0, 1400000.0, 2500000.0] - }) - - st.session_state.manual_items = project_items.copy() - st.success(f"تم استيراد بيانات المشروع '{selected_project}' بنجاح!") - st.dataframe(project_items) - - # زر بدء التسعير - if styled_button("بدء التسعير", key="start_pricing_button", type="success", icon="✅", full_width=True): - # تحقق من صحة البيانات - if 'manual_items' in st.session_state and not st.session_state.manual_items.empty: - # التأكد من حساب الإجمالي قبل الحفظ - st.session_state.manual_items['الإجمالي'] = st.session_state.manual_items['الكمية'] * st.session_state.manual_items['سعر الوحدة'] - - # حفظ بيانات التسعير الحالي - st.session_state.current_pricing = { - 'name': tender_name, - 'number': tender_number, - 'client': client, - 'location': location, - 'method': pricing_method, - 'submission_date': submission_date, - 'items': st.session_state.manual_items.copy(), - 'status': 'جديد', - 'created_at': datetime.now() - } - - # الانتقال إلى تبويب نموذج التسعير الشامل - st.success("تم إنشاء التسعير بنجاح! يمكنك الانتقال إلى نموذج التسعير الشامل.") - else: - st.error("يرجى إدخال بيانات البنود أولاً.") - - def _render_comprehensive_pricing_tab(self): - """عرض تبويب نموذج التسعير الشامل""" - - st.markdown("### نموذج التسعير الشامل") - - # التحقق من وجود تسعير حالي - if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: - st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") - return - - # عرض معلومات التسعير الحالي - pricing = st.session_state.current_pricing - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("اسم المناقصة", pricing['name']) - st.metric("الجهة المالكة", pricing['client']) - - with col2: - st.metric("رقم المناقصة", pricing['number']) - st.metric("تاريخ التقديم", pricing['submission_date'].strftime("%Y-%m-%d")) - - with col3: - st.metric("طريقة التسعير", pricing['method']) - st.metric("الموقع", pricing['location']) - - # عرض البنود والتسعير - st.markdown("### بنود التسعير") - - items = pricing['items'].copy() - - # إضافة أسعار الوحدة للمحاكاة - if 'سعر الوحدة' in items.columns and (items['سعر الوحدة'] == 0).all(): - items['سعر الوحدة'] = [ - round(random.uniform(1000, 3000), 2), # الخرسانة - round(random.uniform(5000, 7000), 2), # الحديد - round(random.uniform(100, 200), 2), # العزل - round(random.uniform(50, 100), 2), # الردم - round(random.uniform(1200, 3500), 2), # الخرسانة للأعمدة - ] - - if len(items) > 5: - for i in range(5, len(items)): - items.at[i, 'سعر الوحدة'] = round(random.uniform(500, 5000), 2) - - # حساب الإجمالي - items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] - - # عرض البنود - st.dataframe(items, use_container_width=True, hide_index=True) - - - # ✅ التوصية الذكية باستخدام OpenAI - with st.expander("🔍 توليد توصية ذكية باستخدام AI"): - if styled_button("توليد توصية ذكية باستخدام AI", key="gen_ai_recommendation_btn", type="secondary", icon="🤖", full_width=True): - import openai - import os - - # تهيئة عميل OpenAI - استخدام واجهة الإصدار الجديد فقط (1.0.0 وما فوق) - api_key = os.environ.get("ai") - client = openai.OpenAI(api_key=api_key) - - items_df = items.copy() - prompt = f"""قم بتحليل الجدول التالي للبنود في مشروع إنشاء، وقدم توصية ذكية لتحسين التسعير وضمان التوازن المالي. الجدول يحتوي على البنود، الكميات، الأسعار، والإجماليات:\n\n{items_df.to_string(index=False)}\n\nالتوصية:\n""" - - try: - with st.spinner("جاري توليد التوصية..."): - # استخدام واجهة OpenAI الجديدة - response = client.chat.completions.create( - model="gpt-4o", # استخدام أحدث نموذج من OpenAI GPT-4o - messages=[ - {"role": "system", "content": "أنت خبير في تسعير مشاريع البناء والبنية التحتية."}, - {"role": "user", "content": prompt} - ], - temperature=0.4, - max_tokens=500 - ) - - # استخراج محتوى الرسالة من واجهة OpenAI الجديدة - recommendation = response.choices[0].message.content - - st.success("تم توليد التوصية بنجاح!") - st.markdown("#### التوصية الذكية:") - st.info(recommendation) - - except Exception as e: - st.error(f"حدث خطأ أثناء الاتصال بنموذج OpenAI: {e}") - st.info("يجب التأكد من تثبيت أحدث إصدار من مكتبة OpenAI: `pip install openai --upgrade`") - - # واجهة تعديل أسعار الوحدات - st.markdown("### تعديل أسعار الوحدات") - - # تقسيم البنود إلى مجموعتين للعرض - col1, col2 = st.columns(2) - half = len(items) // 2 + len(items) % 2 - - with col1: - for idx in range(half): - if idx < len(items): - row = items.iloc[idx] - price = st.number_input( - f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})", - value=float(row['سعر الوحدة']), - min_value=0.0, - key=f"price1_{idx}" - ) - items.at[idx, 'سعر الوحدة'] = price - items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية'] - - with col2: - for idx in range(half, len(items)): - row = items.iloc[idx] - price = st.number_input( - f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})", - value=float(row['سعر الوحدة']), - min_value=0.0, - key=f"price2_{idx}" - ) - items.at[idx, 'سعر الوحدة'] = price - items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية'] - - # حساب وعرض إجماليات التسعير - total_price = items['الإجمالي'].sum() - - st.markdown("### إجماليات التسعير") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي التكاليف المباشرة", f"{total_price:,.2f} ريال") - - with col2: - overhead_percentage = st.slider("نسبة المصاريف العامة والأرباح (%)", 5, 30, 15) - overhead_value = total_price * overhead_percentage / 100 - st.metric("المصاريف العامة والأرباح", f"{overhead_value:,.2f} ريال") - - with col3: - grand_total = total_price + overhead_value - st.metric("الإجمالي النهائي", f"{grand_total:,.2f} ريال") - - # رسم بياني لتوزيع التكاليف - st.markdown("### تحليل التكاليف") - - # حساب النسب المئوية لكل بند - pie_data = items.copy() - pie_data['نسبة من إجمالي التكاليف'] = pie_data['الإجمالي'] / total_price * 100 - - fig = px.pie( - pie_data, - values='نسبة من إجمالي التكاليف', - names='وصف البند', - title='توزيع التكاليف حسب البنود', - hole=0.4 - ) - - st.plotly_chart(fig, use_container_width=True) - - # أزرار العمليات - col1, col2, col3 = st.columns(3) - - with col1: - if styled_button("حفظ التسعير", key="save_comprehensive_pricing_button", type="primary", icon="💾"): - # تحديث بيانات التسعير الحالي - st.session_state.current_pricing['items'] = items.copy() - st.success("تم حفظ التسعير بنجاح!") - - with col2: - if styled_button("تصدير إلى Excel", key="export_to_excel_button", type="info", icon="📊"): - try: - # إنشاء دليل الصادرات إذا لم يكن موجودًا - export_dir = "exports" - create_directory_if_not_exists(export_dir) - - # تصدير البيانات إلى ملف Excel - file_path = os.path.join(export_dir, f"تسعير_{tender_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx") - export_to_excel(items, file_path, tender_name) - - st.success(f"تم تصدير التسعير إلى Excel بنجاح! مسار الملف: {file_path}") - except Exception as e: - st.error(f"حدث خطأ أثناء تصدير البيانات: {str(e)}") - - if styled_button("تصدير إلى PDF", key="export_to_pdf_button", type="info", icon="📄"): - try: - # إنشاء دليل الصادرات إذا لم يكن موجودًا - export_dir = "exports" - create_directory_if_not_exists(export_dir) - - # تصدير البيانات إلى ملف PDF - file_path = os.path.join(export_dir, f"تسعير_{tender_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf") - - # التحقق من وجود بيانات تحليل الأسعار - if 'items_price_analysis' in st.session_state and st.session_state.items_price_analysis: - # استخراج المعلومات الأساسية للمشروع - project_info = { - 'اسم_المشروع': tender_name, - 'وصف_المشروع': f"مناقصة رقم: {tender_number} - الجهة المالكة: {client} - الموقع: {location}" - } - - # تصدير البيانات مع تحليل الأسعار - export_pricing_with_analysis_to_pdf( - items, - st.session_state.items_price_analysis, - file_path, - title=f"تسعير مناقصة: {tender_name}", - project_info=project_info - ) - else: - # تصدير البيانات بدون تحليل الأسعار - export_pricing_to_pdf( - items, - file_path, - title=f"تسعير مناقصة: {tender_name}", - description=f"مناقصة رقم: {tender_number} - الجهة المالكة: {client} - الموقع: {location}" - ) - - st.success(f"تم تصدير التسعير إلى PDF بنجاح! مسار الملف: {file_path}") - except Exception as e: - st.error(f"حدث خطأ أثناء تصدير البيانات: {str(e)}") - - with col3: - if styled_button("تحليل المخاطر المالية", key="financial_risk_analysis_button", type="warning", icon="⚠️"): - st.success("تم إرسال الطلب إلى وحدة تحليل المخاطر!") - - def _render_unbalanced_pricing_tab(self): - """عرض تبويب التسعير غير المتزن""" - - st.markdown("### التسعير غير المتزن") - - # التحقق من وجود تسعير حالي - if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: - st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") - return - - # شرح التسعير غير المتزن - with st.expander("ما هو التسعير غير المتزن؟", expanded=False): - st.markdown(""" - **التسعير غير المتزن** هو استراتيجية تسعير تقوم على توزيع التكاليف بين بنود المناقصة بشكل غير متساوٍ، مع الحفاظ على إجمالي قيمة العطاء. - - ### استراتيجيات التسعير غير المتزن: - - 1. **التحميل الأمامي (Front Loading)**: زيادة أسعار البنود المبكرة في المشروع للحصول على تدفق نقدي أفضل في بداية المشروع. - 2. **التحميل الخلفي (Back Loading)**: زيادة أسعار البنود المتأخرة في المشروع. - 3. **تحميل البنود المؤكدة**: زيادة أسعار البنود التي من المؤكد تنفيذها بالكميات المحددة. - 4. **تخفيض أسعار البنود المحتملة**: تخفيض أسعار البنود التي قد تزيد كمياتها أثناء التنفيذ. - - ### مزايا التسعير غير المتزن: - - - تحسين التدفق النقدي للمشروع. - - تعظيم الربحية في حالة التغييرات والأوامر التغييرية. - - زيادة فرص الفوز بالمناقصة. - - ### مخاطر التسعير غير المتزن: - - - قد يتم رفض العطاء إذا كان عدم التوازن واضحاً. - - قد تتأثر السمعة سلباً إذا تم استخدامه بشكل مفرط. - - قد يؤدي إلى خسائر إذا لم يتم تنفيذ البنود ذات الأسعار العالية. - """) - - # عرض بنود التسعير الحالي - items = st.session_state.current_pricing['items'].copy() - - # إضافة عمود إستراتيجية التسعير - if 'إستراتيجية التسعير' not in items.columns: - items['إستراتيجية التسعير'] = 'متوازن' - - st.markdown("### إستراتيجية التسعير غير المتزن") - - # اختيار الإستراتيجية - strategy = st.selectbox( - "اختر إستراتيجية التسعير", - [ - "تحميل أمامي (Front Loading)", - "تحميل البنود المؤكدة", - "تخفيض البنود المحتمل زيادتها", - "إستراتيجية مخصصة" - ], - key="pricing_strategy_selector" - ) - - # تطبيق الإستراتيجية المختارة - if strategy == "تحميل أمامي (Front Loading)": - # محاكاة تحميل أمامي - items_count = len(items) - early_items = items.iloc[:items_count//3].index - middle_items = items.iloc[items_count//3:2*items_count//3].index - late_items = items.iloc[2*items_count//3:].index - - # تطبيق الزيادة والنقصان - for idx in early_items: - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.3 # زيادة 30% - items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - for idx in middle_items: - items.at[idx, 'إستراتيجية التسعير'] = 'متوازن' - - for idx in late_items: - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30% - items.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - elif strategy == "تحميل البنود المؤكدة": - # محاكاة - اعتبار بعض البنود مؤكدة - confirmed_items = [0, 2, 4] # الأصفار-مستندة - variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items] - - # تطبيق الزيادة والنقصان - for idx in confirmed_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 # زيادة 25% - items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - for idx in variable_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 # نقص 15% - items.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - elif strategy == "تخفيض البنود المحتمل زيادتها": - # محاكاة - اعتبار بعض البنود محتمل زيادتها - variable_items = [1, 3] # الأصفار-مستندة - other_items = [idx for idx in range(len(items)) if idx not in variable_items] - - # تطبيق الزيادة والنقصان - for idx in variable_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30% - items.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - for idx in other_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 # زيادة 15% - items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - else: # إستراتيجية مخصصة - st.markdown("### تعديل أسعار البنود يدوياً") - st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً من خلال النموذج أدناه.") - - # إضافة واجهة لتعديل الأسعار يدوياً - if 'edit_items' not in st.session_state: - st.session_state.edit_items = items.copy() - - # عرض نموذج التعديل لكل بند - for index, row in items.iterrows(): - with st.container(): - col1, col2, col3, col4 = st.columns([3, 1, 1, 1]) - - with col1: - st.markdown(f"**البند:** {row['وصف البند']}") - - with col2: - original_price = st.session_state.current_pricing['items'].iloc[index]['سعر الوحدة'] - new_price = st.number_input( - "سعر الوحدة الجديد", - min_value=0.01, - value=float(row['سعر الوحدة']), - key=f"price_{index}" - ) - items.at[index, 'سعر الوحدة'] = new_price - - with col3: - percent_change = ((new_price - original_price) / original_price) * 100 - st.metric( - "نسبة التغيير", - f"{percent_change:.1f}%", - delta=f"{new_price - original_price:.2f}" - ) - - with col4: - strategy_options = ['متوازن', 'زيادة', 'نقص'] - current_strategy = row['إستراتيجية التسعير'] - strategy_index = strategy_options.index(current_strategy) if current_strategy in strategy_options else 0 - - new_strategy = st.selectbox( - "الإستراتيجية", - options=strategy_options, - index=strategy_index, - key=f"strategy_{index}" - ) - items.at[index, 'إستراتيجية التسعير'] = new_strategy - - st.markdown("---") - - # حساب الإجمالي بعد التعديل - items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] - - # تعيين ألوان للإستراتيجيات وتنسيق الجدول بشكل متقدم - def highlight_row(row): - strategy = row['إستراتيجية التسعير'] - styles = [''] * len(row) - - # تطبيق لون خلفية لكل صف حسب الإستراتيجية - if strategy == 'زيادة': - background = 'linear-gradient(90deg, rgba(168, 230, 207, 0.3), rgba(168, 230, 207, 0.1))' - text_color = '#1F7A8C' - elif strategy == 'نقص': - background = 'linear-gradient(90deg, rgba(255, 154, 162, 0.3), rgba(255, 154, 162, 0.1))' - text_color = '#9D2A45' - else: - background = 'linear-gradient(90deg, rgba(220, 237, 255, 0.3), rgba(220, 237, 255, 0.1))' - text_color = '#555555' - - # تطبيق النمط على جميع الخلايا في الصف - for i in range(len(styles)): - styles[i] = f'background: {background}; color: {text_color}; border-bottom: 1px solid #ddd;' - - # تطبيق نمط خاص على خلية الإستراتيجية - if strategy == 'زيادة': - styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #a8e6cf; color: #007263; font-weight: bold; border-radius: 5px; text-align: center;' - elif strategy == 'نقص': - styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #ff9aa2; color: #9D2A45; font-weight: bold; border-radius: 5px; text-align: center;' - else: - styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #dceeff; color: #555555; font-weight: bold; border-radius: 5px; text-align: center;' - - # تنسيق عمود السعر - price_idx = list(row.index).index('سعر الوحدة') - styles[price_idx] = styles[price_idx] + 'font-weight: bold;' - - # تنسيق عمود الإجمالي - total_idx = list(row.index).index('الإجمالي') - styles[total_idx] = styles[total_idx] + 'font-weight: bold;' - - return styles - - # عرض الجدول مع تنسيق متقدم - st.subheader("بنود التسعير غير المتزن") - - # تطبيق التنسيق على الجدول - styled_items = items.style.apply(highlight_row, axis=1) - - # تنسيق تنسيق الأرقام - styled_items = styled_items.format({ - 'الكمية': '{:,.2f}', - 'سعر الوحدة': '{:,.2f}', - 'الإجمالي': '{:,.2f}' - }) - - st.dataframe(styled_items, use_container_width=True, height=None) - - # المقارنة بين التسعير المتوازن وغير المتوازن - st.subheader("مقارنة التسعير المتوازن وغير المتوازن") - - original_items = st.session_state.current_pricing['items'].copy() - original_total = original_items['الإجمالي'].sum() - unbalanced_total = items['الإجمالي'].sum() - - # عرض بطاقات المقارنة بتصميم متقدم - st.markdown(""" - - """, unsafe_allow_html=True) - - col1, col2, col3 = st.columns(3) - - with col1: - st.markdown(""" -
-
إجمالي التسعير المتوازن
-
{:,.2f} ريال
-
التسعير الأصلي
-
- """.format(original_total), unsafe_allow_html=True) - - with col2: - st.markdown(""" -
-
إجمالي التسعير غير المتوازن
-
{:,.2f} ريال
-
بعد إعادة توزيع الأسعار
-
- """.format( - unbalanced_total, - "positive-delta" if unbalanced_total > original_total else "negative-delta" if unbalanced_total < original_total else "neutral-delta" - ), unsafe_allow_html=True) - - with col3: - diff = unbalanced_total - original_total - delta_percent = diff/original_total*100 if original_total > 0 else 0 - - st.markdown(""" -
-
الفرق بين التسعيرين
-
{:,.2f} ريال
-
نسبة الفرق: {:+.1f}%
-
- """.format( - diff, - "positive-delta" if diff > 0 else "negative-delta" if diff < 0 else "neutral-delta", - delta_percent - ), unsafe_allow_html=True) - - # المعايرة للحفاظ على إجمالي التسعير - if abs(diff) > 1: # إذا كان هناك فرق كبير - if styled_button("معايرة الأسعار للحفاظ على إجمالي التسعير", key="calibrate_prices_button", type="primary", icon="⚖️", full_width=True): - # تعديل الأسعار للحفاظ على إجمالي التكلفة - adjustment_factor = original_total / unbalanced_total - items['سعر الوحدة'] = items['سعر الوحدة'] * adjustment_factor - items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] - - st.success(f"تم تعديل الأسعار للحفاظ على إجمالي التسعير الأصلي ({original_total:,.2f} ريال)") - st.dataframe(items, use_container_width=True) - - # رسم بياني للمقارنة - st.subheader("تحليل بصري للتسعير غير المتوازن") - - # إعداد البيانات للرسم البياني - chart_data = pd.DataFrame({ - 'وصف البند': original_items['وصف البند'], - 'التسعير المتوازن': original_items['الإجمالي'], - 'التسعير غير المتوازن': items['الإجمالي'] - }) - - # إضافة عمود للنسبة المئوية للتغيير - chart_data['نسبة التغيير'] = (chart_data['التسعير غير المتوازن'] - chart_data['التسعير المتوازن']) / chart_data['التسعير المتوازن'] * 100 - - # تحديد لون الأعمدة بناءً على نسبة التغيير - bar_colors = [] - for change in chart_data['نسبة التغيير']: - if change > 5: # زيادة كبيرة - bar_colors.append('#1F7A8C') # أزرق مخضر - elif change > 0: # زيادة صغيرة - bar_colors.append('#81B29A') # أخضر فاتح - elif change > -5: # نقص صغير - bar_colors.append('#F2CC8F') # أصفر - else: # نقص كبير - bar_colors.append('#E07A5F') # أحمر - - # التبويب بين مخططات مختلفة للمقارنة - chart_tabs = st.tabs(["مخطط شريطي", "مخطط مقارنة", "مخطط نسبة التغيير"]) - - with chart_tabs[0]: # رسم بياني شريطي - # رسم بياني شريطي للمقارنة - fig = go.Figure() - - fig.add_trace(go.Bar( - x=chart_data['وصف البند'], - y=chart_data['التسعير المتوازن'], - name='التسعير المتوازن', - marker_color='rgba(55, 83, 109, 0.7)' - )) - - fig.add_trace(go.Bar( - x=chart_data['وصف البند'], - y=chart_data['التسعير غير المتوازن'], - name='التسعير غير المتوازن', - marker_color=bar_colors - )) - - fig.update_layout( - title='مقارنة بين التسعير المتوازن وغير المتوازن', - xaxis_tickfont_size=14, - yaxis=dict( - title='الإجمالي (ريال)', - titlefont_size=16, - tickfont_size=14, - ), - legend=dict( - x=0.01, - y=0.99, - bgcolor='rgba(255, 255, 255, 0.8)', - bordercolor='rgba(0, 0, 0, 0.1)', - borderwidth=1 - ), - barmode='group', - bargap=0.15, - bargroupgap=0.1, - plot_bgcolor='rgba(240, 249, 255, 0.5)', - margin=dict(t=50, b=50, l=20, r=20) - ) - - st.plotly_chart(fig, use_container_width=True) - - with chart_tabs[1]: # رسم مقارنة - # رسم مقارنة بين التسعيرين - fig = go.Figure() - - # إضافة خط للتسعير المتوازن - fig.add_trace(go.Scatter( - x=chart_data['وصف البند'], - y=chart_data['التسعير المتوازن'], - name='التسعير المتوازن', - mode='lines+markers', - line=dict(color='rgb(55, 83, 109)', width=3), - marker=dict(size=10, color='rgb(55, 83, 109)') - )) - - # إضافة نقاط للتسعير غير المتوازن - fig.add_trace(go.Scatter( - x=chart_data['وصف البند'], - y=chart_data['التسعير غير المتوازن'], - name='التسعير غير المتوازن', - mode='lines+markers', - line=dict(color='rgb(26, 118, 255)', width=3), - marker=dict( - size=12, - color=bar_colors, - line=dict(width=2, color='white') - ) - )) - - # تحديثات التخطيط - fig.update_layout( - title='مقارنة مرئية بين استراتيجيات التسعير', - xaxis_tickfont_size=14, - yaxis=dict( - title='القيمة الإجمالية (ريال)', - titlefont_size=16, - tickfont_size=14, - gridcolor='rgba(200, 200, 200, 0.2)' - ), - legend=dict( - x=0.01, - y=0.99, - bgcolor='rgba(255, 255, 255, 0.8)', - bordercolor='rgba(0, 0, 0, 0.1)', - borderwidth=1 - ), - plot_bgcolor='rgba(240, 249, 255, 0.5)', - margin=dict(t=50, b=50, l=20, r=20) - ) - - st.plotly_chart(fig, use_container_width=True) - - with chart_tabs[2]: # مخطط نسبة التغيير - # مخطط للنسبة المئوية للتغيير - fig = go.Figure() - - # إضافة أعمدة لنسبة التغيير مع ألوان مختلفة حسب القيمة - fig.add_trace(go.Bar( - x=chart_data['وصف البند'], - y=chart_data['نسبة التغيير'], - name='نسبة التغيير', - marker_color=bar_colors, - text=[f"{val:.1f}%" for val in chart_data['نسبة التغيير']], - textposition='auto' - )) - - # إضافة خط أفقي عند الصفر - fig.add_shape( - type="line", - x0=-0.5, - y0=0, - x1=len(chart_data['وصف البند'])-0.5, - y1=0, - line=dict( - color="black", - width=2, - dash="dash", - ) - ) - - # تحديثات التخطيط - fig.update_layout( - title='نسبة التغيير في أسعار البنود (%)', - xaxis_tickfont_size=14, - yaxis=dict( - title='نسبة التغيير (%)', - titlefont_size=16, - tickfont_size=14, - gridcolor='rgba(200, 200, 200, 0.2)', - zeroline=True, - zerolinecolor='black', - zerolinewidth=2 - ), - plot_bgcolor='rgba(240, 249, 255, 0.5)', - margin=dict(t=50, b=50, l=20, r=20) - ) - - st.plotly_chart(fig, use_container_width=True) - - # إضافة جدول مع نسب التغيير - st.markdown("#### جدول مفصل بنسب التغيير") - - # إعداد بيانات الجدول - table_data = chart_data[['وصف البند', 'التسعير المتوازن', 'التسعير غير المتوازن', 'نسبة التغيير']] - - # تنسيق الجدول - def highlight_change(row): - change = row['نسبة التغيير'] - if change > 5: - return ['', '', '', 'background-color: rgba(31, 122, 140, 0.3); color: #1F7A8C; font-weight: bold;'] - elif change > 0: - return ['', '', '', 'background-color: rgba(129, 178, 154, 0.3); color: #2A9D8F; font-weight: bold;'] - elif change > -5: - return ['', '', '', 'background-color: rgba(242, 204, 143, 0.3); color: #BC6C25; font-weight: bold;'] - else: - return ['', '', '', 'background-color: rgba(224, 122, 95, 0.3); color: #AE2012; font-weight: bold;'] - - # تطبيق التنسيق - styled_table = table_data.style.apply(highlight_change, axis=1).format({ - 'التسعير المتوازن': '{:,.2f} ريال', - 'التسعير غير المتوازن': '{:,.2f} ريال', - 'نسبة التغيير': '{:+.1f}%' - }) - - st.dataframe(styled_table, use_container_width=True) - - # قسم لإضافة أو حذف البنود - st.markdown("### إدارة بنود التسعير") - - # إضافة بند جديد - with st.expander("إضافة بند جديد"): - col1, col2, col3, col4 = st.columns([3, 1, 1, 1]) - - with col1: - new_item_desc = st.text_input("وصف البند", placeholder="أدخل وصف البند الجديد") - - with col2: - new_item_unit = st.selectbox( - "الوحدة", - options=["م3", "م2", "متر طولي", "طن", "قطعة", "كجم", "لتر"], - index=0, - key="construction_item_unit" - ) - - with col3: - new_item_qty = st.number_input("الكمية", min_value=0.1, value=1.0, format="%.2f") - - with col4: - new_item_price = st.number_input("سعر الوحدة", min_value=0.1, value=100.0, format="%.2f") - - if st.button("إضافة البند"): - if new_item_desc: - # إنشاء رقم البند الجديد - new_id = f"UB{len(items)+1}" - - # إنشاء صف جديد - new_row = pd.DataFrame({ - 'رقم البند': [new_id], - 'وصف البند': [new_item_desc], - 'الوحدة': [new_item_unit], - 'الكمية': [float(new_item_qty)], - 'سعر الوحدة': [float(new_item_price)], - 'الإجمالي': [float(new_item_qty * new_item_price)], - 'إستراتيجية التسعير': ['متوازن'] - }) - - # إضافة الصف إلى DataFrame - items = pd.concat([items, new_row], ignore_index=True) - - # تحديث الإجمالي - items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] - - st.success(f"تم إضافة البند \"{new_item_desc}\" بنجاح!") - st.rerun() - else: - st.warning("يرجى إدخال وصف للبند") - - # حذف بند - with st.expander("حذف بند"): - if len(items) > 0: - # قائمة بالبنود الحالية - item_options = [f"{row['رقم البند']} - {row['وصف البند']}" for idx, row in items.iterrows()] - selected_item_to_delete = st.selectbox( - "اختر البند المراد حذفه", - options=item_options, - key="item_to_delete_selector" - ) - - # استخراج رقم البند - item_id_to_delete = selected_item_to_delete.split(" - ")[0] - - if st.button("حذف البند", key="delete_item_button_2"): - # البحث عن البند وحذفه - items = items[items['رقم البند'] != item_id_to_delete] - st.success(f"تم حذف البند {item_id_to_delete} بنجاح!") - st.rerun() - else: - st.info("لا توجد بنود لحذفها") - - # أزرار الحفظ والتصدير مع تصميم محسن - st.divider() - st.subheader("حفظ وتصدير البيانات") - - st.markdown(""" - - """, unsafe_allow_html=True) - - col1, col2 = st.columns(2) - - with col1: - # بطاقة حفظ التسعير - st.markdown(""" -
-
💾
-
حفظ التسعير غير المتوازن
-
قم بحفظ التسعير الحالي في المشروع لاستخدامه لاحقاً في التقارير وفي إجمالي التسعير.
-
- """, unsafe_allow_html=True) - - # زر حفظ التسعير غير المتوازن - if st.button("حفظ التسعير غير المتوازن", type="primary", use_container_width=True, key="save_unbalanced_pricing_button"): - st.session_state.current_pricing['items'] = items.copy() - st.session_state.current_pricing['method'] = "التسعير غير المتزن" - st.success("تم حفظ التسعير غير المتوازن بنجاح!") - st.balloons() # إضافة تأثير احتفالي عند الحفظ - - with col2: - # بطاقة تصدير التسعير - st.markdown(""" -
-
📊
-
تصدير البيانات
-
قم بتصدير جدول التسعير الحالي بصيغة CSV لاستخدامه في برامج أخرى مثل Excel.
-
- """, unsafe_allow_html=True) - - # زر تصدير التسعير - export_button = st.button("تجهيز ملف للتصدير", use_container_width=True, key="prepare_export_file_button") - if export_button: - # تحويل البيانات إلى CSV - csv = items.to_csv(index=False) - st.success("تم تجهيز ملف التصدير بنجاح! يمكنك تنزيله الآن.") - # تقديم البيانات للتنزيل - st.download_button( - label="تنزيل ملف CSV", - data=csv, - file_name="unbalanced_pricing.csv", - mime="text/csv", - use_container_width=True - ) - - def _render_construction_calculator_tab(self): - """عرض تبويب حاسبة تكاليف البناء المتكاملة""" - - # استدعاء حاسبة تكاليف البناء المتكاملة الجديدة - from .construction_calculator import render_construction_calculator - - st.markdown("### حاسبة تكاليف البناء المتكاملة") - - # شرح حاسبة تكاليف البناء - with st.expander("دليل استخدام حاسبة تكاليف البناء", expanded=False): - st.markdown(""" - **حاسبة تكاليف البناء المتكاملة** هي أداة تساعد في حساب تكاليف البناء بشكل تفصيلي، مع مراعاة جميع عناصر التكلفة: - - ### مكونات التكلفة: - - المواد الخام - - العمالة - - المعدات - - المصاريف الإدارية - - هامش الربح - - ### كيفية الاستخدام: - 1. اختر إما حساب تكلفة بند واحد أو حساب تكلفة مشروع. - 2. أدخل بيانات المواد والعمالة والمعدات. - 3. حدد نسب المصاريف الإدارية وهامش الربح. - 4. اضبط عوامل التعديل حسب ظروف المشروع. - 5. احصل على تحليل تفصيلي للتكاليف والسعر النهائي. - - ### مميزات الحاسبة: - - قاعدة بيانات مدمجة للأسعار المرجعية للمواد والعمالة. - - تحليل نسب مساهمة كل عنصر في التكلفة. - - إمكانية تعديل عوامل التكلفة حسب الموقع والظروف. - - تصدير النتائج بتنسيقات متعددة. - - الربط مع وحدة التسعير لنقل النتائج مباشرة إلى جدول البنود. - - قاعدة بيانات للبنود النموذجية في أعمال المقاولات. - """) - - render_construction_calculator() - - # شرح حاسبة تكاليف البناء - with st.expander("دليل استخدام حاسبة تكاليف البناء", expanded=False): - st.markdown(""" - **حاسبة تكاليف البناء المتكاملة** هي أداة تساعد في حساب تكاليف البناء بشكل تفصيلي، مع مراعاة جميع عناصر التكلفة: - - ### مكونات التكلفة: - - 1. **المواد الخام**: جميع المواد المستخدمة في البناء مثل الخرسانة، الحديد، الطوب، الأسمنت، وغيرها. - 2. **العمالة**: تكاليف جميع العمالة المطلوبة بمختلف تخصصاتها. - 3. **المعدات**: تكاليف استخدام أو استئجار المعدات اللازمة للمشروع. - 4. **المصاريف الإدارية**: النفقات العامة والإدارية للمشروع (نسبة من التكلفة المباشرة). - 5. **هامش الربح**: نسبة الربح المضافة على التكلفة. - - ### طريقة الاستخدام: - - 1. اختر إما حساب تكلفة بند واحد أو حساب تكلفة مشروع كامل. - 2. أدخل بيانات المواد والعمالة والمعدات المستخدمة. - 3. حدد نسب المصاريف الإدارية وهامش الربح. - 4. اضبط عوامل التعديل حسب ظروف المشروع. - 5. احصل على تحليل تفصيلي للتكاليف والسعر النهائي. - - ### ميزات الحاسبة: - - - قاعدة بيانات مدمجة للأسعار المرجعية للمواد والعمالة والمعدات. - - تحليل نسب مساهمة كل عنصر في التكلفة الإجمالية. - - إمكانية تعديل عوامل التكلفة حسب الموقع والظروف السوقية. - - تصدير النتائج بتنسيقات مختلفة. - - الربط مع وحدة التسعير لنقل النتائج مباشرة إلى جدول التسعير. - - قاعدة بيانات للبنود النموذجية في أعمال المقاولات (مناهل، مواسير، إسفلت، إلخ). - """) - - # تبويبات حاسبة التكاليف - calc_tabs = st.tabs(["حساب تكلفة بند", "حساب تكلفة مشروع", "قوائم الأسعار المرجعية", "كتالوج أعمال المقاولات"]) - - with calc_tabs[0]: # حساب تكلفة بند - st.markdown("#### حساب تكلفة بند بناء") - - # تهيئة بيانات البند الافتراضية إذا لم تكن موجودة - if 'construction_item' not in st.session_state: - st.session_state.construction_item = { - 'وصف_البند': "توريد وصب خرسانة مسلحة للأساسات", - 'الكمية': 25.0, - 'الوحدة': "م3", - 'المواد': [ - {'الاسم': 'خرسانة جاهزة', 'الكمية': 25.0, 'الوحدة': 'م3', 'سعر_الوحدة': 750.0}, - {'الاسم': 'حديد تسليح', 'الكمية': 3.5, 'الوحدة': 'طن', 'سعر_الوحدة': 5500.0} - ], - 'العمالة': [ - {'النوع': 'عامل خرسانة', 'العدد': 6, 'المدة': 1.0, 'سعر_اليوم': 150.0}, - {'النوع': 'حداد مسلح', 'العدد': 4, 'المدة': 3.0, 'سعر_اليوم': 250.0}, - {'النوع': 'نجار مسلح', 'العدد': 4, 'المدة': 3.0, 'سعر_اليوم': 250.0} - ], - 'المعدات': [ - {'النوع': 'مضخة خرسانة', 'العدد': 1.0, 'المدة': 0.5, 'سعر_اليوم': 5000.0}, - {'النوع': 'هزاز خرسانة', 'العدد': 2.0, 'المدة': 1.0, 'سعر_اليوم': 150.0} - ], - 'المصاريف_الإدارية': 0.05, # 5% - 'هامش_الربح': 0.10, # 10% - 'عوامل_التعديل': { - 'location_factor': 1.0, - 'time_factor': 1.0, - 'risk_factor': 1.0, - 'market_factor': 1.0 - } - } - - # نموذج إدخال بيانات البند - col1, col2 = st.columns(2) - - with col1: - st.session_state.construction_item['وصف_البند'] = st.text_area( - "وصف البند", - value=st.session_state.construction_item['وصف_البند'], - key="construction_item_description" - ) - - with col2: - st.session_state.construction_item['الكمية'] = st.number_input( - "الكمية", - value=st.session_state.construction_item['الكمية'], - min_value=0.1, - format="%.2f" - ) - - unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"] - st.session_state.construction_item['الوحدة'] = st.selectbox( - "الوحدة", - options=unit_options, - index=unit_options.index(st.session_state.construction_item['الوحدة']) if st.session_state.construction_item['الوحدة'] in unit_options else 0, - key="construction_item_unit_2" - ) - - # إدخال تفاصيل المواد - st.markdown("#### تفاصيل المواد") - - material_controls = [] - for i, material in enumerate(st.session_state.construction_item['المواد']): - col1, col2, col3, col4, col5 = st.columns([3, 2, 1, 2, 1]) - - with col1: - material_name = st.text_input( - "اسم المادة", - value=material['الاسم'], - key=f"material_name_{i}" - ) - - with col2: - material_qty = st.number_input( - "الكمية", - value=material['الكمية'], - min_value=0.0, - format="%.2f", - key=f"material_qty_{i}" - ) - - with col3: - material_unit = st.selectbox( - "الوحدة", - options=unit_options, - index=unit_options.index(material['الوحدة']) if material['الوحدة'] in unit_options else 0, - key=f"material_unit_{i}" - ) - - with col4: - material_price = st.number_input( - "سعر الوحدة", - value=material['سعر_الوحدة'], - min_value=0.0, - format="%.2f", - key=f"material_price_{i}" - ) - - with col5: - delete_button = st.button("حذف", key=f"delete_material_{i}") - - material_controls.append({ - 'الاسم': material_name, - 'الكمية': material_qty, - 'الوحدة': material_unit, - 'سعر_الوحدة': material_price, - 'delete': delete_button - }) - - # إضافة مادة جديدة - if st.button("إضافة مادة جديدة"): - st.session_state.construction_item['المواد'].append({ - 'الاسم': '', - 'الكمية': 0.0, - 'الوحدة': 'م3', - 'سعر_الوحدة': 0.0 - }) - st.rerun() - - # تحديث أو حذف المواد - new_materials = [] - for i, control in enumerate(material_controls): - if not control['delete']: - new_materials.append({ - 'الاسم': control['الاسم'], - 'الكمية': control['الكمية'], - 'الوحدة': control['الوحدة'], - 'سعر_الوحدة': control['سعر_الوحدة'] - }) - - if len(new_materials) != len(st.session_state.construction_item['المواد']): - st.session_state.construction_item['المواد'] = new_materials - st.rerun() - else: - for i, material in enumerate(new_materials): - st.session_state.construction_item['المواد'][i] = material - - # إدخال تفاصيل العمالة - st.markdown("#### تفاصيل العمالة") - - labor_controls = [] - for i, labor in enumerate(st.session_state.construction_item['العمالة']): - col1, col2, col3, col4, col5 = st.columns([3, 1, 1, 2, 1]) - - with col1: - labor_type = st.text_input( - "نوع العامل", - value=labor['النوع'], - key=f"labor_type_{i}" - ) - - with col2: - labor_count = st.number_input( - "العدد", - value=float(labor['العدد']), - min_value=1.0, - step=1.0, - key=f"labor_count_{i}" - ) - - with col3: - labor_days = st.number_input( - "المدة (أيام)", - value=labor['المدة'], - min_value=0.1, - format="%.1f", - key=f"labor_days_{i}" - ) - - with col4: - labor_daily_rate = st.number_input( - "سعر اليوم", - value=labor['سعر_اليوم'], - min_value=0.0, - format="%.2f", - key=f"labor_daily_rate_{i}" - ) - - with col5: - delete_button = st.button("حذف", key=f"delete_labor_{i}") - - labor_controls.append({ - 'النوع': labor_type, - 'العدد': labor_count, - 'المدة': labor_days, - 'سعر_اليوم': labor_daily_rate, - 'delete': delete_button - }) - - # إضافة عامل جديد - if st.button("إضافة عامل جديد"): - st.session_state.construction_item['العمالة'].append({ - 'النوع': '', - 'العدد': 1.0, - 'المدة': 1.0, - 'سعر_اليوم': 0.0 - }) - st.rerun() - - # تحديث أو حذف العمالة - new_labor = [] - for i, control in enumerate(labor_controls): - if not control['delete']: - new_labor.append({ - 'النوع': control['النوع'], - 'العدد': control['العدد'], - 'المدة': control['المدة'], - 'سعر_اليوم': control['سعر_اليوم'] - }) - - if len(new_labor) != len(st.session_state.construction_item['العمالة']): - st.session_state.construction_item['العمالة'] = new_labor - st.rerun() - else: - for i, labor in enumerate(new_labor): - st.session_state.construction_item['العمالة'][i] = labor - - # إدخال تفاصيل المعدات - st.markdown("#### تفاصيل المعدات") - - equipment_controls = [] - for i, equipment in enumerate(st.session_state.construction_item['المعدات']): - col1, col2, col3, col4, col5 = st.columns([3, 1, 1, 2, 1]) - - with col1: - equip_type = st.text_input( - "نوع المعدة", - value=equipment['النوع'], - key=f"equip_type_{i}" - ) - - with col2: - equip_count = st.number_input( - "العدد", - value=float(equipment['العدد']), - min_value=1.0, - step=1.0, - key=f"equip_count_{i}" - ) - - with col3: - equip_days = st.number_input( - "المدة (أيام)", - value=equipment['المدة'], - min_value=0.1, - format="%.1f", - key=f"equip_days_{i}" - ) - - with col4: - equip_daily_rate = st.number_input( - "سعر اليوم", - value=equipment['سعر_اليوم'], - min_value=0.0, - format="%.2f", - key=f"equip_daily_rate_{i}" - ) - - with col5: - delete_button = st.button("حذف", key=f"delete_equipment_{i}") - - equipment_controls.append({ - 'النوع': equip_type, - 'العدد': equip_count, - 'المدة': equip_days, - 'سعر_اليوم': equip_daily_rate, - 'delete': delete_button - }) - - # إضافة معدة جديدة - if st.button("إضافة معدة جديدة"): - st.session_state.construction_item['المعدات'].append({ - 'النوع': '', - 'العدد': 1.0, - 'المدة': 1.0, - 'سعر_اليوم': 0.0 - }) - st.rerun() - - # تحديث أو حذف المعدات - new_equipment = [] - for i, control in enumerate(equipment_controls): - if not control['delete']: - new_equipment.append({ - 'النوع': control['النوع'], - 'العدد': control['العدد'], - 'المدة': control['المدة'], - 'سعر_اليوم': control['سعر_اليوم'] - }) - - if len(new_equipment) != len(st.session_state.construction_item['المعدات']): - st.session_state.construction_item['المعدات'] = new_equipment - st.rerun() - else: - for i, equipment in enumerate(new_equipment): - st.session_state.construction_item['المعدات'][i] = equipment - - # النسب والعوامل - st.markdown("#### المصاريف الإدارية وهامش الربح") - - col1, col2 = st.columns(2) - - with col1: - st.session_state.construction_item['المصاريف_الإدارية'] = st.slider( - "نسبة المصاريف الإدارية (%)", - min_value=0.0, - max_value=20.0, - value=st.session_state.construction_item['المصاريف_الإدارية'] * 100, - step=0.5 - ) / 100 - - with col2: - st.session_state.construction_item['هامش_الربح'] = st.slider( - "نسبة هامش الربح (%)", - min_value=0.0, - max_value=30.0, - value=st.session_state.construction_item['هامش_الربح'] * 100, - step=0.5 - ) / 100 - - # عوامل التعديل - st.markdown("#### عوامل تعديل التكلفة") - - col1, col2 = st.columns(2) - - with col1: - st.session_state.construction_item['عوامل_التعديل']['location_factor'] = st.slider( - "معامل الموقع", - min_value=0.5, - max_value=2.0, - value=st.session_state.construction_item['عوامل_التعديل']['location_factor'], - step=0.05, - help="يؤثر في التكلفة حسب صعوبة أو سهولة الوصول للموقع وتوفر الخدمات" - ) - - st.session_state.construction_item['عوامل_التعديل']['time_factor'] = st.slider( - "معامل الوقت", - min_value=0.8, - max_value=1.5, - value=st.session_state.construction_item['عوامل_التعديل']['time_factor'], - step=0.05, - help="يؤثر في التكلفة حسب الجدول الزمني للمشروع وضرورة الإسراع في التنفيذ" - ) - - with col2: - st.session_state.construction_item['عوامل_التعديل']['risk_factor'] = st.slider( - "معامل المخاطر", - min_value=1.0, - max_value=1.5, - value=st.session_state.construction_item['عوامل_التعديل']['risk_factor'], - step=0.05, - help="يؤثر في التكلفة حسب المخاطر المتوقعة في المشروع" - ) - - st.session_state.construction_item['عوامل_التعديل']['market_factor'] = st.slider( - "معامل السوق", - min_value=0.8, - max_value=1.3, - value=st.session_state.construction_item['عوامل_التعديل']['market_factor'], - step=0.05, - help="يؤثر في التكلفة حسب حالة السوق الحالية وتغيرات الأسعار" - ) - - # صف أزرار العمليات - col1, col2 = st.columns(2) - - with col1: - # زر حساب التكلفة - if st.button("حساب تكلفة البند", type="primary"): - with st.spinner("جاري حساب التكلفة..."): - # حساب التكلفة باستخدام الحاسبة - item_cost = self.construction_calculator.calculate_item_cost(st.session_state.construction_item) - st.session_state.item_cost_result = item_cost - - with col2: - # زر استيراد بند من كتالوج أعمال المقاولات - if st.button("استيراد بند من الكتالوج"): - # تهيئة session state للاختيار من الكتالوج - if 'show_catalog_selection' not in st.session_state: - st.session_state.show_catalog_selection = True - else: - st.session_state.show_catalog_selection = True - st.rerun() - - # عرض واجهة اختيار البند من الكتالوج - if 'show_catalog_selection' in st.session_state and st.session_state.show_catalog_selection: - st.markdown("#### اختيار بند من كتالوج أعمال المقاولات") - - # الحصول على فئات البنود - categories = self.construction_templates.get_all_templates()['categories'] - category_options = [f"{cat_data['name']}" for cat_id, cat_data in categories.items()] - category_ids = list(categories.keys()) - - selected_category_index = st.selectbox( - "اختر فئة البند", - options=range(len(category_options)), - format_func=lambda i: category_options[i], - key="construction_category_selector" - ) - - selected_category_id = category_ids[selected_category_index] - - # الحصول على البنود في الفئة المحددة - templates = self.construction_templates.get_templates_by_category(selected_category_id) - - if templates: - template_options = [f"{template['name']}" for template in templates] - - selected_template_index = st.selectbox( - "اختر البند", - options=range(len(template_options)), - format_func=lambda i: template_options[i], - key="construction_template_selector" - ) - - selected_template = templates[selected_template_index] - - # عرض تفاصيل البند المحدد - st.markdown(f"**وصف البند**: {selected_template['description']}") - st.markdown(f"**الوحدة**: {selected_template['unit']}") - - if st.button("استخدام هذا البند", type="primary"): - # تحويل البند إلى صيغة الحاسبة - template_id = selected_template['id'] - construction_item = self.construction_templates.convert_template_to_item(template_id) - - # تحديث بيانات البند في session state - st.session_state.construction_item = construction_item - st.session_state.show_catalog_selection = False - st.rerun() - else: - st.info("لا توجد بنود في هذه الفئة") - - if st.button("إلغاء"): - st.session_state.show_catalog_selection = False - st.rerun() - - # عرض نتائج الحساب - if 'item_cost_result' in st.session_state: - st.markdown("### نتائج حساب تكلفة البند") - - result = st.session_state.item_cost_result - - # ملخص البند - st.markdown(f"**البند:** {result['وصف_البند']}") - st.markdown(f"**الكمية:** {result['الكمية']} {result['الوحدة']}") - - # المكونات الرئيسية للتكلفة - col1, col2, col3, col4 = st.columns(4) - - with col1: - st.metric( - "تكلفة المواد", - f"{result['تكاليف_مباشرة']['المواد']['الإجمالي']:,.2f} ريال" - ) - - with col2: - st.metric( - "تكلفة العمالة", - f"{result['تكاليف_مباشرة']['العمالة']['الإجمالي']:,.2f} ريال" - ) - - with col3: - st.metric( - "تكلفة المعدات", - f"{result['تكاليف_مباشرة']['المعدات']['الإجمالي']:,.2f} ريال" - ) - - with col4: - st.metric( - "التكلفة المباشرة", - f"{result['تكاليف_مباشرة']['إجمالي_تكاليف_مباشرة']:,.2f} ريال" - ) - - # تفاصيل المصاريف والربح والسعر النهائي - col1, col2, col3 = st.columns(3) - - with col1: - st.metric( - f"المصاريف الإدارية ({result['مصاريف_إدارية']['نسبة']}%)", - f"{result['مصاريف_إدارية']['قيمة']:,.2f} ريال" - ) - - with col2: - st.metric( - f"هامش الربح ({result['هامش_ربح']['نسبة']}%)", - f"{result['هامش_ربح']['قيمة']:,.2f} ريال" - ) - - with col3: - st.metric( - "التكلفة الإجمالية", - f"{result['التكلفة_الإجمالية']:,.2f} ريال" - ) - - # التكلفة بعد تطبيق عوامل التعديل - adjustment_factor = result['عوامل_التعديل']['المعامل_الإجمالي'] - st.metric( - f"السعر النهائي المعدل (معامل التعديل: {adjustment_factor:.2f})", - f"{result['السعر_المعدل']['إجمالي']:,.2f} ريال", - delta=f"{(adjustment_factor - 1) * 100:.1f}%" - ) - - # سعر الوحدة وزر نقل السعر إلى جدول التسعير - col1, col2 = st.columns([2, 1]) - - with col1: - st.metric( - f"سعر الوحدة ({result['الوحدة']})", - f"{result['السعر_المعدل']['سعر_الوحدة']:,.2f} ريال" - ) - - with col2: - if st.button("نقل السعر إلى جدول التسعير", key="transfer_to_pricing"): - if 'manual_items' not in st.session_state: - st.session_state.manual_items = pd.DataFrame(columns=[ - 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي' - ]) - - # إنشاء رقم البند الجديد - new_id = f"B{len(st.session_state.manual_items)+1}" - - # إنشاء صف جديد - new_row = pd.DataFrame({ - 'رقم البند': [new_id], - 'وصف البند': [result['وصف_البند']], - 'الوحدة': [result['الوحدة']], - 'الكمية': [float(result['الكمية'])], - 'سعر الوحدة': [float(result['السعر_المعدل']['سعر_الوحدة'])], - 'الإجمالي': [float(result['السعر_المعدل']['إجمالي'])] - }) - - # إضافة الصف إلى DataFrame - st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True) - - # إنشاء حالة التسعير الحالي إذا لم تكن موجودة - if 'current_pricing' not in st.session_state: - st.session_state.current_pricing = { - 'name': 'تسعير جديد', - 'method': 'تسعير مستورد من حاسبة تكاليف البناء', - 'items': st.session_state.manual_items - } - else: - # تحديث البنود في التسعير الحالي - st.session_state.current_pricing['items'] = st.session_state.manual_items - - st.success(f"تم نقل البند \"{result['وصف_البند']}\" إلى جدول التسعير بنجاح!") - - # تفاصيل التكلفة - المواد - st.markdown("#### تفاصيل تكلفة المواد") - if len(result['تكاليف_مباشرة']['المواد']['التفاصيل']) > 0: - materials_df = pd.DataFrame(result['تكاليف_مباشرة']['المواد']['التفاصيل']) - st.dataframe(materials_df, use_container_width=True, hide_index=True) - else: - st.info("لا توجد مواد مضافة") - - # تفاصيل التكلفة - العمالة - st.markdown("#### تفاصيل تكلفة العمالة") - if len(result['تكاليف_مباشرة']['العمالة']['التفاصيل']) > 0: - labor_df = pd.DataFrame(result['تكاليف_مباشرة']['العمالة']['التفاصيل']) - st.dataframe(labor_df, use_container_width=True, hide_index=True) - else: - st.info("لا توجد عمالة مضافة") - - # تفاصيل التكلفة - المعدات - st.markdown("#### تفاصيل تكلفة المعدات") - if len(result['تكاليف_مباشرة']['المعدات']['التفاصيل']) > 0: - equipment_df = pd.DataFrame(result['تكاليف_مباشرة']['المعدات']['التفاصيل']) - st.dataframe(equipment_df, use_container_width=True, hide_index=True) - else: - st.info("لا توجد معدات مضافة") - - # رسم بياني لتوزيع التكلفة - st.markdown("#### توزيع مكونات التكلفة") - - cost_components = { - 'المواد': result['تكاليف_مباشرة']['المواد']['الإجمالي'], - 'العمالة': result['تكاليف_مباشرة']['العمالة']['الإجمالي'], - 'المعدات': result['تكاليف_مباشرة']['المعدات']['الإجمالي'], - 'المصاريف الإدارية': result['مصاريف_إدارية']['قيمة'], - 'هامش الربح': result['هامش_ربح']['قيمة'] - } - - colors = ['#2E86C1', '#28B463', '#EB984E', '#8E44AD', '#C0392B'] - - fig = px.pie( - values=list(cost_components.values()), - names=list(cost_components.keys()), - title='توزيع مكونات التكلفة', - color_discrete_sequence=colors - ) - - st.plotly_chart(fig, use_container_width=True) - - with calc_tabs[1]: # حساب تكلفة مشروع - st.markdown("#### حساب تكلفة مشروع كامل") - - # تهيئة بيانات المشروع الافتراضية إذا لم تكن موجودة - if 'construction_project' not in st.session_state: - st.session_state.construction_project = self.construction_calculator.generate_sample_project_data() - - # نموذج إدخال بيانات المشروع - col1, col2 = st.columns(2) - - with col1: - st.session_state.construction_project['اسم_المشروع'] = st.text_input( - "اسم المشروع", - value=st.session_state.construction_project['اسم_المشروع'] - ) - - with col2: - st.session_state.construction_project['وصف_المشروع'] = st.text_area( - "وصف المشروع", - value=st.session_state.construction_project['وصف_المشروع'], - height=100, - key="construction_project_description" - ) - - # النسب والعوامل الإجمالية للمشروع - st.markdown("#### النسب والعوامل الإجمالية للمشروع") - - col1, col2 = st.columns(2) - - with col1: - st.session_state.construction_project['المصاريف_الإدارية'] = st.slider( - "نسبة المصاريف الإدارية الإجمالية (%)", - min_value=0.0, - max_value=20.0, - value=st.session_state.construction_project['المصاريف_الإدارية'] * 100, - step=0.5, - key="project_admin_expenses" - ) / 100 - - with col2: - st.session_state.construction_project['هامش_الربح'] = st.slider( - "نسبة هامش الربح الإجمالي (%)", - min_value=0.0, - max_value=30.0, - value=st.session_state.construction_project['هامش_الربح'] * 100, - step=0.5, - key="project_profit_margin" - ) / 100 - - # عوامل التعديل للمشروع - st.markdown("#### عوامل تعديل التكلفة للمشروع") - - col1, col2 = st.columns(2) - - with col1: - st.session_state.construction_project['عوامل_التعديل']['location_factor'] = st.slider( - "معامل الموقع", - min_value=0.5, - max_value=2.0, - value=st.session_state.construction_project['عوامل_التعديل']['location_factor'], - step=0.05, - help="يؤثر في التكلفة حسب صعوبة أو سهولة الوصول للموقع وتوفر الخدمات", - key="project_location_factor" - ) - - st.session_state.construction_project['عوامل_التعديل']['time_factor'] = st.slider( - "معامل الوقت", - min_value=0.8, - max_value=1.5, - value=st.session_state.construction_project['عوامل_التعديل']['time_factor'], - step=0.05, - help="يؤثر في التكلفة حسب الجدول الزمني للمشروع وضرورة الإسراع في التنفيذ", - key="project_time_factor" - ) - - with col2: - st.session_state.construction_project['عوامل_التعديل']['risk_factor'] = st.slider( - "معامل المخاطر", - min_value=1.0, - max_value=1.5, - value=st.session_state.construction_project['عوامل_التعديل']['risk_factor'], - step=0.05, - help="يؤثر في التكلفة حسب المخاطر المتوقعة في المشروع", - key="project_risk_factor" - ) - - st.session_state.construction_project['عوامل_التعديل']['market_factor'] = st.slider( - "معامل السوق", - min_value=0.8, - max_value=1.3, - value=st.session_state.construction_project['عوامل_التعديل']['market_factor'], - step=0.05, - help="يؤثر في التكلفة حسب حالة السوق الحالية وتغيرات الأسعار", - key="project_market_factor" - ) - - # زر حساب تكلفة المشروع - if st.button("حساب تكلفة المشروع", type="primary"): - with st.spinner("جاري حساب تكلفة المشروع..."): - # حساب التكلفة باستخدام الحاسبة - project_cost = self.construction_calculator.calculate_project_cost(st.session_state.construction_project) - st.session_state.project_cost_result = project_cost - - # عرض نتائج حساب المشروع - if 'project_cost_result' in st.session_state: - st.markdown("### نتائج حساب تكلفة المشروع") - - result = st.session_state.project_cost_result - - # ملخص المشروع - st.markdown(f"**المشروع:** {result['اسم_المشروع']}") - st.markdown(f"**الوصف:** {result['وصف_المشروع']}") - st.markdown(f"**عدد البنود:** {result['عدد_البنود']} بند") - - # المكونات الرئيسية للتكلفة - col1, col2, col3 = st.columns(3) - - with col1: - st.metric( - "إجمالي تكلفة المواد", - f"{result['تكاليف_مباشرة']['المواد']['الإجمالي']:,.2f} ريال", - delta=f"{result['تكاليف_مباشرة']['المواد']['النسبة_المئوية']:.1f}% من التكلفة المباشرة" - ) - - with col2: - st.metric( - "إجمالي تكلفة العمالة", - f"{result['تكاليف_مباشرة']['العمالة']['الإجمالي']:,.2f} ريال", - delta=f"{result['تكاليف_مباشرة']['العمالة']['النسبة_المئوية']:.1f}% من التكلفة المباشرة" - ) - - with col3: - st.metric( - "إجمالي تكلفة المعدات", - f"{result['تكاليف_مباشرة']['المعدات']['الإجمالي']:,.2f} ريال", - delta=f"{result['تكاليف_مباشرة']['المعدات']['النسبة_المئوية']:.1f}% من التكلفة المباشرة" - ) - - # إجمالي التكاليف المباشرة - st.metric( - "إجمالي التكاليف المباشرة", - f"{result['تكاليف_مباشرة']['إجمالي_تكاليف_مباشرة']:,.2f} ريال" - ) - - # تفاصيل المصاريف والربح والسعر النهائي - col1, col2, col3 = st.columns(3) - - with col1: - st.metric( - f"المصاريف الإدارية ({result['مصاريف_إدارية']['نسبة']}%)", - f"{result['مصاريف_إدارية']['قيمة']:,.2f} ريال" - ) - - with col2: - st.metric( - f"هامش الربح ({result['هامش_ربح']['نسبة']}%)", - f"{result['هامش_ربح']['قيمة']:,.2f} ريال" - ) - - with col3: - st.metric( - "التكلفة الإجمالية", - f"{result['التكلفة_الإجمالية']:,.2f} ريال" - ) - - # التكلفة بعد تطبيق عوامل التعديل - adjustment_factor = result['عوامل_التعديل']['المعامل_الإجمالي'] - st.metric( - f"السعر النهائي المعدل (معامل التعديل: {adjustment_factor:.2f})", - f"{result['التكلفة_النهائية_المعدلة']:,.2f} ريال", - delta=f"{(adjustment_factor - 1) * 100:.1f}%" - ) - - # رسم بياني لتوزيع التكلفة - st.markdown("#### توزيع مكونات التكلفة") - - cost_components = { - 'المواد': result['تكاليف_مباشرة']['المواد']['الإجمالي'], - 'العمالة': result['تكاليف_مباشرة']['العمالة']['الإجمالي'], - 'المعدات': result['تكاليف_مباشرة']['المعدات']['الإجمالي'], - 'المصاريف الإدارية': result['مصاريف_إدارية']['قيمة'], - 'هامش الربح': result['هامش_ربح']['قيمة'] - } - - colors = ['#2E86C1', '#28B463', '#EB984E', '#8E44AD', '#C0392B'] - - fig = px.pie( - values=list(cost_components.values()), - names=list(cost_components.keys()), - title='توزيع مكونات التكلفة', - color_discrete_sequence=colors - ) - - st.plotly_chart(fig, use_container_width=True) - - # جدول تفاصيل البنود - st.markdown("#### تفاصيل بنود المشروع") - - items_data = [] - for i, item in enumerate(result['تفاصيل_البنود']): - items_data.append({ - 'رقم': i + 1, - 'الوصف': item['وصف_البند'], - 'الكمية': item['الكمية'], - 'الوحدة': item['الوحدة'], - 'سعر الوحدة': item['سعر_الوحدة'], - 'الإجمالي': item['التكلفة_الإجمالية'], - 'بعد التعديل': item['السعر_المعدل']['إجمالي'] - }) - - if len(items_data) > 0: - items_df = pd.DataFrame(items_data) - - # تنسيق الجدول لعرض الأرقام بشكل أفضل - def highlight_row(row): - """تنسيق الجدول مع تمييز الصفوف بالتناوب""" - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = items_df.style.apply(highlight_row, axis=1) - styled_df = styled_df.format({ - 'الكمية': '{:,.2f}', - 'سعر الوحدة': '{:,.2f}', - 'الإجمالي': '{:,.2f}', - 'بعد التعديل': '{:,.2f}' - }) - - st.dataframe(styled_df, use_container_width=True) - else: - st.info("لا توجد بنود في المشروع") - - with calc_tabs[2]: # قوائم الأسعار المرجعية - st.markdown("#### قوائم الأسعار المرجعية") - - ref_tabs = st.tabs(["قائمة المواد", "قائمة العمالة", "قائمة المعدات"]) - - with ref_tabs[0]: # قائمة المواد - st.markdown("#### قائمة أسعار المواد المرجعية") - - # الحصول على قائمة المواد - materials = self.construction_calculator.get_all_rates(item_type='مادة') - - if materials and 'المواد' in materials: - # تحويل قاموس المواد إلى DataFrame - materials_list = [] - for name, data in materials['المواد'].items(): - materials_list.append({ - 'اسم المادة': name, - 'الوحدة': data.get('وحدة', ''), - 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), - 'الوصف': data.get('وصف', ''), - 'الفئة': data.get('فئة', '') - }) - - if materials_list: - materials_df = pd.DataFrame(materials_list) - - # تصفية حسب الفئة - categories = ['الكل'] + sorted(materials_df['الفئة'].unique().tolist()) - selected_category = st.selectbox("تصفية حسب الفئة", categories, key="materials_cat_filter_section1") - - if selected_category != 'الكل': - filtered_df = materials_df[materials_df['الفئة'] == selected_category] - else: - filtered_df = materials_df - - # تنسيق الجدول - def highlight_materials_row(row): - """تنسيق الجدول مع تمييز الصفوف بالتناوب""" - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = filtered_df.style.apply(highlight_materials_row, axis=1) - styled_df = styled_df.format({ - 'سعر الوحدة': '{:,.2f}' - }) - - st.dataframe(styled_df, use_container_width=True, hide_index=True) - else: - st.info("لا توجد مواد في قاعدة البيانات") - else: - st.info("لا توجد قائمة مواد متاحة") - - with ref_tabs[1]: # قائمة العمالة - st.markdown("#### قائمة أسعار العمالة المرجعية") - - # الحصول على قائمة العمالة - labor = self.construction_calculator.get_all_rates(item_type='عمالة') - - if labor and 'العمالة' in labor: - # تحويل قاموس العمالة إلى DataFrame - labor_list = [] - for name, data in labor['العمالة'].items(): - labor_list.append({ - 'نوع العامل': name, - 'وحدة الأجر': data.get('وحدة', ''), - 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), - 'الوصف': data.get('وصف', ''), - 'الفئة': data.get('فئة', '') - }) - - if labor_list: - labor_df = pd.DataFrame(labor_list) - - # تصفية حسب الفئة - categories = ['الكل'] + sorted(labor_df['الفئة'].unique().tolist()) - selected_category = st.selectbox("تصفية حسب الفئة", categories, key="labor_cat_filter_section1") - - if selected_category != 'الكل': - filtered_df = labor_df[labor_df['الفئة'] == selected_category] - else: - filtered_df = labor_df - - # تنسيق الجدول - def highlight_labor_row(row): - """تنسيق الجدول مع تمييز الصفوف بالتناوب""" - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = filtered_df.style.apply(highlight_labor_row, axis=1) - styled_df = styled_df.format({ - 'سعر الوحدة': '{:,.2f}' - }) - - st.dataframe(styled_df, use_container_width=True, hide_index=True) - else: - st.info("لا توجد عمالة في قاعدة البيانات") - else: - st.info("لا توجد قائمة عمالة متاحة") - - with ref_tabs[2]: # قائمة المعدات - st.markdown("#### قائمة أسعار المعدات المرجعية") - - # الحصول على قائمة المعدات - equipment = self.construction_calculator.get_all_rates(item_type='معدة') - - if equipment and 'المعدات' in equipment: - # تحويل قاموس المعدات إلى DataFrame - equipment_list = [] - for name, data in equipment['المعدات'].items(): - equipment_list.append({ - 'نوع المعدة': name, - 'وحدة الإيجار': data.get('وحدة', ''), - 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), - 'الوصف': data.get('وصف', ''), - 'الفئة': data.get('فئة', '') - }) - - if equipment_list: - equipment_df = pd.DataFrame(equipment_list) - - # تصفية حسب الفئة - categories = ['الكل'] + sorted(equipment_df['الفئة'].unique().tolist()) - selected_category = st.selectbox("تصفية حسب الفئة", categories, key="equipment_cat_filter_section1") - - if selected_category != 'الكل': - filtered_df = equipment_df[equipment_df['الفئة'] == selected_category] - else: - filtered_df = equipment_df - - # تنسيق الجدول - def highlight_equipment_row(row): - """تنسيق الجدول مع تمييز الصفوف بالتناوب""" - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = filtered_df.style.apply(highlight_equipment_row, axis=1) - styled_df = styled_df.format({ - 'سعر الوحدة': '{:,.2f}' - }) - - st.dataframe(styled_df, use_container_width=True, hide_index=True) - else: - st.info("لا توجد معدات في قاعدة البيانات") - else: - st.info("لا توجد قائمة معدات متاحة") - - with calc_tabs[3]: # كتالوج أعمال المقاولات - st.markdown("#### كتالوج أعمال المقاولات") - - # شرح كتالوج أعمال المقاولات - with st.expander("معلومات عن كتالوج أعمال المقاولات", expanded=False): - st.markdown(""" - **كتالوج أعمال المقاولات** هو قاعدة بيانات شاملة للبنود النموذجية المستخدمة في مشاريع المقاولات ويوفر: - - - بنود جاهزة لمختلف أنواع الأعمال الإنشائية (خرسانة مناهل مواسير طرق إلخ). - - تفاصيل دقيقة للمواد والعمالة والمعدات المطلوبة لكل بند. - - تحليل تكلفة تفصيلي يمكن استخدامه مباشرة في عروض الأسعار والمناقصات. - - ربط مباشر مع حاسبة تكاليف البناء وحاسبة التسعير. - - **استخدامات الكتالوج:** - - 1. استخدام البنود النموذجية مباشرة في التسعير. - 2. تعديل البنود النموذجية لتناسب متطلبات المشروع. - 3. إضافة بنود جديدة إلى الكتالوج للاستخدام المستقبلي. - """) - - # الفئات وعرض محتوى الكتالوج - category_col, template_col = st.columns([1, 2]) - - with category_col: - st.markdown("### فئات البنود") - - # الحصول على فئات البنود - templates = self.construction_templates.get_all_templates() - categories = templates['categories'] - - for cat_id, cat_data in categories.items(): - st.markdown(f"#### {cat_data['name']}") - st.markdown(f"{cat_data['description']}") - - if st.button(f"عرض بنود {cat_data['name']}", key=f"cat_btn_{cat_id}"): - st.session_state.selected_category = cat_id - st.rerun() - - with template_col: - st.markdown("### البنود النموذجية") - - selected_category = st.session_state.get("selected_category", list(categories.keys())[0]) - - # عرض البنود في الفئة المحددة - cat_templates = self.construction_templates.get_templates_by_category(selected_category) - - if cat_templates: - st.markdown(f"#### بنود فئة {categories[selected_category]['name']}") - - for template in cat_templates: - with st.expander(template['name'], expanded=False): - st.markdown(f"**الوصف**: {template['description']}") - st.markdown(f"**الوحدة**: {template['unit']}") - - # عرض مكونات البند - st.markdown("##### مكونات البند") - - # المواد - materials = template['components']['materials'] - if materials: - materials_df = pd.DataFrame(materials) - st.markdown("**المواد:**") - st.dataframe(materials_df, hide_index=True) - - # العمالة - labor = template['components']['labor'] - if labor: - labor_df = pd.DataFrame(labor) - st.markdown("**العمالة:**") - st.dataframe(labor_df, hide_index=True) - - # المعدات - equipment = template['components']['equipment'] - if equipment: - equipment_df = pd.DataFrame(equipment) - st.markdown("**المعدات:**") - st.dataframe(equipment_df, hide_index=True) - - # أزرار العمليات - col1, col2 = st.columns(2) - - with col1: - if st.button("استخدام هذا البند في حاسبة التكاليف", key=f"use_template_{template['id']}"): - # تحويل البند إلى صيغة الحاسبة - construction_item = self.construction_templates.convert_template_to_item(template['id']) - - # تحديث بيانات البند في session state - st.session_state.construction_item = construction_item - st.session_state.active_tab = 0 # الانتقال إلى تبويب حساب تكلفة البند - st.rerun() - - with col2: - if st.button("إضافة مباشرة إلى جدول التسعير", key=f"add_to_pricing_{template['id']}"): - # تحويل البند إلى صيغة الحاسبة - construction_item = self.construction_templates.convert_template_to_item(template['id']) - - # حساب التكلفة - item_cost = self.construction_calculator.calculate_item_cost(construction_item) - - # إضافة البند إلى جدول التسعير - if 'manual_items' not in st.session_state: - st.session_state.manual_items = pd.DataFrame(columns=[ - 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي' - ]) - - # إنشاء رقم البند الجديد - new_id = f"C{len(st.session_state.manual_items)+1}" - - # إنشاء صف جديد - new_row = pd.DataFrame({ - 'رقم البند': [new_id], - 'وصف البند': [item_cost['وصف_البند']], - 'الوحدة': [item_cost['الوحدة']], - 'الكمية': [float(item_cost['الكمية'])], - 'سعر الوحدة': [float(item_cost['السعر_المعدل']['سعر_الوحدة'])], - 'الإجمالي': [float(item_cost['السعر_المعدل']['إجمالي'])] - }) - - # إضافة الصف إلى DataFrame - st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True) - - # إنشاء حالة التسعير الحالي إذا لم تكن موجودة - if 'current_pricing' not in st.session_state: - st.session_state.current_pricing = { - 'name': 'تسعير جديد', - 'method': 'تسعير مستورد من كتالوج المقاولات', - 'items': st.session_state.manual_items - } - else: - # تحديث البنود في التسعير الحالي - st.session_state.current_pricing['items'] = st.session_state.manual_items - - st.success(f"تم إضافة البند \"{item_cost['وصف_البند']}\" إلى جدول التسعير بنجاح!") - - # إضافة قسم لإضافة بند جديد إلى الكتالوج - st.markdown("### إضافة بند جديد إلى الكتالوج") - - if st.button("إضافة البند الحالي إلى الكتالوج"): - if 'item_cost_result' in st.session_state: - # عرض نموذج لإضافة البند إلى الكتالوج - st.session_state.show_add_to_catalog = True - st.rerun() - else: - st.warning("يجب حساب تكلفة البند أولاً في تبويب 'حساب تكلفة بند' قبل إضافته إلى الكتالوج.") - - # عرض نموذج إضافة البند إلى الكتالوج - if 'show_add_to_catalog' in st.session_state and st.session_state.show_add_to_catalog: - st.markdown("#### إضافة البند الحالي إلى كتالوج المقاولات") - - # اختيار الفئة - category_options = [f"{cat_data['name']}" for cat_id, cat_data in categories.items()] - category_ids = list(categories.keys()) - - selected_category_index = st.selectbox( - "اختر فئة البند", - options=range(len(category_options)), - format_func=lambda i: category_options[i], - key="new_template_category" - ) - - selected_category_id = category_ids[selected_category_index] - - # معلومات البند - item_result = st.session_state.item_cost_result - - template_name = st.text_input("اسم البند في الكتالوج", value=item_result['وصف_البند'][:50]) - template_description = st.text_area("وصف البند", value=item_result['وصف_البند'], key="template_item_description") - - # الكلمات المفتاحية - tags_input = st.text_input("الكلمات المفتاحية (مفصولة بفواصل)", value="بناء, مقاولات") - tags = [tag.strip() for tag in tags_input.split(",")] - - if st.button("إضافة إلى الكتالوج", type="primary"): - # تحويل البند إلى صيغة قالب - template_data = { - "category": selected_category_id, - "name": template_name, - "description": template_description, - "unit": item_result['الوحدة'], - "components": { - "materials": item_result['تكاليف_مباشرة']['المواد']['التفاصيل'], - "labor": item_result['تكاليف_مباشرة']['العمالة']['التفاصيل'], - "equipment": item_result['تكاليف_مباشرة']['المعدات']['التفاصيل'] - }, - "admin_expenses": item_result['مصاريف_إدارية']['نسبة'] / 100, - "profit_margin": item_result['هامش_ربح']['نسبة'] / 100, - "tags": tags - } - - # إضافة القالب إلى الكتالوج - template_id = self.construction_templates.add_template(template_data) - - st.success(f"تم إضافة البند \"{template_name}\" إلى كتالوج المقاولات بنجاح!") - st.session_state.show_add_to_catalog = False - st.rerun() - - if st.button("إلغاء", key="cancel_add_to_catalog"): - st.session_state.show_add_to_catalog = False - st.rerun() - - with ref_tabs[0]: # قائمة المواد - st.markdown("#### قائمة أسعار المواد المرجعية") - - # الحصول على قائمة المواد - materials = self.construction_calculator.get_all_rates(item_type='مادة') - - if materials and 'المواد' in materials: - # تحويل قاموس المواد إلى DataFrame - materials_list = [] - for name, data in materials['المواد'].items(): - materials_list.append({ - 'اسم المادة': name, - 'الوحدة': data.get('وحدة', ''), - 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), - 'الوصف': data.get('وصف', ''), - 'الفئة': data.get('فئة', '') - }) - - if materials_list: - materials_df = pd.DataFrame(materials_list) - - # تصفية حسب الفئة - categories = ['الكل'] + sorted(materials_df['الفئة'].unique().tolist()) - selected_category = st.selectbox("تصفية حسب الفئة", categories, key="materials_cat_filter_section2") - - if selected_category != 'الكل': - filtered_df = materials_df[materials_df['الفئة'] == selected_category] - else: - filtered_df = materials_df - - # تنسيق الجدول - def highlight_materials_row(row): - """تنسيق الجدول مع تمييز الصفوف بالتناوب""" - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = filtered_df.style.apply(highlight_materials_row, axis=1) - styled_df = styled_df.format({ - 'سعر الوحدة': '{:,.2f}' - }) - - st.dataframe(styled_df, use_container_width=True, hide_index=True) - else: - st.info("لا توجد مواد في قاعدة البيانات") - else: - st.info("لا توجد قائمة مواد متاحة") - - with ref_tabs[1]: # قائمة العمالة - st.markdown("#### قائمة أسعار العمالة المرجعية") - - # الحصول على قائمة العمالة - labor = self.construction_calculator.get_all_rates(item_type='عمالة') - - if labor and 'العمالة' in labor: - # تحويل قاموس العمالة إلى DataFrame - labor_list = [] - for name, data in labor['العمالة'].items(): - labor_list.append({ - 'نوع العامل': name, - 'وحدة الأجر': data.get('وحدة', ''), - 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), - 'الوصف': data.get('وصف', ''), - 'الفئة': data.get('فئة', '') - }) - - if labor_list: - labor_df = pd.DataFrame(labor_list) - - # تصفية حسب الفئة - categories = ['الكل'] + sorted(labor_df['الفئة'].unique().tolist()) - selected_category = st.selectbox("تصفية حسب الفئة", categories, key="labor_cat_filter_section2") - - if selected_category != 'الكل': - filtered_df = labor_df[labor_df['الفئة'] == selected_category] - else: - filtered_df = labor_df - - # تنسيق الجدول - def highlight_labor_row(row): - """تنسيق الجدول مع تمييز الصفوف بالتناوب""" - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = filtered_df.style.apply(highlight_labor_row, axis=1) - styled_df = styled_df.format({ - 'سعر الوحدة': '{:,.2f}' - }) - - st.dataframe(styled_df, use_container_width=True, hide_index=True) - else: - st.info("لا توجد عمالة في قاعدة البيانات") - else: - st.info("لا توجد قائمة عمالة متاحة") - - with ref_tabs[2]: # قائمة المعدات - st.markdown("#### قائمة أسعار المعدات المرجعية") - - # الحصول على قائمة المعدات - equipment = self.construction_calculator.get_all_rates(item_type='معدة') - - if equipment and 'المعدات' in equipment: - # تحويل قاموس المعدات إلى DataFrame - equipment_list = [] - for name, data in equipment['المعدات'].items(): - equipment_list.append({ - 'نوع المعدة': name, - 'وحدة الأجر': data.get('وحدة', ''), - 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), - 'الوصف': data.get('وصف', ''), - 'الفئة': data.get('فئة', '') - }) - - if equipment_list: - equipment_df = pd.DataFrame(equipment_list) - - # تصفية حسب الفئة - categories = ['الكل'] + sorted(equipment_df['الفئة'].unique().tolist()) - selected_category = st.selectbox("تصفية حسب الفئة", categories, key="equipment_cat_filter_section2") - - if selected_category != 'الكل': - filtered_df = equipment_df[equipment_df['الفئة'] == selected_category] - else: - filtered_df = equipment_df - - # تنسيق الجدول - def highlight_equipment_row(row): - """تنسيق الجدول مع تمييز الصفوف بالتناوب""" - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = filtered_df.style.apply(highlight_equipment_row, axis=1) - styled_df = styled_df.format({ - 'سعر الوحدة': '{:,.2f}' - }) - - st.dataframe(styled_df, use_container_width=True, hide_index=True) - else: - st.info("لا توجد معدات في قاعدة البيانات") - else: - st.info("لا توجد قائمة معدات متاحة") - - def _render_local_content_tab(self): - """عرض تبويب المحتوى المحلي""" - - st.markdown("

تحليل المحتوى المحلي

", unsafe_allow_html=True) - - # التحقق من وجود تسعير حالي - if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: - st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") - return - - # شرح المحتوى المحلي - with st.expander("ما هو المحتوى المحلي؟", expanded=False): - st.markdown(""" - **المحتوى المحلي** هو نسبة المنتجات والخدمات والقوى العاملة المحلية المستخدمة في المشروع. يهدف إلى زيادة مساهمة المنتجات والخدمات المحلية في المشاريع. - - ### مكونات المحتوى المحلي: - - 1. **المنتجات**: المنتجات والمواد المصنعة محلياً. - 2. **الخدمات**: الخدمات المقدمة من شركات محلية. - 3. **القوى العاملة**: العمالة والكوادر الفنية والإدارية المحلية. - - ### أهمية المحتوى المحلي: - - - تعزيز الاقتصاد المحلي وخلق فرص عمل. - - تحقيق أهداف رؤية 2030 في زيادة المحتوى المحلي. - - التأهل للمشاريع الحكومية التي تتطلب نسبة محتوى محلي محددة. - - الحصول على حوافز وأفضلية في المناقصات الحكومية. - - ### متطلبات المحتوى المحلي: - - - نسبة المحتوى المحلي للقوى العاملة: 80% - - نسبة المحتوى المحلي للمنتجات: 70% - - نسبة المحتوى المحلي للخدمات: 60% - """) - - # عرض لوحة إدخال بيانات المحتوى المحلي - st.markdown("### بيانات المحتوى المحلي") - - # التبويبات لأنواع المحتوى المحلي - lc_tabs = st.tabs(["المنتجات", "الخدمات", "القوى العاملة", "التحليل"]) - - with lc_tabs[0]: # المنتجات - st.markdown("#### بيانات المنتجات") - - # إنشاء بيانات افتراضية للمنتجات إذا لم تكن موجودة - if 'local_content_products' not in st.session_state: - st.session_state.local_content_products = pd.DataFrame({ - 'المنتج': [ - "خرسانة مسلحة", - "حديد تسليح", - "بلوك خرساني", - "عزل مائي", - "دهانات" - ], - 'الكمية': [250, 25, 400, 500, 600], - 'سعر_الوحدة': [1200, 6000, 200, 100, 50], - 'التكلفة_الإجمالية': [300000, 150000, 80000, 50000, 30000], - 'نسبة_المحتوى_المحلي': [0.95, 0.70, 0.98, 0.60, 0.80] - }) - - # حساب التكلفة الإجمالية - st.session_state.local_content_products['التكلفة_الإجمالية'] = st.session_state.local_content_products['الكمية'] * st.session_state.local_content_products['سعر_الوحدة'] - - # نموذج إضافة منتج جديد - st.markdown("#### إضافة منتج جديد") - col1, col2, col3, col4 = st.columns(4) - - with col1: - new_product_name = st.text_input("اسم المنتج", key="new_product_name", value="") - with col2: - new_product_quantity = st.number_input("الكمية", key="new_product_quantity", min_value=0, value=0) - with col3: - new_product_price = st.number_input("سعر الوحدة", key="new_product_price", min_value=0, value=0) - with col4: - new_product_local_content = st.slider("نسبة المحتوى المحلي", key="new_product_local_content", min_value=0.0, max_value=1.0, value=0.8, step=0.01, format="%.2f") - - if st.button("إضافة المنتج"): - if new_product_name: - # حساب التكلفة الإجمالية - total_cost = new_product_quantity * new_product_price - - # إضافة منتج جديد للجدول - new_product = pd.DataFrame({ - 'المنتج': [new_product_name], - 'الكمية': [new_product_quantity], - 'سعر_الوحدة': [new_product_price], - 'التكلفة_الإجمالية': [total_cost], - 'نسبة_المحتوى_المحلي': [new_product_local_content] - }) - - # إضافة المنتج الجديد للجدول الحالي - st.session_state.local_content_products = pd.concat([st.session_state.local_content_products, new_product], ignore_index=True) - st.success(f"تم إضافة المنتج {new_product_name} بنجاح!") - else: - st.warning("يرجى إدخال اسم المنتج") - - # عرض جدول البنود مع إمكانية التعديل - st.markdown("#### جدول المنتجات") - edited_products = st.data_editor( - st.session_state.local_content_products, - use_container_width=True, - hide_index=True, - key="products_editor" - ) - st.session_state.local_content_products = edited_products - - # زر لحذف المنتجات المحددة - if st.button("حذف المنتجات المحددة"): - st.session_state.local_content_products = pd.DataFrame({ - 'المنتج': [], - 'الكمية': [], - 'سعر_الوحدة': [], - 'التكلفة_الإجمالية': [], - 'نسبة_المحتوى_المحلي': [] - }) - st.success("تم حذف جميع المنتجات!") - st.rerun() - - # عرض ملخص المنتجات - total_products_cost = edited_products['التكلفة_الإجمالية'].sum() - avg_local_content = (edited_products['التكلفة_الإجمالية'] * edited_products['نسبة_المحتوى_المحلي']).sum() / total_products_cost if total_products_cost > 0 else 0 - - st.markdown(f""" - **إجمالي تكلفة المنتجات**: {total_products_cost:,.2f} ريال - - **متوسط نسبة المحتوى المحلي للمنتجات**: {avg_local_content*100:.2f}% - - **المستهدف**: 70% - - **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.7 else "❌ غير ملتزم"} - """) - - with lc_tabs[1]: # الخدمات - st.markdown("#### بيانات الخدمات") - - # إنشاء بيانات افتراضية للخدمات إذا لم تكن موجودة - if 'local_content_services' not in st.session_state: - st.session_state.local_content_services = pd.DataFrame({ - 'الخدمة': [ - "تصميم معماري", - "إشراف هندسي", - "خدمات نقل", - "خدمات أمن وسلامة", - "صيانة ونظافة" - ], - 'التكلفة': [100000, 120000, 50000, 30000, 20000], - 'نسبة_المحتوى_المحلي': [0.90, 0.85, 0.90, 0.95, 0.95] - }) - - # نموذج إضافة خدمة جديدة - st.markdown("#### إضافة خدمة جديدة") - col1, col2, col3 = st.columns(3) - - with col1: - new_service_name = st.text_input("اسم الخدمة", key="new_service_name", value="") - with col2: - new_service_cost = st.number_input("التكلفة", key="new_service_cost", min_value=0, value=0) - with col3: - new_service_local_content = st.slider("نسبة المحتوى المحلي", key="new_service_local_content", min_value=0.0, max_value=1.0, value=0.8, step=0.01, format="%.2f") - - if st.button("إضافة الخدمة"): - if new_service_name: - # إضافة خدمة جديدة للجدول - new_service = pd.DataFrame({ - 'الخدمة': [new_service_name], - 'التكلفة': [new_service_cost], - 'نسبة_المحتوى_المحلي': [new_service_local_content] - }) - - # إضافة الخدمة الجديدة للجدول الحالي - st.session_state.local_content_services = pd.concat([st.session_state.local_content_services, new_service], ignore_index=True) - st.success(f"تم إضافة الخدمة {new_service_name} بنجاح!") - else: - st.warning("يرجى إدخال اسم الخدمة") - - # عرض جدول الخدمات مع إمكانية التعديل - st.markdown("#### جدول الخدمات") - edited_services = st.data_editor( - st.session_state.local_content_services, - use_container_width=True, - hide_index=True, - key="services_editor" - ) - st.session_state.local_content_services = edited_services - - # زر لحذف الخدمات المحددة - if st.button("حذف الخدمات المحددة"): - st.session_state.local_content_services = pd.DataFrame({ - 'الخدمة': [], - 'التكلفة': [], - 'نسبة_المحتوى_المحلي': [] - }) - st.success("تم حذف جميع الخدمات!") - st.rerun() - - # عرض ملخص الخدمات - total_services_cost = edited_services['التكلفة'].sum() - avg_local_content = (edited_services['التكلفة'] * edited_services['نسبة_المحتوى_المحلي']).sum() / total_services_cost if total_services_cost > 0 else 0 - - st.markdown(f""" - **إجمالي تكلفة الخدمات**: {total_services_cost:,.2f} ريال - - **متوسط نسبة المحتوى المحلي للخدمات**: {avg_local_content*100:.2f}% - - **المستهدف**: 60% - - **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.6 else "❌ غير ملتزم"} - """) - - with lc_tabs[2]: # القوى العاملة - st.markdown("#### بيانات القوى العاملة") - - # إنشاء بيانات افتراضية للقوى العاملة إذا لم تكن موجودة - if 'local_content_labor' not in st.session_state: - st.session_state.local_content_labor = pd.DataFrame({ - 'فئة_العمالة': [ - "مهندسون", - "فنيون", - "عمال بناء", - "إداريون", - "مشرفون" - ], - 'العدد': [5, 10, 30, 3, 4], - 'الراتب_الشهري': [15000, 8000, 3000, 10000, 12000], - 'المدة_بالأشهر': [12, 12, 12, 12, 12], - 'نسبة_المحتوى_المحلي': [0.75, 0.65, 0.60, 0.90, 0.80] - }) - - # حساب التكلفة الإجمالية - st.session_state.local_content_labor['التكلفة_الإجمالية'] = st.session_state.local_content_labor['العدد'] * st.session_state.local_content_labor['الراتب_الشهري'] * st.session_state.local_content_labor['المدة_بالأشهر'] - - # نموذج إضافة فئة عمالة جديدة - st.markdown("#### إضافة فئة عمالة جديدة") - col1, col2 = st.columns(2) - - with col1: - new_labor_category = st.text_input("فئة العمالة", key="new_labor_category", value="") - new_labor_count = st.number_input("العدد", key="new_labor_count", min_value=0, value=0) - - with col2: - new_labor_salary = st.number_input("الراتب الشهري", key="new_labor_salary", min_value=0, value=0) - new_labor_months = st.number_input("المدة بالأشهر", key="new_labor_months", min_value=1, value=12) - - new_labor_local_content = st.slider("نسبة المحتوى المحلي", key="new_labor_local_content", min_value=0.0, max_value=1.0, value=0.8, step=0.01, format="%.2f") - - if st.button("إضافة فئة العمالة"): - if new_labor_category: - # حساب التكلفة الإجمالية - total_cost = new_labor_count * new_labor_salary * new_labor_months - - # إضافة فئة عمالة جديدة للجدول - new_labor = pd.DataFrame({ - 'فئة_العمالة': [new_labor_category], - 'العدد': [new_labor_count], - 'الراتب_الشهري': [new_labor_salary], - 'المدة_بالأشهر': [new_labor_months], - 'نسبة_المحتوى_المحلي': [new_labor_local_content], - 'التكلفة_الإجمالية': [total_cost] - }) - - # إضافة فئة العمالة الجديدة للجدول الحالي - st.session_state.local_content_labor = pd.concat([st.session_state.local_content_labor, new_labor], ignore_index=True) - st.success(f"تم إضافة فئة العمالة {new_labor_category} بنجاح!") - else: - st.warning("يرجى إدخال اسم فئة العمالة") - - # عرض جدول القوى العاملة مع إمكانية التعديل - st.markdown("#### جدول القوى العاملة") - edited_labor = st.data_editor( - st.session_state.local_content_labor, - use_container_width=True, - hide_index=True, - key="labor_editor" - ) - - # إعادة حساب التكلفة الإجمالية بعد التعديل - edited_labor['التكلفة_الإجمالية'] = edited_labor['العدد'] * edited_labor['الراتب_الشهري'] * edited_labor['المدة_بالأشهر'] - st.session_state.local_content_labor = edited_labor - - # زر لحذف فئات العمالة المحددة - if st.button("حذف فئات العمالة المحددة"): - st.session_state.local_content_labor = pd.DataFrame({ - 'فئة_العمالة': [], - 'العدد': [], - 'الراتب_الشهري': [], - 'المدة_بالأشهر': [], - 'نسبة_المحتوى_المحلي': [], - 'التكلفة_الإجمالية': [] - }) - st.success("تم حذف جميع فئات العمالة!") - st.rerun() - - # عرض ملخص القوى العاملة - total_labor_cost = edited_labor['التكلفة_الإجمالية'].sum() - avg_local_content = (edited_labor['التكلفة_الإجمالية'] * edited_labor['نسبة_المحتوى_المحلي']).sum() / total_labor_cost if total_labor_cost > 0 else 0 - - st.markdown(f""" - **إجمالي تكلفة القوى العاملة**: {total_labor_cost:,.2f} ريال - - **متوسط نسبة المحتوى المحلي للقوى العاملة**: {avg_local_content*100:.2f}% - - **المستهدف**: 80% - - **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.8 else "❌ غير ملتزم"} - """) - - with lc_tabs[3]: # التحليل - st.markdown("#### تحليل المحتوى المحلي") - - # حساب المحتوى المحلي الإجمالي - try: - # تجميع بيانات تحليل المحتوى المحلي - products_cost = st.session_state.local_content_products['التكلفة_الإجمالية'].sum() - products_local_content = (st.session_state.local_content_products['التكلفة_الإجمالية'] * st.session_state.local_content_products['نسبة_المحتوى_المحلي']).sum() / products_cost if products_cost > 0 else 0 - - services_cost = st.session_state.local_content_services['التكلفة'].sum() - services_local_content = (st.session_state.local_content_services['التكلفة'] * st.session_state.local_content_services['نسبة_المحتوى_المحلي']).sum() / services_cost if services_cost > 0 else 0 - - labor_cost = st.session_state.local_content_labor['التكلفة_الإجمالية'].sum() - labor_local_content = (st.session_state.local_content_labor['التكلفة_الإجمالية'] * st.session_state.local_content_labor['نسبة_المحتوى_المحلي']).sum() / labor_cost if labor_cost > 0 else 0 - - # حساب الوزن النسبي لكل مكون - total_cost = products_cost + services_cost + labor_cost - products_weight = products_cost / total_cost if total_cost > 0 else 0 - services_weight = services_cost / total_cost if total_cost > 0 else 0 - labor_weight = labor_cost / total_cost if total_cost > 0 else 0 - - # حساب المحتوى المحلي الإجمالي - total_local_content = (products_local_content * products_weight) + (services_local_content * services_weight) + (labor_local_content * labor_weight) - - # عرض ملخص المحتوى المحلي - st.markdown("### ملخص المحتوى المحلي") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال") - - with col2: - st.metric("نسبة المحتوى المحلي الإجمالية", f"{total_local_content*100:.2f}%") - - with col3: - target_local_content = 0.7 # 70% - st.metric("الحالة", "ملتزم" if total_local_content >= target_local_content else "غير ملتزم", delta=f"{(total_local_content - target_local_content)*100:.2f}%") - - # عرض رسم بياني للمقارنة - st.markdown("### تحليل بصري للمحتوى المحلي") - - # رسم بياني شريطي لنسب المحتوى المحلي - categories = ['المنتجات', 'الخدمات', 'القوى العاملة', 'الإجمالي'] - actual_values = [products_local_content * 100, services_local_content * 100, labor_local_content * 100, total_local_content * 100] - target_values = [70, 60, 80, 70] # المستهدفات - - # تهيئة البيانات للرسم البياني - chart_data = pd.DataFrame({ - 'الفئة': categories, - 'النسبة الفعلية': actual_values, - 'النسبة المستهدفة': target_values - }) - - # رسم بياني شريطي للمقارنة - fig = go.Figure() - - fig.add_trace(go.Bar( - x=chart_data['الفئة'], - y=chart_data['النسبة الفعلية'], - name='النسبة الفعلية', - marker_color='rgb(26, 118, 255)' - )) - - fig.add_trace(go.Bar( - x=chart_data['الفئة'], - y=chart_data['النسبة المستهدفة'], - name='النسبة المستهدفة', - marker_color='rgb(55, 83, 109)' - )) - - fig.update_layout( - title='مقارنة بين النسب الفعلية والمستهدفة للمحتوى المحلي', - xaxis_tickfont_size=14, - yaxis=dict( - title='النسبة %', - titlefont_size=16, - tickfont_size=14, - ), - legend=dict( - x=0, - y=1.0, - bgcolor='rgba(255, 255, 255, 0)', - bordercolor='rgba(255, 255, 255, 0)' - ), - barmode='group', - bargap=0.15, - bargroupgap=0.1 - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض توصيات لتحسين نسبة المحتوى المحلي - st.markdown("### توصيات لتحسين نسبة المحتوى المحلي") - - recommendations = [] - - if products_local_content < 0.7: - recommendations.append("- زيادة نسبة المحتوى المحلي للمنتجات من خلال:") - recommendations.append(" - البحث عن موردين محليين للمنتجات ذات النسبة المنخفضة") - recommendations.append(" - استبدال المنتجات المستوردة ببدائل محلية") - recommendations.append(" - التعاون مع المصانع المحلية لتوطين صناعة المنتجات") - - if services_local_content < 0.6: - recommendations.append("- زيادة نسبة المحتوى المحلي للخدمات من خلال:") - recommendations.append(" - التعاقد مع شركات خدمات محلية") - recommendations.append(" - تحويل الخدمات المستعان بها من الخارج إلى شركات محلية") - recommendations.append(" - تأهيل الشركات المحلية لتقديم الخدمات المطلوبة") - - if labor_local_content < 0.8: - recommendations.append("- زيادة نسبة المحتوى المحلي للقوى العاملة من خلال:") - recommendations.append(" - زيادة توظيف الكوادر المحلية") - recommendations.append(" - تدريب وتأهيل العمالة المحلية") - recommendations.append(" - استبدال العمالة الأجنبية بكوادر محلية تدريجياً") - - if total_local_content < 0.7: - recommendations.append("- زيادة نسبة المحتوى المحلي الإجمالية من خلال:") - recommendations.append(" - إعادة توزيع الميزانية لصالح المكونات ذات النسبة العالية من المحتوى المحلي") - recommendations.append(" - وضع خطة مرحلية لزيادة المحتوى المحلي") - recommendations.append(" - التعاون مع اللجنة المحلية لزيادة المحتوى المحلي") - - if recommendations: - for rec in recommendations: - st.markdown(rec) - else: - st.success("تهانينا! نسبة المحتوى المحلي متوافقة مع المتطلبات.") - - # حساب تأثير المحتوى المحلي على التسعير - st.markdown("### تأثير المحتوى المحلي على التسعير") - - # تحديد عامل تعديل السعر بناءً على نسبة المحتوى المحلي - price_adjustment_factor = 1.0 - - if total_local_content >= 0.9: - price_adjustment_factor = 0.92 # خصم 8% للمحتوى المحلي العالي جداً - price_discount = "8%" - elif total_local_content >= 0.8: - price_adjustment_factor = 0.94 # خصم 6% للمحتوى المحلي العالي - price_discount = "6%" - elif total_local_content >= 0.7: - price_adjustment_factor = 0.96 # خصم 4% للمحتوى المحلي المتوسط - price_discount = "4%" - elif total_local_content >= 0.6: - price_adjustment_factor = 0.98 # خصم 2% للمحتوى المحلي المنخفض - price_discount = "2%" - else: - price_adjustment_factor = 1.0 # لا خصم - price_discount = "0%" - - # عرض تأثير المحتوى المحلي على التسعير - original_total = st.session_state.current_pricing['items']['الإجمالي'].sum() - adjusted_total = original_total * price_adjustment_factor - discount_amount = original_total - adjusted_total - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي التسعير الأصلي", f"{original_total:,.2f} ريال") - - with col2: - st.metric("نسبة الخصم بسبب المحتوى المحلي", price_discount) - - with col3: - st.metric("إجمالي التسعير بعد الخصم", f"{adjusted_total:,.2f} ريال", delta=f"-{discount_amount:,.2f} ريال") - - # أزرار العمليات - col1, col2 = st.columns(2) - - with col1: - if st.button("حفظ تحليل المحتوى المحلي"): - # حفظ بيانات المحتوى المحلي في التسعير الحالي - st.session_state.current_pricing['local_content'] = { - 'products': st.session_state.local_content_products.copy(), - 'services': st.session_state.local_content_services.copy(), - 'labor': st.session_state.local_content_labor.copy(), - 'total_local_content': total_local_content, - 'price_adjustment_factor': price_adjustment_factor - } - - st.success("تم حفظ تحليل المحتوى المحلي بنجاح!") - - with col2: - if st.button("تصدير تقرير المحتوى المحلي"): - st.success("تم تصدير تقرير المحتوى المحلي بنجاح!") - - except Exception as e: - st.error(f"حدث خطأ أثناء تحليل المحتوى المحلي: {str(e)}") - st.warning("تأكد من إدخال بيانات المحتوى المحلي بشكل صحيح في التبويبات السابقة.") - - def _render_utilities_tab(self): - """عرض تبويب الأدوات المساعدة""" - import json - import copy - from datetime import datetime - - st.markdown("## الأدوات المساعدة") - - utilities_tab1, utilities_tab2, utilities_tab3, utilities_tab4, utilities_tab5 = st.tabs([ - "الرسوم البيانية المتقدمة", - "استيراد/تصدير الإعدادات", - "النسخ الاحتياطي والاستعادة", - "مقارنة نماذج التسعير", - "إنشاء التقارير" - ]) - - with utilities_tab1: - st.markdown("### الرسوم البيانية المتقدمة لتحليل التكلفة") - - if 'item_cost_result' in st.session_state: - result = st.session_state.item_cost_result - - # تبويب الرسوم البيانية - chart_tab1, chart_tab2, chart_tab3 = st.tabs(["توزيع التكلفة", "مقارنة المكونات", "تأثير العوامل"]) - - with chart_tab1: - # رسم بياني دائري لتوزيع التكلفة - fig = go.Figure(data=[go.Pie( - labels=["المواد", "العمالة", "المعدات", "المصاريف الإدارية", "هامش الربح"], - values=[ - result['تكاليف_مباشرة']['المواد']['الإجمالي'], - result['تكاليف_مباشرة']['العمالة']['الإجمالي'], - result['تكاليف_مباشرة']['المعدات']['الإجمالي'], - result['مصاريف_إدارية']['قيمة'], - result['هامش_ربح']['قيمة'] - ], - hole=.3, - marker_colors=['#36a2eb', '#ff6384', '#4bc0c0', '#ffcd56', '#9966ff'] - )]) - fig.update_layout(title_text="توزيع مكونات التكلفة") - st.plotly_chart(fig, use_container_width=True) - - with chart_tab2: - # رسم بياني شريطي للمكونات الرئيسية - categories = ["المواد", "العمالة", "المعدات"] - values = [ - result['تكاليف_مباشرة']['المواد']['الإجمالي'], - result['تكاليف_مباشرة']['العمالة']['الإجمالي'], - result['تكاليف_مباشرة']['المعدات']['الإجمالي'] - ] - - fig = go.Figure(data=[go.Bar(x=categories, y=values, marker_color=['#36a2eb', '#ff6384', '#4bc0c0'])]) - fig.update_layout(title_text="مقارنة بين المكونات الرئيسية للتكلفة") - st.plotly_chart(fig, use_container_width=True) - - with chart_tab3: - # مخطط شريطي يوضح تأثير عوامل التعديل على التكلفة - original_cost = result['التكلفة_الإجمالية'] - final_cost = result['السعر_المعدل']['إجمالي'] - - # التحقق من وجود العوامل، وإضافتها بقيم افتراضية إذا كانت غير موجودة - if 'عوامل_التعديل' not in result: - result['عوامل_التعديل'] = {} - - # إضافة المفاتيح الناقصة بقيم افتراضية - default_factors = { - 'location_factor': 1.0, - 'time_factor': 1.0, - 'risk_factor': 1.0, - 'market_factor': 1.0 - } - - for key, default_value in default_factors.items(): - if key not in result['عوامل_التعديل']: - result['عوامل_التعديل'][key] = default_value - - factors = { - "معامل الموقع": result['عوامل_التعديل']['location_factor'], - "معامل الوقت": result['عوامل_التعديل']['time_factor'], - "معامل المخاطر": result['عوامل_التعديل']['risk_factor'], - "معامل السوق": result['عوامل_التعديل']['market_factor'] - } - - # حساب القيمة المضافة من كل عامل - factor_effects = {} - for factor_name, factor_value in factors.items(): - factor_effects[factor_name] = original_cost * (factor_value - 1) - - fig = go.Figure() - fig.add_trace(go.Waterfall( - name="تأثير العوامل", - orientation="v", - measure=["absolute"] + ["relative"] * len(factor_effects) + ["total"], - x=["التكلفة الأصلية"] + list(factor_effects.keys()) + ["التكلفة النهائية"], - y=[original_cost] + list(factor_effects.values()) + [0], - text=[f"{original_cost:,.2f}"] + [f"{val:,.2f}" for val in factor_effects.values()] + [f"{final_cost:,.2f}"], - connector={"line": {"color": "rgb(63, 63, 63)"}}, - )) - - fig.update_layout(title_text="تأثير عوامل التعديل على التكلفة النهائية") - st.plotly_chart(fig, use_container_width=True) - else: - st.warning("لم يتم العثور على بيانات تحليل التكلفة. يرجى إجراء تحليل تكلفة في تبويب 'تحليل سعر البند' أولاً.") - - with utilities_tab2: - st.markdown("### استيراد/تصدير إعدادات التسعير") - - export_col, import_col = st.columns(2) - - with export_col: - st.markdown("#### تصدير الإعدادات الحالية") - if st.button("تصدير إعدادات التسعير", key="export_pricing_settings"): - pricing_settings = { - "construction_item": st.session_state.construction_item if 'construction_item' in st.session_state else None, - "current_pricing": st.session_state.current_pricing if 'current_pricing' in st.session_state else None - } - settings_json = json.dumps(pricing_settings, ensure_ascii=False, indent=2) - st.download_button( - label="تنزيل إعدادات التسعير", - data=settings_json, - file_name="pricing_settings.json", - mime="application/json", - key="download_settings_button" - ) - - with import_col: - st.markdown("#### استيراد إعدادات سابقة") - uploaded_file = st.file_uploader("استيراد إعدادات تسعير سابقة", type=["json"], key="upload_pricing_settings") - if uploaded_file is not None: - try: - settings_data = json.loads(uploaded_file.getvalue().decode('utf-8')) - # تحديث بيانات التسعير في الجلسة - if 'construction_item' in settings_data and settings_data['construction_item']: - st.session_state.construction_item = settings_data['construction_item'] - if 'current_pricing' in settings_data and settings_data['current_pricing']: - st.session_state.current_pricing = settings_data['current_pricing'] - st.success("تم استيراد الإعدادات بنجاح!") - st.rerun() - except Exception as e: - st.error(f"حدث خطأ أثناء استيراد الإعدادات: {str(e)}") - - with utilities_tab3: - st.markdown("### النسخ الاحتياطي والاستعادة") - backup_tab1, backup_tab2 = st.tabs(["إنشاء نسخة احتياطية", "استعادة من نسخة احتياطية"]) - - with backup_tab1: - st.markdown("#### إنشاء نسخة احتياطية كاملة") - st.markdown("تقوم هذه العملية بإنشاء نسخة احتياطية كاملة لجميع بيانات التسعير الحالية.") - - if st.button("إنشاء نسخة احتياطية كاملة", key="create_full_backup"): - backup_data = { - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "construction_item": st.session_state.construction_item if 'construction_item' in st.session_state else None, - "current_pricing": st.session_state.current_pricing if 'current_pricing' in st.session_state else None, - "pricing_models": st.session_state.pricing_models if 'pricing_models' in st.session_state else [], - "manual_items": st.session_state.manual_items.to_dict('records') if 'manual_items' in st.session_state else [] - } - - backup_json = json.dumps(backup_data, ensure_ascii=False, indent=2) - - filename = f"wahbi_pricing_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" - st.download_button( - label="تنزيل النسخة الاحتياطية", - data=backup_json, - file_name=filename, - mime="application/json", - key="download_backup_button" - ) - st.success("تم إنشاء النسخة الاحتياطية بنجاح!") - - with backup_tab2: - st.markdown("#### استعادة من نسخة احتياطية") - st.markdown("يمكنك استعادة بيانات التسعير من نسخة احتياطية سابقة.") - - backup_file = st.file_uploader("استعادة من نسخة احتياطية", type=["json"], key="restore_backup_file") - if backup_file is not None: - if st.button("استعادة البيانات", key="restore_from_backup"): - try: - backup_data = json.loads(backup_file.getvalue().decode('utf-8')) - - # استعادة البيانات إلى الحالة الحالية - if 'construction_item' in backup_data and backup_data['construction_item']: - st.session_state.construction_item = backup_data['construction_item'] - - if 'current_pricing' in backup_data and backup_data['current_pricing']: - st.session_state.current_pricing = backup_data['current_pricing'] - - if 'pricing_models' in backup_data: - st.session_state.pricing_models = backup_data['pricing_models'] - - if 'manual_items' in backup_data and backup_data['manual_items']: - st.session_state.manual_items = pd.DataFrame(backup_data['manual_items']) - - st.success(f"تم استعادة البيانات من النسخة الاحتياطية بنجاح! (تاريخ النسخة: {backup_data.get('timestamp', 'غير معروف')})") - st.rerun() - except Exception as e: - st.error(f"حدث خطأ أثناء استعادة البيانات: {str(e)}") - - with utilities_tab4: - st.markdown("### مقارنة نماذج التسعير") - st.markdown("هذه الأداة تتيح لك مقارنة نماذج التسعير المختلفة واختيار الأفضل منها.") - - # تهيئة قائمة نماذج التسعير إذا لم تكن موجودة - if 'pricing_models' not in st.session_state: - st.session_state.pricing_models = [] - - # إضافة النموذج الحالي للمقارنة - if st.button("إضافة النموذج الحالي للمقارنة", key="add_current_model"): - if 'current_pricing' in st.session_state and st.session_state.current_pricing is not None: - model_name = st.session_state.current_pricing.get('name', 'نموذج بدون اسم') - model_data = copy.deepcopy(st.session_state.current_pricing) - # تحقق من عدم وجود نموذج بنفس الاسم - exists = False - for model in st.session_state.pricing_models: - if model.get('name') == model_name: - exists = True - break - - if not exists: - st.session_state.pricing_models.append(model_data) - st.success(f"تم إضافة نموذج '{model_name}' للمقارنة!") - else: - st.warning("يوجد نموذج بنفس الاسم في المقارنة بالفعل!") - else: - st.error("لا يوجد نموذج تسعير حالي للإضافة. يرجى إنشاء تسعير جديد أولاً.") - - # عرض جدول المقارنة إذا كان هناك نماذج مضافة - if len(st.session_state.pricing_models) > 0: - st.markdown("### جدول مقارنة نماذج التسعير") - - comparison_data = [] - for model in st.session_state.pricing_models: - # حساب إجمالي التكلفة - items_df = pd.DataFrame(model.get('items', {})) - total_price = 0 - if not items_df.empty and 'الإجمالي' in items_df.columns: - total_price = items_df['الإجمالي'].sum() - - comparison_data.append({ - "اسم النموذج": model.get('name', 'غير معروف'), - "طريقة التسعير": model.get('method', 'غير معروفة'), - "إجمالي التكلفة": f"{total_price:,.2f} ريال", - "عدد البنود": len(items_df) if not items_df.empty else 0, - }) - - comparison_df = pd.DataFrame(comparison_data) - st.dataframe(comparison_df, use_container_width=True, hide_index=True) - - # عرض رسم بياني للمقارنة - if len(comparison_data) > 1: - st.markdown("### رسم بياني للمقارنة") - - # استخراج البيانات للرسم البياني - models = [data["اسم النموذج"] for data in comparison_data] - values = [float(data["إجمالي التكلفة"].replace("ريال", "").replace(",", "")) for data in comparison_data] - - # رسم بياني شريطي - fig = go.Figure(data=[ - go.Bar(x=models, y=values, marker_color='rgb(26, 118, 255)') - ]) - - fig.update_layout( - title="مقارنة التكلفة الإجمالية بين نماذج التسعير", - xaxis_title="نموذج التسعير", - yaxis_title="التكلفة الإجمالية (ريال)" - ) - - st.plotly_chart(fig, use_container_width=True) - - # أزرار إدارة النماذج - col1, col2 = st.columns(2) - with col1: - if st.button("حذف جميع النماذج", key="clear_comparison"): - st.session_state.pricing_models = [] - st.success("تم مسح جميع نماذج المقارنة!") - st.rerun() - - with col2: - # تحديد نموذج للحذف - model_to_delete = st.selectbox( - "اختر نموذج للحذف من المقارنة", - options=[model.get('name', f"نموذج {i+1}") for i, model in enumerate(st.session_state.pricing_models)], - key="model_to_delete" - ) - - if st.button("حذف النموذج المحدد", key="delete_selected_model"): - for i, model in enumerate(st.session_state.pricing_models): - if model.get('name') == model_to_delete: - st.session_state.pricing_models.pop(i) - st.warning(f"تم حذف النموذج '{model_to_delete}'") - st.rerun() - break - else: - st.info("لا توجد نماذج للمقارنة حالياً. يرجى إضافة النموذج الحالي للمقارنة أولاً.") - - with utilities_tab5: - st.markdown("### إنشاء تقارير التسعير") - st.markdown("يمكنك استخدام هذه الأداة لإنشاء تقارير مفصلة عن التسعير.") - - report_type = st.selectbox( - "اختر نوع التقرير", - options=["تقرير ملخص", "تقرير تفصيلي", "تقرير المقارنة"], - key="report_type_selector" - ) - - if st.button("إنشاء التقرير", key="generate_report_button"): - if report_type == "تقرير ملخص" and 'current_pricing' in st.session_state and st.session_state.current_pricing is not None: - # إنشاء تقرير ملخص - if isinstance(st.session_state.current_pricing.get('items'), pd.DataFrame): - df = st.session_state.current_pricing['items'].copy() - - # حساب الإجماليات - total_price = df['الإجمالي'].sum() if 'الإجمالي' in df.columns else 0 - - # تقدير تكاليف المكونات - materials_cost = total_price * 0.6 # تقدير تقريبي للمواد - labor_cost = total_price * 0.25 # تقدير تقريبي للعمالة - equipment_cost = total_price * 0.15 # تقدير تقريبي للمعدات - admin_cost = total_price * 0.05 # تقدير تقريبي للمصاريف الإدارية - profit_margin = total_price * 0.1 # تقدير تقريبي لهامش الربح - final_total = total_price * 1.15 # إجمالي بعد إضافة المصاريف الإدارية وهامش الربح - - # إنشاء جدول الملخص - summary = pd.DataFrame({ - "بند التكلفة": ["إجمالي المواد", "إجمالي الأجور", "إجمالي المعدات", "المصاريف الإدارية", "هامش الربح", "الإجمالي النهائي"], - "القيمة": [ - materials_cost, - labor_cost, - equipment_cost, - admin_cost, - profit_margin, - final_total - ] - }) - - # تنسيق التقرير - styled_df = summary.style.format({ - "القيمة": "{:,.2f} ريال" - }) - - # تحويل الجدول إلى HTML - html = f""" - - - - تقرير ملخص التسعير - - - -
-

تقرير ملخص التسعير

-

{st.session_state.current_pricing.get('name', 'تسعير بدون اسم')}

-

تاريخ التقرير: {datetime.now().strftime('%Y-%m-%d %H:%M')}

-
- -

ملخص التكاليف

- {styled_df.to_html()} - -

البيانات الأساسية

- - - - - - """ - - # تقديم زر التنزيل - st.download_button( - label="تنزيل التقرير الملخص", - data=html, - file_name="pricing_summary_report.html", - mime="text/html", - key="download_summary_report" - ) - - st.success("تم إنشاء التقرير الملخص بنجاح!") - else: - st.error("تعذر قراءة بيانات التسعير الحالي. يرجى التأكد من وجود تسعير صالح.") - - elif report_type == "تقرير تفصيلي" and 'current_pricing' in st.session_state and st.session_state.current_pricing is not None: - # سيتم تنفيذ التقرير التفصيلي - st.info("جاري إعداد التقرير التفصيلي...") - # هنا يمكن تنفيذ كود إنشاء التقرير التفصيلي - st.success("تم إنشاء التقرير التفصيلي. سيتم تطوير هذه الميزة قريباً.") - - elif report_type == "تقرير المقارنة" and 'pricing_models' in st.session_state and len(st.session_state.pricing_models) > 0: - # سيتم تنفيذ تقرير المقارنة - st.info("جاري إعداد تقرير المقارنة...") - # هنا يمكن تنفيذ كود إنشاء تقرير المقارنة - st.success("تم إنشاء تقرير المقارنة. سيتم تطوير هذه الميزة قريباً.") - - else: - st.error("لا توجد بيانات كافية لإنشاء التقرير المطلوب. يرجى التأكد من وجود تسعير أو نماذج مقارنة.") \ No newline at end of file diff --git a/modules/pricing/pricing_app.py.backup b/modules/pricing/pricing_app.py.backup deleted file mode 100644 index a3a67844c9b1aebbe11cb78b097f39389a221963..0000000000000000000000000000000000000000 --- a/modules/pricing/pricing_app.py.backup +++ /dev/null @@ -1,1242 +0,0 @@ -""" -تطبيق وحدة التسعير المتكاملة -""" - -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 random -import os -import time -import io - -from modules.pricing.services.standard_pricing import StandardPricing -from modules.pricing.services.unbalanced_pricing import UnbalancedPricing -from modules.pricing.services.local_content_calculator import LocalContentCalculator -from modules.pricing.services.price_prediction import PricePrediction -from utils.excel_handler import export_to_excel -from utils.helpers import format_number, format_currency - - -class PricingApp: - """وحدة التسعير المتكاملة""" - - def __init__(self): - """تهيئة وحدة التسعير المتكاملة""" - self.pricing_methods = [ - "التسعير القياسي", - "التسعير غير المتزن", - "التسعير التنافسي", - "التسعير الموجه بالربحية" - ] - - # تهيئة خدمات التسعير - self.standard_pricing = StandardPricing() - self.unbalanced_pricing = UnbalancedPricing() - self.local_content = LocalContentCalculator() - self.price_prediction = PricePrediction() - - def render(self): - """عرض واجهة وحدة التسعير""" - - st.markdown("

وحدة التسعير المتكاملة

", unsafe_allow_html=True) - - tabs = st.tabs([ - "إنشاء تسعير جديد", - "تحليل سعر البند", - "نموذج التسعير الشامل", - "التسعير غير المتزن", - "المحتوى المحلي" - ]) - - with tabs[0]: - self._render_new_pricing_tab() - - with tabs[1]: - self._render_item_analysis_tab() - - with tabs[2]: - self._render_comprehensive_pricing_tab() - - with tabs[3]: - self._render_unbalanced_pricing_tab() - - with tabs[4]: - self._render_local_content_tab() - - def _render_item_analysis_tab(self): - """عرض تبويب تحليل سعر البند""" - - st.markdown("### تحليل سعر البند") - - # التحقق من وجود تسعير حالي - if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: - st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") - return - - # اختيار البند للتحليل - if 'current_pricing' in st.session_state and st.session_state.current_pricing is not None: - items = st.session_state.current_pricing['items'] - item_options = items['رقم البند'].tolist() - selected_item = st.selectbox("اختر البند للتحليل", item_options) - - if selected_item: - item_data = items[items['رقم البند'] == selected_item].iloc[0] - - st.markdown(f"### تحليل البند: {selected_item}") - st.markdown(f"**وصف البند**: {item_data['وصف البند']}") - st.markdown(f"**الوحدة**: {item_data['الوحدة']}") - st.markdown(f"**الكمية**: {item_data['الكمية']}") - st.markdown(f"**سعر الوحدة**: {item_data['سعر الوحدة']:,.2f} ريال") - - # تحليل مكونات السعر - st.markdown("### تحليل مكونات السعر") - - # عناصر التكلفة الافتراضية - cost_components = { - 'المواد': 0.6, # 60% من التكلفة - 'العمالة': 0.25, # 25% من التكلفة - 'المعدات': 0.1, # 10% من التكلفة - 'نفقات عامة': 0.05 # 5% من التكلفة - } - - # حساب تكلفة كل عنصر - unit_price = item_data['سعر الوحدة'] - component_values = {k: v * unit_price for k, v in cost_components.items()} - - # عرض مكونات التكلفة في جدول - components_df = pd.DataFrame({ - 'العنصر': component_values.keys(), - 'نسبة من التكلفة': [f"{v*100:.1f}%" for v in cost_components.values()], - 'القيمة (ريال)': [f"{v:,.2f}" for v in component_values.values()] - }) - - st.table(components_df) - - # رسم بياني لمكونات التكلفة - fig = px.pie( - names=list(component_values.keys()), - values=list(component_values.values()), - title='توزيع مكونات التكلفة' - ) - - st.plotly_chart(fig) - - # تحليل تاريخي للأسعار - st.markdown("### تحليل تاريخي للأسعار") - - # بيانات تاريخية افتراضية - historical_data = { - 'التاريخ': ['2020-01', '2020-07', '2021-01', '2021-07', '2022-01', '2022-07', '2023-01', '2023-07'], - 'السعر': [ - unit_price * 0.7, - unit_price * 0.75, - unit_price * 0.8, - unit_price * 0.85, - unit_price * 0.9, - unit_price * 0.95, - unit_price, - unit_price * 1.05 - ] - } - - hist_df = pd.DataFrame(historical_data) - - # رسم بياني للتحليل التاريخي - fig = px.line( - hist_df, - x='التاريخ', - y='السعر', - title='تطور سعر الوحدة عبر الزمن', - markers=True - ) - - st.plotly_chart(fig) - - # المقارنة مع الأسعار المرجعية - st.markdown("### المقارنة مع الأسعار المرجعية") - - # بيانات مرجعية افتراضية - reference_data = { - 'المصدر': ['قاعدة البيانات الداخلية', 'دليل الأسعار الاسترشادي', 'متوسط أسعار السوق', 'أسعار المشاريع المماثلة'], - 'السعر المرجعي': [ - unit_price * 0.95, - unit_price * 1.05, - unit_price * 1.1, - unit_price * 0.9 - ] - } - - ref_df = pd.DataFrame(reference_data) - ref_df['الفرق عن السعر الحالي'] = ref_df['السعر المرجعي'] - unit_price - ref_df['نسبة الفرق'] = (ref_df['الفرق عن السعر الحالي'] / unit_price * 100).round(2).astype(str) + '%' - - st.table(ref_df) - - def _render_new_pricing_tab(self): - """عرض تبويب إنشاء تسعير جديد""" - - st.markdown("### إنشاء تسعير جديد") - - col1, col2 = st.columns(2) - - with col1: - tender_name = st.text_input("اسم المناقصة") - client = st.text_input("الجهة المالكة") - pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods) - - with col2: - tender_number = st.text_input("رقم المناقصة") - location = st.text_input("الموقع") - submission_date = st.date_input("تاريخ التقديم") - - # خيارات بيانات البنود - st.markdown("### بيانات البنود") - - data_source = st.radio( - "مصدر بيانات البنود", - ["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات"] - ) - - if data_source == "إدخال يدوي": - # ضبط CSS لتحسين ظهور الواجهة العربية - st.markdown(""" - - """, unsafe_allow_html=True) - - # تهيئة قائمة الوحدات المتاحة - unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"] - - # إنشاء بيانات افتراضية إذا لم تكن موجودة - if 'manual_items' not in st.session_state: - # إنشاء DataFrame فارغ - manual_items = pd.DataFrame(columns=[ - 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي' - ]) - - # إضافة بضعة صفوف افتراضية - default_items = pd.DataFrame({ - 'رقم البند': ["A1", "A2", "A3", "A4", "A5"], - 'وصف البند': [ - "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", - "توريد وتركيب حديد التسليح للأساسات", - "أعمال العزل المائي للأساسات", - "أعمال الردم والدك للأساسات", - "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة" - ], - 'الوحدة': ["م3", "طن", "م2", "م3", "م3"], - 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0], - 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0], - 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0] - }) - - manual_items = pd.concat([manual_items, default_items]) - st.session_state.manual_items = manual_items - - # عرض واجهة إدخال البنود - st.markdown("### إدخال تفاصيل البنود") - - # التحقق من استخدام طريقة الإدخال البسيطة - use_simple_input = st.checkbox("استخدام طريقة الإدخال البسيطة", value=True) - - if use_simple_input: - # عرض البنود الحالية كجدول للعرض فقط - st.markdown("### جدول البنود الحالية") - st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True) - - # إضافة بند جديد - st.markdown("### إضافة بند جديد") - col1, col2 = st.columns(2) - - with col1: - new_id = st.text_input("رقم البند", value=f"A{len(st.session_state.manual_items)+1}") - new_desc = st.text_area("وصف البند", value="") - - with col2: - new_unit = st.selectbox("الوحدة", options=unit_options) - new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f") - new_price = st.number_input("سعر الوحدة", value=0.0, min_value=0.0, format="%.2f") - - new_total = new_qty * new_price - st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال") - - if st.button("إضافة البند"): - # التحقق من صحة البيانات - if new_id and new_desc and new_qty > 0: - # إنشاء صف جديد - new_row = pd.DataFrame({ - 'رقم البند': [new_id], - 'وصف البند': [new_desc], - 'الوحدة': [new_unit], - 'الكمية': [float(new_qty)], - 'سعر الوحدة': [float(new_price)], - 'الإجمالي': [float(new_total)] - }) - - # إضافة الصف إلى DataFrame - st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True) - st.success("تم إضافة البند بنجاح!") - st.rerun() - else: - st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.") - - # تعديل البنود الحالية - st.markdown("### تعديل البنود الحالية") - - # تحديد البند المراد تعديله - item_to_edit = st.selectbox( - "اختر البند للتعديل", - options=st.session_state.manual_items['رقم البند'].tolist(), - format_func=lambda x: f"{x}: {st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == x]['وصف البند'].values[0][:30]}..." - ) - - if item_to_edit: - # الحصول على مؤشر الصف للبند المحدد - idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == item_to_edit].index[0] - row = st.session_state.manual_items.loc[idx] - - # إنشاء نموذج تعديل - col1, col2 = st.columns(2) - - with col1: - edited_id = st.text_input("رقم البند (تعديل)", value=row['رقم البند'], key="edit_id") - edited_desc = st.text_area("وصف البند (تعديل)", value=row['وصف البند'], key="edit_desc") - - with col2: - edited_unit = st.selectbox( - "الوحدة (تعديل)", - options=unit_options, - index=unit_options.index(row['الوحدة']) if row['الوحدة'] in unit_options else 0, - key="edit_unit" - ) - edited_qty = st.number_input("الكمية (تعديل)", value=float(row['الكمية']), min_value=0.0, format="%.2f", key="edit_qty") - edited_price = st.number_input("سعر الوحدة (تعديل)", value=float(row['سعر الوحدة']), min_value=0.0, format="%.2f", key="edit_price") - - edited_total = edited_qty * edited_price - st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال") - - col1, col2 = st.columns(2) - with col1: - if st.button("حفظ التعديلات"): - # تحديث البند - st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id - st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc - st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit - st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty - st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price - st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total - - st.success("تم تحديث البند بنجاح!") - st.rerun() - - with col2: - if st.button("حذف هذا البند"): - st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True) - st.warning("تم حذف البند!") - st.rerun() - - # المجموع الكلي - total = st.session_state.manual_items['الإجمالي'].sum() - st.metric("المجموع الكلي", f"{total:,.2f} ريال") - - # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية - edited_items = st.session_state.manual_items.copy() - - else: - # عرض رسالة توضح أن طريقة الإدخال البسيطة هي الأفضل - st.warning("لتجنب مشاكل عدم التوافق في أنواع البيانات، يُفضل استخدام طريقة الإدخال البسيطة.") - - # محاولة استخدام المحرر القياسي مع معالجة الأخطاء - try: - # تحويل البيانات إلى الأنواع المناسبة - for col in st.session_state.manual_items.columns: - if col in ['رقم البند', 'وصف البند', 'الوحدة']: - st.session_state.manual_items[col] = st.session_state.manual_items[col].astype(str) - - # عرض المحرر (للقراءة فقط) - st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True) - - # إنشاء نظام تعديل منفصل - st.markdown("### تعديل أسعار الوحدات") - - for idx, row in st.session_state.manual_items.iterrows(): - col1, col2 = st.columns([3, 1]) - - with col1: - st.text(f"{row['رقم البند']}: {row['وصف البند'][:50]}") - - with col2: - price = st.number_input( - f"سعر الوحدة ({row['الوحدة']})", - value=float(row['سعر الوحدة']), - min_value=0.0, - key=f"price_{idx}" - ) - - # تحديث السعر والإجمالي - st.session_state.manual_items.at[idx, 'سعر الوحدة'] = price - st.session_state.manual_items.at[idx, 'الإجمالي'] = price * row['الكمية'] - - # المجموع الكلي - total = st.session_state.manual_items['الإجمالي'].sum() - st.metric("المجموع الكلي", f"{total:,.2f} ريال") - - # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية - edited_items = st.session_state.manual_items.copy() - - except Exception as e: - st.error(f"حدث خطأ: {str(e)}") - st.info("يرجى استخدام طريقة الإدخال البسيطة لتجنب هذه المشكلة.") - - elif data_source == "استيراد من Excel": - uploaded_file = st.file_uploader("رفع ملف Excel", type=["xlsx", "xls"]) - - if uploaded_file is not None: - st.success("تم رفع الملف بنجاح") - # محاكاة قراءة الملف - st.markdown("### معاينة البيانات المستوردة") - - # إنشاء بيانات افتراضية - import_items = pd.DataFrame({ - 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"], - 'وصف البند': [ - "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", - "توريد وتركيب حديد التسليح للأساسات", - "أعمال العزل المائي للأساسات", - "أعمال الردم والدك للأساسات", - "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", - "توريد وتركيب حديد التسليح للأعمدة", - "أعمال البلوك للجدران" - ], - 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"], - 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0], - 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }) - - st.dataframe(import_items) - - if st.button("استيراد البيانات"): - st.session_state.manual_items = import_items.copy() - st.session_state.manual_items_modified = True - st.success("تم استيراد البيانات بنجاح!") - st.rerun() - - else: # استيراد من وحدة تحليل المستندات - available_documents = [ - "كراسة شروط مشروع توسعة مستشفى الملك فهد", - "جدول كميات صيانة محطات المياه", - "مخططات إنشاء مدرسة ثانوية" - ] - - selected_doc = st.selectbox("اختر المستند", available_documents) - - if st.button("استيراد البيانات من تحليل المستند"): - # محاكاة استيراد البيانات - with st.spinner("جاري استيراد البيانات..."): - time.sleep(2) - - # إنشاء بيانات افتراضية - doc_items = pd.DataFrame({ - 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"], - 'وصف البند': [ - "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", - "توريد وتركيب حديد التسليح للأساسات", - "أعمال العزل المائي للأساسات", - "أعمال الردم والدك للأساسات", - "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", - "توريد وتركيب حديد التسليح للأعمدة", - "أعمال البلوك للجدران" - ], - 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"], - 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0], - 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }) - - st.session_state.manual_items = doc_items.copy() - st.success("تم استيراد البيانات من تحليل المستند بنجاح!") - st.dataframe(doc_items) - - # زر بدء التسعير - if st.button("بدء التسعير"): - # تحقق من صحة البيانات - if 'manual_items' in st.session_state and not st.session_state.manual_items.empty: - # التأكد من حساب الإجمالي قبل الحفظ - st.session_state.manual_items['الإجمالي'] = st.session_state.manual_items['الكمية'] * st.session_state.manual_items['سعر الوحدة'] - - # حفظ بيانات التسعير الحالي - st.session_state.current_pricing = { - 'name': tender_name, - 'number': tender_number, - 'client': client, - 'location': location, - 'method': pricing_method, - 'submission_date': submission_date, - 'items': st.session_state.manual_items.copy(), - 'status': 'جديد', - 'created_at': datetime.now() - } - - # الانتقال إلى تبويب نموذج التسعير الشامل - st.success("تم إنشاء التسعير بنجاح! يمكنك الانتقال إلى نموذج التسعير الشامل.") - else: - st.error("يرجى إدخال بيانات البنود أولاً.") - - def _render_comprehensive_pricing_tab(self): - """عرض تبويب نموذج التسعير الشامل""" - - st.markdown("### نموذج التسعير الشامل") - - # التحقق من وجود تسعير حالي - if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: - st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") - return - - # عرض معلومات التسعير الحالي - pricing = st.session_state.current_pricing - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("اسم المناقصة", pricing['name']) - st.metric("الجهة المالكة", pricing['client']) - - with col2: - st.metric("رقم المناقصة", pricing['number']) - st.metric("تاريخ التقديم", pricing['submission_date'].strftime("%Y-%m-%d")) - - with col3: - st.metric("طريقة التسعير", pricing['method']) - st.metric("الموقع", pricing['location']) - - # عرض البنود والتسعير - st.markdown("### بنود التسعير") - - items = pricing['items'].copy() - - # إضافة أسعار الوحدة للمحاكاة - if 'سعر الوحدة' in items.columns and (items['سعر الوحدة'] == 0).all(): - items['سعر الوحدة'] = [ - round(random.uniform(1000, 3000), 2), # الخرسانة - round(random.uniform(5000, 7000), 2), # الحديد - round(random.uniform(100, 200), 2), # العزل - round(random.uniform(50, 100), 2), # الردم - round(random.uniform(1200, 3500), 2), # الخرسانة للأعمدة - ] - - if len(items) > 5: - for i in range(5, len(items)): - items.at[i, 'سعر الوحدة'] = round(random.uniform(500, 5000), 2) - - # حساب الإجمالي - items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] - - # عرض البنود - st.dataframe(items, use_container_width=True, hide_index=True) - - - # ✅ التوصية الذكية باستخدام OpenAI - with st.expander("🔍 توليد توصية ذكية باستخدام AI"): - if st.button("🔍 توليد توصية ذكية باستخدام AI", use_container_width=True): - import openai - import os - - client = openai.OpenAI(api_key=os.environ.get("ai")) - - items_df = items.copy() - prompt = f"""قم بتحليل الجدول التالي للبنود في مشروع إنشاء، وقدم توصية ذكية لتحسين التسعير وضمان التوازن المالي. الجدول يحتوي على البنود، الكميات، الأسعار، والإجماليات:\n\n{items_df.to_string(index=False)}\n\nالتوصية:\n""" - - try: - with st.spinner("جاري توليد التوصية..."): - response = client.chat.completions.create( - model="gpt-4", - messages=[ - {"role": "system", "content": "أنت خبير في تسعير مشاريع البناء والبنية التحتية."}, - {"role": "user", "content": prompt} - ], - temperature=0.4, - max_tokens=500 - ) - - recommendation = response.choices[0].message.content - st.success("تم توليد التوصية بنجاح!") - st.markdown("#### التوصية الذكية:") - st.info(recommendation) - - except Exception as e: - st.error(f"حدث خطأ أثناء الاتصال بنموذج OpenAI: {e}") - st.info("يجب التأكد من تثبيت أحدث إصدار من مكتبة OpenAI: `pip install openai --upgrade`") - - # واجهة تعديل أسعار الوحدات - st.markdown("### تعديل أسعار الوحدات") - - # تقسيم البنود إلى مجموعتين للعرض - col1, col2 = st.columns(2) - half = len(items) // 2 + len(items) % 2 - - with col1: - for idx in range(half): - if idx < len(items): - row = items.iloc[idx] - price = st.number_input( - f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})", - value=float(row['سعر الوحدة']), - min_value=0.0, - key=f"price1_{idx}" - ) - items.at[idx, 'سعر الوحدة'] = price - items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية'] - - with col2: - for idx in range(half, len(items)): - row = items.iloc[idx] - price = st.number_input( - f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})", - value=float(row['سعر الوحدة']), - min_value=0.0, - key=f"price2_{idx}" - ) - items.at[idx, 'سعر الوحدة'] = price - items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية'] - - # حساب وعرض إجماليات التسعير - total_price = items['الإجمالي'].sum() - - st.markdown("### إجماليات التسعير") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي التكاليف المباشرة", f"{total_price:,.2f} ريال") - - with col2: - overhead_percentage = st.slider("نسبة المصاريف العامة والأرباح (%)", 5, 30, 15) - overhead_value = total_price * overhead_percentage / 100 - st.metric("المصاريف العامة والأرباح", f"{overhead_value:,.2f} ريال") - - with col3: - grand_total = total_price + overhead_value - st.metric("الإجمالي النهائي", f"{grand_total:,.2f} ريال") - - # رسم بياني لتوزيع التكاليف - st.markdown("### تحليل التكاليف") - - # حساب النسب المئوية لكل بند - pie_data = items.copy() - pie_data['نسبة من إجمالي التكاليف'] = pie_data['الإجمالي'] / total_price * 100 - - fig = px.pie( - pie_data, - values='نسبة من إجمالي التكاليف', - names='وصف البند', - title='توزيع التكاليف حسب البنود', - hole=0.4 - ) - - st.plotly_chart(fig, use_container_width=True) - - # أزرار العمليات - col1, col2, col3 = st.columns(3) - - with col1: - if st.button("حفظ التسعير"): - # تحديث بيانات التسعير الحالي - st.session_state.current_pricing['items'] = items.copy() - st.success("تم حفظ التسعير بنجاح!") - - with col2: - if st.button("تصدير إلى Excel"): - st.success("تم تصدير التسعير إلى Excel بنجاح!") - - with col3: - if st.button("تحليل المخاطر المالية"): - st.success("تم إرسال الطلب إلى وحدة تحليل المخاطر!") - - def _render_unbalanced_pricing_tab(self): - """عرض تبويب التسعير غير المتزن""" - - st.markdown("### التسعير غير المتزن") - - # التحقق من وجود تسعير حالي - if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: - st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") - return - - # شرح التسعير غير المتزن - with st.expander("ما هو التسعير غير المتزن؟", expanded=False): - st.markdown(""" - **التسعير غير المتزن** هو استراتيجية تسعير تقوم على توزيع التكاليف بين بنود المناقصة بشكل غير متساوٍ، مع الحفاظ على إجمالي قيمة العطاء. - - ### استراتيجيات التسعير غير المتزن: - - 1. **التحميل الأمامي (Front Loading)**: زيادة أسعار البنود المبكرة في المشروع للحصول على تدفق نقدي أفضل في بداية المشروع. - 2. **التحميل الخلفي (Back Loading)**: زيادة أسعار البنود المتأخرة في المشروع. - 3. **تحميل البنود المؤكدة**: زيادة أسعار البنود التي من المؤكد تنفيذها بالكميات المحددة. - 4. **تخفيض أسعار البنود المحتملة**: تخفيض أسعار البنود التي قد تزيد كمياتها أثناء التنفيذ. - - ### مزايا التسعير غير المتزن: - - - تحسين التدفق النقدي للمشروع. - - تعظيم الربحية في حالة التغييرات والأوامر التغييرية. - - زيادة فرص الفوز بالمناقصة. - - ### مخاطر التسعير غير المتزن: - - - قد يتم رفض العطاء إذا كان عدم التوازن واضحاً. - - قد تتأثر السمعة سلباً إذا تم استخدامه بشكل مفرط. - - قد يؤدي إلى خسائر إذا لم يتم تنفيذ البنود ذات الأسعار العالية. - """) - - # عرض بنود التسعير الحالي - items = st.session_state.current_pricing['items'].copy() - - # إضافة عمود إستراتيجية التسعير - if 'إستراتيجية التسعير' not in items.columns: - items['إستراتيجية التسعير'] = 'متوازن' - - st.markdown("### إستراتيجية التسعير غير المتزن") - - # اختيار الإستراتيجية - strategy = st.selectbox( - "اختر إستراتيجية التسعير", - [ - "تحميل أمامي (Front Loading)", - "تحميل البنود المؤكدة", - "تخفيض البنود المحتمل زيادتها", - "إستراتيجية مخصصة" - ] - ) - - # تطبيق الإستراتيجية المختارة - if strategy == "تحميل أمامي (Front Loading)": - # محاكاة تحميل أمامي - items_count = len(items) - early_items = items.iloc[:items_count//3].index - middle_items = items.iloc[items_count//3:2*items_count//3].index - late_items = items.iloc[2*items_count//3:].index - - # تطبيق الزيادة والنقصان - for idx in early_items: - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.3 # زيادة 30% - items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - for idx in middle_items: - items.at[idx, 'إستراتيجية التسعير'] = 'متوازن' - - for idx in late_items: - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30% - items.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - elif strategy == "تحميل البنود المؤكدة": - # محاكاة - اعتبار بعض البنود مؤكدة - confirmed_items = [0, 2, 4] # الأصفار-مستندة - variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items] - - # تطبيق الزيادة والنقصان - for idx in confirmed_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 # زيادة 25% - items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - for idx in variable_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 # نقص 15% - items.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - elif strategy == "تخفيض البنود المحتمل زيادتها": - # محاكاة - اعتبار بعض البنود محتمل زيادتها - variable_items = [1, 3] # الأصفار-مستندة - other_items = [idx for idx in range(len(items)) if idx not in variable_items] - - # تطبيق الزيادة والنقصان - for idx in variable_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30% - items.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - for idx in other_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 # زيادة 15% - items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - else: # إستراتيجية مخصصة - st.markdown("### تعديل أسعار البنود يدوياً") - st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً.") - - # حساب الإجمالي بعد التعديل - items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] - - # تعيين ألوان للإستراتيجيات - def highlight_strategy(val): - if val == 'زيادة': - return 'background-color: #a8e6cf' - elif val == 'نقص': - return 'background-color: #ff9aa2' - return '' - - # عرض الجدول مع تنسيق - st.markdown("### بنود التسعير غير المتزن") - styled_items = items.style.applymap(highlight_strategy, subset=['إستراتيجية التسعير']) - st.dataframe(styled_items, use_container_width=True) - - # المقارنة بين التسعير المتوازن وغير المتوازن - st.markdown("### مقارنة التسعير المتوازن وغير المتوازن") - - original_items = st.session_state.current_pricing['items'].copy() - original_total = original_items['الإجمالي'].sum() - unbalanced_total = items['الإجمالي'].sum() - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي التسعير المتوازن", f"{original_total:,.2f} ريال") - - with col2: - st.metric("إجمالي التسعير غير المتوازن", f"{unbalanced_total:,.2f} ريال") - - with col3: - diff = unbalanced_total - original_total - st.metric("الفرق", f"{diff:,.2f} ريال", delta=f"{diff/original_total*100:.1f}%") - - # المعايرة للحفاظ على إجمالي التسعير - if abs(diff) > 1: # إذا كان هناك فرق كبير - if st.button("معايرة الأسعار للحفاظ على إجمالي التسعير"): - # تعديل الأسعار للحفاظ على إجمالي التكلفة - adjustment_factor = original_total / unbalanced_total - items['سعر الوحدة'] = items['سعر الوحدة'] * adjustment_factor - items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] - - st.success(f"تم تعديل الأسعار للحفاظ على إجمالي التسعير الأصلي ({original_total:,.2f} ريال)") - st.dataframe(items, use_container_width=True) - - # رسم بياني للمقارنة - st.markdown("### تحليل بصري للتسعير غير المتوازن") - - # إعداد البيانات للرسم البياني - chart_data = pd.DataFrame({ - 'وصف البند': original_items['وصف البند'], - 'التسعير المتوازن': original_items['الإجمالي'], - 'التسعير غير المتوازن': items['الإجمالي'] - }) - - # رسم بياني شريطي للمقارنة - fig = go.Figure() - - fig.add_trace(go.Bar( - x=chart_data['وصف البند'], - y=chart_data['التسعير المتوازن'], - name='التسعير المتوازن', - marker_color='rgb(55, 83, 109)' - )) - - fig.add_trace(go.Bar( - x=chart_data['وصف البند'], - y=chart_data['التسعير غير المتوازن'], - name='التسعير غير المتوازن', - marker_color='rgb(26, 118, 255)' - )) - - fig.update_layout( - title='مقارنة بين التسعير المتوازن وغير المتوازن', - xaxis_tickfont_size=14, - yaxis=dict( - title='الإجمالي (ريال)', - titlefont_size=16, - tickfont_size=14, - ), - legend=dict( - x=0, - y=1.0, - bgcolor='rgba(255, 255, 255, 0)', - bordercolor='rgba(255, 255, 255, 0)' - ), - barmode='group', - bargap=0.15, - bargroupgap=0.1 - ) - - st.plotly_chart(fig, use_container_width=True) - - # زر حفظ التسعير غير المتوازن - if st.button("حفظ التسعير غير المتوازن"): - st.session_state.current_pricing['items'] = items.copy() - st.session_state.current_pricing['method'] = "التسعير غير المتزن" - st.success("تم حفظ التسعير غير المتوازن بنجاح!") - - def _render_local_content_tab(self): - """عرض تبويب المحتوى المحلي""" - - st.markdown("### تحليل المحتوى المحلي") - - # التحقق من وجود تسعير حالي - if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: - st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") - return - - # شرح المحتوى المحلي - with st.expander("ما هو المحتوى المحلي؟", expanded=False): - st.markdown(""" - **المحتوى المحلي** هو نسبة المنتجات والخدمات والقوى العاملة المحلية المستخدمة في المشروع. يهدف إلى زيادة مساهمة المنتجات والخدمات المحلية في المشاريع. - - ### مكونات المحتوى المحلي: - - 1. **المنتجات**: المنتجات والمواد المصنعة محلياً. - 2. **الخدمات**: الخدمات المقدمة من شركات محلية. - 3. **القوى العاملة**: العمالة والكوادر الفنية والإدارية المحلية. - - ### أهمية المحتوى المحلي: - - - تعزيز الاقتصاد المحلي وخلق فرص عمل. - - تحقيق أهداف رؤية 2030 في زيادة المحتوى المحلي. - - التأهل للمشاريع الحكومية التي تتطلب نسبة محتوى محلي محددة. - - الحصول على حوافز وأفضلية في المناقصات الحكومية. - - ### متطلبات المحتوى المحلي: - - - نسبة المحتوى المحلي للقوى العاملة: 80% - - نسبة المحتوى المحلي للمنتجات: 70% - - نسبة المحتوى المحلي للخدمات: 60% - """) - - # عرض لوحة إدخال بيانات المحتوى المحلي - st.markdown("### بيانات المحتوى المحلي") - - # التبويبات لأنواع المحتوى المحلي - lc_tabs = st.tabs(["المنتجات", "الخدمات", "القوى العاملة", "التحليل"]) - - with lc_tabs[0]: # المنتجات - st.markdown("#### بيانات المنتجات") - - # إنشاء بيانات افتراضية للمنتجات إذا لم تكن موجودة - if 'local_content_products' not in st.session_state: - st.session_state.local_content_products = pd.DataFrame({ - 'المنتج': [ - "خرسانة مسلحة", - "حديد تسليح", - "بلوك خرساني", - "عزل مائي", - "دهانات" - ], - 'الكمية': [250, 25, 400, 500, 600], - 'سعر_الوحدة': [1200, 6000, 200, 100, 50], - 'التكلفة_الإجمالية': [300000, 150000, 80000, 50000, 30000], - 'نسبة_المحتوى_المحلي': [0.95, 0.70, 0.98, 0.60, 0.80] - }) - - # حساب التكلفة الإجمالية - st.session_state.local_content_products['التكلفة_الإجمالية'] = st.session_state.local_content_products['الكمية'] * st.session_state.local_content_products['سعر_الوحدة'] - - # عرض جدول البنود مع إمكانية التعديل - edited_products = st.data_editor( - st.session_state.local_content_products, - use_container_width=True, - hide_index=True, - num_rows="dynamic" - ) - st.session_state.local_content_products = edited_products - - # عرض ملخص المنتجات - total_products_cost = edited_products['التكلفة_الإجمالية'].sum() - avg_local_content = (edited_products['التكلفة_الإجمالية'] * edited_products['نسبة_المحتوى_المحلي']).sum() / total_products_cost if total_products_cost > 0 else 0 - - st.markdown(f""" - **إجمالي تكلفة المنتجات**: {total_products_cost:,.2f} ريال - - **متوسط نسبة المحتوى المحلي للمنتجات**: {avg_local_content*100:.2f}% - - **المستهدف**: 70% - - **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.7 else "❌ غير ملتزم"} - """) - - with lc_tabs[1]: # الخدمات - st.markdown("#### بيانات الخدمات") - - # إنشاء بيانات افتراضية للخدمات إذا لم تكن موجودة - if 'local_content_services' not in st.session_state: - st.session_state.local_content_services = pd.DataFrame({ - 'الخدمة': [ - "تصميم معماري", - "إشراف هندسي", - "خدمات نقل", - "خدمات أمن وسلامة", - "صيانة ونظافة" - ], - 'التكلفة': [100000, 120000, 50000, 30000, 20000], - 'نسبة_المحتوى_المحلي': [0.90, 0.85, 0.90, 0.95, 0.95] - }) - - # عرض جدول الخدمات مع إمكانية التعديل - edited_services = st.data_editor( - st.session_state.local_content_services, - use_container_width=True, - hide_index=True, - num_rows="dynamic" - ) - st.session_state.local_content_services = edited_services - - # عرض ملخص الخدمات - total_services_cost = edited_services['التكلفة'].sum() - avg_local_content = (edited_services['التكلفة'] * edited_services['نسبة_المحتوى_المحلي']).sum() / total_services_cost if total_services_cost > 0 else 0 - - st.markdown(f""" - **إجمالي تكلفة الخدمات**: {total_services_cost:,.2f} ريال - - **متوسط نسبة المحتوى المحلي للخدمات**: {avg_local_content*100:.2f}% - - **المستهدف**: 60% - - **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.6 else "❌ غير ملتزم"} - """) - - with lc_tabs[2]: # القوى العاملة - st.markdown("#### بيانات القوى العاملة") - - # إنشاء بيانات افتراضية للقوى العاملة إذا لم تكن موجودة - if 'local_content_labor' not in st.session_state: - st.session_state.local_content_labor = pd.DataFrame({ - 'فئة_العمالة': [ - "مهندسون", - "فنيون", - "عمال بناء", - "إداريون", - "مشرفون" - ], - 'العدد': [5, 10, 30, 3, 4], - 'الراتب_الشهري': [15000, 8000, 3000, 10000, 12000], - 'المدة_بالأشهر': [12, 12, 12, 12, 12], - 'نسبة_المحتوى_المحلي': [0.75, 0.65, 0.60, 0.90, 0.80] - }) - - # حساب التكلفة الإجمالية - st.session_state.local_content_labor['التكلفة_الإجمالية'] = st.session_state.local_content_labor['العدد'] * st.session_state.local_content_labor['الراتب_الشهري'] * st.session_state.local_content_labor['المدة_بالأشهر'] - - # عرض جدول القوى العاملة مع إمكانية التعديل - edited_labor = st.data_editor( - st.session_state.local_content_labor, - use_container_width=True, - hide_index=True, - num_rows="dynamic" - ) - - # إعادة حساب التكلفة الإجمالية بعد التعديل - edited_labor['التكلفة_الإجمالية'] = edited_labor['العدد'] * edited_labor['الراتب_الشهري'] * edited_labor['المدة_بالأشهر'] - st.session_state.local_content_labor = edited_labor - - # عرض ملخص القوى العاملة - total_labor_cost = edited_labor['التكلفة_الإجمالية'].sum() - avg_local_content = (edited_labor['التكلفة_الإجمالية'] * edited_labor['نسبة_المحتوى_المحلي']).sum() / total_labor_cost if total_labor_cost > 0 else 0 - - st.markdown(f""" - **إجمالي تكلفة القوى العاملة**: {total_labor_cost:,.2f} ريال - - **متوسط نسبة المحتوى المحلي للقوى العاملة**: {avg_local_content*100:.2f}% - - **المستهدف**: 80% - - **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.8 else "❌ غير ملتزم"} - """) - - with lc_tabs[3]: # التحليل - st.markdown("#### تحليل المحتوى المحلي") - - # حساب المحتوى المحلي الإجمالي - try: - # تجميع بيانات تحليل المحتوى المحلي - products_cost = st.session_state.local_content_products['التكلفة_الإجمالية'].sum() - products_local_content = (st.session_state.local_content_products['التكلفة_الإجمالية'] * st.session_state.local_content_products['نسبة_المحتوى_المحلي']).sum() / products_cost if products_cost > 0 else 0 - - services_cost = st.session_state.local_content_services['التكلفة'].sum() - services_local_content = (st.session_state.local_content_services['التكلفة'] * st.session_state.local_content_services['نسبة_المحتوى_المحلي']).sum() / services_cost if services_cost > 0 else 0 - - labor_cost = st.session_state.local_content_labor['التكلفة_الإجمالية'].sum() - labor_local_content = (st.session_state.local_content_labor['التكلفة_الإجمالية'] * st.session_state.local_content_labor['نسبة_المحتوى_المحلي']).sum() / labor_cost if labor_cost > 0 else 0 - - # حساب الوزن النسبي لكل مكون - total_cost = products_cost + services_cost + labor_cost - products_weight = products_cost / total_cost if total_cost > 0 else 0 - services_weight = services_cost / total_cost if total_cost > 0 else 0 - labor_weight = labor_cost / total_cost if total_cost > 0 else 0 - - # حساب المحتوى المحلي الإجمالي - total_local_content = (products_local_content * products_weight) + (services_local_content * services_weight) + (labor_local_content * labor_weight) - - # عرض ملخص المحتوى المحلي - st.markdown("### ملخص المحتوى المحلي") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال") - - with col2: - st.metric("نسبة المحتوى المحلي الإجمالية", f"{total_local_content*100:.2f}%") - - with col3: - target_local_content = 0.7 # 70% - st.metric("الحالة", "ملتزم" if total_local_content >= target_local_content else "غير ملتزم", delta=f"{(total_local_content - target_local_content)*100:.2f}%") - - # عرض رسم بياني للمقارنة - st.markdown("### تحليل بصري للمحتوى المحلي") - - # رسم بياني شريطي لنسب المحتوى المحلي - categories = ['المنتجات', 'الخدمات', 'القوى العاملة', 'الإجمالي'] - actual_values = [products_local_content * 100, services_local_content * 100, labor_local_content * 100, total_local_content * 100] - target_values = [70, 60, 80, 70] # المستهدفات - - # تهيئة البيانات للرسم البياني - chart_data = pd.DataFrame({ - 'الفئة': categories, - 'النسبة الفعلية': actual_values, - 'النسبة المستهدفة': target_values - }) - - # رسم بياني شريطي للمقارنة - fig = go.Figure() - - fig.add_trace(go.Bar( - x=chart_data['الفئة'], - y=chart_data['النسبة الفعلية'], - name='النسبة الفعلية', - marker_color='rgb(26, 118, 255)' - )) - - fig.add_trace(go.Bar( - x=chart_data['الفئة'], - y=chart_data['النسبة المستهدفة'], - name='النسبة المستهدفة', - marker_color='rgb(55, 83, 109)' - )) - - fig.update_layout( - title='مقارنة بين النسب الفعلية والمستهدفة للمحتوى المحلي', - xaxis_tickfont_size=14, - yaxis=dict( - title='النسبة %', - titlefont_size=16, - tickfont_size=14, - ), - legend=dict( - x=0, - y=1.0, - bgcolor='rgba(255, 255, 255, 0)', - bordercolor='rgba(255, 255, 255, 0)' - ), - barmode='group', - bargap=0.15, - bargroupgap=0.1 - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض توصيات لتحسين نسبة المحتوى المحلي - st.markdown("### توصيات لتحسين نسبة المحتوى المحلي") - - recommendations = [] - - if products_local_content < 0.7: - recommendations.append("- زيادة نسبة المحتوى المحلي للمنتجات من خلال:") - recommendations.append(" - البحث عن موردين محليين للمنتجات ذات النسبة المنخفضة") - recommendations.append(" - استبدال المنتجات المستوردة ببدائل محلية") - recommendations.append(" - التعاون مع المصانع المحلية لتوطين صناعة المنتجات") - - if services_local_content < 0.6: - recommendations.append("- زيادة نسبة المحتوى المحلي للخدمات من خلال:") - recommendations.append(" - التعاقد مع شركات خدمات محلية") - recommendations.append(" - تحويل الخدمات المستعان بها من الخارج إلى شركات محلية") - recommendations.append(" - تأهيل الشركات المحلية لتقديم الخدمات المطلوبة") - - if labor_local_content < 0.8: - recommendations.append("- زيادة نسبة المحتوى المحلي للقوى العاملة من خلال:") - recommendations.append(" - زيادة توظيف الكوادر المحلية") - recommendations.append(" - تدريب وتأهيل العمالة المحلية") - recommendations.append(" - استبدال العمالة الأجنبية بكوادر محلية تدريجياً") - - if total_local_content < 0.7: - recommendations.append("- زيادة نسبة المحتوى المحلي الإجمالية من خلال:") - recommendations.append(" - إعادة توزيع الميزانية لصالح المكونات ذات النسبة العالية من المحتوى المحلي") - recommendations.append(" - وضع خطة مرحلية لزيادة المحتوى المحلي") - recommendations.append(" - التعاون مع اللجنة المحلية لزيادة المحتوى المحلي") - - if recommendations: - for rec in recommendations: - st.markdown(rec) - else: - st.success("تهانينا! نسبة المحتوى المحلي متوافقة مع المتطلبات.") - - # حساب تأثير المحتوى المحلي على التسعير - st.markdown("### تأثير المحتوى المحلي على التسعير") - - # تحديد عامل تعديل السعر بناءً على نسبة المحتوى المحلي - price_adjustment_factor = 1.0 - - if total_local_content >= 0.9: - price_adjustment_factor = 0.92 # خصم 8% للمحتوى المحلي العالي جداً - price_discount = "8%" - elif total_local_content >= 0.8: - price_adjustment_factor = 0.94 # خصم 6% للمحتوى المحلي العالي - price_discount = "6%" - elif total_local_content >= 0.7: - price_adjustment_factor = 0.96 # خصم 4% للمحتوى المحلي المتوسط - price_discount = "4%" - elif total_local_content >= 0.6: - price_adjustment_factor = 0.98 # خصم 2% للمحتوى المحلي المنخفض - price_discount = "2%" - else: - price_adjustment_factor = 1.0 # لا خصم - price_discount = "0%" - - # عرض تأثير المحتوى المحلي على التسعير - original_total = st.session_state.current_pricing['items']['الإجمالي'].sum() - adjusted_total = original_total * price_adjustment_factor - discount_amount = original_total - adjusted_total - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي التسعير الأصلي", f"{original_total:,.2f} ريال") - - with col2: - st.metric("نسبة الخصم بسبب المحتوى المحلي", price_discount) - - with col3: - st.metric("إجمالي التسعير بعد الخصم", f"{adjusted_total:,.2f} ريال", delta=f"-{discount_amount:,.2f} ريال") - - # أزرار العمليات - col1, col2 = st.columns(2) - - with col1: - if st.button("حفظ تحليل المحتوى المحلي"): - # حفظ بيانات المحتوى المحلي في التسعير الحالي - st.session_state.current_pricing['local_content'] = { - 'products': st.session_state.local_content_products.copy(), - 'services': st.session_state.local_content_services.copy(), - 'labor': st.session_state.local_content_labor.copy(), - 'total_local_content': total_local_content, - 'price_adjustment_factor': price_adjustment_factor - } - - st.success("تم حفظ تحليل المحتوى المحلي بنجاح!") - - with col2: - if st.button("تصدير تقرير المحتوى المحلي"): - st.success("تم تصدير تقرير المحتوى المحلي بنجاح!") - - except Exception as e: - st.error(f"حدث خطأ أثناء تحليل المحتوى المحلي: {str(e)}") - st.warning("تأكد من إدخال بيانات المحتوى المحلي بشكل صحيح في التبويبات السابقة.") \ No newline at end of file diff --git a/modules/pricing/pricing_engine.py b/modules/pricing/pricing_engine.py deleted file mode 100644 index 5bf6307ec13ede246bf6173c576e33cfe6acd644..0000000000000000000000000000000000000000 --- a/modules/pricing/pricing_engine.py +++ /dev/null @@ -1,430 +0,0 @@ -""" -وحدة التسعير المتكامل لنظام إدارة المناقصات - 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/pricing/services/construction_cost_calculator.py b/modules/pricing/services/construction_cost_calculator.py deleted file mode 100644 index 60c49b9505c8e5137b78c6794c4103e2f8114559..0000000000000000000000000000000000000000 --- a/modules/pricing/services/construction_cost_calculator.py +++ /dev/null @@ -1,1006 +0,0 @@ -""" -خدمة حاسبة تكاليف البناء -تقوم هذه الخدمة بحساب تكاليف البناء بشكل تفصيلي بناءً على المكونات المختلفة: -- المواد الخام -- العمالة -- المعدات -- المصاريف الإدارية -- هامش الربح -""" - -import pandas as pd -import numpy as np -from datetime import datetime -import os -import json -import sys -from typing import Dict, List, Optional, Union, Any - -# إضافة مسار النظام للوصول لملفات التكوين -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))) -try: - import config -except ImportError: - # إذا لم يتم العثور على ملف التكوين، نستخدم قيم افتراضية - class DefaultConfig: - DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../data")) - config = DefaultConfig - - # إنشاء مجلد البيانات إذا لم يكن موجودًا - if not os.path.exists(config.DATA_DIR): - os.makedirs(config.DATA_DIR) - -class ConstructionCostCalculator: - """خدمة حاسبة تكاليف البناء""" - - def __init__(self): - """تهيئة حاسبة تكاليف البناء""" - # تحميل بيانات المواد والأسعار المرجعية - self.material_rates = self._load_material_rates() - self.labor_rates = self._load_labor_rates() - self.equipment_rates = self._load_equipment_rates() - - # النسب الافتراضية للمصاريف الإدارية وهامش الربح - self.default_admin_expenses_percentage = 0.05 # 5% - self.default_profit_margin_percentage = 0.10 # 10% - - # معاملات التعديل الافتراضية - self.default_adjustment_factors = { - 'location_factor': 1.0, # معامل الموقع - 'time_factor': 1.0, # معامل الوقت - 'risk_factor': 1.0, # معامل المخاطر - 'market_factor': 1.0 # معامل السوق - } - - def _load_material_rates(self) -> Dict[str, Dict[str, Any]]: - """تحميل أسعار المواد""" - # محاكاة تحميل البيانات من مصدر بيانات - material_rates = { - # مواد الخرسانة - 'خرسانة جاهزة': { - 'وحدة': 'م3', - 'سعر_الوحدة': 750.0, - 'وصف': 'خرسانة جاهزة بقوة 350 كجم/سم2', - 'فئة': 'أعمال خرسانية' - }, - 'حديد تسليح': { - 'وحدة': 'طن', - 'سعر_الوحدة': 5500.0, - 'وصف': 'حديد تسليح قطر 8-32 مم', - 'فئة': 'أعمال خرسانية' - }, - 'أسمنت': { - 'وحدة': 'كيس', - 'سعر_الوحدة': 30.0, - 'وصف': 'أسمنت بورتلاندي عادي', - 'فئة': 'أعمال خرسانية' - }, - 'رمل': { - 'وحدة': 'م3', - 'سعر_الوحدة': 120.0, - 'وصف': 'رمل خشن للخرسانة', - 'فئة': 'أعمال خرسانية' - }, - 'زلط': { - 'وحدة': 'م3', - 'سعر_الوحدة': 150.0, - 'وصف': 'زلط مقاس 10-20 مم للخرسانة', - 'فئة': 'أعمال خرسانية' - }, - - # مواد البناء - 'طوب أحمر': { - 'وحدة': '1000 قطعة', - 'سعر_الوحدة': 900.0, - 'وصف': 'طوب أحمر مقاس 25×12×6 سم', - 'فئة': 'أعمال بناء' - }, - 'طوب أسمنتي': { - 'وحدة': 'قطعة', - 'سعر_الوحدة': 4.5, - 'وصف': 'بلوك أسمنتي مقاس 20×20×40 سم', - 'فئة': 'أعمال بناء' - }, - 'مونة بناء': { - 'وحدة': 'م3', - 'سعر_الوحدة': 350.0, - 'وصف': 'مونة أسمنتية للبناء', - 'فئة': 'أعمال بناء' - }, - - # مواد التشطيبات - 'بلاط سيراميك': { - 'وحدة': 'م2', - 'سعر_الوحدة': 120.0, - 'وصف': 'بلاط سيراميك للأرضيات مقاس 40×40 سم', - 'فئة': 'تشطيبات' - }, - 'بلاط بورسلين': { - 'وحدة': 'م2', - 'سعر_الوحدة': 180.0, - 'وصف': 'بلاط بورسلين للأرضيات مقاس 60×60 سم', - 'فئة': 'تشطيبات' - }, - 'دهانات بلاستيك': { - 'وحدة': 'لتر', - 'سعر_الوحدة': 35.0, - 'وصف': 'دهان بلاستيك أساس وتشطيب', - 'فئة': 'تشطيبات' - }, - 'جبس بورد': { - 'وحدة': 'م2', - 'سعر_الوحدة': 95.0, - 'وصف': 'ألواح جبس بورد سمك 12 مم', - 'فئة': 'تشطيبات' - }, - - # مواد العزل - 'عزل مائي': { - 'وحدة': 'م2', - 'سعر_الوحدة': 45.0, - 'وصف': 'عزل مائي من البيتومين المؤكسد', - 'فئة': 'أعمال عزل' - }, - 'عزل حراري': { - 'وحدة': 'م2', - 'سعر_الوحدة': 65.0, - 'وصف': 'ألواح عزل حراري من البوليسترين سمك 5 سم', - 'فئة': 'أعمال عزل' - } - } - - # محاولة تحميل البيانات من ملف إذا كان متاحًا - try: - file_path = os.path.join(config.DATA_DIR, 'material_rates.json') - if os.path.exists(file_path): - with open(file_path, 'r', encoding='utf-8') as f: - loaded_data = json.load(f) - material_rates.update(loaded_data) - except Exception as e: - print(f"خطأ في تحميل بيانات أسعار المواد: {str(e)}") - - return material_rates - - def _load_labor_rates(self) -> Dict[str, Dict[str, Any]]: - """تحميل أسعار العمالة""" - # محاكاة تحميل البيانات من مصدر بيانات - labor_rates = { - # عمالة الخرسانات - 'نجار مسلح': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 250.0, - 'وصف': 'نجار مسلح لأعمال الشدات والفرم', - 'فئة': 'أعمال خرسانية', - 'إنتاجية_يومية': { - 'شدة أساسات': 12, # متر مربع - 'شدة أعمدة': 10, # متر مربع - 'شدة أسقف': 12 # متر مربع - } - }, - 'حداد مسلح': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 250.0, - 'وصف': 'حداد مسلح لأعمال حديد التسليح', - 'فئة': 'أعمال خرسانية', - 'إنتاجية_يومية': { - 'تجهيز وتركيب حديد أساسات': 700, # كجم - 'تجهيز وتركيب حديد أعمدة': 600, # كجم - 'تجهيز وتركيب حديد أسقف': 650 # كجم - } - }, - 'عامل خرسانة': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 150.0, - 'وصف': 'عامل لصب وتسوية الخرسانة', - 'فئة': 'أعمال خرسانية', - 'إنتاجية_يومية': { - 'صب خرسانة': 15 # متر مكعب - } - }, - - # عمالة البناء - 'بناء': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 200.0, - 'وصف': 'عامل بناء للطوب والبلوك', - 'فئة': 'أعمال بناء', - 'إنتاجية_يومية': { - 'بناء طوب أحمر': 500, # قطعة - 'بناء بلوك أسمنتي': 80 # قطعة - } - }, - 'مساعد بناء': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 120.0, - 'وصف': 'مساعد عامل بناء', - 'فئة': 'أعمال بناء', - 'إنتاجية_يومية': {} - }, - - # عمالة التشطيبات - 'مبلط': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 250.0, - 'وصف': 'عامل تركيب بلاط وسيراميك', - 'فئة': 'تشطيبات', - 'إنتاجية_يومية': { - 'تركيب سيراميك أرضيات': 15, # متر مربع - 'تركيب سيراميك حوائط': 12, # متر مربع - 'تركيب بورسلين': 12 # متر مربع - } - }, - 'نقاش': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 200.0, - 'وصف': 'عامل دهانات', - 'فئة': 'تشطيبات', - 'إنتاجية_يومية': { - 'دهانات بلاستيك': 35, # متر مربع - 'دهانات زيتية': 25 # متر مربع - } - }, - 'كهربائي': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 270.0, - 'وصف': 'فني كهرباء', - 'فئة': 'تشطيبات', - 'إنتاجية_يومية': { - 'تأسيس نقاط كهرباء': 15, # نقطة - 'تركيب لوحات توزيع': 2 # لوحة - } - }, - 'سباك': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 250.0, - 'وصف': 'فني سباكة', - 'فئة': 'تشطيبات', - 'إنتاجية_يومية': { - 'تأسيس نقاط صرف': 8, # نقطة - 'تأسيس نقاط تغذية': 10, # نقطة - 'تركيب أطقم حمامات': 2 # طقم - } - }, - - # مراقبة وإشراف - 'مهندس موقع': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 500.0, - 'وصف': 'مهندس إشراف موقع', - 'فئة': 'إشراف' - }, - 'مراقب فني': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 300.0, - 'وصف': 'مراقب فني للتنفيذ', - 'فئة': 'إشراف' - } - } - - # محاولة تحميل البيانات من ملف إذا كان متاحًا - try: - file_path = os.path.join(config.DATA_DIR, 'labor_rates.json') - if os.path.exists(file_path): - with open(file_path, 'r', encoding='utf-8') as f: - loaded_data = json.load(f) - labor_rates.update(loaded_data) - except Exception as e: - print(f"خطأ في تحميل بيانات أسعار العمالة: {str(e)}") - - return labor_rates - - def _load_equipment_rates(self) -> Dict[str, Dict[str, Any]]: - """تحميل أسعار المعدات""" - # محاكاة تحميل البيانات من مصدر بيانات - equipment_rates = { - # معدات الحفر والتسوية - 'حفار صغير': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 1200.0, - 'وصف': 'حفار صغير (بوبكات) بقدرة 70 حصان', - 'فئة': 'معدات حفر', - 'إنتاجية_يومية': { - 'حفر في تربة عادية': 60 # متر مكعب - } - }, - 'حفار متوسط': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 2500.0, - 'وصف': 'حفار متوسط الحجم بقدرة 150 حصان', - 'فئة': 'معدات حفر', - 'إنتاجية_يومية': { - 'حفر في تربة عادية': 200 # متر مكعب - } - }, - 'لودر': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 2000.0, - 'وصف': 'لودر أمامي لنقل التربة', - 'فئة': 'معدات حفر', - 'إنتاجية_يومية': { - 'تحميل تربة': 300, # متر مكعب - 'تسوية موقع': 1500 # متر مربع - } - }, - 'جريدر': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 2200.0, - 'وصف': 'جريدر لتسوية الموقع', - 'فئة': 'معدات حفر', - 'إنتاجية_يومية': { - 'تسوية طرق': 3000 # متر مربع - } - }, - - # معدات الخرسانة - 'خلاطة خرسانة': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 350.0, - 'وصف': 'خلاطة خرسانة بسعة 0.5 متر مكعب', - 'فئة': 'معدات خرسانة', - 'إنتاجية_يومية': { - 'خلط خرسانة': 15 # متر مكعب - } - }, - 'هزاز خرسانة': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 150.0, - 'وصف': 'هزاز خرسانة كهربائي', - 'فئة': 'معدات خرسانة', - 'إنتاجية_يومية': { - 'دمك خرسانة': 40 # متر مكعب - } - }, - 'شاحنة خرسانة جاهزة': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 3000.0, - 'وصف': 'شاحنة خرسانة جاهزة (مكسر) سعة 8 متر مكعب', - 'فئة': 'معدات خرسانة', - 'إنتاجية_يومية': { - 'نقل وصب خرسانة': 50 # متر مكعب - } - }, - 'مضخة خرسانة': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 5000.0, - 'وصف': 'مضخة خرسانة بذراع 42 متر', - 'فئة': 'معدات خرسانة', - 'إنتاجية_يومية': { - 'ضخ خرسانة': 120 # متر مكعب - } - }, - - # معدات رفع ونقل - 'رافعة برجية': { - 'وحدة': 'شهر', - 'سعر_الوحدة': 35000.0, - 'وصف': 'رافعة برجية بارتفاع 40 متر', - 'فئة': 'معدات رفع', - }, - 'ونش شوكة': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 1500.0, - 'وصف': 'ونش شوكة لرفع مواد البناء', - 'فئة': 'معدات رفع', - 'إنتاجية_يومية': { - 'رفع ونقل مواد': 100 # طن - } - }, - 'شاحنة نقل': { - 'وحدة': 'يوم', - 'سعر_الوحدة': 1200.0, - 'وصف': 'شاحنة نقل حمولة 20 طن', - 'فئة': 'معدات نقل', - 'إنتاجية_يومية': { - 'نقل مواد': 80 # طن - } - } - } - - # محاولة تحميل البيانات من ملف إذا كان متاحًا - try: - file_path = os.path.join(config.DATA_DIR, 'equipment_rates.json') - if os.path.exists(file_path): - with open(file_path, 'r', encoding='utf-8') as f: - loaded_data = json.load(f) - equipment_rates.update(loaded_data) - except Exception as e: - print(f"خطأ في تحميل بيانات أسعار المعدات: {str(e)}") - - return equipment_rates - - def calculate_item_cost(self, item_data: Dict[str, Any]) -> Dict[str, Any]: - """ - حساب تكلفة بند محدد بكافة مكوناته - - المعلمات: - item_data (dict): بيانات البند، تتضمن: - - وصف_البند (str): وصف البند - - الكمية (float): كمية البند - - الوحدة (str): وحدة القياس - - المواد (list): قائمة المواد المستخدمة وكمياتها - - العمالة (list): قائمة العمالة المستخدمة وعددها - - المعدات (list): قائمة المعدات المستخدمة وساعات عملها - - المصاريف_الإدارية (float, optional): نسبة المصاريف الإدارية (افتراضياً 5%) - - هامش_الربح (float, optional): نسبة هامش الربح (افتراضياً 10%) - - عوامل_التعديل (dict, optional): عوامل تعديل التكلفة - - العوائد: - dict: تفاصيل تكلفة البند بكافة عناصرها - """ - # استخراج البيانات الأساسية للبند - item_description = item_data.get('وصف_البند', 'بند غير محدد') - quantity = item_data.get('الكمية', 0.0) - unit = item_data.get('الوحدة', 'وحدة') - - # حساب تكلفة المواد - materials_cost = self._calculate_materials_cost(item_data.get('المواد', [])) - - # حساب تكلفة العمالة - labor_cost = self._calculate_labor_cost(item_data.get('العمالة', [])) - - # حساب تكلفة المعدات - equipment_cost = self._calculate_equipment_cost(item_data.get('المعدات', [])) - - # حساب التكلفة المباشرة الإجمالية - direct_cost = materials_cost['الإجمالي'] + labor_cost['الإجمالي'] + equipment_cost['الإجمالي'] - - # حساب المصاريف الإدارية - admin_percentage = item_data.get('المصاريف_الإدارية', self.default_admin_expenses_percentage) - admin_cost = direct_cost * admin_percentage - - # حساب هامش الربح - profit_percentage = item_data.get('هامش_الربح', self.default_profit_margin_percentage) - profit_margin = (direct_cost + admin_cost) * profit_percentage - - # حساب التكلفة الإجمالية - total_cost = direct_cost + admin_cost + profit_margin - - # حساب سعر الوحدة - unit_price = total_cost / quantity if quantity > 0 else 0.0 - - # تطبيق عوامل التعديل إذا وجدت - adjustment_factors = item_data.get('عوامل_التعديل', self.default_adjustment_factors) - adjustment_factor = self._calculate_adjustment_factor(adjustment_factors) - - adjusted_unit_price = unit_price * adjustment_factor - adjusted_total_cost = total_cost * adjustment_factor - - # إعداد النتائج - result = { - 'وصف_البند': item_description, - 'الكمية': quantity, - 'الوحدة': unit, - 'تكاليف_مباشرة': { - 'المواد': materials_cost, - 'العمالة': labor_cost, - 'المعدات': equipment_cost, - 'إجمالي_تكاليف_مباشرة': direct_cost - }, - 'مصاريف_إدارية': { - 'نسبة': admin_percentage * 100, - 'قيمة': admin_cost - }, - 'هامش_ربح': { - 'نسبة': profit_percentage * 100, - 'قيمة': profit_margin - }, - 'التكلفة_الإجمالية': total_cost, - 'سعر_الوحدة': unit_price, - 'عوامل_التعديل': { - 'المعامل_الإجمالي': adjustment_factor, - 'التفاصيل': adjustment_factors - }, - 'السعر_المعدل': { - 'سعر_الوحدة': adjusted_unit_price, - 'إجمالي': adjusted_total_cost - } - } - - return result - - def _calculate_materials_cost(self, materials: List[Dict[str, Any]]) -> Dict[str, Any]: - """ - حساب تكلفة المواد - - المعلمات: - materials (list): قائمة المواد المستخدمة وكمياتها - - الاسم (str): اسم المادة - - الكمية (float): الكمية المستخدمة - - الوحدة (str, optional): وحدة القياس - - سعر_الوحدة (float, optional): سعر الوحدة (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده) - - العوائد: - dict: تفاصيل تكلفة المواد - """ - materials_details = [] - total_cost = 0.0 - - for material in materials: - material_name = material.get('الاسم', '') - quantity = material.get('الكمية', 0.0) - - # البحث عن سعر المادة من البيانات المرجعية إذا لم يتم تحديده - if 'سعر_الوحدة' in material: - unit_price = material.get('سعر_الوحدة', 0.0) - unit = material.get('الوحدة', 'وحدة') - elif material_name in self.material_rates: - ref_material = self.material_rates[material_name] - unit_price = ref_material.get('سعر_الوحدة', 0.0) - unit = ref_material.get('وحدة', 'وحدة') - else: - unit_price = 0.0 - unit = material.get('الوحدة', 'وحدة') - - # حساب التكلفة - cost = quantity * unit_price - total_cost += cost - - # إضافة التفاصيل - materials_details.append({ - 'الاسم': material_name, - 'الكمية': quantity, - 'الوحدة': unit, - 'سعر_الوحدة': unit_price, - 'التكلفة': cost - }) - - return { - 'التفاصيل': materials_details, - 'الإجمالي': total_cost - } - - def _calculate_labor_cost(self, labor: List[Dict[str, Any]]) -> Dict[str, Any]: - """ - حساب تكلفة العمالة - - المعلمات: - labor (list): قائمة العمالة المستخدمة وعددها - - النوع (str): نوع العامل - - العدد (int): عدد العمال - - المدة (float): مدة العمل بالأيام - - سعر_اليوم (float, optional): أجر اليوم (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده) - - العوائد: - dict: تفاصيل تكلفة العمالة - """ - labor_details = [] - total_cost = 0.0 - - for worker in labor: - worker_type = worker.get('النوع', '') - count = worker.get('العدد', 0) - duration = worker.get('المدة', 0.0) - - # البحث عن سعر العامل من البيانات المرجعية إذا لم يتم تحديده - if 'سعر_اليوم' in worker: - daily_rate = worker.get('سعر_اليوم', 0.0) - elif worker_type in self.labor_rates: - daily_rate = self.labor_rates[worker_type].get('سعر_الوحدة', 0.0) - else: - daily_rate = 0.0 - - # حساب التكلفة - cost = count * duration * daily_rate - total_cost += cost - - # إضافة التفاصيل - labor_details.append({ - 'النوع': worker_type, - 'العدد': count, - 'المدة': duration, - 'سعر_اليوم': daily_rate, - 'التكلفة': cost - }) - - return { - 'التفاصيل': labor_details, - 'الإجمالي': total_cost - } - - def _calculate_equipment_cost(self, equipment: List[Dict[str, Any]]) -> Dict[str, Any]: - """ - حساب تكلفة المعدات - - المعلمات: - equipment (list): قائمة المعدات المستخدمة وساعات عملها - - النوع (str): نوع المعدة - - العدد (int): عدد المعدات - - المدة (float): مدة الاستخدام بالأيام - - سعر_اليوم (float, optional): أجر اليوم (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده) - - العوائد: - dict: تفاصيل تكلفة المعدات - """ - equipment_details = [] - total_cost = 0.0 - - for equip in equipment: - equip_type = equip.get('النوع', '') - count = equip.get('العدد', 0) - duration = equip.get('المدة', 0.0) - - # البحث عن سعر المعدة من البيانات المرجعية إذا لم يتم تحديده - if 'سعر_اليوم' in equip: - daily_rate = equip.get('سعر_اليوم', 0.0) - elif equip_type in self.equipment_rates: - daily_rate = self.equipment_rates[equip_type].get('سعر_الوحدة', 0.0) - else: - daily_rate = 0.0 - - # حساب التكلفة - cost = count * duration * daily_rate - total_cost += cost - - # إضافة التفاصيل - equipment_details.append({ - 'النوع': equip_type, - 'العدد': count, - 'المدة': duration, - 'سعر_اليوم': daily_rate, - 'التكلفة': cost - }) - - return { - 'التفاصيل': equipment_details, - 'الإجمالي': total_cost - } - - def _calculate_adjustment_factor(self, factors: Dict[str, float]) -> float: - """ - حساب المعامل الإجمالي لتعديل التكلفة - - المعلمات: - factors (dict): عوامل التعديل - - العوائد: - float: المعامل الإجمالي - """ - # دمج العوامل المحددة مع العوامل الافتراضية - effective_factors = self.default_adjustment_factors.copy() - effective_factors.update(factors) - - # حساب المعامل الإجمالي - total_factor = 1.0 - for factor in effective_factors.values(): - total_factor *= factor - - return total_factor - - def calculate_project_cost(self, project_data: Dict[str, Any]) -> Dict[str, Any]: - """ - حساب التكلفة الإجمالية لمشروع بناء كامل - - المعلمات: - project_data (dict): بيانات المشروع، تتضمن: - - اسم_المشروع (str): اسم المشروع - - وصف_المشروع (str): وصف المشروع - - البنود (list): قائمة بنود المشروع - - المصاريف_الإدارية (float, optional): نسبة المصاريف الإدارية الإجمالية (افتراضياً 5%) - - هامش_الربح (float, optional): نسبة هامش الربح الإجمالي (افتراضياً 10%) - - عوامل_التعديل (dict, optional): عوامل تعديل التكلفة للمشروع - - العوائد: - dict: تفاصيل تكلفة المشروع بكافة عناصرها - """ - # استخراج البيانات الأساسية للمشروع - project_name = project_data.get('اسم_المشروع', 'مشروع غير محدد') - project_description = project_data.get('وصف_المشروع', '') - items = project_data.get('البنود', []) - - # استخراج النسب الإجمالية - admin_percentage = project_data.get('المصاريف_الإدارية', self.default_admin_expenses_percentage) - profit_percentage = project_data.get('هامش_الربح', self.default_profit_margin_percentage) - - # حساب تكلفة كل بند - items_costs = [] - total_direct_cost = 0.0 - total_materials_cost = 0.0 - total_labor_cost = 0.0 - total_equipment_cost = 0.0 - - for item_data in items: - # تحديث نسب المصاريف والربح للبند إذا لم تكن محددة - if 'المصاريف_الإدارية' not in item_data: - item_data['المصاريف_الإدارية'] = admin_percentage - - if 'هامش_الربح' not in item_data: - item_data['هامش_الربح'] = profit_percentage - - # حساب تكلفة البند - item_cost = self.calculate_item_cost(item_data) - items_costs.append(item_cost) - - # تحديث الإجماليات - total_materials_cost += item_cost['تكاليف_مباشرة']['المواد']['الإجمالي'] - total_labor_cost += item_cost['تكاليف_مباشرة']['العمالة']['الإجمالي'] - total_equipment_cost += item_cost['تكاليف_مباشرة']['المعدات']['الإجمالي'] - total_direct_cost += item_cost['تكاليف_مباشرة']['إجمالي_تكاليف_مباشرة'] - - # حساب المصاريف الإدارية - admin_cost = total_direct_cost * admin_percentage - - # حساب هامش الربح - profit_margin = (total_direct_cost + admin_cost) * profit_percentage - - # حساب التكلفة الإجمالية - total_cost = total_direct_cost + admin_cost + profit_margin - - # تطبيق عوامل التعديل إذا وجدت - adjustment_factors = project_data.get('عوامل_التعديل', self.default_adjustment_factors) - adjustment_factor = self._calculate_adjustment_factor(adjustment_factors) - - adjusted_total_cost = total_cost * adjustment_factor - - # إعداد النتائج - result = { - 'اسم_المشروع': project_name, - 'وصف_المشروع': project_description, - 'تكاليف_مباشرة': { - 'المواد': { - 'الإجمالي': total_materials_cost, - 'النسبة_المئوية': (total_materials_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0 - }, - 'العمالة': { - 'الإجمالي': total_labor_cost, - 'النسبة_المئوية': (total_labor_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0 - }, - 'المعدات': { - 'الإجمالي': total_equipment_cost, - 'النسبة_المئوية': (total_equipment_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0 - }, - 'إجمالي_تكاليف_مباشرة': total_direct_cost - }, - 'مصاريف_إدارية': { - 'نسبة': admin_percentage * 100, - 'قيمة': admin_cost - }, - 'هامش_ربح': { - 'نسبة': profit_percentage * 100, - 'قيمة': profit_margin - }, - 'التكلفة_الإجمالية': total_cost, - 'عوامل_التعديل': { - 'المعامل_الإجمالي': adjustment_factor, - 'التفاصيل': adjustment_factors - }, - 'التكلفة_النهائية_المعدلة': adjusted_total_cost, - 'تفاصيل_البنود': items_costs, - 'عدد_البنود': len(items) - } - - return result - - def get_rate_info(self, item_type: str, item_name: str) -> Dict[str, Any]: - """ - الحصول على معلومات تفصيلية عن معدل وسعر عنصر محدد (مادة، عمالة، معدة) - - المعلمات: - item_type (str): نوع العنصر - 'مادة'، 'عمالة'، 'معدة' - item_name (str): اسم العنصر - - العوائد: - dict: معلومات تفصيلية عن العنصر - """ - # تحديد القاموس المناسب حسب نوع العنصر - if item_type == 'مادة': - rates_dict = self.material_rates - elif item_type == 'عمالة': - rates_dict = self.labor_rates - elif item_type == 'معدة': - rates_dict = self.equipment_rates - else: - return {'خطأ': 'نوع العنصر غير صحيح'} - - # البحث عن العنصر في القاموس - if item_name in rates_dict: - return rates_dict[item_name] - else: - return {'خطأ': 'العنصر غير موجود'} - - def get_all_rates(self, item_type: str = None, category: str = None) -> Dict[str, Any]: - """ - الحصول على قوائم معدلات الأسعار (لجميع المواد أو العمالة أو المعدات) - - المعلمات: - item_type (str, optional): نوع العنصر - 'مادة'، 'عمالة'، 'معدة'، أو None لجميع الأنواع - category (str, optional): فئة محددة للتصفية - - العوائد: - dict: قوائم معدلات الأسعار - """ - result = {} - - # جمع المواد حسب الفئة - if item_type is None or item_type == 'مادة': - materials = {} - for name, info in self.material_rates.items(): - if category is None or info.get('فئة') == category: - materials[name] = info - result['المواد'] = materials - - # جمع العمالة حسب الفئة - if item_type is None or item_type == 'عمالة': - labor = {} - for name, info in self.labor_rates.items(): - if category is None or info.get('فئة') == category: - labor[name] = info - result['العمالة'] = labor - - # جمع المعدات حسب الفئة - if item_type is None or item_type == 'معدة': - equipment = {} - for name, info in self.equipment_rates.items(): - if category is None or info.get('فئة') == category: - equipment[name] = info - result['المعدات'] = equipment - - return result - - def generate_sample_project_data(self) -> Dict[str, Any]: - """ - توليد بيانات نموذجية لمشروع بناء صغير للاختبار - - العوائد: - dict: بيانات المشروع النموذجية - """ - # إنشاء بيانات المشروع - project_data = { - 'اسم_المشروع': 'مبنى سكني صغير', - 'وصف_المشروع': 'مبنى سكني مكون من دور أرضي بمساحة 250 متر مربع', - 'المصاريف_الإدارية': 0.05, # 5% - 'هامش_الربح': 0.10, # 10% - 'عوامل_التعديل': { - 'location_factor': 1.2, # معامل الموقع (منطقة مرتفعة التكلفة) - 'time_factor': 1.0, # معامل الوقت - 'risk_factor': 1.05, # معامل المخاطر - 'market_factor': 1.0 # معامل السوق - }, - 'البنود': [ - # الأساسات - { - 'وصف_البند': 'حفر الأساسات بعمق 2 متر', - 'الكمية': 150.0, - 'الوحدة': 'م3', - 'المواد': [], - 'العمالة': [ - {'النوع': 'عامل خرسانة', 'العدد': 4, 'المدة': 3} - ], - 'المعدات': [ - {'النوع': 'حفار متوسط', 'العدد': 1, 'المدة': 2} - ] - }, - { - 'وصف_البند': 'توريد وصب خرسانة عادية للأساسات', - 'الكمية': 25.0, - 'الوحدة': 'م3', - 'المواد': [ - {'الاسم': 'خرسانة جاهزة', 'الكمية': 25.0} - ], - 'العمالة': [ - {'النوع': 'عامل خرسانة', 'العدد': 6, 'المدة': 1} - ], - 'المعدات': [ - {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5} - ] - }, - { - 'وصف_البند': 'توريد وتركيب حديد تسليح للأساسات', - 'الكمية': 3.5, - 'الوحدة': 'طن', - 'المواد': [ - {'الاسم': 'حديد تسليح', 'الكمية': 3.5} - ], - 'العمالة': [ - {'النوع': 'حداد مسلح', 'العدد': 4, 'المدة': 3} - ], - 'المعدات': [] - }, - { - 'وصف_البند': 'نجارة وفك شدة الأساسات', - 'الكمية': 120.0, - 'الوحدة': 'م2', - 'المواد': [], - 'العمالة': [ - {'النوع': 'نجار مسلح', 'العدد': 4, 'المدة': 3} - ], - 'المعدات': [] - }, - { - 'وصف_البند': 'توريد وصب خرسانة مسلحة للأساسات', - 'الكمية': 30.0, - 'الوحدة': 'م3', - 'المواد': [ - {'الاسم': 'خرسانة جاهزة', 'الكمية': 30.0} - ], - 'العمالة': [ - {'النوع': 'عامل خرسانة', 'العدد': 6, 'المدة': 1} - ], - 'المعدات': [ - {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5}, - {'النوع': 'هزاز خرسانة', 'العدد': 2, 'المدة': 1} - ] - }, - - # الأعمدة والأسقف - { - 'وصف_البند': 'توريد وتركيب حديد تسليح للأعمدة', - 'الكمية': 2.8, - 'الوحدة': 'طن', - 'المواد': [ - {'الاسم': 'حديد تسليح', 'الكمية': 2.8} - ], - 'العمالة': [ - {'النوع': 'حداد مسلح', 'العدد': 4, 'المدة': 3} - ], - 'المعدات': [] - }, - { - 'وصف_البند': 'نجارة وفك شدة الأعمدة', - 'الكمية': 85.0, - 'الوحدة': 'م2', - 'المواد': [], - 'العمالة': [ - {'النوع': 'نجار مسلح', 'العدد': 3, 'المدة': 3} - ], - 'المعدات': [] - }, - { - 'وصف_البند': 'توريد وصب خرسانة مسلحة للأعمدة', - 'الكمية': 12.0, - 'الوحدة': 'م3', - 'المواد': [ - {'الاسم': 'خرسانة جاهزة', 'الكمية': 12.0} - ], - 'العمالة': [ - {'النوع': 'عامل خرسانة', 'العدد': 4, 'المدة': 1} - ], - 'المعدات': [ - {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5}, - {'النوع': 'هزاز خرسانة', 'العدد': 2, 'المدة': 1} - ] - }, - - # أعمال البناء - { - 'وصف_البند': 'توريد وبناء حوائط من الطوب الأحمر', - 'الكمية': 220.0, - 'الوحدة': 'م2', - 'المواد': [ - {'الاسم': 'طوب أحمر', 'الكمية': 16.5} # بالألف - ], - 'العمالة': [ - {'النوع': 'بناء', 'العدد': 4, 'المدة': 8}, - {'النوع': 'مساعد بناء', 'العدد': 4, 'المدة': 8} - ], - 'المعدات': [] - }, - - # أعمال التشطيبات - { - 'وصف_البند': 'توريد وتركيب بلاط سيراميك للأرضيات', - 'الكمية': 250.0, - 'الوحدة': 'م2', - 'المواد': [ - {'الاسم': 'بلاط سيراميك', 'الكمية': 250.0} - ], - 'العمالة': [ - {'النوع': 'مبلط', 'العدد': 4, 'المدة': 7} - ], - 'المعدات': [] - }, - { - 'وصف_البند': 'توريد وتنفيذ دهانات للحوائط', - 'الكمية': 450.0, - 'الوحدة': 'م2', - 'المواد': [ - {'الاسم': 'دهانات بلاستيك', 'الكمية': 90.0} # بالتر - ], - 'العمالة': [ - {'النوع': 'نقاش', 'العدد': 3, 'المدة': 8} - ], - 'المعدات': [] - } - ] - } - - return project_data \ No newline at end of file diff --git a/modules/pricing/services/construction_templates.py b/modules/pricing/services/construction_templates.py deleted file mode 100644 index bdbdd1586dcb54cb9d806ecb016bac7f6dfc0f91..0000000000000000000000000000000000000000 --- a/modules/pricing/services/construction_templates.py +++ /dev/null @@ -1,748 +0,0 @@ -""" -كتالوج بنود نموذجية للمقاولات -يحتوي هذا الملف على قائمة كاملة من النماذج الجاهزة للبنود الشائعة في مشاريع المقاولات، مثل: -- أعمال الخرسانة بأنواعها -- المناهل وأنواع المواسير -- التركيبات المختلفة -- الطرق والأسفلت -- وغيرها من أعمال المقاولات -""" - -import os -import json -import sys -from typing import Dict, List, Any, Optional -from datetime import datetime - -# إضافة مسار النظام للوصول لملفات التكوين -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))) -try: - import config -except ImportError: - # إذا لم يتم العثور على ملف التكوين، نستخدم قيم افتراضية - class DefaultConfig: - DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../data")) - config = DefaultConfig - - # إنشاء مجلد البيانات إذا لم يكن موجودًا - if not os.path.exists(config.DATA_DIR): - os.makedirs(config.DATA_DIR) - - -class ConstructionTemplates: - """كتالوج بنود نموذجية للمقاولات""" - - def __init__(self): - """تهيئة كتالوج البنود النموذجية""" - self.templates_file = os.path.join(config.DATA_DIR, 'construction_templates.json') - self.market_prices_file = os.path.join(config.DATA_DIR, 'saudi_market_prices.json') - - # تحميل قوالب البنود النموذجية - self.templates = self._load_templates() - - # تحميل أسعار السوق السعودي - self.market_prices = self._load_market_prices() - - def _load_templates(self) -> Dict[str, Dict[str, Any]]: - """تحميل قوالب البنود النموذجية من الملف""" - if os.path.exists(self.templates_file): - try: - with open(self.templates_file, 'r', encoding='utf-8') as f: - return json.load(f) - except Exception as e: - print(f"خطأ في تحميل قوالب البنود النموذجية: {str(e)}") - - # إنشاء بيانات افتراضية إذا لم يتم العثور على الملف - default_templates = self._create_default_templates() - - # حفظ البيانات الافتراضية - self._save_templates(default_templates) - - return default_templates - - def _save_templates(self, templates: Dict[str, Dict[str, Any]]) -> None: - """حفظ قوالب البنود النموذجية إلى الملف""" - try: - with open(self.templates_file, 'w', encoding='utf-8') as f: - json.dump(templates, f, ensure_ascii=False, indent=4) - except Exception as e: - print(f"خطأ في حفظ قوالب البنود النموذجية: {str(e)}") - - def _load_market_prices(self) -> Dict[str, Dict[str, Any]]: - """تحميل أسعار السوق السعودي من الملف""" - if os.path.exists(self.market_prices_file): - try: - with open(self.market_prices_file, 'r', encoding='utf-8') as f: - return json.load(f) - except Exception as e: - print(f"خطأ في تحميل أسعار السوق السعودي: {str(e)}") - - # إنشاء بيانات افتراضية إذا لم يتم العثور على الملف - default_prices = self._create_default_market_prices() - - # حفظ البيانات الافتراضية - self._save_market_prices(default_prices) - - return default_prices - - def _save_market_prices(self, prices: Dict[str, Dict[str, Any]]) -> None: - """حفظ أسعار السوق السعودي إلى الملف""" - try: - with open(self.market_prices_file, 'w', encoding='utf-8') as f: - json.dump(prices, f, ensure_ascii=False, indent=4) - except Exception as e: - print(f"خطأ في حفظ أسعار السوق السعودي: {str(e)}") - - def _create_default_templates(self) -> Dict[str, Dict[str, Any]]: - """إنشاء قوالب افتراضية للبنود النموذجية""" - templates = { - "categories": { - "أعمال_خرسانية": { - "name": "أعمال خرسانية", - "description": "بنود أعمال الخرسانة المسلحة والعادية", - "icon": "building" - }, - "أعمال_صحية": { - "name": "أعمال صحية", - "description": "بنود أعمال المناهل والمواسير والتركيبات الصحية", - "icon": "pipe" - }, - "أعمال_طرق": { - "name": "أعمال طرق", - "description": "بنود أعمال الطرق والأسفلت والرصف", - "icon": "road" - }, - "أعمال_كهربائية": { - "name": "أعمال كهربائية", - "description": "بنود أعمال الكهرباء والإنارة", - "icon": "zap" - }, - "أعمال_ميكانيكية": { - "name": "أعمال ميكانيكية", - "description": "بنود أعمال التكييف والتهوية والتبريد", - "icon": "thermometer" - } - }, - "templates": { - # نماذج أعمال خرسانية - "خرسانة_مسلحة_أساسات": { - "category": "أعمال_خرسانية", - "name": "خرسانة مسلحة للأساسات", - "description": "توريد وصب خرسانة مسلحة للأساسات بقوة لا تقل عن 300 كجم/سم2", - "unit": "م3", - "components": { - "materials": [ - {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0}, - {"الاسم": "حديد تسليح", "الكمية": 0.12, "الوحدة": "طن", "سعر_الوحدة": 5500.0} - ], - "labor": [ - {"النوع": "عامل خرسانة", "العدد": 4, "المدة": 0.3, "سعر_اليوم": 150.0}, - {"النوع": "نجار مسلح", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 250.0}, - {"النوع": "حداد مسلح", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 250.0} - ], - "equipment": [ - {"النوع": "هزاز خرسانة", "العدد": 1, "المدة": 0.3, "سعر_اليوم": 150.0} - ] - }, - "admin_expenses": 0.05, - "profit_margin": 0.10, - "tags": ["خرسانة", "أساسات", "مسلحة"] - }, - "خرسانة_مسلحة_أعمدة": { - "category": "أعمال_خرسانية", - "name": "خرسانة مسلحة للأعمدة", - "description": "توريد وصب خرسانة مسلحة للأعمدة بقوة لا تقل عن 350 كجم/سم2", - "unit": "م3", - "components": { - "materials": [ - {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0}, - {"الاسم": "حديد تسليح", "الكمية": 0.18, "الوحدة": "طن", "سعر_الوحدة": 5500.0} - ], - "labor": [ - {"النوع": "عامل خرسانة", "العدد": 4, "المدة": 0.4, "سعر_اليوم": 150.0}, - {"النوع": "نجار مسلح", "العدد": 2, "المدة": 0.7, "سعر_اليوم": 250.0}, - {"النوع": "حداد مسلح", "العدد": 2, "المدة": 0.7, "سعر_اليوم": 250.0} - ], - "equipment": [ - {"النوع": "هزاز خرسانة", "العدد": 2, "المدة": 0.4, "سعر_اليوم": 150.0} - ] - }, - "admin_expenses": 0.05, - "profit_margin": 0.10, - "tags": ["خرسانة", "أعمدة", "مسلحة"] - }, - "خرسانة_مسلحة_أسقف": { - "category": "أعمال_خرسانية", - "name": "خرسانة مسلحة للأسقف", - "description": "توريد وصب خرسانة مسلحة للأسقف والبلاطات بقوة لا تقل عن 350 كجم/سم2", - "unit": "م3", - "components": { - "materials": [ - {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0}, - {"الاسم": "حديد تسليح", "الكمية": 0.16, "الوحدة": "طن", "سعر_الوحدة": 5500.0} - ], - "labor": [ - {"النوع": "عامل خرسانة", "العدد": 5, "المدة": 0.5, "سعر_اليوم": 150.0}, - {"النوع": "نجار مسلح", "العدد": 3, "المدة": 0.8, "سعر_اليوم": 250.0}, - {"النوع": "حداد مسلح", "العدد": 3, "المدة": 0.8, "سعر_اليوم": 250.0} - ], - "equipment": [ - {"النوع": "هزاز خرسانة", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 150.0} - ] - }, - "admin_expenses": 0.05, - "profit_margin": 0.10, - "tags": ["خرسانة", "أسقف", "بلاطات", "مسلحة"] - }, - - # نماذج أعمال صحية - "منهل_تفتيش_خرساني": { - "category": "أعمال_صحية", - "name": "منهل تفتيش خرساني", - "description": "توريد وتركيب منهل تفتيش خرساني قطر 1 متر وعمق 2 متر", - "unit": "عدد", - "components": { - "materials": [ - {"الاسم": "خرسانة جاهزة", "الكمية": 1.5, "الوحدة": "م3", "سعر_الوحدة": 750.0}, - {"الاسم": "حديد تسليح", "الكمية": 0.15, "الوحدة": "طن", "سعر_الوحدة": 5500.0}, - {"الاسم": "غطاء منهل حديد", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 1500.0} - ], - "labor": [ - {"النوع": "عامل خرسانة", "العدد": 3, "المدة": 1, "سعر_اليوم": 150.0}, - {"النوع": "نجار مسلح", "العدد": 2, "المدة": 1, "سعر_اليوم": 250.0}, - {"النوع": "حداد مسلح", "العدد": 1, "المدة": 1, "سعر_اليوم": 250.0}, - {"النوع": "سباك", "العدد": 2, "المدة": 1, "سعر_اليوم": 250.0} - ], - "equipment": [ - {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.5, "سعر_اليوم": 1200.0} - ] - }, - "admin_expenses": 0.05, - "profit_margin": 0.12, - "tags": ["صرف صحي", "منهل", "تفتيش"] - }, - "مواسير_بلاستيك_قطر_200_مم": { - "category": "أعمال_صحية", - "name": "مواسير بلاستيك قطر 200 مم", - "description": "توريد وتركيب مواسير بلاستيك UPVC قطر 200 مم لشبكات الصرف الصحي", - "unit": "م.ط", - "components": { - "materials": [ - {"الاسم": "مواسير بلاستيك UPVC قطر 200 مم", "الكمية": 1.05, "الوحدة": "م.ط", "سعر_الوحدة": 180.0}, - {"الاسم": "وصلات ومثبتات", "الكمية": 1, "الوحدة": "مجموعة", "سعر_الوحدة": 35.0}, - {"الاسم": "مواد لاصقة", "الكمية": 0.1, "الوحدة": "لتر", "سعر_الوحدة": 120.0} - ], - "labor": [ - {"النوع": "سباك", "العدد": 2, "المدة": 0.2, "سعر_اليوم": 250.0}, - {"النوع": "مساعد سباك", "العدد": 2, "المدة": 0.2, "سعر_اليوم": 120.0} - ], - "equipment": [ - {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.1, "سعر_اليوم": 1200.0} - ] - }, - "admin_expenses": 0.05, - "profit_margin": 0.12, - "tags": ["صرف صحي", "مواسير", "بلاستيك"] - }, - - # نماذج أعمال طرق - "طبقة_أساس_للطرق": { - "category": "أعمال_طرق", - "name": "طبقة أساس للطرق", - "description": "توريد وفرد ودمك طبقة أساس للطرق سمك 20 سم، درجة دمك 98%", - "unit": "م3", - "components": { - "materials": [ - {"الاسم": "مواد طبقة أساس", "الكمية": 1.25, "الوحدة": "م3", "سعر_الوحدة": 90.0}, - {"الاسم": "مياه للدمك", "الكمية": 0.2, "الوحدة": "م3", "سعر_الوحدة": 10.0} - ], - "labor": [ - {"النوع": "عامل طرق", "العدد": 4, "المدة": 0.05, "سعر_اليوم": 150.0}, - {"النوع": "مراقب فني", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 300.0} - ], - "equipment": [ - {"النوع": "جريدر", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 2200.0}, - {"النوع": "رصاصة دمك", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 1800.0}, - {"النوع": "شاحنة نقل", "العدد": 2, "المدة": 0.05, "سعر_اليوم": 1200.0} - ] - }, - "admin_expenses": 0.05, - "profit_margin": 0.12, - "tags": ["طرق", "أساس", "دمك"] - }, - "طبقة_إسفلت_سطحية": { - "category": "أعمال_طرق", - "name": "طبقة إسفلت سطحية", - "description": "توريد وفرد ودمك طبقة إسفلت سطحية سمك 5 سم", - "unit": "م2", - "components": { - "materials": [ - {"الاسم": "خلطة إسفلتية ساخنة", "الكمية": 0.125, "الوحدة": "طن", "سعر_الوحدة": 400.0}, - {"الاسم": "مواد رش تأسيسي", "الكمية": 0.5, "الوحدة": "لتر", "سعر_الوحدة": 8.0} - ], - "labor": [ - {"النوع": "عامل طرق", "العدد": 6, "المدة": 0.01, "سعر_اليوم": 150.0}, - {"النوع": "مراقب فني", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 300.0} - ], - "equipment": [ - {"النوع": "فرادة إسفلت", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 4000.0}, - {"النوع": "رصاصة دمك", "العدد": 2, "المدة": 0.01, "سعر_اليوم": 1800.0}, - {"النوع": "سيارة رش إسفلت", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 2000.0}, - {"النوع": "شاحنة نقل", "العدد": 4, "المدة": 0.01, "سعر_اليوم": 1200.0} - ] - }, - "admin_expenses": 0.05, - "profit_margin": 0.12, - "tags": ["طرق", "إسفلت", "سطحية"] - }, - - # نماذج أعمال كهربائية - "عمود_إنارة_10_متر": { - "category": "أعمال_كهربائية", - "name": "عمود إنارة 10 متر", - "description": "توريد وتركيب عمود إنارة جلفانيزي بارتفاع 10 متر مع ذراع مفردة وكشاف LED بقدرة 150 واط", - "unit": "عدد", - "components": { - "materials": [ - {"الاسم": "عمود إنارة جلفانيزي 10 متر", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 3500.0}, - {"الاسم": "ذراع إنارة مفردة", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 450.0}, - {"الاسم": "كشاف LED 150 واط", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 850.0}, - {"الاسم": "كابل كهرباء 3×4 مم²", "الكمية": 15, "الوحدة": "م.ط", "سعر_الوحدة": 32.0}, - {"الاسم": "قاعدة خرسانية مسلحة", "الكمية": 0.25, "الوحدة": "م3", "سعر_الوحدة": 750.0} - ], - "labor": [ - {"النوع": "كهربائي", "العدد": 2, "المدة": 1, "سعر_اليوم": 270.0}, - {"النوع": "مساعد كهربائي", "العدد": 2, "المدة": 1, "سعر_اليوم": 120.0}, - {"النوع": "عامل خرسانة", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 150.0} - ], - "equipment": [ - {"النوع": "ونش شوكة", "العدد": 1, "المدة": 0.5, "سعر_اليوم": 1500.0}, - {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.2, "سعر_اليوم": 1200.0} - ] - }, - "admin_expenses": 0.05, - "profit_margin": 0.12, - "tags": ["كهرباء", "إنارة", "LED"] - } - } - } - - return templates - - def _create_default_market_prices(self) -> Dict[str, Dict[str, Any]]: - """إنشاء بيانات افتراضية لأسعار السوق السعودي""" - current_date = datetime.now().strftime("%Y-%m-%d") - - prices = { - "metadata": { - "last_update": current_date, - "source": "أسعار السوق السعودي الافتراضية", - "disclaimer": "هذه الأسعار تقريبية وقد تختلف حسب المنطقة والكميات والموردين" - }, - "materials": { - # مواد الخرسانة - "خرسانة_جاهزة": { - "name": "خرسانة جاهزة", - "unit": "م3", - "current_price": 750.0, - "previous_price": 730.0, - "price_trend": "up", - "category": "أعمال خرسانية", - "specifications": "خرسانة جاهزة بقوة 350 كجم/سم2", - "note": "السعر يشمل توريد فقط، الضخ بتكلفة إضافية", - "price_history": [ - {"date": "2023-06-01", "price": 700.0}, - {"date": "2023-09-01", "price": 715.0}, - {"date": "2023-12-01", "price": 730.0}, - {"date": current_date, "price": 750.0} - ] - }, - "حديد_تسليح": { - "name": "حديد تسليح", - "unit": "طن", - "current_price": 5500.0, - "previous_price": 5200.0, - "price_trend": "up", - "category": "أعمال خرسانية", - "specifications": "حديد تسليح قطر 8-32 مم، انتاج سابك", - "note": "السعر يتغير بشكل دوري حسب أسعار الحديد العالمية", - "price_history": [ - {"date": "2023-06-01", "price": 4800.0}, - {"date": "2023-09-01", "price": 5000.0}, - {"date": "2023-12-01", "price": 5200.0}, - {"date": current_date, "price": 5500.0} - ] - }, - "أسمنت": { - "name": "أسمنت", - "unit": "كيس", - "current_price": 30.0, - "previous_price": 28.0, - "price_trend": "up", - "category": "أعمال خرسانية", - "specifications": "أسمنت بورتلاندي عادي، كيس 50 كجم", - "note": "السعر للكميات الكبيرة", - "price_history": [ - {"date": "2023-06-01", "price": 25.0}, - {"date": "2023-09-01", "price": 27.0}, - {"date": "2023-12-01", "price": 28.0}, - {"date": current_date, "price": 30.0} - ] - }, - - # مواد الطرق والإسفلت - "خلطة_إسفلتية_ساخنة": { - "name": "خلطة إسفلتية ساخنة", - "unit": "طن", - "current_price": 400.0, - "previous_price": 380.0, - "price_trend": "up", - "category": "أعمال طرق", - "specifications": "خلطة إسفلتية ساخنة للطبقة السطحية", - "note": "السعر يشمل التوريد من المصنع، النقل بتكلفة إضافية", - "price_history": [ - {"date": "2023-06-01", "price": 350.0}, - {"date": "2023-09-01", "price": 370.0}, - {"date": "2023-12-01", "price": 380.0}, - {"date": current_date, "price": 400.0} - ] - }, - - # مواد صحية - "مواسير_بلاستيك_UPVC": { - "name": "مواسير بلاستيك UPVC قطر 200 مم", - "unit": "م.ط", - "current_price": 180.0, - "previous_price": 165.0, - "price_trend": "up", - "category": "أعمال صحية", - "specifications": "مواسير بلاستيك UPVC قطر 200 مم لشبكات الصرف الصحي", - "note": "السعر للكميات الكبيرة", - "price_history": [ - {"date": "2023-06-01", "price": 150.0}, - {"date": "2023-09-01", "price": 160.0}, - {"date": "2023-12-01", "price": 165.0}, - {"date": current_date, "price": 180.0} - ] - }, - - # مواد كهربائية - "كشاف_LED": { - "name": "كشاف LED 150 واط", - "unit": "عدد", - "current_price": 850.0, - "previous_price": 820.0, - "price_trend": "up", - "category": "أعمال كهربائية", - "specifications": "كشاف إنارة LED بقدرة 150 واط للاستخدام الخارجي، IP65", - "note": "السعر شامل الضريبة", - "price_history": [ - {"date": "2023-06-01", "price": 780.0}, - {"date": "2023-09-01", "price": 800.0}, - {"date": "2023-12-01", "price": 820.0}, - {"date": current_date, "price": 850.0} - ] - } - }, - "labor": { - "عامل_خرسانة": { - "name": "عامل خرسانة", - "unit": "يوم", - "current_price": 150.0, - "previous_price": 140.0, - "price_trend": "up", - "category": "عمالة", - "specifications": "عامل لصب وتسوية الخرسانة", - "note": "أجرة اليوم الواحد لا تشمل السكن والمواصلات", - "price_history": [ - {"date": "2023-06-01", "price": 130.0}, - {"date": "2023-09-01", "price": 135.0}, - {"date": "2023-12-01", "price": 140.0}, - {"date": current_date, "price": 150.0} - ] - }, - "مهندس_موقع": { - "name": "مهندس موقع", - "unit": "يوم", - "current_price": 500.0, - "previous_price": 480.0, - "price_trend": "up", - "category": "إشراف", - "specifications": "مهندس إشراف موقع", - "note": "أجرة اليوم الواحد لا تشمل السكن والمواصلات", - "price_history": [ - {"date": "2023-06-01", "price": 450.0}, - {"date": "2023-09-01", "price": 470.0}, - {"date": "2023-12-01", "price": 480.0}, - {"date": current_date, "price": 500.0} - ] - } - }, - "equipment": { - "حفار_صغير": { - "name": "حفار صغير", - "unit": "يوم", - "current_price": 1200.0, - "previous_price": 1150.0, - "price_trend": "up", - "category": "معدات حفر", - "specifications": "حفار صغير (بوبكات) بقدرة 70 حصان", - "note": "السعر يشمل المشغل والوقود", - "price_history": [ - {"date": "2023-06-01", "price": 1100.0}, - {"date": "2023-09-01", "price": 1120.0}, - {"date": "2023-12-01", "price": 1150.0}, - {"date": current_date, "price": 1200.0} - ] - }, - "فرادة_إسفلت": { - "name": "فرادة إسفلت", - "unit": "يوم", - "current_price": 4000.0, - "previous_price": 3800.0, - "price_trend": "up", - "category": "معدات طرق", - "specifications": "فرادة إسفلت بعرض 3 متر", - "note": "السعر يشمل المشغل والوقود", - "price_history": [ - {"date": "2023-06-01", "price": 3500.0}, - {"date": "2023-09-01", "price": 3650.0}, - {"date": "2023-12-01", "price": 3800.0}, - {"date": current_date, "price": 4000.0} - ] - } - } - } - - return prices - - def get_all_templates(self) -> Dict[str, Dict[str, Any]]: - """الحصول على جميع القوالب النموذجية""" - return self.templates - - def get_templates_by_category(self, category_id: str) -> List[Dict[str, Any]]: - """الحصول على القوالب النموذجية حسب الفئة""" - result = [] - - # التحقق من وجود الفئة - if category_id not in self.templates["categories"]: - return result - - # جمع القوالب التي تنتمي إلى الفئة المحددة - for template_id, template in self.templates["templates"].items(): - if template["category"] == category_id: - template_copy = template.copy() - template_copy["id"] = template_id - result.append(template_copy) - - return result - - def get_template_by_id(self, template_id: str) -> Optional[Dict[str, Any]]: - """الحصول على قالب نموذجي بواسطة المعرف""" - if template_id in self.templates["templates"]: - template = self.templates["templates"][template_id].copy() - template["id"] = template_id - return template - - return None - - def add_template(self, template_data: Dict[str, Any]) -> str: - """إضافة قالب نموذجي جديد""" - # إنشاء معرف فريد للقالب - template_name = template_data.get("name", "").strip() - if not template_name: - raise ValueError("يجب تحديد اسم القالب") - - # تحويل الاسم إلى معرف (باستبدال المسافات بالشرطات السفلية وإزالة الأحرف الخاصة) - import re - template_id = re.sub(r'[^\w\s]', '', template_name) - template_id = template_id.replace(" ", "_") - - # إضافة رقم عشوائي لتجنب التكرار - import random - if template_id in self.templates["templates"]: - template_id = f"{template_id}_{random.randint(1000, 9999)}" - - # إضافة القالب إلى القائمة - self.templates["templates"][template_id] = template_data - - # حفظ التغييرات - self._save_templates(self.templates) - - return template_id - - def update_template(self, template_id: str, template_data: Dict[str, Any]) -> bool: - """تحديث قالب نموذجي موجود""" - if template_id not in self.templates["templates"]: - return False - - # تحديث القالب - self.templates["templates"][template_id] = template_data - - # حفظ التغييرات - self._save_templates(self.templates) - - return True - - def delete_template(self, template_id: str) -> bool: - """حذف قالب نموذجي""" - if template_id not in self.templates["templates"]: - return False - - # حذف القالب - del self.templates["templates"][template_id] - - # حفظ التغييرات - self._save_templates(self.templates) - - return True - - def get_market_prices(self, category: Optional[str] = None, item_type: Optional[str] = None) -> Dict[str, Any]: - """الحصول على أسعار السوق السعودي""" - result = { - "metadata": self.market_prices["metadata"] - } - - # تحديد نوع العناصر المطلوبة - sections = [] - if item_type: - if item_type in ["materials", "المواد"]: - sections = ["materials"] - elif item_type in ["labor", "العمالة"]: - sections = ["labor"] - elif item_type in ["equipment", "المعدات"]: - sections = ["equipment"] - else: - sections = ["materials", "labor", "equipment"] - - # جمع العناصر - for section in sections: - result[section] = {} - for item_id, item_data in self.market_prices[section].items(): - if not category or (item_data.get("category", "") == category): - result[section][item_id] = item_data - - return result - - def update_market_price(self, item_type: str, item_id: str, new_price: float) -> bool: - """تحديث سعر في قائمة أسعار السوق""" - section = "" - if item_type in ["materials", "المواد"]: - section = "materials" - elif item_type in ["labor", "العمالة"]: - section = "labor" - elif item_type in ["equipment", "المعدات"]: - section = "equipment" - else: - return False - - if item_id not in self.market_prices[section]: - return False - - # تحديث السعر - current_price = self.market_prices[section][item_id]["current_price"] - self.market_prices[section][item_id]["previous_price"] = current_price - self.market_prices[section][item_id]["current_price"] = new_price - - # تحديد اتجاه السعر - if new_price > current_price: - self.market_prices[section][item_id]["price_trend"] = "up" - elif new_price < current_price: - self.market_prices[section][item_id]["price_trend"] = "down" - else: - self.market_prices[section][item_id]["price_trend"] = "stable" - - # إضافة السعر الجديد إلى تاريخ الأسعار - current_date = datetime.now().strftime("%Y-%m-%d") - self.market_prices[section][item_id]["price_history"].append({ - "date": current_date, - "price": new_price - }) - - # تحديث تاريخ آخر تحديث - self.market_prices["metadata"]["last_update"] = current_date - - # حفظ التغييرات - self._save_market_prices(self.market_prices) - - return True - - def add_market_price_item(self, item_type: str, item_data: Dict[str, Any]) -> str: - """إضافة عنصر جديد إلى قائمة أسعار السوق""" - section = "" - if item_type in ["materials", "المواد"]: - section = "materials" - elif item_type in ["labor", "العمالة"]: - section = "labor" - elif item_type in ["equipment", "المعدات"]: - section = "equipment" - else: - raise ValueError("نوع العنصر غير صحيح") - - # التحقق من البيانات الأساسية - if "name" not in item_data or "current_price" not in item_data or "unit" not in item_data: - raise ValueError("يجب تحديد الاسم والسعر الحالي والوحدة") - - # إنشاء معرف فريد للعنصر - item_name = item_data["name"].strip() - import re - item_id = re.sub(r'[^\w\s]', '', item_name) - item_id = item_id.replace(" ", "_") - - # إضافة رقم عشوائي لتجنب التكرار - import random - if item_id in self.market_prices[section]: - item_id = f"{item_id}_{random.randint(1000, 9999)}" - - # إعداد بيانات العنصر - current_date = datetime.now().strftime("%Y-%m-%d") - new_item = { - "name": item_name, - "unit": item_data["unit"], - "current_price": item_data["current_price"], - "previous_price": item_data.get("previous_price", item_data["current_price"]), - "price_trend": "stable", - "category": item_data.get("category", ""), - "specifications": item_data.get("specifications", ""), - "note": item_data.get("note", ""), - "price_history": [ - {"date": current_date, "price": item_data["current_price"]} - ] - } - - # إضافة العنصر إلى القائمة - self.market_prices[section][item_id] = new_item - - # تحديث تاريخ آخر تحديث - self.market_prices["metadata"]["last_update"] = current_date - - # حفظ التغييرات - self._save_market_prices(self.market_prices) - - return item_id - - def convert_template_to_item(self, template_id: str) -> Dict[str, Any]: - """تحويل قالب نموذجي إلى بند للاستخدام في حاسبة تكاليف البناء""" - template = self.get_template_by_id(template_id) - if not template: - raise ValueError("القالب غير موجود") - - # تحويل القالب إلى صيغة بند - item = { - "وصف_البند": template["description"], - "الكمية": 1.0, - "الوحدة": template["unit"], - "المواد": template["components"]["materials"], - "العمالة": template["components"]["labor"], - "المعدات": template["components"]["equipment"], - "المصاريف_الإدارية": template["admin_expenses"], - "هامش_الربح": template["profit_margin"], - "عوامل_التعديل": { - "location_factor": 1.0, - "time_factor": 1.0, - "risk_factor": 1.0, - "market_factor": 1.0 - } - } - - return item \ No newline at end of file diff --git a/modules/pricing/services/local_content_calculator.py b/modules/pricing/services/local_content_calculator.py deleted file mode 100644 index 859c3d004512fc247e2a801608da335bc4c07f29..0000000000000000000000000000000000000000 --- a/modules/pricing/services/local_content_calculator.py +++ /dev/null @@ -1,577 +0,0 @@ -""" -خدمة حساب المحتوى المحلي -""" -import pandas as pd -import numpy as np -from datetime import datetime -import os -import config - -class LocalContentCalculator: - """خدمة حساب وتحسين المحتوى المحلي""" - - def __init__(self): - """تهيئة خدمة حساب المحتوى المحلي""" - # تحميل بيانات المواد المحلية ونسب المحتوى المحلي - self.local_products = self._load_local_products() - self.local_services = self._load_local_services() - self.local_labor = self._load_local_labor() - - # تحديد الأوزان النسبية لمكونات المحتوى المحلي - self.component_weights = { - 'القوى العاملة': 0.3, # 30% من وزن المحتوى المحلي - 'المنتجات': 0.5, # 50% من وزن المحتوى المحلي - 'الخدمات': 0.2 # 20% من وزن المحتوى المحلي - } - - # تحديد المستهدفات (متطلبات المحتوى المحلي) - self.targets = { - 'القوى العاملة': 0.8, # 80% محتوى محلي للقوى العاملة - 'المنتجات': 0.7, # 70% محتوى محلي للمنتجات - 'الخدمات': 0.6 # 60% محتوى محلي للخدمات - } - - def _load_local_products(self): - """تحميل بيانات المنتجات المحلية ونسب المحتوى المحلي""" - # محاكاة تحميل البيانات من مصدر بيانات - local_products = { - 'خرسانة': { - 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'منتج محلي بالكامل' - }, - 'حديد تسليح': { - 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/مستورد', - 'ملاحظات': 'متوفر من مصانع محلية ومستورد' - }, - 'عزل مائي': { - 'نسبة_المحتوى_المحلي': 0.60, # 60% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/مستورد', - 'ملاحظات': 'منتج محلي متوفر بجودة معقولة' - }, - 'بلوك خرساني': { - 'نسبة_المحتوى_المحلي': 0.98, # 98% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'منتج محلي بالكامل' - }, - 'رخام': { - 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'متوفر من محاجر محلية' - }, - 'أثاث مكتبي': { - 'نسبة_المحتوى_المحلي': 0.75, # 75% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'يُصنع محليًا ويستخدم بعض المكونات المستوردة' - }, - 'أجهزة تكييف': { - 'نسبة_المحتوى_المحلي': 0.40, # 40% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/مستورد', - 'ملاحظات': 'تجميع محلي مع مكونات مستوردة' - }, - 'أنظمة إضاءة': { - 'نسبة_المحتوى_المحلي': 0.55, # 55% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/مستورد', - 'ملاحظات': 'متوفر محليًا وبجودة متفاوتة' - }, - 'زجاج': { - 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/مستورد', - 'ملاحظات': 'إنتاج محلي بمواصفات جيدة' - }, - 'أسلاك كهربائية': { - 'نسبة_المحتوى_المحلي': 0.85, # 85% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'تصنيع محلي بجودة عالية' - } - } - - # محاولة تحميل البيانات من ملف إذا كان متاحًا - try: - file_path = os.path.join(config.DATA_DIR, 'local_products.csv') - if os.path.exists(file_path): - df = pd.read_csv(file_path, encoding='utf-8') - local_products = {} - for _, row in df.iterrows(): - local_products[row['اسم_المنتج']] = { - 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'], - 'بديل_محلي': row['بديل_محلي'], - 'مصدر': row['مصدر'], - 'ملاحظات': row['ملاحظات'] - } - except Exception as e: - print(f"خطأ في تحميل بيانات المنتجات المحلية: {str(e)}") - - return local_products - - def _load_local_services(self): - """تحميل بيانات الخدمات المحلية ونسب المحتوى المحلي""" - # محاكاة تحميل البيانات من مصدر بيانات - local_services = { - 'تصميم معماري': { - 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'متوفرة من مكاتب استشارية محلية' - }, - 'إشراف هندسي': { - 'نسبة_المحتوى_المحلي': 0.85, # 85% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'متوفر من شركات محلية' - }, - 'خدمات تنسيق المواقع': { - 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'شركات محلية متخصصة' - }, - 'خدمات أمن وسلامة': { - 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'شركات محلية متخصصة' - }, - 'استشارات بيئية': { - 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/دولي', - 'ملاحظات': 'متوفرة محليًا مع بعض الخبرات الأجنبية' - }, - 'دراسات جدوى': { - 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/دولي', - 'ملاحظات': 'متوفرة من مكاتب استشارية محلية' - }, - 'خدمات نقل': { - 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'شركات نقل محلية متعددة' - }, - 'صيانة ونظافة': { - 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'شركات محلية متخصصة' - } - } - - # محاولة تحميل البيانات من ملف إذا كان متاحًا - try: - file_path = os.path.join(config.DATA_DIR, 'local_services.csv') - if os.path.exists(file_path): - df = pd.read_csv(file_path, encoding='utf-8') - local_services = {} - for _, row in df.iterrows(): - local_services[row['اسم_الخدمة']] = { - 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'], - 'بديل_محلي': row['بديل_محلي'], - 'مصدر': row['مصدر'], - 'ملاحظات': row['ملاحظات'] - } - except Exception as e: - print(f"خطأ في تحميل بيانات الخدمات المحلية: {str(e)}") - - return local_services - - def _load_local_labor(self): - """تحميل بيانات القوى العاملة المحلية ونسب المحتوى المحلي""" - # محاكاة تحميل البيانات من مصدر بيانات - local_labor = { - 'عمال بناء': { - 'نسبة_المحتوى_المحلي': 0.60, # 60% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/أجنبي', - 'ملاحظات': 'متوفر محليًا مع نسبة من العمالة الأجنبية' - }, - 'مهندسون': { - 'نسبة_المحتوى_المحلي': 0.75, # 75% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/أجنبي', - 'ملاحظات': 'كفاءات محلية متوفرة' - }, - 'فنيون': { - 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/أجنبي', - 'ملاحظات': 'متوفر محليًا بنسب متفاوتة' - }, - 'إداريون': { - 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'معظمهم من الكوادر المحلية' - }, - 'مشرفون': { - 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي', - 'ملاحظات': 'معظمهم من الكوادر المحلية' - }, - 'مصممون': { - 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/أجنبي', - 'ملاحظات': 'كفاءات محلية مع بعض الخبرات الأجنبية' - }, - 'عمال مهرة': { - 'نسبة_المحتوى_المحلي': 0.55, # 55% محتوى محلي - 'بديل_محلي': True, - 'مصدر': 'محلي/أجنبي', - 'ملاحظات': 'نسبة من العمالة الأجنبية ذات الخبرة' - } - } - - # محاولة تحميل البيانات من ملف إذا كان متاحًا - try: - file_path = os.path.join(config.DATA_DIR, 'local_labor.csv') - if os.path.exists(file_path): - df = pd.read_csv(file_path, encoding='utf-8') - local_labor = {} - for _, row in df.iterrows(): - local_labor[row['فئة_العمالة']] = { - 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'], - 'بديل_محلي': row['بديل_محلي'], - 'مصدر': row['مصدر'], - 'ملاحظات': row['ملاحظات'] - } - except Exception as e: - print(f"خطأ في تحميل بيانات القوى العاملة المحلية: {str(e)}") - - return local_labor - - def calculate_project_local_content(self, project_data): - """ - حساب نسبة المحتوى المحلي للمشروع - - المعلمات: - project_data: بيانات المشروع، تتضمن مكونات المنتجات والخدمات والقوى العاملة - - إرجاع: - نسبة المحتوى المحلي الإجمالية، وتفاصيل حسب كل مكون - """ - # تهيئة نتائج الحساب - results = { - 'نسبة_المحتوى_المحلي_الإجمالية': 0, - 'تفاصيل_المكونات': { - 'المنتجات': {'نسبة': 0, 'تفاصيل': {}}, - 'الخدمات': {'نسبة': 0, 'تفاصيل': {}}, - 'القوى العاملة': {'نسبة': 0, 'تفاصيل': {}} - }, - 'ملخص_المحتوى_المحلي': {}, - 'توصيات_التحسين': [] - } - - # حساب نسبة المحتوى المحلي للمنتجات - if 'المنتجات' in project_data: - products_local_content = self._calculate_products_local_content(project_data['المنتجات']) - results['تفاصيل_المكونات']['المنتجات'] = products_local_content - - # حساب نسبة المحتوى المحلي للخدمات - if 'الخدمات' in project_data: - services_local_content = self._calculate_services_local_content(project_data['الخدمات']) - results['تفاصيل_المكونات']['الخدمات'] = services_local_content - - # حساب نسبة المحتوى المحلي للقوى العاملة - if 'القوى العاملة' in project_data: - labor_local_content = self._calculate_labor_local_content(project_data['القوى العاملة']) - results['تفاصيل_المكونات']['القوى العاملة'] = labor_local_content - - # حساب النسبة الإجمالية للمحتوى المحلي بناءً على الأوزان النسبية - total_local_content = 0 - for component, weight in self.component_weights.items(): - if component in results['تفاصيل_المكونات']: - component_percentage = results['تفاصيل_المكونات'][component]['نسبة'] - total_local_content += component_percentage * weight - - results['نسبة_المحتوى_المحلي_الإجمالية'] = total_local_content - - # تحديد ملخص المحتوى المحلي ومقارنته بالمستهدف - for component, target in self.targets.items(): - if component in results['تفاصيل_المكونات']: - actual = results['تفاصيل_المكونات'][component]['نسبة'] - status = 'مطابق' if actual >= target else 'غير مطابق' - gap = round((target - actual) * 100, 2) if actual < target else 0 - - results['ملخص_المحتوى_المحلي'][component] = { - 'المستهدف': target * 100, - 'الفعلي': round(actual * 100, 2), - 'الحالة': status, - 'الفجوة (%)': gap - } - - # توليد توصيات لتحسين نسبة المحتوى المحلي - results['توصيات_التحسين'] = self._generate_improvement_recommendations(results) - - return results - - def _calculate_products_local_content(self, products_data): - """ - حساب نسبة المحتوى المحلي للمنتجات - - المعلمات: - products_data: بيانات المنتجات المستخدمة في المشروع - - إرجاع: - تفاصيل نسبة المحتوى المحلي للمنتجات - """ - total_cost = 0 - local_content_value = 0 - details = {} - - for product_name, product_info in products_data.items(): - quantity = product_info.get('الكمية', 0) - unit_price = product_info.get('سعر_الوحدة', 0) - total_product_cost = quantity * unit_price - - # البحث عن نسبة المحتوى المحلي للمنتج - local_content_percentage = 0 - if product_name in self.local_products: - local_content_percentage = self.local_products[product_name]['نسبة_المحتوى_المحلي'] - - # حساب قيمة المحتوى المحلي للمنتج - product_local_content_value = total_product_cost * local_content_percentage - - # تحديث الإجماليات - total_cost += total_product_cost - local_content_value += product_local_content_value - - # تسجيل التفاصيل - details[product_name] = { - 'الكمية': quantity, - 'سعر_الوحدة': unit_price, - 'التكلفة_الإجمالية': total_product_cost, - 'نسبة_المحتوى_المحلي': local_content_percentage, - 'قيمة_المحتوى_المحلي': product_local_content_value, - 'مصدر': self.local_products.get(product_name, {}).get('مصدر', 'غير معروف'), - 'ملاحظات': self.local_products.get(product_name, {}).get('ملاحظات', '') - } - - # حساب النسبة الإجمالية للمحتوى المحلي للمنتجات - local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0 - - return { - 'نسبة': local_content_percentage, - 'إجمالي_التكلفة': total_cost, - 'قيمة_المحتوى_المحلي': local_content_value, - 'تفاصيل': details - } - - def _calculate_services_local_content(self, services_data): - """ - حساب نسبة المحتوى المحلي للخدمات - - المعلمات: - services_data: بيانات الخدمات المستخدمة في المشروع - - إرجاع: - تفاصيل نسبة المحتوى المحلي للخدمات - """ - total_cost = 0 - local_content_value = 0 - details = {} - - for service_name, service_info in services_data.items(): - cost = service_info.get('التكلفة', 0) - - # البحث عن نسبة المحتوى المحلي للخدمة - local_content_percentage = 0 - if service_name in self.local_services: - local_content_percentage = self.local_services[service_name]['نسبة_المحتوى_المحلي'] - - # حساب قيمة المحتوى المحلي للخدمة - service_local_content_value = cost * local_content_percentage - - # تحديث الإجماليات - total_cost += cost - local_content_value += service_local_content_value - - # تسجيل التفاصيل - details[service_name] = { - 'التكلفة': cost, - 'نسبة_المحتوى_المحلي': local_content_percentage, - 'قيمة_المحتوى_المحلي': service_local_content_value, - 'مصدر': self.local_services.get(service_name, {}).get('مصدر', 'غير معروف'), - 'ملاحظات': self.local_services.get(service_name, {}).get('ملاحظات', '') - } - - # حساب النسبة الإجمالية للمحتوى المحلي للخدمات - local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0 - - return { - 'نسبة': local_content_percentage, - 'إجمالي_التكلفة': total_cost, - 'قيمة_المحتوى_المحلي': local_content_value, - 'تفاصيل': details - } - - def _calculate_labor_local_content(self, labor_data): - """ - حساب نسبة المحتوى المحلي للقوى العاملة - - المعلمات: - labor_data: بيانات القوى العاملة المستخدمة في المشروع - - إرجاع: - تفاصيل نسبة المحتوى المحلي للقوى العاملة - """ - total_cost = 0 - local_content_value = 0 - details = {} - - for labor_type, labor_info in labor_data.items(): - count = labor_info.get('العدد', 0) - monthly_salary = labor_info.get('الراتب_الشهري', 0) - duration_months = labor_info.get('المدة_بالأشهر', 0) - - total_labor_cost = count * monthly_salary * duration_months - - # البحث عن نسبة المحتوى المحلي للقوى العاملة - local_content_percentage = 0 - if labor_type in self.local_labor: - local_content_percentage = self.local_labor[labor_type]['نسبة_المحتوى_المحلي'] - - # حساب قيمة المحتوى المحلي للقوى العاملة - labor_local_content_value = total_labor_cost * local_content_percentage - - # تحديث الإجماليات - total_cost += total_labor_cost - local_content_value += labor_local_content_value - - # تسجيل التفاصيل - details[labor_type] = { - 'العدد': count, - 'الراتب_الشهري': monthly_salary, - 'المدة_بالأشهر': duration_months, - 'التكلفة_الإجمالية': total_labor_cost, - 'نسبة_المحتوى_المحلي': local_content_percentage, - 'قيمة_المحتوى_المحلي': labor_local_content_value, - 'مصدر': self.local_labor.get(labor_type, {}).get('مصدر', 'غير معروف'), - 'ملاحظات': self.local_labor.get(labor_type, {}).get('ملاحظات', '') - } - - # حساب النسبة الإجمالية للمحتوى المحلي للقوى العاملة - local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0 - - return { - 'نسبة': local_content_percentage, - 'إجمالي_التكلفة': total_cost, - 'قيمة_المحتوى_المحلي': local_content_value, - 'تفاصيل': details - } - - def _generate_improvement_recommendations(self, results): - """ - توليد توصيات لتحسين نسبة المحتوى المحلي - - المعلمات: - results: نتائج حساب المحتوى المحلي - - إرجاع: - قائمة بالتوصيات لتحسين نسبة المحتوى المحلي - """ - recommendations = [] - - # تحليل المكونات التي تحتاج إلى تحسين - for component, summary in results['ملخص_المحتوى_المحلي'].items(): - if summary['الحالة'] == 'غير مطابق': - if component == 'المنتجات': - # تحديد المنتجات ذات المحتوى المحلي المنخفض - low_content_products = [] - for product, details in results['تفاصيل_المكونات']['المنتجات']['تفاصيل'].items(): - if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50% - low_content_products.append({ - 'اسم': product, - 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'], - 'التكلفة_الإجمالية': details['التكلفة_الإجمالية'] - }) - - elif component == 'الخدمات': - # تحديد البنود ذات المحتوى المحلي المنخفض - low_content_services = [] - for service, details in results['تفاصيل_المكونات']['الخدمات']['تفاصيل'].items(): - if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50% - low_content_services.append({ - 'اسم': service, - 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'], - 'التكلفة': details['التكلفة'] - }) - - elif component == 'القوى العاملة': - # تحديد فئات العمالة ذات المحتوى المحلي المنخفض - low_content_labor = [] - for labor_type, details in results['تفاصيل_المكونات']['القوى العاملة']['تفاصيل'].items(): - if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50% - low_content_labor.append({ - 'اسم': labor_type, - 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'], - 'التكلفة_الإجمالية': details['التكلفة_الإجمالية'] - }) - - # إنشاء توصيات لتحسين المحتوى المحلي - # توصيات للمنتجات - if 'المنتجات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['المنتجات']['الحالة'] == 'غير مطابق': - low_content_products = [] - for product, details in results['تفاصيل_المكونات']['المنتجات']['تفاصيل'].items(): - if details['نسبة_المحتوى_المحلي'] < 0.5: - low_content_products.append({ - 'اسم': product, - 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'] - }) - - if low_content_products: - recommendations.append(f"استبدال المنتجات ذات المحتوى المحلي المنخفض: {', '.join([p['اسم'] for p in low_content_products[:3]])}") - recommendations.append("البحث عن موردين محليين للمنتجات ذات الأولوية العالية") - - # توصيات للخدمات - if 'الخدمات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['الخدمات']['الحالة'] == 'غير مطابق': - low_content_services = [] - for service, details in results['تفاصيل_المكونات']['الخدمات']['تفاصيل'].items(): - if details['نسبة_المحتوى_المحلي'] < 0.5: - low_content_services.append({ - 'اسم': service, - 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'] - }) - - if low_content_services: - recommendations.append(f"تحسين نسبة المحتوى المحلي للخدمات: {', '.join([s['اسم'] for s in low_content_services[:3]])}") - recommendations.append("التعاقد مع شركات خدمية محلية") - - # توصيات للقوى العاملة - if 'القوى العاملة' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['القوى العاملة']['الحالة'] == 'غير مطابق': - low_content_labor = [] - for labor_type, details in results['تفاصيل_المكونات']['القوى العاملة']['تفاصيل'].items(): - if details['نسبة_المحتوى_المحلي'] < 0.5: - low_content_labor.append({ - 'اسم': labor_type, - 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'] - }) - - if low_content_labor: - recommendations.append(f"زيادة توظيف العمالة المحلية في الفئات: {', '.join([l['اسم'] for l in low_content_labor[:3]])}") - recommendations.append("الاستثمار في برامج تدريب وتأهيل الكوادر المحلية") - - # توصيات عامة - if 'المنتجات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['المنتجات'].get('الفجوة (%)', 0) > 10: - recommendations.append(f"خطة تطوير المحتوى المحلي للمنتجات لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['المنتجات']['الفجوة (%)']}%") - - if 'الخدمات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['الخدمات'].get('الفجوة (%)', 0) > 10: - recommendations.append(f"خطة تطوير المحتوى المحلي للخدمات لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['الخدمات']['الفجوة (%)']}%") - - if 'القوى العاملة' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['القوى العاملة'].get('الفجوة (%)', 0) > 10: - recommendations.append(f"خطة تطوير المحتوى المحلي للقوى العاملة لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['القوى العاملة']['الفجوة (%)']}%") - - return recommendations \ No newline at end of file diff --git a/modules/pricing/services/price_prediction.py b/modules/pricing/services/price_prediction.py deleted file mode 100644 index 8579c10d5cb8f388dff85c08a6131a2e0dd44afe..0000000000000000000000000000000000000000 --- a/modules/pricing/services/price_prediction.py +++ /dev/null @@ -1,444 +0,0 @@ -""" -خدمة التنبؤ بالأسعار -""" - -import pandas as pd -import numpy as np -import joblib -import os -from datetime import datetime, timedelta -from sklearn.ensemble import RandomForestRegressor -from sklearn.model_selection import train_test_split -from sklearn.preprocessing import StandardScaler -from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score - -import config - - -class PricePrediction: - """خدمة التنبؤ بالأسعار باستخدام التعلم الآلي""" - - def __init__(self): - """تهيئة خدمة التنبؤ بالأسعار""" - self.model_path = config.PRICE_PREDICTION_MODEL - self.model = self._load_model() - self.scaler = None - self.materials_data = self._load_materials_data() - self.market_indices = self._load_market_indices() - - def _load_model(self): - """تحميل نموذج التنبؤ المدرب مسبقاً""" - try: - if os.path.exists(self.model_path): - model = joblib.load(self.model_path) - return model - else: - # إذا لم يكن النموذج موجوداً، قم بإنشاء نموذج جديد - model = RandomForestRegressor( - n_estimators=100, - max_depth=15, - min_samples_split=5, - min_samples_leaf=2, - random_state=42 - ) - return model - except Exception as e: - print(f"خطأ في تحميل نموذج التنبؤ: {str(e)}") - return RandomForestRegressor(random_state=42) - - def _load_materials_data(self): - """تحميل بيانات المواد وأسعارها التاريخية""" - # محاكاة تحميل البيانات من مصدر بيانات - materials_data = { - 'خرسانة': { - 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)], - 'سعر': [750, 740, 735, 730, 720, 715, 710, 700, 695, 690, 685, 680], - 'وحدة': 'م3' - }, - 'حديد تسليح': { - 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)], - 'سعر': [5500, 5450, 5400, 5350, 5300, 5250, 5200, 5150, 5100, 5050, 5000, 4950], - 'وحدة': 'طن' - }, - 'إسمنت': { - 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)], - 'سعر': [25, 25, 24.5, 24.5, 24, 24, 23.5, 23.5, 23, 23, 22.5, 22.5], - 'وحدة': 'كيس' - }, - 'رمل': { - 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)], - 'سعر': [140, 140, 135, 135, 130, 130, 125, 125, 120, 120, 115, 115], - 'وحدة': 'م3' - }, - 'بلوك خرساني': { - 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)], - 'سعر': [11, 11, 10.5, 10.5, 10, 10, 9.5, 9.5, 9, 9, 8.5, 8.5], - 'وحدة': 'قطعة' - } - } - return materials_data - - def _load_market_indices(self): - """تحميل مؤشرات السوق المؤثرة على الأسعار""" - # محاكاة تحميل البيانات من مصدر بيانات - market_indices = { - 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)], - 'مؤشر_البناء': [105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94], - 'مؤشر_النفط': [80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69], - 'مؤشر_سعر_الصرف': [3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75], - 'مؤشر_التضخم': [2.5, 2.4, 2.3, 2.2, 2.1, 2.0, 1.9, 1.8, 1.7, 1.6, 1.5, 1.4] - } - return market_indices - - def train(self, training_data=None): - """ - تدريب نموذج التنبؤ بالأسعار - - المعلمات: - training_data: بيانات التدريب (اختياري)، إذا لم يتم توفيرها سيتم استخدام البيانات المتاحة - - إرجاع: - مؤشرات أداء النموذج - """ - # تجهيز بيانات التدريب - if training_data is None: - # استخدام البيانات المتاحة لتوليد مجموعة تدريب - X, y = self._prepare_training_data() - else: - X, y = self._extract_features_target(training_data) - - # تقسيم البيانات إلى تدريب واختبار - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42) - - # تطبيع البيانات - self.scaler = StandardScaler() - X_train_scaled = self.scaler.fit_transform(X_train) - X_test_scaled = self.scaler.transform(X_test) - - # تدريب النموذج - self.model.fit(X_train_scaled, y_train) - - # تقييم النموذج - y_pred = self.model.predict(X_test_scaled) - - # حساب مؤشرات الأداء - mae = mean_absolute_error(y_test, y_pred) - rmse = np.sqrt(mean_squared_error(y_test, y_pred)) - r2 = r2_score(y_test, y_pred) - - # حفظ النموذج - try: - joblib.dump(self.model, self.model_path) - joblib.dump(self.scaler, os.path.join(os.path.dirname(self.model_path), 'price_scaler.pkl')) - except Exception as e: - print(f"خطأ في حفظ النموذج: {str(e)}") - - return { - 'mae': mae, - 'rmse': rmse, - 'r2': r2 - } - - def _prepare_training_data(self): - """تجهيز بيانات التدريب من البيانات المتاحة""" - # توليد بيانات تدريب افتراضية - data = [] - target = [] - - # استخدام بيانات المواد وأسعارها التاريخية - for material_name, material_info in self.materials_data.items(): - for i in range(len(material_info['تاريخ'])): - # استخراج المؤشرات في التاريخ المقابل - date_index = self.market_indices['تاريخ'].index(material_info['تاريخ'][i]) if material_info['تاريخ'][i] in self.market_indices['تاريخ'] else 0 - - # تكوين ميزات التدريب (المؤشرات السوقية والشهر) - features = [ - material_info['تاريخ'][i].month, # الشهر - self.market_indices['مؤشر_البناء'][date_index], - self.market_indices['مؤشر_النفط'][date_index], - self.market_indices['مؤشر_سعر_الصرف'][date_index], - self.market_indices['مؤشر_التضخم'][date_index] - ] - - # إضافة معرّف للمادة (تمثيل رقمي) - material_id = list(self.materials_data.keys()).index(material_name) - features.append(material_id) - - data.append(features) - target.append(material_info['سعر'][i]) - - # إضافة ضوضاء عشوائية لزيادة حجم البيانات - for _ in range(5): - noisy_features = features.copy() - for j in range(1, 5): # إضافة ضوضاء للمؤشرات فقط - noisy_features[j] += np.random.normal(0, 0.5) - - noisy_price = material_info['سعر'][i] * (1 + np.random.normal(0, 0.02)) # ضوضاء 2% - - data.append(noisy_features) - target.append(noisy_price) - - return np.array(data), np.array(target) - - def _extract_features_target(self, training_data): - """استخراج الميزات والأهداف من بيانات التدريب""" - # استخراج الميزات والأهداف من البيانات المقدمة - features = [] - target = [] - - for item in training_data: - features.append([ - item['date'].month, # الشهر - item['building_index'], - item['oil_index'], - item['exchange_rate'], - item['inflation_rate'], - item['material_id'] - ]) - target.append(item['price']) - - return np.array(features), np.array(target) - - def predict_prices(self, materials, prediction_date=None, market_conditions=None): - """ - التنبؤ بأسعار المواد - - المعلمات: - materials: قائمة المواد المطلوب التنبؤ بأسعارها - prediction_date: تاريخ التنبؤ (اختياري)، إذا لم يتم توفيره سيتم استخدام التاريخ الحالي - market_conditions: ظروف السوق (اختياري)، إذا لم يتم توفيرها سيتم استخدام آخر قيم متاحة - - إرجاع: - قاموس بأسعار المواد المتنبأ بها - """ - if prediction_date is None: - prediction_date = datetime.now() - - if market_conditions is None: - # استخدام آخر قيم متاحة للمؤشرات - market_conditions = { - 'مؤشر_البناء': self.market_indices['مؤشر_البناء'][0], - 'مؤشر_النفط': self.market_indices['مؤشر_النفط'][0], - 'مؤشر_سعر_الصرف': self.market_indices['مؤشر_سعر_الصرف'][0], - 'مؤشر_التضخم': self.market_indices['مؤشر_التضخم'][0] - } - - # التحقق من وجود المواد في البيانات - material_names = list(self.materials_data.keys()) - valid_materials = [m for m in materials if m in material_names] - - if not valid_materials: - return {} - - # تحميل المعايير إذا كانت متوفرة - scaler_path = os.path.join(os.path.dirname(self.model_path), 'price_scaler.pkl') - if self.scaler is None and os.path.exists(scaler_path): - try: - self.scaler = joblib.load(scaler_path) - except Exception as e: - print(f"خطأ في تحميل المعايير: {str(e)}") - # إنشاء معايير جديدة - X, _ = self._prepare_training_data() - self.scaler = StandardScaler() - self.scaler.fit(X) - - # إعداد ميزات التنبؤ - features = [] - for material in valid_materials: - material_id = material_names.index(material) - - material_features = [ - prediction_date.month, # الشهر - market_conditions['مؤشر_البناء'], - market_conditions['مؤشر_النفط'], - market_conditions['مؤشر_سعر_الصرف'], - market_conditions['مؤشر_التضخم'], - material_id - ] - - features.append(material_features) - - # تطبيع الميزات - if self.scaler is not None: - features_scaled = self.scaler.transform(features) - else: - features_scaled = features - - # التنبؤ بالأسعار - predicted_prices = self.model.predict(features_scaled) - - # إرجاع النتائج - results = {} - for i, material in enumerate(valid_materials): - # تطبيق عامل تصحيح (2% عشوائية) - correction_factor = 1.0 + np.random.uniform(-0.02, 0.02) - price = max(0, predicted_prices[i] * correction_factor) - - results[material] = { - 'سعر': price, - 'وحدة': self.materials_data[material]['وحدة'], - 'تاريخ_التنبؤ': prediction_date.strftime('%Y-%m-%d'), - 'هامش_الخطأ': '±5%' # تقدير هامش الخطأ - } - - return results - - def get_price_trends(self, material, periods=6): - """ - الحصول على اتجاهات الأسعار المستقبلية - - المعلمات: - material: المادة المطلوب التنبؤ باتجاهات أسعارها - periods: عدد الفترات المستقبلية (الشهور) - - إرجاع: - قائمة بالأسعار المتوقعة للفترات المستقبلية - """ - if material not in self.materials_data: - return [] - - # الحصول على التاريخ الحالي - current_date = datetime.now() - - # التنبؤ بالأسعار للفترات المستقبلية - price_trends = [] - - for i in range(periods): - prediction_date = current_date + timedelta(days=30 * (i + 1)) - - # افتراض تغيرات طفيفة في المؤشرات مع مرور الوقت - market_conditions = { - 'مؤشر_البناء': self.market_indices['مؤشر_البناء'][0] * (1 + 0.01 * i), # زيادة 1% شهرياً - 'مؤشر_النفط': self.market_indices['مؤشر_النفط'][0] * (1 + 0.005 * i), # زيادة 0.5% شهرياً - 'مؤشر_سعر_الصرف': self.market_indices['مؤشر_سعر_الصرف'][0], # ثابت - 'مؤشر_التضخم': self.market_indices['مؤشر_التضخم'][0] * (1 + 0.01 * i) # زيادة 1% شهرياً - } - - # التنبؤ بالسعر - predicted_price = self.predict_prices([material], prediction_date, market_conditions) - - price_trends.append({ - 'تاريخ': prediction_date.strftime('%Y-%m'), - 'سعر': predicted_price[material]['سعر'] if material in predicted_price else 0 - }) - - return price_trends - - def analyze_factors(self, material): - """ - تحليل العوامل المؤثرة على سعر المادة - - المعلمات: - material: المادة المطلوب تحليلها - - إرجاع: - قاموس بالعوامل المؤثرة وأهميتها النسبية - """ - if material not in self.materials_data or not hasattr(self.model, 'feature_importances_'): - return {} - - # الحصول على أهمية الميزات من النموذج - feature_importances = self.model.feature_importances_ - - # أسماء الميزات - feature_names = ['الشهر', 'مؤشر البناء', 'مؤشر النفط', 'سعر الصرف', 'معدل التضخم', 'نوع المادة'] - - # ترتيب الميزات حسب الأهمية - importance_pairs = [(name, importance) for name, importance in zip(feature_names, feature_importances)] - importance_pairs.sort(key=lambda x: x[1], reverse=True) - - # إرجاع العوامل المؤثرة وأهميتها - factors = {} - for name, importance in importance_pairs: - factors[name] = round(importance * 100, 2) # تحويل إلى نسبة مئوية - - return { - 'العوامل_المؤثرة': factors, - 'المادة': material, - 'وحدة': self.materials_data[material]['وحدة'], - 'سعر_حالي': self.materials_data[material]['سعر'][0], - 'اتجاه_السعر': self._get_price_trend(material) - } - - def _get_price_trend(self, material): - """تحديد اتجاه سعر المادة بناءً على البيانات التاريخية""" - if material not in self.materials_data: - return "غير معروف" - - prices = self.materials_data[material]['سعر'] - if len(prices) < 2: - return "غير معروف" - - # حساب متوسط التغير الشهري - price_changes = [(prices[i] - prices[i+1]) / prices[i+1] * 100 for i in range(len(prices)-1)] - avg_monthly_change = sum(price_changes) / len(price_changes) - - if avg_monthly_change > 1: - return "ارتفاع حاد" - elif avg_monthly_change > 0.2: - return "ارتفاع معتدل" - elif avg_monthly_change > -0.2: - return "استقرار" - elif avg_monthly_change > -1: - return "انخفاض معتدل" - else: - return "انخفاض حاد" - - def export_price_forecast(self, materials, periods=6, output_file=None): - """ - تصدير توقعات الأسعار إلى ملف - - المعلمات: - materials: قائمة المواد المطلوب التنبؤ بأسعارها - periods: عدد الفترات المستقبلية (الشهور) - output_file: مسار ملف الإخراج (اختياري) - - إرجاع: - مسار الملف المصدر أو البيانات مباشرة إذا لم يتم تحديد ملف - """ - # التحقق من وجود المواد في البيانات - valid_materials = [m for m in materials if m in self.materials_data] - - if not valid_materials: - return None - - # إعداد بيانات التوقعات - forecast_data = [] - - for material in valid_materials: - # الحصول على اتجاهات الأسعار - price_trends = self.get_price_trends(material, periods) - - for trend in price_trends: - forecast_data.append({ - 'المادة': material, - 'الوحدة': self.materials_data[material]['وحدة'], - 'التاريخ': trend['تاريخ'], - 'السعر المتوقع': trend['سعر'], - 'هامش الخطأ': '±5%' - }) - - # تحويل البيانات إلى DataFrame - forecast_df = pd.DataFrame(forecast_data) - - # تصدير البيانات إلى ملف إذا تم تحديده - if output_file: - try: - ext = os.path.splitext(output_file)[1].lower() - - if ext == '.csv': - forecast_df.to_csv(output_file, index=False, encoding='utf-8-sig') - elif ext in ['.xlsx', '.xls']: - forecast_df.to_excel(output_file, index=False) - elif ext == '.json': - forecast_df.to_json(output_file, orient='records', force_ascii=False) - else: - print(f"تنسيق غير مدعوم: {ext}") - return None - - return output_file - except Exception as e: - print(f"خطأ في تصدير توقعات الأسعار: {str(e)}") - return None - - return forecast_df \ No newline at end of file diff --git a/modules/pricing/services/standard_pricing.py b/modules/pricing/services/standard_pricing.py deleted file mode 100644 index ec89e61a101f89432b815718b8b615c5c00ecf45..0000000000000000000000000000000000000000 --- a/modules/pricing/services/standard_pricing.py +++ /dev/null @@ -1,232 +0,0 @@ -""" -خدمة التسعير القياسي -""" - -import pandas as pd -import numpy as np -from datetime import datetime -import os -import config - - -class StandardPricing: - """خدمة التسعير القياسي للبنود""" - - def __init__(self): - """تهيئة خدمة التسعير القياسي""" - # تحميل بيانات المواد والأسعار المرجعية - self.material_prices = self._load_material_prices() - self.labor_rates = self._load_labor_rates() - self.equipment_rates = self._load_equipment_rates() - - def _load_material_prices(self): - """تحميل أسعار المواد""" - # محاكاة تحميل البيانات من مصدر بيانات - material_prices = { - 'خرسانة': { - 'م3': 750.0, # سعر المتر المكعب بالريال - 'وحدة_قياسية': 'م3', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'حديد تسليح': { - 'طن': 5500.0, # سعر الطن بالريال - 'وحدة_قياسية': 'طن', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'عزل مائي': { - 'م2': 80.0, # سعر المتر المربع بالريال - 'وحدة_قياسية': 'م2', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'بلوك خرساني': { - '20سم': 11.0, # سعر البلكة بالريال - 'وحدة_قياسية': 'عدد', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'رمل': { - 'م3': 140.0, # سعر المتر المكعب بالريال - 'وحدة_قياسية': 'م3', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'اسمنت': { - 'كيس': 25.0, # سعر الكيس بالريال - 'وحدة_قياسية': 'كيس', - 'آخر_تحديث': datetime(2025, 3, 1) - } - } - return material_prices - - def _load_labor_rates(self): - """تحميل معدلات أجور العمالة""" - # محاكاة تحميل البيانات من مصدر بيانات - labor_rates = { - 'عامل': { - 'يومي': 150.0, # الأجر اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'نجار': { - 'يومي': 250.0, # الأجر اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'حداد': { - 'يومي': 250.0, # الأجر اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'سباك': { - 'يومي': 300.0, # الأجر اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'كهربائي': { - 'يومي': 300.0, # الأجر اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'مراقب': { - 'يومي': 400.0, # الأجر اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - } - } - return labor_rates - - def _load_equipment_rates(self): - """تحميل معدلات تأجير المعدات""" - # محاكاة تحميل البيانات من مصدر بيانات - equipment_rates = { - 'خلاطة خرسانة': { - 'يومي': 800.0, # الإيجار اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'هزاز خرسانة': { - 'يومي': 150.0, # الإيجار اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'حفارة': { - 'يومي': 1500.0, # الإيجار اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'لودر': { - 'يومي': 1200.0, # الإيجار اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'رافعة': { - 'يومي': 2000.0, # الإيجار اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - }, - 'شاحنة نقل': { - 'يومي': 900.0, # الإيجار اليومي بالريال - 'وحدة_قياسية': 'يوم', - 'آخر_تحديث': datetime(2025, 3, 1) - } - } - return equipment_rates - - def calculate_prices(self, items_df): - """حساب الأسعار للبنود باستخدام التسعير القياسي""" - # نسخة من البيانات المدخلة للعمل عليها - df = items_df.copy() - - # التأكد من وجود العمود المطلوب - if 'سعر الوحدة' not in df.columns: - df['سعر الوحدة'] = 0.0 - - if 'الإجمالي' not in df.columns: - df['الإجمالي'] = 0.0 - - # حساب أسعار الوحدات لكل بند - for idx, row in df.iterrows(): - # حساب سعر الوحدة بناءً على وصف البند - unit_price = self._estimate_unit_price(row['وصف البند'], row['الوحدة']) - df.at[idx, 'سعر الوحدة'] = unit_price - - # حساب الإجمالي لكل بند - df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة'] - - return df - - def _estimate_unit_price(self, description, unit): - """تقدير سعر الوحدة بناءً على وصف البند ووحدة القياس""" - description = description.lower() - - # تقدير سعر الوحدة بناءً على وصف البند - if 'خرسان' in description: - if 'أساسات' in description: - return 1200.0 if unit == 'م3' else 0.0 - elif 'أعمدة' in description: - return 1800.0 if unit == 'م3' else 0.0 - elif 'سقف' in description: - return 1500.0 if unit == 'م3' else 0.0 - else: - return 1400.0 if unit == 'م3' else 0.0 - - elif 'حديد' in description and 'تسليح' in description: - if 'أساسات' in description: - return 6000.0 if unit == 'طن' else 0.0 - elif 'أعمدة' in description or 'سقف' in description: - return 6500.0 if unit == 'طن' else 0.0 - else: - return 6200.0 if unit == 'طن' else 0.0 - - elif 'عزل' in description: - if 'مائي' in description: - return 120.0 if unit == 'م2' else 0.0 - elif 'حراري' in description: - return 90.0 if unit == 'م2' else 0.0 - else: - return 100.0 if unit == 'م2' else 0.0 - - elif 'ردم' in description or 'حفر' in description: - return 75.0 if unit == 'م3' else 0.0 - - elif 'بلوك' in description or 'طوب' in description: - return 250.0 if unit == 'م2' else 0.0 - - elif 'لياسة' in description or 'بياض' in description: - return 80.0 if unit == 'م2' else 0.0 - - elif 'دهان' in description or 'طلاء' in description: - return 65.0 if unit == 'م2' else 0.0 - - elif 'سيراميك' in description or 'بلاط' in description: - return 180.0 if unit == 'م2' else 0.0 - - elif 'كهرباء' in description: - return 150.0 if unit == 'نقطة' else 500.0 - - # قيمة افتراضية إذا لم تتطابق مع أي وصف - return 100.0 - - def adjust_prices_for_factors(self, items_df, factors=None): - """تعديل الأسعار بناءً على عوامل مؤثرة""" - # نسخة من البيانات المدخلة للعمل عليها - df = items_df.copy() - - # إذا لم يتم تحديد عوامل، استخدم العوامل الافتراضية - if factors is None: - factors = { - 'location_factor': 1.0, # معامل الموقع - 'time_factor': 1.0, # معامل الوقت - 'risk_factor': 1.1, # معامل المخاطر - 'market_factor': 1.05 # معامل السوق - } - - # حساب المعامل الإجمالي - total_factor = (factors['location_factor'] * factors['time_factor'] * - factors['risk_factor'] * factors['market_factor']) - - # تعديل سعر الوحدة بناءً على المعامل الإجمالي - df['سعر الوحدة'] = df['سعر الوحدة'] * total_factor - - # حساب الإجمالي بعد التعديل - df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة'] - - return df \ No newline at end of file diff --git a/modules/pricing/services/templates_catalog/__init__.py b/modules/pricing/services/templates_catalog/__init__.py deleted file mode 100644 index 8c7a39ff16d8fa482e8d44f777bee35f84728795..0000000000000000000000000000000000000000 --- a/modules/pricing/services/templates_catalog/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .templates_catalog import TemplatesCatalog - -__all__ = ['TemplatesCatalog'] \ No newline at end of file diff --git a/modules/pricing/services/templates_catalog/templates_catalog.py b/modules/pricing/services/templates_catalog/templates_catalog.py deleted file mode 100644 index 23d6006f8f7781a90a22203f309372e16bcc84f6..0000000000000000000000000000000000000000 --- a/modules/pricing/services/templates_catalog/templates_catalog.py +++ /dev/null @@ -1,949 +0,0 @@ -""" -كتالوج قوالب البناء والمقاولات -واجهة مستخدم متكاملة لعرض واستخدام نماذج بنود البناء الجاهزة -""" - -import os -import sys -import json -import pandas as pd -import streamlit as st -from typing import Dict, List, Any, Optional - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../.."))) - -# استيراد مكونات واجهة المستخدم -from utils.components.header import render_header -# استيراد الدالة مباشرة من الملف -from utils.components.credits import display_credits -# استخدام display_credits كبديل لـ render_credits -render_credits = display_credits -from utils.helpers import format_number, format_currency, styled_button, filter_dataframe - -# النموذج المستخدم عند إضافة بند نموذجي جديد من صفحة التسعير -class NewTemplateForm: - """نموذج إضافة بند نموذجي جديد من صفحة التسعير""" - pass - -class TemplatesCatalog: - """كتالوج قوالب البناء والمقاولات""" - - def __init__(self, construction_templates): - """تهيئة كتالوج قوالب البناء والمقاولات""" - self.construction_templates = construction_templates - - # تهيئة حالة الجلسة للطلبات والعروض - if 'material_requests' not in st.session_state: - st.session_state.material_requests = [] - - if 'equipment_requests' not in st.session_state: - st.session_state.equipment_requests = [] - - if 'material_offers' not in st.session_state: - st.session_state.material_offers = [] - - if 'equipment_offers' not in st.session_state: - st.session_state.equipment_offers = [] - - # تهيئة قوائم الأسعار المرجعية - if 'reference_price_list' not in st.session_state: - st.session_state.reference_price_list = [] - - # تعبئة قوائم الأسعار المرجعية من كتالوج البنود النموذجية - self._populate_reference_price_list() - - def render(self): - """عرض واجهة كتالوج القوالب""" - # عرض الشعار والعنوان الرئيسي - render_header("كتالوج بنود المقاولات النموذجية") - - # تبويبات الكتالوج - tabs = st.tabs(["تصفح البنود النموذجية", "طلب تسعير مواد جديدة", "طلب تسعير معدات جديدة", "قوائم الأسعار المرجعية"]) - - with tabs[0]: - self._render_templates_browser() - - with tabs[1]: - self._render_material_request_form() - - with tabs[2]: - self._render_equipment_request_form() - - with tabs[3]: - self._render_reference_price_list() - - def _render_templates_browser(self): - """عرض واجهة تصفح كتالوج القوالب""" - st.markdown(""" -
-

🗃️ نماذج بنود المقاولات الجاهزة للاستخدام

-

يمكنك الاختيار من بين مجموعة متنوعة من نماذج البنود المعرفة مسبقًا والجاهزة للاستخدام في مشاريعك وعروض أسعارك.

-

تشمل النماذج تفاصيل كاملة عن:

- -
- """, unsafe_allow_html=True) - - # قسم البحث والتصفية - st.markdown("### تصفية البنود النموذجية") - - # الحصول على الفئات من الكتالوج - templates = self.construction_templates.get_all_templates() - categories = templates.get("categories", {}) - - # إنشاء قائمة الفئات - category_list = [{"id": cat_id, "name": cat_data.get("name", cat_id)} for cat_id, cat_data in categories.items()] - category_names = ["الكل"] + [cat["name"] for cat in category_list] - - # تصفية حسب الفئة - col1, col2 = st.columns(2) - with col1: - selected_category_name = st.selectbox("فئة البنود", category_names, index=0) - - with col2: - search_query = st.text_input("بحث في النماذج", placeholder="اكتب كلمة للبحث...") - - # تحديد الفئة المختارة - selected_category_id = None - if selected_category_name != "الكل": - for cat in category_list: - if cat["name"] == selected_category_name: - selected_category_id = cat["id"] - break - - # الحصول على النماذج المصفاة - filtered_templates = [] - all_templates = templates.get("templates", {}) - - for template_id, template in all_templates.items(): - # التصفية حسب الفئة إذا تم اختيار فئة محددة - if selected_category_id and template.get("category") != selected_category_id: - continue - - # التصفية حسب البحث إذا تم إدخال نص للبحث - if search_query: - template_name = template.get("name", "") - template_desc = template.get("description", "") - searchable_text = f"{template_name} {template_desc}" - if search_query.lower() not in searchable_text.lower(): - continue - - # إضافة النموذج إلى القائمة المصفاة - template_copy = template.copy() - template_copy["id"] = template_id - filtered_templates.append(template_copy) - - # عرض النماذج المصفاة - self._render_templates_list(filtered_templates) - - # عرض نموذج إضافة قالب جديد - with st.expander("إضافة قالب نموذجي جديد"): - self._render_new_template_form() - - # عرض الحقوق - render_credits() - - def _render_templates_list(self, templates: List[Dict[str, Any]]): - """عرض قائمة النماذج المصفاة""" - - if not templates: - st.warning("لا توجد نماذج بنود متاحة تطابق معايير البحث.") - return - - # تحويل النماذج إلى DataFrame - templates_data = [] - for template in templates: - # حساب التكلفة التقديرية للنموذج - estimated_cost = self._calculate_template_cost(template) - - templates_data.append({ - "الرقم التعريفي": template["id"], - "اسم النموذج": template.get("name", ""), - "الوصف": template.get("description", "")[:50] + "..." if len(template.get("description", "")) > 50 else template.get("description", ""), - "التكلفة التقديرية": estimated_cost, - "الوحدة": template.get("unit", ""), - "الفئة": template.get("category", "") - }) - - templates_df = pd.DataFrame(templates_data) - - # تنسيق الجدول - def highlight_row(row): - """تنسيق الجدول مع تمييز الصفوف بالتناوب""" - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = templates_df.style.apply(highlight_row, axis=1) - styled_df = styled_df.format({ - "التكلفة التقديرية": "{:,.2f} ريال" - }) - - # عرض الجدول - st.markdown("### قائمة النماذج المتاحة") - st.dataframe(styled_df, use_container_width=True, hide_index=True) - - # عرض تفاصيل النموذج المحدد - selected_template_id = st.selectbox( - "اختر نموذجًا لعرض التفاصيل", - options=[t["الرقم التعريفي"] for t in templates_data], - format_func=lambda x: next((t["اسم النموذج"] for t in templates_data if t["الرقم التعريفي"] == x), x) - ) - - if selected_template_id: - # الحصول على النموذج المحدد - selected_template = next((t for t in templates if t["id"] == selected_template_id), None) - if selected_template: - self._render_template_details(selected_template) - - def _render_template_details(self, template: Dict[str, Any]): - """عرض تفاصيل النموذج المحدد""" - st.markdown(f"### تفاصيل نموذج: {template.get('name', '')}") - - # معلومات النموذج الأساسية - col1, col2, col3 = st.columns(3) - with col1: - st.markdown(f"**الوصف:** {template.get('description', '')}") - with col2: - st.markdown(f"**وحدة القياس:** {template.get('unit', '')}") - with col3: - # حساب التكلفة التقديرية للنموذج - estimated_cost = self._calculate_template_cost(template) - st.markdown(f"**التكلفة التقديرية:** {estimated_cost:,.2f} ريال") - - # تبويبات لعرض مكونات النموذج - tabs = st.tabs(["المواد", "العمالة", "المعدات", "التكاليف"]) - - # تبويب المواد - with tabs[0]: - self._render_materials_tab(template) - - # تبويب العمالة - with tabs[1]: - self._render_labor_tab(template) - - # تبويب المعدات - with tabs[2]: - self._render_equipment_tab(template) - - # تبويب التكاليف - with tabs[3]: - self._render_costs_tab(template) - - # أزرار العمليات - col1, col2, col3 = st.columns(3) - - with col1: - if styled_button("إضافة إلى حاسبة التكاليف", key=f"add_to_calc_{template['id']}", type="primary", icon="➕"): - st.session_state.selected_template_for_calculator = template["id"] - st.success("تم إضافة النموذج إلى حاسبة التكاليف!") - - with col2: - if styled_button("استخدام في بند جديد", key=f"use_in_new_item_{template['id']}", type="success", icon="🔄"): - st.session_state.selected_template_for_new_item = template["id"] - st.success("تم اختيار النموذج لاستخدامه في بند جديد!") - - with col3: - if styled_button("تحرير النموذج", key=f"edit_template_{template['id']}", type="secondary", icon="✏️"): - st.session_state.template_to_edit = template["id"] - - def _render_materials_tab(self, template: Dict[str, Any]): - """عرض تبويب المواد""" - components = template.get("components", {}) - materials = components.get("materials", []) - - if not materials: - st.info("لا توجد مواد محددة لهذا النموذج.") - return - - # تحويل المواد إلى DataFrame - materials_data = [] - total_materials_cost = 0 - - for material in materials: - material_cost = material.get("الكمية", 0) * material.get("سعر_الوحدة", 0) - total_materials_cost += material_cost - - materials_data.append({ - "اسم المادة": material.get("الاسم", ""), - "الكمية": material.get("الكمية", 0), - "الوحدة": material.get("الوحدة", ""), - "سعر الوحدة": material.get("سعر_الوحدة", 0), - "التكلفة": material_cost - }) - - materials_df = pd.DataFrame(materials_data) - - # تنسيق الجدول - def highlight_row(row): - """تنسيق الجدول مع تمييز الصفوف بالتناوب""" - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = materials_df.style.apply(highlight_row, axis=1) - styled_df = styled_df.format({ - "الكمية": "{:.2f}", - "سعر الوحدة": "{:,.2f} ريال", - "التكلفة": "{:,.2f} ريال" - }) - - # عرض الجدول - st.dataframe(styled_df, use_container_width=True, hide_index=True) - st.markdown(f"**إجمالي تكلفة المواد:** {total_materials_cost:,.2f} ريال") - - def _render_labor_tab(self, template: Dict[str, Any]): - """عرض تبويب العمالة""" - components = template.get("components", {}) - labor = components.get("labor", []) - - if not labor: - st.info("لا توجد عمالة محددة لهذا النموذج.") - return - - # تحويل العمالة إلى DataFrame - labor_data = [] - total_labor_cost = 0 - - for worker in labor: - labor_cost = worker.get("العدد", 0) * worker.get("المدة", 0) * worker.get("سعر_اليوم", 0) - total_labor_cost += labor_cost - - labor_data.append({ - "نوع العامل": worker.get("النوع", ""), - "العدد": worker.get("العدد", 0), - "المدة (يوم)": worker.get("المدة", 0), - "سعر اليوم": worker.get("سعر_اليوم", 0), - "التكلفة": labor_cost - }) - - labor_df = pd.DataFrame(labor_data) - - # تنسيق الجدول - def highlight_row(row): - """تنسيق الجدول مع تمييز الصفوف بالتناوب""" - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = labor_df.style.apply(highlight_row, axis=1) - styled_df = styled_df.format({ - "المدة (يوم)": "{:.2f}", - "سعر اليوم": "{:,.2f} ريال", - "التكلفة": "{:,.2f} ريال" - }) - - # عرض الجدول - st.dataframe(styled_df, use_container_width=True, hide_index=True) - st.markdown(f"**إجمالي تكلفة العمالة:** {total_labor_cost:,.2f} ريال") - - def _render_equipment_tab(self, template: Dict[str, Any]): - """عرض تبويب المعدات""" - components = template.get("components", {}) - equipment = components.get("equipment", []) - - if not equipment: - st.info("لا توجد معدات محددة لهذا النموذج.") - return - - # تحويل المعدات إلى DataFrame - equipment_data = [] - total_equipment_cost = 0 - - for eq in equipment: - equipment_cost = eq.get("العدد", 0) * eq.get("المدة", 0) * eq.get("سعر_اليوم", 0) - total_equipment_cost += equipment_cost - - equipment_data.append({ - "نوع المعدة": eq.get("النوع", ""), - "العدد": eq.get("العدد", 0), - "المدة (يوم)": eq.get("المدة", 0), - "سعر اليوم": eq.get("سعر_اليوم", 0), - "التكلفة": equipment_cost - }) - - equipment_df = pd.DataFrame(equipment_data) - - # تنسيق الجدول - def highlight_row(row): - """تنسيق الجدول مع تمييز الصفوف بالتناوب""" - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = equipment_df.style.apply(highlight_row, axis=1) - styled_df = styled_df.format({ - "المدة (يوم)": "{:.2f}", - "سعر اليوم": "{:,.2f} ريال", - "التكلفة": "{:,.2f} ريال" - }) - - # عرض الجدول - st.dataframe(styled_df, use_container_width=True, hide_index=True) - st.markdown(f"**إجمالي تكلفة المعدات:** {total_equipment_cost:,.2f} ريال") - - def _render_costs_tab(self, template: Dict[str, Any]): - """عرض تبويب التكاليف""" - # حساب إجمالي التكاليف المباشرة - direct_cost = self._calculate_direct_cost(template) - - # المصاريف الإدارية - admin_expenses_pct = template.get("admin_expenses", 0.05) - admin_expenses = direct_cost * admin_expenses_pct - - # هامش الربح - profit_margin_pct = template.get("profit_margin", 0.1) - profit_margin = direct_cost * profit_margin_pct - - # إجمالي التكلفة - total_cost = direct_cost + admin_expenses + profit_margin - - # عرض ملخص التكاليف - st.markdown("#### ملخص التكاليف") - - col1, col2 = st.columns(2) - - with col1: - st.markdown(f"**التكاليف المباشرة:** {direct_cost:,.2f} ريال") - st.markdown(f"**المصاريف الإدارية ({admin_expenses_pct*100:.0f}%):** {admin_expenses:,.2f} ريال") - st.markdown(f"**هامش الربح ({profit_margin_pct*100:.0f}%):** {profit_margin:,.2f} ريال") - - with col2: - # رسم بياني دائري لتوزيع التكاليف - import plotly.express as px - - # حساب تكاليف المكونات - components = template.get("components", {}) - - materials_cost = sum( - mat.get("الكمية", 0) * mat.get("سعر_الوحدة", 0) - for mat in components.get("materials", []) - ) - - labor_cost = sum( - worker.get("العدد", 0) * worker.get("المدة", 0) * worker.get("سعر_اليوم", 0) - for worker in components.get("labor", []) - ) - - equipment_cost = sum( - eq.get("العدد", 0) * eq.get("المدة", 0) * eq.get("سعر_اليوم", 0) - for eq in components.get("equipment", []) - ) - - # إنشاء البيانات للرسم البياني - cost_distribution = [ - {"النوع": "المواد", "القيمة": materials_cost}, - {"النوع": "العمالة", "القيمة": labor_cost}, - {"النوع": "المعدات", "القيمة": equipment_cost}, - {"النوع": "المصاريف الإدارية", "القيمة": admin_expenses}, - {"النوع": "هامش الربح", "القيمة": profit_margin} - ] - - cost_df = pd.DataFrame(cost_distribution) - - fig = px.pie( - cost_df, - values="القيمة", - names="النوع", - title="توزيع التكاليف", - color_discrete_sequence=px.colors.sequential.Teal, - hole=0.4 - ) - - fig.update_traces(textposition='inside', textinfo='percent+label') - - fig.update_layout( - annotations=[dict(text=f"{total_cost:,.0f} ريال", x=0.5, y=0.5, font_size=14, showarrow=False)], - font=dict(family="Almarai, Arial", size=12), - margin=dict(t=30, b=30, l=10, r=10) - ) - - st.plotly_chart(fig, use_container_width=True) - - st.markdown(f"**السعر النهائي:** {total_cost:,.2f} ريال لكل {template.get('unit', 'وحدة')}") - - def _render_new_template_form(self): - """عرض نموذج إضافة قالب جديد""" - st.markdown("### إضافة قالب نموذجي جديد") - - # معلومات القالب الأساسية - col1, col2 = st.columns(2) - - with col1: - template_name = st.text_input("اسم القالب", key="new_template_name") - template_description = st.text_area("وصف القالب", key="new_template_description") - - with col2: - # الحصول على الفئات من الكتالوج - templates = self.construction_templates.get_all_templates() - categories = templates.get("categories", {}) - - # إنشاء قائمة الفئات - category_list = [{"id": cat_id, "name": cat_data.get("name", cat_id)} for cat_id, cat_data in categories.items()] - category_options = [cat["id"] for cat in category_list] - category_labels = [cat["name"] for cat in category_list] - - category_index = 0 - if category_options: - template_category = st.selectbox( - "فئة القالب", - options=category_options, - format_func=lambda x: next((cat["name"] for cat in category_list if cat["id"] == x), x), - key="new_template_category" - ) - else: - template_category = st.text_input("فئة القالب (لا توجد فئات محددة)", key="new_template_category_text") - - template_unit = st.selectbox( - "وحدة القياس", - options=["م²", "م³", "م.ط", "عدد", "طن", "كجم", "لتر", "يوم", "ساعة", "مقطوعية"], - index=0, - key="new_template_unit" - ) - - # إضافة المواد - st.markdown("#### المواد المستخدمة") - - # إنشاء مصفوفة لتخزين المواد - if "new_template_materials" not in st.session_state: - st.session_state.new_template_materials = [] - - # عرض المواد المضافة حاليًا - if st.session_state.new_template_materials: - materials_df = pd.DataFrame(st.session_state.new_template_materials) - st.dataframe(materials_df, hide_index=True) - - # إضافة مادة جديدة - st.markdown("##### 🧱 إضافة مادة جديدة") - st.markdown('
', unsafe_allow_html=True) - - col1, col2 = st.columns(2) - with col1: - material_name = st.text_input("اسم المادة", key="new_template_material_name") - material_quantity = st.number_input("الكمية", min_value=0.0, step=0.1, key="new_template_material_quantity") - - with col2: - material_unit = st.selectbox( - "وحدة القياس", - options=["م²", "م³", "م.ط", "عدد", "طن", "كجم", "لتر", "قطعة", "كيس", "لوح"], - key="new_template_material_unit" - ) - material_price = st.number_input("سعر الوحدة", min_value=0.0, step=1.0, key="new_template_material_price") - - st.markdown('
', unsafe_allow_html=True) - - if st.button("إضافة المادة", key="add_new_template_material"): - if material_name and material_quantity > 0 and material_price > 0: - st.session_state.new_template_materials.append({ - "الاسم": material_name, - "الكمية": material_quantity, - "الوحدة": material_unit, - "سعر_الوحدة": material_price - }) - st.rerun() - else: - st.error("يرجى ملء جميع الحقول المطلوبة للمادة.") - - # إضافة العمالة - st.markdown("#### العمالة المطلوبة") - - # إنشاء مصفوفة لتخزين العمالة - if "new_template_labor" not in st.session_state: - st.session_state.new_template_labor = [] - - # عرض العمالة المضافة حاليًا - if st.session_state.new_template_labor: - labor_df = pd.DataFrame(st.session_state.new_template_labor) - st.dataframe(labor_df, hide_index=True) - - # إضافة عامل جديد - st.markdown("##### 👷 إضافة عامل جديد") - st.markdown('
', unsafe_allow_html=True) - - col1, col2 = st.columns(2) - with col1: - labor_type = st.text_input("نوع العامل", key="new_template_labor_type") - labor_count = st.number_input("العدد", min_value=1, step=1, key="new_template_labor_count") - - with col2: - labor_duration = st.number_input("المدة (يوم)", min_value=0.1, step=0.1, key="new_template_labor_duration") - labor_price = st.number_input("سعر اليوم", min_value=0.0, step=10.0, key="new_template_labor_price") - - st.markdown('
', unsafe_allow_html=True) - - if st.button("إضافة العامل", key="add_new_template_labor"): - if labor_type and labor_count > 0 and labor_duration > 0 and labor_price > 0: - st.session_state.new_template_labor.append({ - "النوع": labor_type, - "العدد": labor_count, - "المدة": labor_duration, - "سعر_اليوم": labor_price - }) - st.rerun() - else: - st.error("يرجى ملء جميع الحقول المطلوبة للعامل.") - - # إضافة المعدات - st.markdown("#### المعدات اللازمة") - - # إنشاء مصفوفة لتخزين المعدات - if "new_template_equipment" not in st.session_state: - st.session_state.new_template_equipment = [] - - # عرض المعدات المضافة حاليًا - if st.session_state.new_template_equipment: - equipment_df = pd.DataFrame(st.session_state.new_template_equipment) - st.dataframe(equipment_df, hide_index=True) - - # إضافة معدة جديدة - st.markdown("##### 🚜 إضافة معدة جديدة") - st.markdown('
', unsafe_allow_html=True) - - col1, col2 = st.columns(2) - with col1: - equipment_type = st.text_input("نوع المعدة", key="new_template_equipment_type") - equipment_count = st.number_input("العدد", min_value=1, step=1, key="new_template_equipment_count") - - with col2: - equipment_duration = st.number_input("المدة (يوم)", min_value=0.1, step=0.1, key="new_template_equipment_duration") - equipment_price = st.number_input("سعر اليوم", min_value=0.0, step=50.0, key="new_template_equipment_price") - - st.markdown('
', unsafe_allow_html=True) - - if st.button("إضافة المعدة", key="add_new_template_equipment"): - if equipment_type and equipment_count > 0 and equipment_duration > 0 and equipment_price > 0: - st.session_state.new_template_equipment.append({ - "النوع": equipment_type, - "العدد": equipment_count, - "المدة": equipment_duration, - "سعر_اليوم": equipment_price - }) - st.rerun() - else: - st.error("يرجى ملء جميع الحقول المطلوبة للمعدة.") - - # المصاريف الإدارية وهامش الربح - st.markdown("#### المصاريف الإدارية وهامش الربح") - - col1, col2 = st.columns(2) - with col1: - admin_expenses = st.slider("نسبة المصاريف الإدارية (%)", min_value=0, max_value=20, value=5, step=1, key="new_template_admin_expenses") / 100 - - with col2: - profit_margin = st.slider("نسبة هامش الربح (%)", min_value=0, max_value=30, value=10, step=1, key="new_template_profit_margin") / 100 - - # الكلمات المفتاحية - st.markdown("#### الكلمات المفتاحية") - tags_input = st.text_input("الكلمات المفتاحية (مفصولة بفواصل)", key="new_template_tags") - tags = [tag.strip() for tag in tags_input.split(",")] if tags_input else [] - - # زر إنشاء القالب - if styled_button("إنشاء القالب النموذجي", key="create_new_template", type="primary", full_width=True, icon="✅"): - # التحقق من صحة البيانات - if not template_name: - st.error("يرجى إدخال اسم القالب.") - elif not template_description: - st.error("يرجى إدخال وصف القالب.") - elif not st.session_state.new_template_materials: - st.error("يرجى إضافة مادة واحدة على الأقل.") - else: - # إنشاء القالب الجديد - new_template = { - "name": template_name, - "description": template_description, - "category": template_category, - "unit": template_unit, - "components": { - "materials": st.session_state.new_template_materials, - "labor": st.session_state.new_template_labor, - "equipment": st.session_state.new_template_equipment - }, - "admin_expenses": admin_expenses, - "profit_margin": profit_margin, - "tags": tags - } - - # إضافة القالب إلى الكتالوج - try: - template_id = self.construction_templates.add_template(new_template) - st.success(f"تم إنشاء القالب النموذجي بنجاح! المعرف: {template_id}") - - # إعادة تعيين البيانات - st.session_state.new_template_materials = [] - st.session_state.new_template_labor = [] - st.session_state.new_template_equipment = [] - - # إعادة تحميل الصفحة - st.rerun() - except Exception as e: - st.error(f"حدث خطأ أثناء إنشاء القالب: {str(e)}") - - def _calculate_template_cost(self, template: Dict[str, Any]) -> float: - """حساب التكلفة التقديرية للنموذج""" - # حساب التكاليف المباشرة - direct_cost = self._calculate_direct_cost(template) - - # المصاريف الإدارية - admin_expenses = direct_cost * template.get("admin_expenses", 0.05) - - # هامش الربح - profit_margin = direct_cost * template.get("profit_margin", 0.1) - - # إجمالي التكلفة - total_cost = direct_cost + admin_expenses + profit_margin - - return total_cost - - def _populate_reference_price_list(self): - """تعبئة قوائم الأسعار المرجعية من كتالوج البنود النموذجية""" - templates = self.construction_templates.get_all_templates() - all_templates = templates.get("templates", {}) - - for template_id, template in all_templates.items(): - # إضافة البند النموذجي إلى قائمة الأسعار المرجعية - self._add_template_to_reference_list(template_id, template) - - def _add_template_to_reference_list(self, template_id: str, template: Dict[str, Any]): - """إضافة بند نموذجي إلى قائمة الأسعار المرجعية""" - # حساب التكلفة التقديرية للنموذج - estimated_cost = self._calculate_template_cost(template) - - # إنشاء عنصر القائمة المرجعية - reference_item = { - "id": template_id, - "name": template.get("name", ""), - "description": template.get("description", ""), - "unit": template.get("unit", ""), - "estimated_cost": estimated_cost, - "category": template.get("category", ""), - "type": "بند نموذجي", - "source": "كتالوج البنود", - "date_added": pd.Timestamp.now().strftime("%Y-%m-%d"), - "is_active": True - } - - # إضافة العنصر إلى قائمة الأسعار المرجعية إذا لم يكن موجوداً بالفعل - existing_item = next((item for item in st.session_state.reference_price_list if item["id"] == template_id), None) - if existing_item: - # تحديث العنصر الموجود - existing_index = st.session_state.reference_price_list.index(existing_item) - st.session_state.reference_price_list[existing_index] = reference_item - else: - # إضافة عنصر جديد - st.session_state.reference_price_list.append(reference_item) - - def _render_reference_price_list(self): - """عرض قوائم الأسعار المرجعية""" - st.markdown(""" -
-

📊 قوائم الأسعار المرجعية

-

قوائم الأسعار المرجعية تحتوي على أسعار المواد والخدمات المستخدمة في المشاريع.

-

يمكنك استخدام هذه القوائم في عمليات التسعير والتخطيط للمشاريع الجديدة.

-
- """, unsafe_allow_html=True) - - # تبويبات قوائم الأسعار - tabs = st.tabs(["البنود النموذجية", "المواد", "العمالة", "المعدات", "إدارة القوائم"]) - - # تبويب البنود النموذجية - with tabs[0]: - self._render_reference_templates_list() - - # تبويب المواد - with tabs[1]: - self._render_reference_materials_list() - - # تبويب العمالة - with tabs[2]: - self._render_reference_labor_list() - - # تبويب المعدات - with tabs[3]: - self._render_reference_equipment_list() - - # تبويب إدارة القوائم - with tabs[4]: - self._render_reference_list_management() - - def _render_reference_templates_list(self): - """عرض قائمة البنود النموذجية في الأسعار المرجعية""" - st.markdown("### قائمة البنود النموذجية المرجعية") - - # فلترة العناصر للحصول على البنود النموذجية فقط - template_items = [item for item in st.session_state.reference_price_list if item["type"] == "بند نموذجي"] - - if not template_items: - st.info("لا توجد بنود نموذجية في قائمة الأسعار المرجعية.") - return - - # إنشاء DataFrame للعرض - df_data = [] - for item in template_items: - df_data.append({ - "الرقم التعريفي": item["id"], - "اسم البند": item["name"], - "الوصف": item["description"][:50] + "..." if len(item["description"]) > 50 else item["description"], - "الوحدة": item["unit"], - "السعر التقديري": item["estimated_cost"], - "الفئة": item["category"], - "المصدر": item["source"] - }) - - df = pd.DataFrame(df_data) - - # تنسيق الجدول - def highlight_row(row): - color = '#F0F8FF' if row.name % 2 == 0 else 'white' - return ['background-color: %s' % color] * len(row) - - styled_df = df.style.apply(highlight_row, axis=1) - styled_df = styled_df.format({ - "السعر التقديري": "{:,.2f} ريال" - }) - - # عرض الجدول - st.dataframe(styled_df, use_container_width=True, hide_index=True) - - # إضافة زر لإضافة بند من القائمة المرجعية إلى حاسبة التكاليف - selected_item_id = st.selectbox( - "اختر بنداً لإضافته إلى حاسبة التكاليف", - options=[item["id"] for item in template_items], - format_func=lambda x: next((item["name"] for item in template_items if item["id"] == x), x) - ) - - if selected_item_id: - if styled_button("إضافة إلى حاسبة التكاليف", key=f"add_ref_to_calc_{selected_item_id}", type="primary", icon="➕"): - st.session_state.selected_template_for_calculator = selected_item_id - st.success("تم إضافة البند المرجعي إلى حاسبة التكاليف!") - - def _render_reference_materials_list(self): - """عرض قائمة المواد في الأسعار المرجعية""" - st.markdown("### قائمة المواد المرجعية") - st.info("سيتم تطوير هذا القسم قريباً. يمكنك إضافة مواد من طلبات التسعير التي تم الرد عليها.") - - def _render_reference_labor_list(self): - """عرض قائمة العمالة في الأسعار المرجعية""" - st.markdown("### قائمة العمالة المرجعية") - st.info("سيتم تطوير هذا القسم قريباً.") - - def _render_reference_equipment_list(self): - """عرض قائمة المعدات في الأسعار المرجعية""" - st.markdown("### قائمة المعدات المرجعية") - st.info("سيتم تطوير هذا القسم قريباً. يمكنك إضافة معدات من طلبات التسعير التي تم الرد عليها.") - - def _render_reference_list_management(self): - """عرض إدارة قوائم الأسعار المرجعية""" - st.markdown("### إدارة قوائم الأسعار المرجعية") - - col1, col2 = st.columns(2) - - with col1: - st.markdown(f"**إجمالي عدد العناصر في القوائم المرجعية:** {len(st.session_state.reference_price_list)}") - st.markdown(f"**عدد البنود النموذجية:** {len([item for item in st.session_state.reference_price_list if item['type'] == 'بند نموذجي'])}") - - with col2: - if styled_button("تحديث جميع القوائم المرجعية", key="update_all_reference_lists", type="primary", icon="🔄"): - self._populate_reference_price_list() - st.success("تم تحديث قوائم الأسعار المرجعية بنجاح!") - - st.markdown("### إضافة عنصر جديد يدوياً") - st.markdown("##### 🛠️ إضافة عنصر جديد إلى قوائم الأسعار المرجعية") - st.markdown('
', unsafe_allow_html=True) - - item_type = st.selectbox( - "نوع العنصر", - options=["مادة", "عمالة", "معدات"], - key="new_reference_item_type" - ) - - item_name = st.text_input("اسم العنصر", key="new_reference_item_name") - item_desc = st.text_area("وصف العنصر", key="new_reference_item_desc") - item_unit = st.text_input("وحدة القياس", key="new_reference_item_unit") - item_price = st.number_input("السعر", min_value=0.0, step=0.1, key="new_reference_item_price") - - st.markdown('
', unsafe_allow_html=True) - - if styled_button("إضافة إلى القائمة المرجعية", key="add_manual_reference_item", type="success", icon="➕"): - if not item_name: - st.error("الرجاء إدخال اسم العنصر") - elif not item_unit: - st.error("الرجاء إدخال وحدة القياس") - elif item_price <= 0: - st.error("الرجاء إدخال سعر صحيح") - else: - # إنشاء معرف فريد للعنصر - item_id = f"MAN-{item_type[:1]}-{len(st.session_state.reference_price_list) + 1:04d}" - - # إنشاء العنصر - new_item = { - "id": item_id, - "name": item_name, - "description": item_desc, - "unit": item_unit, - "estimated_cost": item_price, - "category": "أخرى", - "type": item_type, - "source": "إدخال يدوي", - "date_added": pd.Timestamp.now().strftime("%Y-%m-%d"), - "is_active": True - } - - # إضافة العنصر إلى القائمة - st.session_state.reference_price_list.append(new_item) - st.success(f"تم إضافة العنصر {item_name} إلى القائمة المرجعية بنجاح!") - - def _calculate_direct_cost(self, template: Dict[str, Any]) -> float: - """حساب التكاليف المباشرة للنموذج""" - components = template.get("components", {}) - - # حساب تكلفة المواد - materials_cost = sum( - mat.get("الكمية", 0) * mat.get("سعر_الوحدة", 0) - for mat in components.get("materials", []) - ) - - # حساب تكلفة العمالة - labor_cost = sum( - worker.get("العدد", 0) * worker.get("المدة", 0) * worker.get("سعر_اليوم", 0) - for worker in components.get("labor", []) - ) - - # حساب تكلفة المعدات - equipment_cost = sum( - eq.get("العدد", 0) * eq.get("المدة", 0) * eq.get("سعر_اليوم", 0) - for eq in components.get("equipment", []) - ) - direct_cost = materials_cost + labor_cost + equipment_cost - - return direct_cost - - -# دالة لتشغيل الكتالوج مباشرة في حالة تنفيذ الملف بشكل مستقل -def main(): - """تشغيل كتالوج القوالب بشكل مستقل""" - from modules.pricing.services.construction_templates import ConstructionTemplates - - # تهيئة الواجهة - st.set_page_config( - page_title="كتالوج بنود المقاولات النموذجية", - page_icon="🗃️", - layout="wide", - initial_sidebar_state="collapsed", - menu_items={ - 'Get Help': 'mailto:support@wahbi-ai.com', - 'Report a bug': 'mailto:support@wahbi-ai.com', - 'About': 'كتالوج بنود المقاولات النموذجية - جزء من نظام WAHBi AI لتحليل المناقصات' - } - ) - - # تهيئة كائن الكتالوج - construction_templates = ConstructionTemplates() - templates_catalog = TemplatesCatalog(construction_templates) - - # عرض الكتالوج - templates_catalog.render() - -# تشغيل الكتالوج مباشرة عند تنفيذ الملف -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/modules/pricing/services/unbalanced_pricing.py b/modules/pricing/services/unbalanced_pricing.py deleted file mode 100644 index 77df2b857fe88bb48c307b3b14c5217c425efad2..0000000000000000000000000000000000000000 --- a/modules/pricing/services/unbalanced_pricing.py +++ /dev/null @@ -1,213 +0,0 @@ -""" -خدمة التسعير غير المتزن -""" - -import pandas as pd -import numpy as np -from datetime import datetime -import os -import config - - -class UnbalancedPricing: - """خدمة التسعير غير المتزن للبنود""" - - def __init__(self): - """تهيئة خدمة التسعير غير المتزن""" - self.strategies = { - 'front_loading': self.apply_front_loading, - 'back_loading': self.apply_back_loading, - 'confirmed_items': self.apply_confirmed_items_loading, - 'variable_items': self.apply_variable_items_discount - } - - def apply_strategy(self, items_df, strategy, params=None): - """تطبيق استراتيجية تسعير غير متزن على البنود""" - # نسخة من البيانات المدخلة للعمل عليها - df = items_df.copy() - - # إضافة عمود إستراتيجية التسعير إذا لم يكن موجوداً - if 'إستراتيجية التسعير' not in df.columns: - df['إستراتيجية التسعير'] = 'متوازن' - - # تطبيق الإستراتيجية المطلوبة - if strategy in self.strategies: - df = self.strategies[strategy](df, params) - else: - # إذا كانت الإستراتيجية غير معروفة، أعد البيانات بدون تغيير - pass - - # حساب الإجمالي بعد التعديل - df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة'] - - return df - - def apply_front_loading(self, items_df, params=None): - """تطبيق استراتيجية التحميل الأمامي (Front Loading)""" - df = items_df.copy() - - # استخراج المعلمات الافتراضية إذا لم يتم تحديدها - if params is None: - params = { - 'early_increase': 1.3, # زيادة 30% للبنود المبكرة - 'late_decrease': 0.7, # تخفيض 30% للبنود المتأخرة - 'early_percentage': 0.33, # نسبة البنود المبكرة 33% - 'late_percentage': 0.33 # نسبة البنود المتأخرة 33% - } - - # تحديد البنود المبكرة والمتأخرة والمتوسطة - items_count = len(df) - early_count = int(items_count * params['early_percentage']) - late_count = int(items_count * params['late_percentage']) - - early_items = df.iloc[:early_count].index - middle_items = df.iloc[early_count:items_count-late_count].index - late_items = df.iloc[items_count-late_count:].index - - # تطبيق الزيادة والنقصان - for idx in early_items: - df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['early_increase'] - df.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - for idx in middle_items: - df.at[idx, 'إستراتيجية التسعير'] = 'متوازن' - - for idx in late_items: - df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['late_decrease'] - df.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - return df - - def apply_back_loading(self, items_df, params=None): - """تطبيق استراتيجية التحميل الخلفي (Back Loading)""" - df = items_df.copy() - - # استخراج المعلمات الافتراضية إذا لم يتم تحديدها - if params is None: - params = { - 'early_decrease': 0.7, # تخفيض 30% للبنود المبكرة - 'late_increase': 1.3, # زيادة 30% للبنود المتأخرة - 'early_percentage': 0.33, # نسبة البنود المبكرة 33% - 'late_percentage': 0.33 # نسبة البنود المتأخرة 33% - } - - # تحديد البنود المبكرة والمتأخرة والمتوسطة - items_count = len(df) - early_count = int(items_count * params['early_percentage']) - late_count = int(items_count * params['late_percentage']) - - early_items = df.iloc[:early_count].index - middle_items = df.iloc[early_count:items_count-late_count].index - late_items = df.iloc[items_count-late_count:].index - - # تطبيق الزيادة والنقصان - for idx in early_items: - df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['early_decrease'] - df.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - for idx in middle_items: - df.at[idx, 'إستراتيجية التسعير'] = 'متوازن' - - for idx in late_items: - df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['late_increase'] - df.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - return df - - def apply_confirmed_items_loading(self, items_df, params=None): - """تطبيق استراتيجية تحميل البنود المؤكدة""" - df = items_df.copy() - - # استخراج المعلمات الافتراضية إذا لم يتم تحديدها - if params is None: - params = { - 'confirmed_increase': 1.25, # زيادة 25% للبنود المؤكدة - 'others_decrease': 0.85, # تخفيض 15% للبنود الأخرى - 'confirmed_items_indices': [] # قائمة مؤشرات البنود المؤكدة - } - - # إذا لم يتم تحديد البنود المؤكدة، استخدم قواعد اختيار افتراضية - if not params['confirmed_items_indices']: - # البنود التي تحتوي على كلمات مثل "أساسات" أو "هيكل" عادة ما تكون مؤكدة - confirmed_items = [] - for idx, row in df.iterrows(): - description = row['وصف البند'].lower() - if any(term in description for term in ['أساس', 'خرسان', 'هيكل', 'إنشائي']): - confirmed_items.append(idx) - else: - confirmed_items = params['confirmed_items_indices'] - - # تحديد البنود غير المؤكدة - all_indices = set(range(len(df))) - confirmed_indices = set(confirmed_items) - variable_indices = list(all_indices - confirmed_indices) - - # تطبيق الزيادة والنقصان - for idx in confirmed_items: - df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['confirmed_increase'] - df.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - for idx in variable_indices: - df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['others_decrease'] - df.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - return df - - def apply_variable_items_discount(self, items_df, params=None): - """تطبيق استراتيجية تخفيض البنود المحتمل زيادتها""" - df = items_df.copy() - - # استخراج المعلمات الافتراضية إذا لم يتم تحديدها - if params is None: - params = { - 'variable_decrease': 0.7, # تخفيض 30% للبنود المحتمل زيادتها - 'others_increase': 1.15, # زيادة 15% للبنود الأخرى - 'variable_items_indices': [] # قائمة مؤشرات البنود المحتمل زيادتها - } - - # إذا لم يتم تحديد البنود المحتمل زيادتها، استخدم قواعد اختيار افتراضية - if not params['variable_items_indices']: - # البنود التي تحتوي على كلمات مثل "حفر" أو "ردم" عادة ما تكون محتمل زيادتها - variable_items = [] - for idx, row in df.iterrows(): - description = row['وصف البند'].lower() - if any(term in description for term in ['حفر', 'ردم', 'تمديد', 'صرف', 'مياه']): - variable_items.append(idx) - else: - variable_items = params['variable_items_indices'] - - # تحديد البنود الأخرى - all_indices = set(range(len(df))) - variable_indices = set(variable_items) - other_indices = list(all_indices - variable_indices) - - # تطبيق الزيادة والنقصان - for idx in variable_items: - df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['variable_decrease'] - df.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - for idx in other_indices: - df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['others_increase'] - df.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - return df - - def calibrate_prices(self, original_df, unbalanced_df): - """معايرة الأسعار للحفاظ على إجمالي التسعير الأصلي""" - # حساب الإجماليات - original_total = original_df['الإجمالي'].sum() - unbalanced_total = unbalanced_df['الإجمالي'].sum() - - # نسخة من البيانات المدخلة للعمل عليها - df = unbalanced_df.copy() - - # حساب معامل التعديل - adjustment_factor = original_total / unbalanced_total if unbalanced_total > 0 else 1.0 - - # تعديل الأسعار - df['سعر الوحدة'] = df['سعر الوحدة'] * adjustment_factor - - # حساب الإجمالي بعد التعديل - df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة'] - - return df \ No newline at end of file diff --git a/modules/pricing/specs_analyzer.py b/modules/pricing/specs_analyzer.py deleted file mode 100644 index 9db1b7f586f7317f57fcf81089a4f79d5bd0d09b..0000000000000000000000000000000000000000 --- a/modules/pricing/specs_analyzer.py +++ /dev/null @@ -1,527 +0,0 @@ -""" -تطبيق وحدة التسعير المتكاملة -""" - -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 random -import os -import time -import io - -from modules.pricing.services.standard_pricing import StandardPricing -from modules.pricing.services.unbalanced_pricing import UnbalancedPricing -from modules.pricing.services.local_content import LocalContentCalculator -from modules.pricing.services.price_prediction import PricePrediction -from utils.excel_handler import export_to_excel -from utils.helpers import format_number, format_currency - - -class PricingApp: - """وحدة التسعير المتكاملة""" - - def __init__(self): - self.pricing_methods = [ - "التسعير القياسي", - "التسعير غير المتزن", - "التسعير التنافسي", - "التسعير الموجه بالربحية" - ] - - # تهيئة خدمات التسعير - self.standard_pricing = StandardPricing() - self.unbalanced_pricing = UnbalancedPricing() - self.local_content = LocalContentCalculator() - self.price_prediction = PricePrediction() - - def render(self): - """عرض واجهة وحدة التسعير""" - - st.markdown("

وحدة التسعير المتكاملة

", unsafe_allow_html=True) - - tabs = st.tabs([ - "إنشاء تسعير جديد", - "نموذج التسعير الشامل", - "التسعير غير المتزن", - "المحتوى المحلي" - ]) - - with tabs[0]: - self._render_new_pricing_tab() - - with tabs[1]: - self._render_comprehensive_pricing_tab() - - with tabs[2]: - self._render_unbalanced_pricing_tab() - - with tabs[3]: - self._render_local_content_tab() - - def _render_new_pricing_tab(self): - """عرض تبويب إنشاء تسعير جديد""" - - st.markdown("### إنشاء تسعير جديد") - - col1, col2 = st.columns(2) - - with col1: - tender_name = st.text_input("اسم المناقصة") - client = st.text_input("الجهة المالكة") - pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods) - - with col2: - tender_number = st.text_input("رقم المناقصة") - location = st.text_input("الموقع") - submission_date = st.date_input("تاريخ التقديم") - - # خيارات بيانات البنود - st.markdown("### بيانات البنود") - - data_source = st.radio( - "مصدر بيانات البنود", - ["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات"] - ) - - if data_source == "إدخال يدوي": - # إنشاء بيانات افتراضية - if 'manual_items' not in st.session_state: - st.session_state.manual_items = pd.DataFrame({ - 'رقم البند': [f"A{i}" for i in range(1, 6)], - 'وصف البند': [ - "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", - "توريد وتركيب حديد التسليح للأساسات", - "أعمال العزل المائي للأساسات", - "أعمال الردم والدك للأساسات", - "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة" - ], - 'الوحدة': ["م3", "طن", "م2", "م3", "م3"], - 'الكمية': [250, 25, 500, 300, 120], - 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0], - 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0] - }) - - # عرض جدول البنود مع إمكانية التعديل - edited_items = st.data_editor( - st.session_state.manual_items, - use_container_width=True, - hide_index=True, - num_rows="dynamic" - ) - st.session_state.manual_items = edited_items - - elif data_source == "استيراد من Excel": - uploaded_file = st.file_uploader("رفع ملف Excel", type=["xlsx", "xls"]) - - if uploaded_file is not None: - st.success("تم رفع الملف بنجاح") - # محاكاة قراءة الملف - st.markdown("### معاينة البيانات المستوردة") - - # إنشاء بيانات افتراضية - import_items = pd.DataFrame({ - 'رقم البند': [f"A{i}" for i in range(1, 8)], - 'وصف البند': [ - "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", - "توريد وتركيب حديد التسليح للأساسات", - "أعمال العزل المائي للأساسات", - "أعمال الردم والدك للأساسات", - "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", - "توريد وتركيب حديد التسليح للأعمدة", - "أعمال البلوك للجدران" - ], - 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"], - 'الكمية': [250, 25, 500, 300, 120, 10, 400], - 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }) - - st.dataframe(import_items) - - if st.button("استيراد البيانات"): - st.session_state.manual_items = import_items.copy() - st.session_state.manual_items_modified = True - st.success("تم استيراد البيانات بنجاح!") - - else: # استيراد من وحدة تحليل المستندات - available_documents = [ - "كراسة شروط مشروع توسعة مستشفى الملك فهد", - "جدول كميات صيانة محطات المياه", - "مخططات إنشاء مدرسة ثانوية" - ] - - selected_doc = st.selectbox("اختر المستند", available_documents) - - if st.button("استيراد البيانات من تحليل المستند"): - # محاكاة استيراد البيانات - with st.spinner("جاري استيراد البيانات..."): - time.sleep(2) - - # إنشاء بيانات افتراضية - doc_items = pd.DataFrame({ - 'رقم البند': [f"A{i}" for i in range(1, 8)], - 'وصف البند': [ - "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", - "توريد وتركيب حديد التسليح للأساسات", - "أعمال العزل المائي للأساسات", - "أعمال الردم والدك للأساسات", - "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", - "توريد وتركيب حديد التسليح للأعمدة", - "أعمال البلوك للجدران" - ], - 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"], - 'الكمية': [250, 25, 500, 300, 120, 10, 400], - 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }) - - st.session_state.manual_items = doc_items.copy() - st.success("تم استيراد البيانات من تحليل المستند بنجاح!") - st.dataframe(doc_items) - - # زر بدء التسعير - if st.button("بدء التسعير"): - # تحقق من صحة البيانات - if 'manual_items' in st.session_state and not st.session_state.manual_items.empty: - # حفظ بيانات التسعير الحالي - st.session_state.current_pricing = { - 'name': tender_name, - 'number': tender_number, - 'client': client, - 'location': location, - 'method': pricing_method, - 'submission_date': submission_date, - 'items': st.session_state.manual_items.copy(), - 'status': 'جديد', - 'created_at': datetime.now() - } - - # الانتقال إلى تبويب نموذج التسعير الشامل - st.success("تم إنشاء التسعير بنجاح! يمكنك الانتقال إلى نموذج التسعير الشامل.") - else: - st.error("يرجى إدخال بيانات البنود أولاً.") - - def _render_comprehensive_pricing_tab(self): - """عرض تبويب نموذج التسعير الشامل""" - - st.markdown("### نموذج التسعير الشامل") - - # التحقق من وجود تسعير حالي - if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: - st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") - return - - # عرض معلومات التسعير الحالي - pricing = st.session_state.current_pricing - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("اسم المناقصة", pricing['name']) - st.metric("الجهة المالكة", pricing['client']) - - with col2: - st.metric("رقم المناقصة", pricing['number']) - st.metric("تاريخ التقديم", pricing['submission_date'].strftime("%Y-%m-%d")) - - with col3: - st.metric("طريقة التسعير", pricing['method']) - st.metric("الموقع", pricing['location']) - - # عرض البنود والتسعير - st.markdown("### بنود التسعير") - - items = pricing['items'].copy() - - # إضافة أسعار الوحدة للمحاكاة - if 'سعر الوحدة' in items.columns and (items['سعر الوحدة'] == 0).all(): - items['سعر الوحدة'] = [ - round(random.uniform(1000, 3000), 2), # الخرسانة - round(random.uniform(5000, 7000), 2), # الحديد - round(random.uniform(100, 200), 2), # العزل - round(random.uniform(50, 100), 2), # الردم - round(random.uniform(1200, 3500), 2), # الخرسانة للأعمدة - ] - - if len(items) > 5: - for i in range(5, len(items)): - items.at[i, 'سعر الوحدة'] = round(random.uniform(500, 5000), 2) - - # حساب الإجمالي - items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] - - # عرض الجدول مع إمكانية التعديل - edited_items = st.data_editor( - items, - use_container_width=True, - hide_index=True, - disabled=('رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'الإجمالي') - ) - - # حساب الإجمالي بعد التعديل - edited_items['الإجمالي'] = edited_items['الكمية'] * edited_items['سعر الوحدة'] - st.session_state.current_pricing['items'] = edited_items - - # حساب وعرض إجماليات التسعير - total_price = edited_items['الإجمالي'].sum() - - st.markdown("### إجماليات التسعير") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي التكاليف المباشرة", f"{total_price:,.2f} ريال") - - with col2: - overhead_percentage = st.slider("نسبة المصاريف العامة والأرباح (%)", 5, 30, 15) - overhead_value = total_price * overhead_percentage / 100 - st.metric("المصاريف العامة والأرباح", f"{overhead_value:,.2f} ريال") - - with col3: - grand_total = total_price + overhead_value - st.metric("الإجمالي النهائي", f"{grand_total:,.2f} ريال") - - # رسم بياني لتوزيع التكاليف - st.markdown("### تحليل التكاليف") - - # حساب النسب المئوية لكل بند - pie_data = edited_items.copy() - pie_data['نسبة من إجمالي التكاليف'] = pie_data['الإجمالي'] / total_price * 100 - - fig = px.pie( - pie_data, - values='نسبة من إجمالي التكاليف', - names='وصف البند', - title='توزيع التكاليف حسب البنود', - hole=0.4 - ) - - st.plotly_chart(fig, use_container_width=True) - - # أزرار العمليات - col1, col2, col3 = st.columns(3) - - with col1: - if st.button("حفظ التسعير"): - st.success("تم حفظ التسعير بنجاح!") - - with col2: - if st.button("تصدير إلى Excel"): - st.success("تم تصدير التسعير إلى Excel بنجاح!") - - with col3: - if st.button("تحليل المخاطر المالية"): - st.success("تم إرسال الطلب إلى وحدة تحليل المخاطر!") - - def _render_unbalanced_pricing_tab(self): - """عرض تبويب التسعير غير المتزن""" - - st.markdown("### التسعير غير المتزن") - - # التحقق من وجود تسعير حالي - if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: - st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") - return - - # شرح التسعير غير المتزن - with st.expander("ما هو التسعير غير المتزن؟", expanded=False): - st.markdown(""" - **التسعير غير المتزن** هو استراتيجية تسعير تقوم على توزيع التكاليف بين بنود المناقصة بشكل غير متساوٍ، مع الحفاظ على إجمالي قيمة العطاء. - - ### استراتيجيات التسعير غير المتزن: - - 1. **التحميل الأمامي (Front Loading)**: زيادة أسعار البنود المبكرة في المشروع للحصول على تدفق نقدي أفضل في بداية المشروع. - 2. **التحميل الخلفي (Back Loading)**: زيادة أسعار البنود المتأخرة في المشروع. - 3. **تحميل البنود المؤكدة**: زيادة أسعار البنود التي من المؤكد تنفيذها بالكميات المحددة. - 4. **تخفيض أسعار البنود المحتملة**: تخفيض أسعار البنود التي قد تزيد كمياتها أثناء التنفيذ. - - ### مزايا التسعير غير المتزن: - - - تحسين التدفق النقدي للمشروع. - - تعظيم الربحية في حالة التغييرات والأوامر التغييرية. - - زيادة فرص الفوز بالمناقصة. - - ### مخاطر التسعير غير المتزن: - - - قد يتم رفض العطاء إذا كان عدم التوازن واضحاً. - - قد تتأثر السمعة سلباً إذا تم استخدامه بشكل مفرط. - - قد يؤدي إلى خسائر إذا لم يتم تنفيذ البنود ذات الأسعار العالية. - """) - - # عرض بنود التسعير الحالي - items = st.session_state.current_pricing['items'].copy() - - # إضافة عمود إستراتيجية التسعير - if 'إستراتيجية التسعير' not in items.columns: - items['إستراتيجية التسعير'] = 'متوازن' - - st.markdown("### إستراتيجية التسعير غير المتزن") - - # اختيار الإستراتيجية - strategy = st.selectbox( - "اختر إستراتيجية التسعير", - [ - "تحميل أمامي (Front Loading)", - "تحميل البنود المؤكدة", - "تخفيض البنود المحتمل زيادتها", - "إستراتيجية مخصصة" - ] - ) - - # تطبيق الإستراتيجية المختارة - if strategy == "تحميل أمامي (Front Loading)": - # محاكاة تحميل أمامي - items_count = len(items) - early_items = items.iloc[:items_count//3].index - middle_items = items.iloc[items_count//3:2*items_count//3].index - late_items = items.iloc[2*items_count//3:].index - - # تطبيق الزيادة والنقصان - for idx in early_items: - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.3 # زيادة 30% - items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - for idx in middle_items: - items.at[idx, 'إستراتيجية التسعير'] = 'متوازن' - - for idx in late_items: - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30% - items.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - elif strategy == "تحميل البنود المؤكدة": - # محاكاة - اعتبار بعض البنود مؤكدة - confirmed_items = [0, 2, 4] # الأصفار-مستندة - variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items] - - # تطبيق الزيادة والنقصان - for idx in confirmed_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 # زيادة 25% - items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - for idx in variable_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 # نقص 15% - items.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - elif strategy == "تخفيض البنود المحتمل زيادتها": - # محاكاة - اعتبار بعض البنود محتمل زيادتها - variable_items = [1, 3] # الأصفار-مستندة - other_items = [idx for idx in range(len(items)) if idx not in variable_items] - - # تطبيق الزيادة والنقصان - for idx in variable_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30% - items.at[idx, 'إستراتيجية التسعير'] = 'نقص' - - for idx in other_items: - if idx < len(items): - items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 # زيادة 15% - items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' - - else: # إستراتيجية مخصصة - st.markdown("### تعديل أسعار البنود يدوياً") - st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً.") - - # حساب الإجمالي بعد التعديل - items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] - - # تعيين ألوان للإستراتيجيات - def highlight_strategy(val): - if val == 'زيادة': - return 'background-color: #a8e6cf' - elif val == 'نقص': - return 'background-color: #ff9aa2' - return '' - - # عرض الجدول مع تنسيق - st.markdown("### بنود التسعير غير المتزن") - styled_items = items.style.applymap(highlight_strategy, subset=['إستراتيجية التسعير']) - st.dataframe(styled_items, use_container_width=True) - - # المقارنة بين التسعير المتوازن وغير المتوازن - st.markdown("### مقارنة التسعير المتوازن وغير المتوازن") - - original_items = st.session_state.current_pricing['items'].copy() - original_total = original_items['الإجمالي'].sum() - unbalanced_total = items['الإجمالي'].sum() - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي التسعير المتوازن", f"{original_total:,.2f} ريال") - - with col2: - st.metric("إجمالي التسعير غير المتوازن", f"{unbalanced_total:,.2f} ريال") - - with col3: - diff = unbalanced_total - original_total - st.metric("الفرق", f"{diff:,.2f} ريال", delta=f"{diff/original_total*100:.1f}%") - - # المعايرة للحفاظ على إجمالي التسعير - if abs(diff) > 1: # إذا كان هناك فرق كبير - if st.button("معايرة الأسعار للحفاظ على إجمالي التسعير"): - # تعديل الأسعار للحفاظ على إجمالي التكلفة - adjustment_factor = original_total / unbalanced_total - items['سعر الوحدة'] = items['سعر الوحدة'] * adjustment_factor - items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] - - st.success(f"تم تعديل الأسعار للحفاظ على إجمالي التسعير الأصلي ({original_total:,.2f} ريال)") - st.dataframe(items, use_container_width=True) - - # رسم بياني للمقارنة - st.markdown("### تحليل بصري للتسعير غير المتوازن") - - # إعداد البيانات للرسم البياني - chart_data = pd.DataFrame({ - 'وصف البند': original_items['وصف البند'], - 'التسعير المتوازن': original_items['الإجمالي'], - 'التسعير غير المتوازن': items['الإجمالي'] - }) - - # رسم بياني شريطي للمقارنة - fig = go.Figure() - - fig.add_trace(go.Bar( - x=chart_data['وصف البند'], - y=chart_data['التسعير المتوازن'], - name='التسعير المتوازن', - marker_color='rgb(55, 83, 109)' - )) - - fig.add_trace(go.Bar( - x=chart_data['وصف البند'], - y=chart_data['التسعير غير المتوازن'], - name='التسعير غير المتوازن', - marker_color='rgb(26, 118, 255)' - )) - - fig.update_layout( - title='مقارنة بين التسعير المتوازن وغير المتوازن', - xaxis_tickfont_size=14, - yaxis=dict( - title='الإجمالي (ريال)', - titlefont_size=16, - tickfont_size=14, - ), - legend=dict( - x=0, - y=1.0, - bgcolor='rgba(255, 255, 255, 0)', - bordercolor='rgba(255, 255, 255, 0)' - ), - barmode='group', - bargap=0.15, - bargroupgap=0.1 - ) - - st.plotly_chart(fig, use_container_width=True) - - # زر حفظ التسعير غير المتوازن - if st.button(" \ No newline at end of file diff --git a/modules/project_management/project_management_app.py b/modules/project_management/project_management_app.py deleted file mode 100644 index d731b68e21c3ae64fb6e8b5b2357e7dc46e8b598..0000000000000000000000000000000000000000 --- a/modules/project_management/project_management_app.py +++ /dev/null @@ -1,666 +0,0 @@ -""" -وحدة إدارة المشاريع - نظام تحليل المناقصات -""" - -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/project_tracker/__init__.py b/modules/project_tracker/__init__.py deleted file mode 100644 index 3d9b980b1d848af2732c586ff297deb2fdf8995e..0000000000000000000000000000000000000000 --- a/modules/project_tracker/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -""" -وحدة متتبع حالة المشروع المتحرك مع تصور التقدم -""" \ No newline at end of file diff --git a/modules/project_tracker/status_tracker.py b/modules/project_tracker/status_tracker.py deleted file mode 100644 index 8a304fdc87c1ca5a30ff7a9dc736adaa764da7ed..0000000000000000000000000000000000000000 --- a/modules/project_tracker/status_tracker.py +++ /dev/null @@ -1,1740 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة متتبع حالة المشروع المتحرك مع تصور التقدم -""" - -import os -import sys -import json -import time -import datetime -import streamlit as st -import pandas as pd -import numpy as np -import plotly.express as px -import plotly.graph_objects as go -from datetime import datetime, timedelta -import random -import math - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد المكونات المساعدة -from utils.helpers import format_time, get_user_info, create_directory_if_not_exists - - -class ProjectStatusTracker: - """فئة متتبع حالة المشروع المتحرك""" - - def __init__(self, project_id=None, user_id=None): - """تهيئة متتبع حالة المشروع""" - self.project_id = project_id or 1 # استخدام المشروع الافتراضي إذا لم يتم توفير معرف - self.user_id = user_id or 1 # استخدام المستخدم الافتراضي إذا لم يتم توفير معرف - self.tracker_path = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'project_tracker') - create_directory_if_not_exists(self.tracker_path) - self.project_data_file = os.path.join(self.tracker_path, f'project_{self.project_id}_status.json') - - # تعريف المراحل الافتراضية للمشروع - self.default_project_phases = [ - { - "id": "planning", - "name": "التخطيط", - "description": "مرحلة التخطيط وإعداد الجدول الزمني", - "order": 1, - "progress": 100, - "status": "completed", - "start_date": (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'), - "end_date": (datetime.now() - timedelta(days=20)).strftime('%Y-%m-%d'), - "actual_end_date": (datetime.now() - timedelta(days=18)).strftime('%Y-%m-%d'), - "deliverables": ["خطة المشروع", "الجدول الزمني", "خطة الموارد"], - "responsible": "فريق التخطيط", - "notes": "تم الانتهاء من مرحلة التخطيط بنجاح قبل الموعد المحدد", - "critical": True - }, - { - "id": "pricing", - "name": "التسعير", - "description": "تسعير المشروع وتحليل التكاليف", - "order": 2, - "progress": 100, - "status": "completed", - "start_date": (datetime.now() - timedelta(days=20)).strftime('%Y-%m-%d'), - "end_date": (datetime.now() - timedelta(days=10)).strftime('%Y-%m-%d'), - "actual_end_date": (datetime.now() - timedelta(days=8)).strftime('%Y-%m-%d'), - "deliverables": ["جدول الكميات المسعر", "تحليل التكاليف", "خطة التدفق النقدي"], - "responsible": "قسم التسعير", - "notes": "تم تحقيق وفر في تكاليف المشروع بنسبة 5%", - "critical": True - }, - { - "id": "bidding", - "name": "تقديم العطاء", - "description": "إعداد وتقديم وثائق العطاء", - "order": 3, - "progress": 100, - "status": "completed", - "start_date": (datetime.now() - timedelta(days=10)).strftime('%Y-%m-%d'), - "end_date": (datetime.now() - timedelta(days=5)).strftime('%Y-%m-%d'), - "actual_end_date": (datetime.now() - timedelta(days=5)).strftime('%Y-%m-%d'), - "deliverables": ["وثائق العطاء", "خطاب التقديم", "الضمان البنكي الابتدائي"], - "responsible": "مدير المشروع", - "notes": "تم تقديم العطاء في الموعد المحدد", - "critical": True - }, - { - "id": "evaluation", - "name": "تقييم العطاء", - "description": "مرحلة تقييم العطاء من قبل العميل", - "order": 4, - "progress": 75, - "status": "in_progress", - "start_date": (datetime.now() - timedelta(days=5)).strftime('%Y-%m-%d'), - "end_date": (datetime.now() + timedelta(days=5)).strftime('%Y-%m-%d'), - "actual_end_date": None, - "deliverables": ["الرد على استفسارات العميل", "العرض التقديمي", "تقديم المستندات الإضافية"], - "responsible": "العميل / مدير المشروع", - "notes": "مرحلة التقييم جارية، تم الرد على جميع استفسارات العميل", - "critical": True - }, - { - "id": "awarding", - "name": "ترسية العطاء", - "description": "مرحلة ترسية العطاء وتوقيع العقد", - "order": 5, - "progress": 0, - "status": "not_started", - "start_date": (datetime.now() + timedelta(days=5)).strftime('%Y-%m-%d'), - "end_date": (datetime.now() + timedelta(days=15)).strftime('%Y-%m-%d'), - "actual_end_date": None, - "deliverables": ["خطاب الترسية", "العقد الموقع", "الضمان البنكي النهائي"], - "responsible": "الإدارة القانونية / مدير المشروع", - "notes": "ننتظر نتيجة الترسية", - "critical": True - }, - { - "id": "mobilization", - "name": "التجهيز", - "description": "تجهيز الموقع وتوفير الموارد", - "order": 6, - "progress": 0, - "status": "not_started", - "start_date": (datetime.now() + timedelta(days=15)).strftime('%Y-%m-%d'), - "end_date": (datetime.now() + timedelta(days=30)).strftime('%Y-%m-%d'), - "actual_end_date": None, - "deliverables": ["تقرير التجهيز", "قائمة الموارد", "خطة التنفيذ التفصيلية"], - "responsible": "قسم العمليات", - "notes": "التجهيز سيبدأ بعد توقيع العقد", - "critical": False - }, - { - "id": "execution", - "name": "التنفيذ", - "description": "تنفيذ أعمال المشروع", - "order": 7, - "progress": 0, - "status": "not_started", - "start_date": (datetime.now() + timedelta(days=30)).strftime('%Y-%m-%d'), - "end_date": (datetime.now() + timedelta(days=180)).strftime('%Y-%m-%d'), - "actual_end_date": None, - "deliverables": ["تقارير التقدم الدورية", "محاضر الاجتماعات", "الفواتير"], - "responsible": "فريق التنفيذ", - "notes": "التنفيذ سيستمر لمدة 6 أشهر", - "critical": True - }, - { - "id": "handover", - "name": "التسليم", - "description": "تسليم المشروع للعميل", - "order": 8, - "progress": 0, - "status": "not_started", - "start_date": (datetime.now() + timedelta(days=180)).strftime('%Y-%m-%d'), - "end_date": (datetime.now() + timedelta(days=195)).strftime('%Y-%m-%d'), - "actual_end_date": None, - "deliverables": ["محضر الاستلام", "وثائق الضمان", "دليل التشغيل والصيانة"], - "responsible": "مدير المشروع / العميل", - "notes": "التسليم يشمل فترة الاختبار والتدريب", - "critical": True - }, - ] - - # تحميل بيانات المشروع - self.load_project_data() - - # تحميل البيانات لمؤشرات الأداء الرئيسية - self.kpi_data_file = os.path.join(self.tracker_path, f'project_{self.project_id}_kpis.json') - self.load_kpi_data() - - def load_project_data(self): - """تحميل بيانات حالة المشروع""" - try: - if os.path.exists(self.project_data_file): - with open(self.project_data_file, 'r', encoding='utf-8') as f: - self.project_data = json.load(f) - else: - # بيانات افتراضية عند عدم وجود ملف - self.project_data = { - 'project_id': self.project_id, - 'project_name': "مشروع إنشاء مبنى إداري", - 'project_code': "PC-2025-001", - 'client': "وزارة الإسكان", - 'location': "الرياض، المملكة العربية السعودية", - 'start_date': (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'), - 'end_date': (datetime.now() + timedelta(days=200)).strftime('%Y-%m-%d'), - 'budget': 10000000, - 'duration': 230, - 'elapsed_days': 30, - 'overall_progress': 25, - 'status': "في التقدم", - 'phases': self.default_project_phases, - 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - self.save_project_data() - except Exception as e: - st.error(f"خطأ في تحميل بيانات المشروع: {e}") - self.project_data = { - 'project_id': self.project_id, - 'project_name': "مشروع إنشاء مبنى إداري", - 'project_code': "PC-2025-001", - 'client': "وزارة الإسكان", - 'location': "الرياض، المملكة العربية السعودية", - 'start_date': (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'), - 'end_date': (datetime.now() + timedelta(days=200)).strftime('%Y-%m-%d'), - 'budget': 10000000, - 'duration': 230, - 'elapsed_days': 30, - 'overall_progress': 25, - 'status': "في التقدم", - 'phases': self.default_project_phases, - 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - - def save_project_data(self): - """حفظ بيانات حالة المشروع""" - try: - with open(self.project_data_file, 'w', encoding='utf-8') as f: - json.dump(self.project_data, f, ensure_ascii=False, indent=2) - except Exception as e: - st.error(f"خطأ في حفظ بيانات المشروع: {e}") - - def load_kpi_data(self): - """تحميل بيانات مؤشرات الأداء الرئيسية""" - try: - if os.path.exists(self.kpi_data_file): - with open(self.kpi_data_file, 'r', encoding='utf-8') as f: - self.kpi_data = json.load(f) - else: - # بيانات افتراضية عند عدم وجود ملف - self.kpi_data = { - 'spi': 1.05, # مؤشر أداء الجدول الزمني (SPI) - 'cpi': 0.98, # مؤشر أداء التكلفة (CPI) - 'quality_score': 92, # درجة جودة المشروع - 'safety_incidents': 0, # عدد حوادث السلامة - 'resource_utilization': 85, # نسبة استغلال الموارد - 'risk_score': 15, # درجة المخاطر (كلما قلت كان أفضل) - 'customer_satisfaction': 90, # درجة رضا العميل - 'environmental_compliance': 95, # نسبة الامتثال البيئي - 'trends': { - 'spi': [0.95, 0.98, 1.02, 1.05], - 'cpi': [1.02, 1.00, 0.99, 0.98], - 'quality_score': [85, 88, 90, 92], - 'risk_score': [25, 22, 18, 15], - 'dates': [ - (datetime.now() - timedelta(days=21)).strftime('%Y-%m-%d'), - (datetime.now() - timedelta(days=14)).strftime('%Y-%m-%d'), - (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'), - datetime.now().strftime('%Y-%m-%d') - ] - }, - 'issues': [ - { - 'id': 1, - 'description': "تأخر في توريد المواد", - 'severity': "متوسط", - 'status': "قيد المعالجة", - 'created_date': (datetime.now() - timedelta(days=10)).strftime('%Y-%m-%d'), - 'responsible': "قسم المشتريات", - 'resolution': "التنسيق مع المورد البديل" - }, - { - 'id': 2, - 'description': "نقص في فريق العمل", - 'severity': "منخفض", - 'status': "تم الحل", - 'created_date': (datetime.now() - timedelta(days=15)).strftime('%Y-%m-%d'), - 'responsible': "قسم الموارد البشرية", - 'resolution': "تم توظيف فريق إضافي" - } - ], - 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - self.save_kpi_data() - except Exception as e: - st.error(f"خطأ في تحميل بيانات مؤشرات الأداء: {e}") - self.kpi_data = { - 'spi': 1.05, - 'cpi': 0.98, - 'quality_score': 92, - 'safety_incidents': 0, - 'resource_utilization': 85, - 'risk_score': 15, - 'customer_satisfaction': 90, - 'environmental_compliance': 95, - 'trends': { - 'spi': [0.95, 0.98, 1.02, 1.05], - 'cpi': [1.02, 1.00, 0.99, 0.98], - 'quality_score': [85, 88, 90, 92], - 'risk_score': [25, 22, 18, 15], - 'dates': [ - (datetime.now() - timedelta(days=21)).strftime('%Y-%m-%d'), - (datetime.now() - timedelta(days=14)).strftime('%Y-%m-%d'), - (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'), - datetime.now().strftime('%Y-%m-%d') - ] - }, - 'issues': [ - { - 'id': 1, - 'description': "تأخر في توريد المواد", - 'severity': "متوسط", - 'status': "قيد المعالجة", - 'created_date': (datetime.now() - timedelta(days=10)).strftime('%Y-%m-%d'), - 'responsible': "قسم المشتريات", - 'resolution': "التنسيق مع المورد البديل" - }, - { - 'id': 2, - 'description': "نقص في فريق العمل", - 'severity': "منخفض", - 'status': "تم الحل", - 'created_date': (datetime.now() - timedelta(days=15)).strftime('%Y-%m-%d'), - 'responsible': "قسم الموارد البشرية", - 'resolution': "تم توظيف فريق إضافي" - } - ], - 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - - def save_kpi_data(self): - """حفظ بيانات مؤشرات الأداء الرئيسية""" - try: - with open(self.kpi_data_file, 'w', encoding='utf-8') as f: - json.dump(self.kpi_data, f, ensure_ascii=False, indent=2) - except Exception as e: - st.error(f"خطأ في حفظ بيانات مؤشرات الأداء: {e}") - - def update_project_status(self, phase_id, progress, status, actual_end_date=None, notes=None): - """تحديث حالة مرحلة في المشروع""" - # البحث عن المرحلة - phase = next((p for p in self.project_data['phases'] if p['id'] == phase_id), None) - if not phase: - return False - - # تحديث البيانات - phase['progress'] = progress - phase['status'] = status - if actual_end_date: - phase['actual_end_date'] = actual_end_date - if notes: - phase['notes'] = notes - - # تحديث التقدم الكلي للمشروع - self._update_overall_progress() - - # حفظ البيانات - self.project_data['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - self.save_project_data() - - return True - - def _update_overall_progress(self): - """تحديث نسبة التقدم الكلية للمشروع""" - total_weight = len(self.project_data['phases']) - total_progress = sum(phase['progress'] for phase in self.project_data['phases']) - - self.project_data['overall_progress'] = round(total_progress / total_weight) - - # تحديث عدد الأيام المنقضية - start_date = datetime.strptime(self.project_data['start_date'], '%Y-%m-%d') - self.project_data['elapsed_days'] = (datetime.now() - start_date).days - - def add_project_issue(self, description, severity, responsible, resolution=None): - """إضافة مشكلة جديدة للمشروع""" - # إنشاء معرف جديد - new_id = max([issue['id'] for issue in self.kpi_data['issues']], default=0) + 1 - - # إضافة المشكلة - new_issue = { - 'id': new_id, - 'description': description, - 'severity': severity, - 'status': "قيد المعالجة", - 'created_date': datetime.now().strftime('%Y-%m-%d'), - 'responsible': responsible, - 'resolution': resolution or "" - } - - self.kpi_data['issues'].append(new_issue) - self.kpi_data['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - self.save_kpi_data() - - return new_issue - - def update_issue_status(self, issue_id, status, resolution=None): - """تحديث حالة مشكلة في المشروع""" - # البحث عن المشكلة - issue = next((i for i in self.kpi_data['issues'] if i['id'] == issue_id), None) - if not issue: - return False - - # تحديث البيانات - issue['status'] = status - if resolution: - issue['resolution'] = resolution - - # حفظ البيانات - self.kpi_data['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - self.save_kpi_data() - - return True - - def update_kpi_values(self, kpi_updates): - """تحديث قيم مؤشرات الأداء الرئيسية""" - # تحديث القيم - for key, value in kpi_updates.items(): - if key in self.kpi_data and key != 'trends' and key != 'issues': - self.kpi_data[key] = value - - # تحديث الاتجاهات - for key, value in kpi_updates.items(): - if key in self.kpi_data and key != 'trends' and key != 'issues' and key in self.kpi_data['trends']: - # إضافة القيمة الجديدة للاتجاه وحذف أقدم قيمة إذا تجاوز العدد 5 - self.kpi_data['trends'][key].append(value) - if len(self.kpi_data['trends'][key]) > 5: - self.kpi_data['trends'][key].pop(0) - - # تحديث تاريخ الاتجاه - today = datetime.now().strftime('%Y-%m-%d') - if today not in self.kpi_data['trends']['dates']: - self.kpi_data['trends']['dates'].append(today) - if len(self.kpi_data['trends']['dates']) > 5: - self.kpi_data['trends']['dates'].pop(0) - - # حفظ البيانات - self.kpi_data['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - self.save_kpi_data() - - return True - - def render_project_status_dashboard(self): - """عرض لوحة تحكم حالة المشروع""" - st.markdown("

متتبع حالة المشروع المتحرك

", unsafe_allow_html=True) - - st.markdown(""" -
- متتبع حالة المشروع المتحرك يوفر عرضاً تفاعلياً ومرئياً لحالة المشروع ومراحله المختلفة، - مع إمكانية متابعة مؤشرات الأداء الرئيسية وتتبع التقدم بشكل مباشر. -
- """, unsafe_allow_html=True) - - # عرض معلومات المشروع - self._render_project_info() - - # عرض بطاقات مؤشرات الأداء الرئيسية - self._render_kpi_cards() - - # عرض تقدم المشروع - self._render_project_progress() - - # عرض نظرة عامة على المراحل - self._render_phases_timeline() - - # عرض اتجاهات مؤشرات الأداء - self._render_kpi_trends() - - # عرض قائمة المشكلات - self._render_issues_table() - - # عرض لوحة تحكم تحديث حالة المشروع - if st.checkbox("تحديث حالة المشروع"): - self._render_update_panel() - - def _render_project_info(self): - """عرض معلومات المشروع""" - st.markdown("

معلومات المشروع

", unsafe_allow_html=True) - - col1, col2, col3 = st.columns([2, 1, 1]) - - with col1: - st.markdown(f""" -
-
{self.project_data['project_name']}
-
رمز المشروع: {self.project_data['project_code']}
-
العميل: {self.project_data['client']}
-
الموقع: {self.project_data['location']}
-
- """, unsafe_allow_html=True) - - with col2: - # حساب الأيام المتبقية - end_date = datetime.strptime(self.project_data['end_date'], '%Y-%m-%d') - remaining_days = (end_date - datetime.now()).days - - st.markdown(f""" -
-
تاريخ البدء
-
{self.project_data['start_date']}
-
تاريخ الانتهاء
-
{self.project_data['end_date']}
-
الأيام المتبقية
-
{remaining_days} يوم
-
- """, unsafe_allow_html=True) - - with col3: - # عرض معلومات التقدم - progress = self.project_data['overall_progress'] - progress_color = "green" if progress >= 80 else "orange" if progress >= 50 else "red" - - st.markdown(f""" -
-
التقدم الكلي
-
{progress}%
-
الميزانية
-
{self.project_data['budget']:,} ريال
-
آخر تحديث
-
{self.project_data['last_updated']}
-
- """, unsafe_allow_html=True) - - def _render_kpi_cards(self): - """عرض بطاقات مؤشرات الأداء الرئيسية""" - st.markdown("

مؤشرات الأداء الرئيسية

", unsafe_allow_html=True) - - col1, col2, col3, col4 = st.columns(4) - - with col1: - # مؤشر أداء الجدول الزمني (SPI) - spi = self.kpi_data['spi'] - spi_color = "green" if spi >= 1.0 else "red" - spi_icon = "⬆️" if spi >= 1.0 else "⬇️" - spi_trend = round((spi - self.kpi_data['trends']['spi'][0]) * 100, 1) - - st.markdown(f""" -
-
مؤشر أداء الجدول الزمني
-
{spi}
-
- {spi_icon} {spi_trend}% -
-
- """, unsafe_allow_html=True) - - # مؤشر درجة الجودة - quality = self.kpi_data['quality_score'] - quality_color = "green" if quality >= 90 else "orange" if quality >= 80 else "red" - quality_icon = "⬆️" if quality >= 90 else "➡️" if quality >= 80 else "⬇️" - - st.markdown(f""" -
-
درجة الجودة
-
{quality}%
-
- {quality_icon} ممتاز -
-
- """, unsafe_allow_html=True) - - with col2: - # مؤشر أداء التكلفة (CPI) - cpi = self.kpi_data['cpi'] - cpi_color = "green" if cpi >= 1.0 else "red" - cpi_icon = "⬆️" if cpi >= 1.0 else "⬇️" - cpi_trend = round((cpi - self.kpi_data['trends']['cpi'][0]) * 100, 1) - - st.markdown(f""" -
-
مؤشر أداء التكلفة
-
{cpi}
-
- {cpi_icon} {cpi_trend}% -
-
- """, unsafe_allow_html=True) - - # مؤشر درجة المخاطر - risk = self.kpi_data['risk_score'] - risk_color = "green" if risk < 20 else "orange" if risk < 40 else "red" - risk_icon = "⬇️" if risk < 20 else "➡️" if risk < 40 else "⬆️" - - st.markdown(f""" -
-
درجة المخاطر
-
{risk}%
-
- {risk_icon} منخفضة -
-
- """, unsafe_allow_html=True) - - with col3: - # مؤشر استغلال الموارد - resources = self.kpi_data['resource_utilization'] - resources_color = "green" if resources >= 85 else "orange" if resources >= 70 else "red" - - st.markdown(f""" -
-
استغلال الموارد
-
{resources}%
-
- استغلال فعال للموارد -
-
- """, unsafe_allow_html=True) - - # عدد حوادث السلامة - safety = self.kpi_data['safety_incidents'] - safety_color = "green" if safety == 0 else "orange" if safety < 3 else "red" - - st.markdown(f""" -
-
حوادث السلامة
-
{safety}
-
- سجل سلامة ممتاز -
-
- """, unsafe_allow_html=True) - - with col4: - # مؤشر رضا العميل - satisfaction = self.kpi_data['customer_satisfaction'] - satisfaction_color = "green" if satisfaction >= 90 else "orange" if satisfaction >= 75 else "red" - - st.markdown(f""" -
-
رضا العميل
-
{satisfaction}%
-
- مستوى رضا ممتاز -
-
- """, unsafe_allow_html=True) - - # مؤشر الامتثال البيئي - compliance = self.kpi_data['environmental_compliance'] - compliance_color = "green" if compliance >= 90 else "orange" if compliance >= 75 else "red" - - st.markdown(f""" -
-
الامتثال البيئي
-
{compliance}%
-
- امتثال كامل للمعايير -
-
- """, unsafe_allow_html=True) - - def _render_project_progress(self): - """عرض تقدم المشروع""" - st.markdown("

تقدم المشروع

", unsafe_allow_html=True) - - # مؤشر التقدم الكلي - st.markdown("
التقدم الكلي للمشروع
", unsafe_allow_html=True) - - # حساب النسبة المئوية للوقت المنقضي - start_date = datetime.strptime(self.project_data['start_date'], '%Y-%m-%d') - end_date = datetime.strptime(self.project_data['end_date'], '%Y-%m-%d') - total_days = (end_date - start_date).days - elapsed_days = self.project_data['elapsed_days'] - time_percentage = min(100, max(0, round((elapsed_days / total_days) * 100))) - - # تقدم المشروع - progress_percentage = self.project_data['overall_progress'] - - # تحديد لون التقدم - progress_status = "ahead" if progress_percentage > time_percentage else "behind" if progress_percentage < time_percentage else "on-track" - progress_color = "#00b894" if progress_status == "ahead" else "#d63031" if progress_status == "behind" else "#fdcb6e" - - # إنشاء المخطط - fig = go.Figure() - - # إضافة خط التقدم المخطط - fig.add_trace(go.Scatter( - x=[start_date, end_date], - y=[0, 100], - mode='lines', - name='التقدم المخطط', - line=dict(color='rgba(0, 0, 0, 0.3)', width=2, dash='dash'), - hoverinfo='text', - hovertext=['بداية المشروع', 'نهاية المشروع'] - )) - - # إضافة نقطة الوقت الحالي - current_date = start_date + timedelta(days=elapsed_days) - fig.add_trace(go.Scatter( - x=[current_date], - y=[time_percentage], - mode='markers', - name='الوقت المنقضي', - marker=dict(color='#2d3436', size=12, symbol='diamond'), - hoverinfo='text', - hovertext=[f'الوقت المنقضي: {time_percentage}%'] - )) - - # إضافة نقطة التقدم الفعلي - fig.add_trace(go.Scatter( - x=[current_date], - y=[progress_percentage], - mode='markers', - name='التقدم الفعلي', - marker=dict(color=progress_color, size=16, symbol='circle'), - hoverinfo='text', - hovertext=[f'التقدم الفعلي: {progress_percentage}%'] - )) - - # تنسيق المخطط - fig.update_layout( - title=f"التقدم: {progress_percentage}% - الوقت المنقضي: {time_percentage}%", - xaxis_title="التاريخ", - yaxis_title="نسبة التقدم (%)", - legend=dict( - x=0, - y=1.1, - orientation='h' - ), - height=300, - margin=dict(l=20, r=20, t=50, b=20), - xaxis=dict(showgrid=False), - yaxis=dict(range=[0, 100]), - hovermode='closest', - plot_bgcolor='rgba(255, 255, 255, 0.8)' - ) - - st.plotly_chart(fig, use_container_width=True) - - # حالة التقدم - status_message = "المشروع متقدم عن الجدول الزمني" if progress_status == "ahead" else "المشروع متأخر عن الجدول الزمني" if progress_status == "behind" else "المشروع في الموعد المحدد" - status_color = "green" if progress_status == "ahead" else "red" if progress_status == "behind" else "orange" - - st.markdown(f""" -
- {status_message} (الفرق: {abs(progress_percentage - time_percentage)}%) -
- """, unsafe_allow_html=True) - - # تحليل المراحل - st.markdown("
تقدم مراحل المشروع
", unsafe_allow_html=True) - - # ترتيب المراحل - sorted_phases = sorted(self.project_data['phases'], key=lambda x: x['order']) - - # إنشاء بيانات للرسم البياني - phase_labels = [phase['name'] for phase in sorted_phases] - phase_progress = [phase['progress'] for phase in sorted_phases] - phase_colors = [ - "#00b894" if phase['status'] == "completed" else - "#fdcb6e" if phase['status'] == "in_progress" else - "#d63031" if phase['status'] == "delayed" else - "#a29bfe" if phase['status'] == "on_hold" else - "#dfe6e9" # لحالة not_started - for phase in sorted_phases - ] - - # إنشاء الرسم البياني - fig = go.Figure() - - fig.add_trace(go.Bar( - y=phase_labels, - x=phase_progress, - orientation='h', - marker=dict( - color=phase_colors - ), - hoverinfo='text', - hovertext=[f"{phase['name']}: {phase['progress']}% - {self._get_status_text(phase['status'])}" for phase in sorted_phases] - )) - - fig.update_layout( - title="تقدم مراحل المشروع", - xaxis_title="نسبة التقدم (%)", - yaxis=dict( - categoryorder='array', - categoryarray=phase_labels - ), - xaxis=dict(range=[0, 100]), - height=400, - margin=dict(l=20, r=20, t=50, b=20), - plot_bgcolor='rgba(255, 255, 255, 0.8)' - ) - - st.plotly_chart(fig, use_container_width=True) - - # وضع مفتاح الألوان - col1, col2, col3, col4, col5 = st.columns(5) - with col1: - st.markdown('
مكتمل
', unsafe_allow_html=True) - with col2: - st.markdown('
قيد التنفيذ
', unsafe_allow_html=True) - with col3: - st.markdown('
متأخر
', unsafe_allow_html=True) - with col4: - st.markdown('
متوقف
', unsafe_allow_html=True) - with col5: - st.markdown('
لم يبدأ
', unsafe_allow_html=True) - - def _render_phases_timeline(self): - """عرض الجدول الزمني للمراحل""" - st.markdown("

الجدول الزمني للمراحل

", unsafe_allow_html=True) - - # ترتيب المراحل - sorted_phases = sorted(self.project_data['phases'], key=lambda x: x['order']) - - # تحويل التواريخ إلى كائنات datetime - for phase in sorted_phases: - phase['start_date_obj'] = datetime.strptime(phase['start_date'], '%Y-%m-%d') - phase['end_date_obj'] = datetime.strptime(phase['end_date'], '%Y-%m-%d') - if phase['actual_end_date']: - phase['actual_end_date_obj'] = datetime.strptime(phase['actual_end_date'], '%Y-%m-%d') - else: - phase['actual_end_date_obj'] = None - - # إيجاد أدنى وأقصى تاريخ - min_date = min(phase['start_date_obj'] for phase in sorted_phases) - max_date = max(phase['end_date_obj'] for phase in sorted_phases) - - # إضافة هامش - date_range = (max_date - min_date).days - min_date = min_date - timedelta(days=date_range * 0.05) - max_date = max_date + timedelta(days=date_range * 0.05) - - # إنشاء الرسم البياني - fig = go.Figure() - - # إضافة خط الوقت الحالي - current_date = datetime.now() - fig.add_shape( - type="line", - x0=current_date, - y0=-0.5, - x1=current_date, - y1=len(sorted_phases) - 0.5, - line=dict( - color="red", - width=2, - dash="dash", - ), - name="اليوم" - ) - - # إضافة المراحل - for i, phase in enumerate(sorted_phases): - # إضافة المرحلة المخططة - fig.add_trace(go.Bar( - x=[phase['end_date_obj'] - phase['start_date_obj']], - y=[phase['name']], - orientation='h', - base=[phase['start_date_obj']], - marker=dict( - color='rgba(171, 214, 255, 0.8)', - line=dict(color='rgba(50, 136, 229, 1.0)', width=1) - ), - name=phase['name'] + " (مخطط)", - hoverinfo='text', - hovertext=[f"{phase['name']} (مخطط)
البداية: {phase['start_date']}
النهاية: {phase['end_date']}
المدة: {(phase['end_date_obj'] - phase['start_date_obj']).days} يوم"] - )) - - # تحديد لون المرحلة الفعلية - if phase['status'] == "completed": - color = 'rgba(0, 184, 148, 0.8)' - line_color = 'rgba(0, 150, 136, 1.0)' - elif phase['status'] == "in_progress": - color = 'rgba(253, 203, 110, 0.8)' - line_color = 'rgba(225, 177, 44, 1.0)' - elif phase['status'] == "delayed": - color = 'rgba(214, 48, 49, 0.8)' - line_color = 'rgba(192, 57, 43, 1.0)' - elif phase['status'] == "on_hold": - color = 'rgba(162, 155, 254, 0.8)' - line_color = 'rgba(108, 92, 231, 1.0)' - else: # not_started - continue # لا نعرض المراحل التي لم تبدأ بعد - - # إضافة المرحلة الفعلية - if phase['status'] in ["completed", "in_progress", "delayed", "on_hold"]: - if phase['status'] == "completed" and phase['actual_end_date_obj']: - # للمراحل المكتملة التي لها تاريخ انتهاء فعلي - duration = phase['actual_end_date_obj'] - phase['start_date_obj'] - fig.add_trace(go.Bar( - x=[duration], - y=[phase['name']], - orientation='h', - base=[phase['start_date_obj']], - marker=dict( - color=color, - line=dict(color=line_color, width=1) - ), - name=phase['name'] + " (فعلي)", - hoverinfo='text', - hovertext=[f"{phase['name']} (فعلي)
البداية: {phase['start_date']}
النهاية الفعلية: {phase['actual_end_date']}
المدة: {duration.days} يوم
التقدم: {phase['progress']}%"] - )) - else: - # للمراحل الجارية أو المتأخرة أو المعلقة - if current_date > phase['start_date_obj']: - duration = current_date - phase['start_date_obj'] - fig.add_trace(go.Bar( - x=[duration], - y=[phase['name']], - orientation='h', - base=[phase['start_date_obj']], - marker=dict( - color=color, - line=dict(color=line_color, width=1) - ), - name=phase['name'] + " (فعلي)", - hoverinfo='text', - hovertext=[f"{phase['name']} (فعلي)
البداية: {phase['start_date']}
حتى تاريخه: {current_date.strftime('%Y-%m-%d')}
المدة: {duration.days} يوم
التقدم: {phase['progress']}%"] - )) - - # تنسيق المخطط - fig.update_layout( - title="الجدول الزمني للمراحل", - xaxis=dict(type='date', range=[min_date, max_date], title='التاريخ'), - yaxis=dict(title=None, autorange="reversed"), - height=400, - margin=dict(l=20, r=20, t=50, b=20), - barmode='overlay', - legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1), - plot_bgcolor='rgba(255, 255, 255, 0.8)' - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض تفاصيل المراحل - with st.expander("عرض تفاصيل المراحل"): - for phase in sorted_phases: - status_class = self._get_status_class(phase['status']) - status_text = self._get_status_text(phase['status']) - critical_badge = 'مسار حرج' if phase.get('critical', False) else '' - - st.markdown(f""" -
-
-
{phase['name']} {critical_badge}
-
{status_text}
-
-
-
-
-
-
{phase['progress']}%
-
-
-
-
البداية: {phase['start_date']}
-
النهاية (مخطط): {phase['end_date']}
- {f'
النهاية (فعلي): {phase["actual_end_date"]}
' if phase['actual_end_date'] else ''} -
-
-
المسؤول: {phase['responsible']}
-
التسليمات: {', '.join(phase['deliverables'])}
-
-
-
{phase['notes']}
-
- """, unsafe_allow_html=True) - - def _render_kpi_trends(self): - """عرض اتجاهات مؤشرات الأداء الرئيسية""" - st.markdown("

اتجاهات مؤشرات الأداء

", unsafe_allow_html=True) - - tabs = st.tabs(["أداء الجدول الزمني والتكلفة", "الجودة والمخاطر"]) - - with tabs[0]: - # تحويل البيانات - dates = [datetime.strptime(date, '%Y-%m-%d') for date in self.kpi_data['trends']['dates']] - spi_values = self.kpi_data['trends']['spi'] - cpi_values = self.kpi_data['trends']['cpi'] - - # إنشاء رسم بياني لمؤشر أداء الجدول الزمني والتكلفة - fig = go.Figure() - - # مؤشر أداء الجدول الزمني - fig.add_trace(go.Scatter( - x=dates, - y=spi_values, - mode='lines+markers', - name='مؤشر أداء الجدول الزمني (SPI)', - line=dict(color='#00b894', width=2), - marker=dict(size=8) - )) - - # مؤشر أداء التكلفة - fig.add_trace(go.Scatter( - x=dates, - y=cpi_values, - mode='lines+markers', - name='مؤشر أداء التكلفة (CPI)', - line=dict(color='#0984e3', width=2), - marker=dict(size=8) - )) - - # إضافة خط المرجع (1.0) - fig.add_shape( - type="line", - x0=min(dates), - y0=1.0, - x1=max(dates), - y1=1.0, - line=dict( - color="#636e72", - width=1, - dash="dash", - ) - ) - - # تنسيق الرسم البياني - fig.update_layout( - title="اتجاهات مؤشرات أداء الجدول الزمني والتكلفة", - xaxis_title="التاريخ", - yaxis_title="القيمة", - legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1), - height=350, - margin=dict(l=20, r=20, t=50, b=20), - yaxis=dict(range=[0.8, 1.2], zeroline=False), - hovermode='x unified', - plot_bgcolor='rgba(255, 255, 255, 0.8)' - ) - - fig.update_yaxes(tickformat=".2f") - - st.plotly_chart(fig, use_container_width=True) - - # شرح المؤشرات - col1, col2 = st.columns(2) - with col1: - st.markdown(""" -
-

مؤشر أداء الجدول الزمني (SPI)

- -
- """, unsafe_allow_html=True) - - with col2: - st.markdown(""" -
-

مؤشر أداء التكلفة (CPI)

- -
- """, unsafe_allow_html=True) - - with tabs[1]: - # تحويل البيانات - dates = [datetime.strptime(date, '%Y-%m-%d') for date in self.kpi_data['trends']['dates']] - quality_values = self.kpi_data['trends']['quality_score'] - risk_values = self.kpi_data['trends']['risk_score'] - - # إنشاء رسم بياني للجودة والمخاطر - fig = go.Figure() - - # مؤشر الجودة - fig.add_trace(go.Scatter( - x=dates, - y=quality_values, - mode='lines+markers', - name='درجة الجودة (%)', - line=dict(color='#00b894', width=2), - marker=dict(size=8) - )) - - # إنشاء محور Y ثانوي للمخاطر - fig.add_trace(go.Scatter( - x=dates, - y=risk_values, - mode='lines+markers', - name='درجة المخاطر (%)', - line=dict(color='#d63031', width=2), - marker=dict(size=8), - yaxis="y2" - )) - - # تنسيق الرسم البياني - fig.update_layout( - title="اتجاهات الجودة والمخاطر", - xaxis_title="التاريخ", - yaxis=dict( - title="درجة الجودة (%)", - range=[0, 100], - side="left", - zeroline=False - ), - yaxis2=dict( - title="درجة المخاطر (%)", - range=[0, 100], - side="right", - overlaying="y", - zeroline=False - ), - legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1), - height=350, - margin=dict(l=20, r=20, t=50, b=20), - hovermode='x unified', - plot_bgcolor='rgba(255, 255, 255, 0.8)' - ) - - st.plotly_chart(fig, use_container_width=True) - - # شرح المؤشرات - col1, col2 = st.columns(2) - with col1: - st.markdown(""" -
-

درجة الجودة

- -
- """, unsafe_allow_html=True) - - with col2: - st.markdown(""" -
-

درجة المخاطر

- -
- """, unsafe_allow_html=True) - - def _render_issues_table(self): - """عرض جدول المشكلات""" - st.markdown("

مشكلات المشروع

", unsafe_allow_html=True) - - if not self.kpi_data['issues']: - st.info("لا توجد مشكلات مسجلة للمشروع.") - return - - # تحويل المشكلات إلى DataFrame - issues_data = pd.DataFrame(self.kpi_data['issues']) - - # تنسيق الجدول - st.markdown(""" - - """, unsafe_allow_html=True) - - # قائمة المشكلات - for issue in self.kpi_data['issues']: - # تحديد فئة الخطورة - severity_class = "" - if issue['severity'] == "عالي": - severity_class = "high" - elif issue['severity'] == "متوسط": - severity_class = "medium" - else: - severity_class = "low" - - # تحديد فئة الحالة - status_class = "" - if issue['status'] == "تم الحل": - status_class = "resolved" - elif issue['status'] == "قيد المعالجة": - status_class = "in-progress" - elif issue['status'] == "معلق": - status_class = "on-hold" - else: - status_class = "open" - - st.markdown(f""" -
-
-
#{issue['id']} - {issue['description']}
-
- {issue['severity']} - {issue['status']} -
-
-
-
-
تاريخ الإنشاء: {issue['created_date']}
-
المسؤول: {issue['responsible']}
-
-
-
خطة المعالجة: {issue['resolution']}
-
-
-
- """, unsafe_allow_html=True) - - if st.checkbox("إضافة مشكلة جديدة"): - with st.form("add_issue_form"): - description = st.text_input("وصف المشكلة") - - col1, col2 = st.columns(2) - with col1: - severity = st.selectbox("الخطورة", ["منخفض", "متوسط", "عالي"]) - with col2: - responsible = st.text_input("المسؤول") - - resolution = st.text_area("خطة المعالجة") - - submitted = st.form_submit_button("إضافة المشكلة") - if submitted: - if description and responsible: - self.add_project_issue(description, severity, responsible, resolution) - st.success("تمت إضافة المشكلة بنجاح") - st.rerun() - else: - st.error("يرجى ملء جميع الحقول المطلوبة") - - def _render_update_panel(self): - """عرض لوحة تحديث حالة المشروع""" - st.markdown("

تحديث حالة المشروع

", unsafe_allow_html=True) - - tabs = st.tabs(["تحديث مرحلة", "تحديث مؤشرات الأداء", "تحديث حالة مشكلة"]) - - with tabs[0]: - # تحديث مرحلة - sorted_phases = sorted(self.project_data['phases'], key=lambda x: x['order']) - phase_names = [phase['name'] for phase in sorted_phases] - phase_ids = [phase['id'] for phase in sorted_phases] - - selected_phase_index = st.selectbox("اختر المرحلة", range(len(phase_names)), format_func=lambda i: phase_names[i]) - selected_phase = sorted_phases[selected_phase_index] - - with st.form("update_phase_form"): - st.markdown(f"**تحديث مرحلة: {selected_phase['name']}**") - - col1, col2 = st.columns(2) - with col1: - progress = st.slider("نسبة التقدم", 0, 100, selected_phase['progress']) - with col2: - status = st.selectbox("الحالة", [ - "not_started", "in_progress", "completed", "delayed", "on_hold" - ], - index=["not_started", "in_progress", "completed", "delayed", "on_hold"].index(selected_phase['status']), - format_func=lambda s: self._get_status_text(s)) - - actual_end_date = None - if status == "completed": - actual_end_date = st.date_input("تاريخ الانتهاء الفعلي", - value=datetime.now() if not selected_phase['actual_end_date'] else datetime.strptime(selected_phase['actual_end_date'], '%Y-%m-%d')) - actual_end_date = actual_end_date.strftime('%Y-%m-%d') - - notes = st.text_area("ملاحظات", selected_phase['notes']) - - submitted = st.form_submit_button("تحديث المرحلة") - if submitted: - success = self.update_project_status(selected_phase['id'], progress, status, actual_end_date, notes) - if success: - st.success("تم تحديث حالة المرحلة بنجاح") - st.rerun() - else: - st.error("حدث خطأ أثناء تحديث حالة المرحلة") - - with tabs[1]: - # تحديث مؤشرات الأداء - with st.form("update_kpi_form"): - st.markdown("**تحديث مؤشرات الأداء**") - - col1, col2 = st.columns(2) - with col1: - spi = st.number_input("مؤشر أداء الجدول الزمني (SPI)", 0.1, 2.0, self.kpi_data['spi'], 0.01) - cpi = st.number_input("مؤشر أداء التكلفة (CPI)", 0.1, 2.0, self.kpi_data['cpi'], 0.01) - quality_score = st.slider("درجة الجودة", 0, 100, self.kpi_data['quality_score']) - risk_score = st.slider("درجة المخاطر", 0, 100, self.kpi_data['risk_score']) - - with col2: - resource_utilization = st.slider("استغلال الموارد", 0, 100, self.kpi_data['resource_utilization']) - safety_incidents = st.number_input("حوادث السلامة", 0, 100, self.kpi_data['safety_incidents']) - customer_satisfaction = st.slider("رضا العميل", 0, 100, self.kpi_data['customer_satisfaction']) - environmental_compliance = st.slider("الامتثال البيئي", 0, 100, self.kpi_data['environmental_compliance']) - - submitted = st.form_submit_button("تحديث مؤشرات الأداء") - if submitted: - kpi_updates = { - 'spi': spi, - 'cpi': cpi, - 'quality_score': quality_score, - 'risk_score': risk_score, - 'resource_utilization': resource_utilization, - 'safety_incidents': safety_incidents, - 'customer_satisfaction': customer_satisfaction, - 'environmental_compliance': environmental_compliance - } - - success = self.update_kpi_values(kpi_updates) - if success: - st.success("تم تحديث مؤشرات الأداء بنجاح") - st.rerun() - else: - st.error("حدث خطأ أثناء تحديث مؤشرات الأداء") - - with tabs[2]: - # تحديث حالة مشكلة - if not self.kpi_data['issues']: - st.info("لا توجد مشكلات لتحديثها.") - else: - issue_descriptions = [f"#{issue['id']} - {issue['description']}" for issue in self.kpi_data['issues']] - issue_ids = [issue['id'] for issue in self.kpi_data['issues']] - - selected_issue_index = st.selectbox("اختر المشكلة", range(len(issue_descriptions)), format_func=lambda i: issue_descriptions[i]) - selected_issue = self.kpi_data['issues'][selected_issue_index] - - with st.form("update_issue_form"): - st.markdown(f"**تحديث حالة المشكلة: {selected_issue['description']}**") - - status = st.selectbox("الحالة", [ - "مفتوح", "قيد المعالجة", "تم الحل", "معلق" - ], - index=["مفتوح", "قيد المعالجة", "تم الحل", "معلق"].index(selected_issue['status']) if selected_issue['status'] in ["مفتوح", "قيد المعالجة", "تم الحل", "معلق"] else 0) - - resolution = st.text_area("خطة المعالجة / الحل", selected_issue['resolution']) - - submitted = st.form_submit_button("تحديث المشكلة") - if submitted: - success = self.update_issue_status(selected_issue['id'], status, resolution) - if success: - st.success("تم تحديث حالة المشكلة بنجاح") - st.rerun() - else: - st.error("حدث خطأ أثناء تحديث حالة المشكلة") - - def _get_status_text(self, status): - """الحصول على النص العربي لحالة المرحلة""" - status_map = { - "not_started": "لم تبدأ", - "in_progress": "قيد التنفيذ", - "completed": "مكتملة", - "delayed": "متأخرة", - "on_hold": "متوقفة" - } - return status_map.get(status, status) - - def _get_status_class(self, status): - """الحصول على فئة CSS لحالة المرحلة""" - status_map = { - "not_started": "not-started", - "in_progress": "in-progress", - "completed": "completed", - "delayed": "delayed", - "on_hold": "on-hold" - } - return status_map.get(status, "") - - def render(self): - """عرض واجهة متتبع حالة المشروع""" - # إضافة CSS مخصص - st.markdown(""" - - """, unsafe_allow_html=True) - - # عرض لوحة تحكم حالة المشروع - self.render_project_status_dashboard() \ No newline at end of file diff --git a/modules/project_tracker/tracker_app.py b/modules/project_tracker/tracker_app.py deleted file mode 100644 index 3fa249317afc63249666dbecd7802b8e6f902e94..0000000000000000000000000000000000000000 --- a/modules/project_tracker/tracker_app.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة تطبيق متتبع حالة المشروع المتحرك مع تصور التقدم -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np -from datetime import datetime, timedelta - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات متتبع حالة المشروع -from modules.project_tracker.status_tracker import ProjectStatusTracker - - -class TrackerApp: - """وحدة تطبيق متتبع حالة المشروع المتحرك""" - - def __init__(self, project_id=None, user_id=None): - """تهيئة وحدة تطبيق متتبع حالة المشروع المتحرك""" - self.project_tracker = ProjectStatusTracker(project_id, user_id) - - def render(self): - """عرض واجهة وحدة تطبيق متتبع حالة المشروع المتحرك""" - self.project_tracker.render() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="متتبع حالة المشروع المتحرك | WAHBi AI", - page_icon="📊", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = TrackerApp() - app.render() \ No newline at end of file diff --git a/modules/projects/__init__.py b/modules/projects/__init__.py deleted file mode 100644 index 4a6c17a73333f06321271be498c9a2e2a175542f..0000000000000000000000000000000000000000 --- a/modules/projects/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# ملف تهيئة حزمة إدارة المشاريع \ No newline at end of file diff --git a/modules/projects/projects_app.py b/modules/projects/projects_app.py deleted file mode 100644 index a2996048f89249e786f202765bc753d054e48c3e..0000000000000000000000000000000000000000 --- a/modules/projects/projects_app.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة إدارة المشاريع لنظام تحليل العقود والمناقصات -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات إدارة المشاريع -from modules.projects.projects_management import ProjectsManagement - - -class ProjectsApp: - """وحدة إدارة المشاريع الرئيسية""" - - def __init__(self): - """تهيئة وحدة إدارة المشاريع""" - self.projects_management = ProjectsManagement() - - def render(self): - """عرض واجهة وحدة إدارة المشاريع""" - st.markdown("

وحدة إدارة المشاريع

", unsafe_allow_html=True) - - st.markdown(""" -
- تمكنك وحدة إدارة المشاريع من إنشاء وتتبع وإدارة المشاريع بكفاءة، مع ميزات متقدمة لمراقبة المواعيد النهائية والموارد. - يمكنك إضافة معلومات تفصيلية للمشاريع، بما في ذلك معلومات الموقع، مرئيات المدير، والمخاطر والمميزات. -
- """, unsafe_allow_html=True) - - # عرض نموذج إدارة المشاريع - self.projects_management.render() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="إدارة المشاريع | WAHBi AI", - page_icon="🏗️", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = ProjectsApp() - app.render() \ No newline at end of file diff --git a/modules/projects/projects_management.py b/modules/projects/projects_management.py deleted file mode 100644 index 812d74bb3eac0b29d54ef2ff3b3210c749983096..0000000000000000000000000000000000000000 --- a/modules/projects/projects_management.py +++ /dev/null @@ -1,942 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -نموذج إدارة المشاريع لنظام WAHBi-AI -يتضمن ميزات إضافة وإدارة المشاريع الجديدة مع معلومات موسعة -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np -import datetime -import tempfile -from pathlib import Path -import plotly.express as px -import plotly.graph_objects as go -import json -import time -from datetime import datetime, timedelta - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات واجهة المستخدم -from utils.components.header import render_header -from utils.components.credits import render_credits -from utils.helpers import format_number, format_currency, styled_button - -class ProjectsManagement: - """نموذج إدارة المشاريع""" - - def __init__(self): - """تهيئة نموذج إدارة المشاريع""" - # تهيئة حالة الجلسة - if 'projects' not in st.session_state: - st.session_state.projects = [] - - if 'project_files' not in st.session_state: - st.session_state.project_files = {} - - if 'project_inquiries' not in st.session_state: - st.session_state.project_inquiries = {} - - # ضمان وجود مجلد لحفظ ملفات المشاريع - self.projects_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/projects")) - os.makedirs(self.projects_dir, exist_ok=True) - - def render(self): - """عرض واجهة إدارة المشاريع""" - # عرض الشعار والعنوان الرئيسي - render_header("إدارة المشاريع") - - # تبويبات إدارة المشاريع - tabs = st.tabs(["المشاريع الحالية", "إضافة مشروع جديد", "أرشيف المشاريع", "التقارير"]) - - with tabs[0]: - self._render_current_projects() - - with tabs[1]: - self._render_new_project_form() - - with tabs[2]: - self._render_archived_projects() - - with tabs[3]: - self._render_projects_reports() - - # عرض الحقوق - render_credits() - - def _render_current_projects(self): - """عرض قائمة المشاريع الحالية""" - st.markdown(""" -
-

🏗️ المشاريع الحالية

-

قائمة المشاريع النشطة التي يتم العمل عليها حالياً.

-
- """, unsafe_allow_html=True) - - # تصفية المشاريع النشطة - active_projects = [p for p in st.session_state.projects if p.get("status") != "archived"] - - if not active_projects: - st.info("لا توجد مشاريع نشطة حالياً. يمكنك إضافة مشروع جديد من تبويب 'إضافة مشروع جديد'.") - return - - # عرض المشاريع النشطة - for idx, project in enumerate(active_projects): - with st.expander(f"{project.get('name')} - {project.get('client')}"): - self._render_project_details(project, idx) - - def _render_project_details(self, project, idx): - """عرض تفاصيل مشروع محدد""" - # معلومات أساسية - col1, col2, col3 = st.columns(3) - - with col1: - st.markdown(f"**اسم المشروع:** {project.get('name')}") - st.markdown(f"**رقم المشروع:** {project.get('number')}") - - with col2: - st.markdown(f"**العميل:** {project.get('client')}") - st.markdown(f"**الموقع:** {project.get('location')}") - - with col3: - st.markdown(f"**تاريخ البدء:** {project.get('start_date')}") - st.markdown(f"**تاريخ التقديم:** {project.get('submission_date')}") - - # تبويبات تفاصيل المشروع - project_tabs = st.tabs([ - "معلومات المشروع", - "مرئيات مدير المنطقة", - "صور وفيديوهات الموقع", - "مميزات ومخاطر المشروع", - "استفسارات المالك", - "معلومات الموقع" - ]) - - # تبويب معلومات المشروع - with project_tabs[0]: - # المعلومات الأساسية - st.markdown("### معلومات المشروع الأساسية") - st.markdown(f"**نوع المشروع:** {project.get('type')}") - st.markdown(f"**القيمة التقديرية:** {format_currency(project.get('estimated_value', 0))} ريال") - st.markdown(f"**المدة المتوقعة:** {project.get('duration')} يوم") - st.markdown(f"**حالة المشروع:** {project.get('status')}") - - # جدول زمني للمشروع - st.markdown("### الجدول الزمني للمشروع") - - # حساب الأيام المتبقية للتقديم - if project.get('submission_date'): - try: - submission_date = datetime.strptime(project.get('submission_date'), "%Y-%m-%d") - today = datetime.now() - days_remaining = (submission_date - today).days - - # عرض شريط التقدم للوقت المتبقي - if days_remaining > 0: - st.markdown(f"**الوقت المتبقي للتقديم:** {days_remaining} يوم") - progress_pct = min(1.0, max(0.0, days_remaining / 30.0)) # افتراض 30 يوم كمدة قياسية - st.progress(progress_pct) - else: - st.error(f"**انتهت مدة التقديم منذ:** {abs(days_remaining)} يوم") - except: - st.warning("تعذر حساب الأيام المتبقية. يرجى التأكد من صحة التاريخ.") - - # أزرار العمليات - col1, col2 = st.columns(2) - with col1: - if styled_button("تحديث حالة المشروع", key=f"update_project_{idx}", type="primary", icon="🔄"): - st.session_state.project_to_update = idx - - with col2: - if styled_button("تصدير معلومات المشروع", key=f"export_project_{idx}", type="success", icon="📤"): - st.session_state.project_to_export = idx - - # تبويب مرئيات مدير المنطقة - with project_tabs[1]: - st.markdown("### مرئيات مدير المنطقة") - - # عرض المرئيات الموجودة - manager_insights = project.get('manager_insights', []) - - if manager_insights: - for i, insight in enumerate(manager_insights): - with st.expander(f"مرئية #{i+1} - {insight.get('date')}"): - st.markdown(f"**العنوان:** {insight.get('title')}") - st.markdown(f"**التاريخ:** {insight.get('date')}") - st.markdown(f"**المحتوى:**\n{insight.get('content')}") - - # عرض المرفقات إن وجدت - attachments = insight.get('attachments', []) - if attachments: - st.markdown("**المرفقات:**") - for att in attachments: - st.markdown(f"- {att}") - else: - st.info("لا توجد مرئيات مضافة لمدير المنطقة.") - - # إضافة مرئية جديدة - st.markdown("### إضافة مرئية جديدة") - - insight_title = st.text_input("عنوان المرئية", key=f"new_insight_title_{idx}") - insight_content = st.text_area("محتوى المرئية", key=f"new_insight_content_{idx}") - insight_file = st.file_uploader("إرفاق ملف (اختياري)", key=f"new_insight_file_{idx}") - - if styled_button("إضافة مرئية", key=f"add_insight_{idx}", type="primary", icon="➕"): - if not insight_title or not insight_content: - st.error("يرجى تعبئة عنوان ومحتوى المرئية.") - else: - # إنشاء مرئية جديدة - new_insight = { - "title": insight_title, - "date": datetime.now().strftime("%Y-%m-%d"), - "content": insight_content, - "attachments": [] - } - - # حفظ الملف المرفق إن وجد - if insight_file: - file_path = self._save_project_file(project.get('number'), "insights", insight_file) - if file_path: - new_insight["attachments"].append(file_path) - - # إضافة المرئية للمشروع - if 'manager_insights' not in project: - project['manager_insights'] = [] - - project['manager_insights'].append(new_insight) - st.success("تمت إضافة المرئية بنجاح!") - st.rerun() - - # تبويب صور وفيديوهات الموقع - with project_tabs[2]: - st.markdown("### صور وفيديوهات الموقع") - - # عرض الصور والفيديوهات الموجودة - site_media = project.get('site_media', []) - - if site_media: - media_tabs = st.tabs(["الصور", "الفيديوهات", "مرفقات أخرى"]) - - # عرض الصور - with media_tabs[0]: - images = [m for m in site_media if m.get('type') == 'image'] - if images: - for img in images: - st.markdown(f"**{img.get('title')}** - {img.get('date')}") - if 'file_path' in img: - try: - # يمكن تنفيذ عرض الصورة هنا إذا كانت متاحة - st.markdown(f"*مسار الملف:* {img.get('file_path')}") - except: - st.warning("تعذر عرض الصورة.") - st.markdown(f"*الوصف:* {img.get('description', '')}") - st.markdown("---") - else: - st.info("لا توجد صور مضافة للموقع.") - - # عرض الفيديوهات - with media_tabs[1]: - videos = [m for m in site_media if m.get('type') == 'video'] - if videos: - for vid in videos: - st.markdown(f"**{vid.get('title')}** - {vid.get('date')}") - if 'file_path' in vid: - st.markdown(f"*مسار الملف:* {vid.get('file_path')}") - st.markdown(f"*الوصف:* {vid.get('description', '')}") - st.markdown("---") - else: - st.info("لا توجد فيديوهات مضافة للموقع.") - - # عرض مرفقات أخرى - with media_tabs[2]: - other_files = [m for m in site_media if m.get('type') not in ['image', 'video']] - if other_files: - for f in other_files: - st.markdown(f"**{f.get('title')}** - {f.get('date')}") - if 'file_path' in f: - st.markdown(f"*مسار الملف:* {f.get('file_path')}") - st.markdown(f"*الوصف:* {f.get('description', '')}") - st.markdown("---") - else: - st.info("لا توجد مرفقات أخرى للموقع.") - else: - st.info("لا توجد صور أو فيديوهات مضافة للموقع.") - - # إضافة وسائط جديدة - st.markdown("### إضافة صور أو فيديوهات جديدة") - - media_type = st.selectbox( - "نوع الملف", - options=["صورة", "فيديو", "ملف آخر"], - key=f"new_media_type_{idx}" - ) - - media_title = st.text_input("عنوان الملف", key=f"new_media_title_{idx}") - media_desc = st.text_area("وصف الملف", key=f"new_media_desc_{idx}") - media_file = st.file_uploader( - "اختر الملف", - type=["jpg", "jpeg", "png", "mp4", "avi", "mov", "pdf", "docx"] if media_type == "ملف آخر" else - ["mp4", "avi", "mov"] if media_type == "فيديو" else - ["jpg", "jpeg", "png"], - key=f"new_media_file_{idx}" - ) - - if styled_button("إضافة للموقع", key=f"add_media_{idx}", type="primary", icon="➕"): - if not media_title or not media_file: - st.error("يرجى تعبئة عنوان الملف واختيار الملف.") - else: - # تحديد نوع الملف لحفظه - file_type = "images" if media_type == "صورة" else "videos" if media_type == "فيديو" else "others" - - # حفظ الملف - file_path = self._save_project_file(project.get('number'), file_type, media_file) - - if file_path: - # إنشاء كائن الوسائط - new_media = { - "title": media_title, - "date": datetime.now().strftime("%Y-%m-%d"), - "description": media_desc, - "type": "image" if media_type == "صورة" else "video" if media_type == "فيديو" else "other", - "file_path": file_path - } - - # إضافة الوسائط للمشروع - if 'site_media' not in project: - project['site_media'] = [] - - project['site_media'].append(new_media) - st.success(f"تمت إضافة {media_type} بنجاح!") - st.rerun() - - # تبويب مميزات ومخاطر المشروع - with project_tabs[3]: - st.markdown("### مميزات ومخاطر المشروع") - - # عرض المميزات والمخاطر في تبويبات - advantage_risk_tabs = st.tabs(["مميزات المشروع", "مخاطر المشروع"]) - - # تبويب مميزات المشروع - with advantage_risk_tabs[0]: - advantages = project.get('advantages', []) - - if advantages: - for i, adv in enumerate(advantages): - st.markdown(f"**{i+1}. {adv.get('title')}**") - st.markdown(f"*التأثير:* {adv.get('impact')}") - st.markdown(f"{adv.get('description')}") - st.markdown("---") - else: - st.info("لم يتم إضافة مميزات للمشروع.") - - # إضافة ميزة جديدة - st.markdown("### إضافة ميزة جديدة") - adv_title = st.text_input("عنوان الميزة", key=f"new_adv_title_{idx}") - adv_impact = st.selectbox( - "مستوى التأثير", - options=["منخفض", "متوسط", "عالي"], - key=f"new_adv_impact_{idx}" - ) - adv_desc = st.text_area("وصف الميزة", key=f"new_adv_desc_{idx}") - - if styled_button("إضافة ميزة", key=f"add_adv_{idx}", type="success", icon="✨"): - if not adv_title or not adv_desc: - st.error("يرجى تعبئة عنوان ووصف الميزة.") - else: - # إنشاء ميزة جديدة - new_adv = { - "title": adv_title, - "impact": adv_impact, - "description": adv_desc, - "date_added": datetime.now().strftime("%Y-%m-%d") - } - - # إضافة الميزة للمشروع - if 'advantages' not in project: - project['advantages'] = [] - - project['advantages'].append(new_adv) - st.success("تمت إضافة الميزة بنجاح!") - st.rerun() - - # تبويب مخاطر المشروع - with advantage_risk_tabs[1]: - risks = project.get('risks', []) - - if risks: - for i, risk in enumerate(risks): - risk_color = "🔴" if risk.get('severity') == "عالي" else "🟠" if risk.get('severity') == "متوسط" else "🟡" - st.markdown(f"{risk_color} **{i+1}. {risk.get('title')}**") - st.markdown(f"*الحدة:* {risk.get('severity')} | *الاحتمالية:* {risk.get('probability')}%") - st.markdown(f"*الوصف:* {risk.get('description')}") - st.markdown(f"*الإجراءات المقترحة:* {risk.get('mitigation_plan')}") - st.markdown("---") - else: - st.info("لم يتم إضافة مخاطر للمشروع.") - - # إضافة مخاطر جديدة - st.markdown("### إضافة مخاطر جديدة") - risk_title = st.text_input("عنوان المخاطرة", key=f"new_risk_title_{idx}") - risk_severity = st.selectbox( - "حدة المخاطرة", - options=["منخفض", "متوسط", "عالي"], - key=f"new_risk_severity_{idx}" - ) - risk_probability = st.slider( - "احتمالية الحدوث (%)", - min_value=0, - max_value=100, - value=50, - key=f"new_risk_prob_{idx}" - ) - risk_desc = st.text_area("وصف المخاطرة", key=f"new_risk_desc_{idx}") - risk_mitigation = st.text_area("خطة التخفيف المقترحة", key=f"new_risk_mitigation_{idx}") - - if styled_button("إضافة مخاطرة", key=f"add_risk_{idx}", type="warning", icon="⚠️"): - if not risk_title or not risk_desc: - st.error("يرجى تعبئة عنوان ووصف المخاطرة.") - else: - # إنشاء مخاطرة جديدة - new_risk = { - "title": risk_title, - "severity": risk_severity, - "probability": risk_probability, - "description": risk_desc, - "mitigation_plan": risk_mitigation, - "date_added": datetime.now().strftime("%Y-%m-%d") - } - - # إضافة المخاطرة للمشروع - if 'risks' not in project: - project['risks'] = [] - - project['risks'].append(new_risk) - st.success("تمت إضافة المخاطرة بنجاح!") - st.rerun() - - # تبويب استفسارات المالك - with project_tabs[4]: - st.markdown("### استفسارات المالك") - - # الحصول على استفسارات المشروع - inquiries = project.get('inquiries', []) - - if inquiries: - for i, inq in enumerate(inquiries): - with st.expander(f"استفسار #{i+1} - {inq.get('date')}"): - st.markdown(f"**السؤال:** {inq.get('question')}") - - if inq.get('answer'): - st.markdown(f"**الإجابة:** {inq.get('answer')}") - st.markdown(f"**تاريخ الإجابة:** {inq.get('answer_date', 'غير محدد')}") - else: - st.warning("لم تتم الإجابة على هذا الاستفسار بعد.") - - # نموذج للإجابة - answer_text = st.text_area("إجابة الاستفسار", key=f"answer_{idx}_{i}") - - if styled_button("إرسال الإجابة", key=f"send_answer_{idx}_{i}", type="primary", icon="✉️"): - if not answer_text: - st.error("يرجى كتابة الإجابة.") - else: - # تحديث الاستفسار بالإجابة - inq['answer'] = answer_text - inq['answer_date'] = datetime.now().strftime("%Y-%m-%d") - st.success("تم إرسال الإجابة بنجاح!") - st.rerun() - else: - st.info("لا توجد استفسارات من المالك لهذا المشروع.") - - # إضافة استفسار جديد - st.markdown("### إضافة استفسار جديد") - - inquiry_question = st.text_area("سؤال الاستفسار", key=f"new_inquiry_{idx}") - - if styled_button("إضافة استفسار", key=f"add_inquiry_{idx}", type="primary", icon="❓"): - if not inquiry_question: - st.error("يرجى كتابة السؤال.") - else: - # إنشاء استفسار جديد - new_inquiry = { - "question": inquiry_question, - "date": datetime.now().strftime("%Y-%m-%d"), - "answer": None, - "answer_date": None - } - - # إضافة الاستفسار للمشروع - if 'inquiries' not in project: - project['inquiries'] = [] - - project['inquiries'].append(new_inquiry) - st.success("تمت إضافة الاستفسار بنجاح!") - st.rerun() - - # تبويب معلومات الموقع - with project_tabs[5]: - st.markdown("### معلومات الموقع") - - # عرض معلومات الموقع الحالية - site_info = project.get('site_info', {}) - - if site_info: - st.markdown("#### معلومات أساسية") - st.markdown(f"**الطبيعة الجغرافية:** {site_info.get('geography', 'غير محدد')}") - st.markdown(f"**إمكانية الوصول:** {site_info.get('accessibility', 'غير محدد')}") - st.markdown(f"**المسافة عن أقرب مدينة:** {site_info.get('distance_to_city', 'غير محدد')}") - - st.markdown("#### بيانات التضاريس") - st.markdown(f"**نوع التربة:** {site_info.get('soil_type', 'غير محدد')}") - st.markdown(f"**متوسط درجة الحرارة:** {site_info.get('avg_temperature', 'غير محدد')}") - st.markdown(f"**موسم الأمطار:** {site_info.get('rainy_season', 'غير محدد')}") - - st.markdown("#### الخدمات المتوفرة") - st.markdown(f"**مياه:** {'متوفر' if site_info.get('has_water', False) else 'غير متوفر'}") - st.markdown(f"**كهرباء:** {'متوفر' if site_info.get('has_electricity', False) else 'غير متوفر'}") - st.markdown(f"**اتصالات:** {'متوفر' if site_info.get('has_communications', False) else 'غير متوفر'}") - - st.markdown("#### ملاحظات إضافية") - st.markdown(f"{site_info.get('notes', '')}") - - # خريطة الموقع (يمكن إضافتها لاحقاً إذا توفرت الإحداثيات) - if 'latitude' in site_info and 'longitude' in site_info: - st.markdown("#### موقع المشروع على الخريطة") - # يمكن استخدام تقنيات مثل folium أو ربط Google Maps API هنا - else: - st.info("لم يتم إضافة معلومات الموقع بعد.") - - # تحديث معلومات الموقع - st.markdown("### تحديث معلومات الموقع") - - # قسم الطبيعة الجغرافية - st.markdown("#### الطبيعة الجغرافية والوصول") - geo_col1, geo_col2 = st.columns(2) - - with geo_col1: - geography = st.selectbox( - "الطبيعة الجغرافية", - options=["صحراوية", "جبلية", "ساحلية", "زراعية", "حضرية", "أخرى"], - index=0 if not site_info else ["صحراوية", "جبلية", "ساحلية", "زراعية", "حضرية", "أخرى"].index(site_info.get('geography', "صحراوية")), - key=f"site_geography_{idx}" - ) - - accessibility = st.selectbox( - "إمكانية الوصول", - options=["سهلة", "متوسطة", "صعبة"], - index=0 if not site_info else ["سهلة", "متوسطة", "صعبة"].index(site_info.get('accessibility', "سهلة")), - key=f"site_accessibility_{idx}" - ) - - with geo_col2: - distance_to_city = st.text_input( - "المسافة عن أقرب مدينة (كم)", - value=site_info.get('distance_to_city', ""), - key=f"site_distance_{idx}" - ) - - nearest_city = st.text_input( - "أقرب مدينة رئيسية", - value=site_info.get('nearest_city', ""), - key=f"site_nearest_city_{idx}" - ) - - # قسم التضاريس والمناخ - st.markdown("#### التضاريس والمناخ") - terrain_col1, terrain_col2 = st.columns(2) - - with terrain_col1: - soil_type = st.selectbox( - "نوع التربة", - options=["رملية", "صخرية", "طينية", "مختلطة", "أخرى"], - index=0 if not site_info else ["رملية", "صخرية", "طينية", "مختلطة", "أخرى"].index(site_info.get('soil_type', "رملية")), - key=f"site_soil_{idx}" - ) - - avg_temperature = st.text_input( - "متوسط درجة الحرارة", - value=site_info.get('avg_temperature', ""), - key=f"site_temp_{idx}" - ) - - with terrain_col2: - rainy_season = st.text_input( - "موسم الأمطار", - value=site_info.get('rainy_season', ""), - key=f"site_rainy_{idx}" - ) - - wind_info = st.text_input( - "معلومات الرياح", - value=site_info.get('wind_info', ""), - key=f"site_wind_{idx}" - ) - - # قسم الخدمات المتوفرة - st.markdown("#### الخدمات المتوفرة") - services_col1, services_col2, services_col3 = st.columns(3) - - with services_col1: - has_water = st.checkbox( - "مياه", - value=site_info.get('has_water', False), - key=f"site_water_{idx}" - ) - - with services_col2: - has_electricity = st.checkbox( - "كهرباء", - value=site_info.get('has_electricity', False), - key=f"site_electricity_{idx}" - ) - - with services_col3: - has_communications = st.checkbox( - "اتصالات", - value=site_info.get('has_communications', False), - key=f"site_communications_{idx}" - ) - - # ملاحظات إضافية - site_notes = st.text_area( - "ملاحظات إضافية عن الموقع", - value=site_info.get('notes', ""), - key=f"site_notes_{idx}" - ) - - # حفظ معلومات الموقع - if styled_button("حفظ معلومات الموقع", key=f"save_site_info_{idx}", type="primary", icon="💾"): - # تجميع معلومات الموقع - updated_site_info = { - "geography": geography, - "accessibility": accessibility, - "distance_to_city": distance_to_city, - "nearest_city": nearest_city, - "soil_type": soil_type, - "avg_temperature": avg_temperature, - "rainy_season": rainy_season, - "wind_info": wind_info, - "has_water": has_water, - "has_electricity": has_electricity, - "has_communications": has_communications, - "notes": site_notes - } - - # تحديث معلومات الموقع في المشروع - project['site_info'] = updated_site_info - st.success("تم حفظ معلومات الموقع بنجاح!") - - def _render_new_project_form(self): - """عرض نموذج إضافة مشروع جديد""" - st.markdown(""" -
-

➕ إضافة مشروع جديد

-

قم بإدخال معلومات المشروع الجديد بالتفصيل.

-
- """, unsafe_allow_html=True) - - # قسم المعلومات الأساسية - st.markdown("### معلومات المشروع الأساسية") - - # عمل تقسيم لحقول المعلومات الأساسية - col1, col2 = st.columns(2) - - with col1: - project_name = st.text_input("اسم المشروع", key="new_project_name") - project_number = st.text_input("رقم المشروع", key="new_project_number") - project_client = st.text_input("العميل", key="new_project_client") - - with col2: - project_location = st.text_input("موقع المشروع", key="new_project_location") - project_type = st.selectbox( - "نوع المشروع", - options=["بنية تحتية", "مباني", "طرق", "جسور", "شبكات مياه", "شبكات كهرباء", "أخرى"], - key="new_project_type" - ) - project_estimated_value = st.number_input( - "القيمة التقديرية (ريال)", - min_value=0.0, - step=100000.0, - format="%.2f", - key="new_project_value" - ) - - # قسم التواريخ والجدول الزمني - st.markdown("### الجدول الزمني") - - date_col1, date_col2 = st.columns(2) - - with date_col1: - project_start_date = st.date_input( - "تاريخ البدء", - value=datetime.now(), - key="new_project_start_date" - ) - - project_submission_date = st.date_input( - "تاريخ التقديم", - value=datetime.now() + timedelta(days=30), - key="new_project_submission_date" - ) - - with date_col2: - project_duration = st.number_input( - "مدة المشروع (يوم)", - min_value=1, - value=180, - step=1, - key="new_project_duration" - ) - - project_status = st.selectbox( - "حالة المشروع", - options=["جديد", "قيد الدراسة", "تم التقديم", "تم الترسية", "قيد التنفيذ", "مكتمل", "ملغي"], - key="new_project_status" - ) - - # زر إضافة المشروع - if styled_button("إضافة المشروع", key="add_new_project", type="success", icon="✅"): - # التحقق من وجود المعلومات الأساسية - if not project_name or not project_number or not project_client or not project_location: - st.error("يرجى تعبئة جميع الحقول الأساسية (اسم المشروع، رقم المشروع، العميل، الموقع).") - else: - # إنشاء كائن المشروع الجديد - new_project = { - "name": project_name, - "number": project_number, - "client": project_client, - "location": project_location, - "type": project_type, - "estimated_value": project_estimated_value, - "start_date": project_start_date.strftime("%Y-%m-%d"), - "submission_date": project_submission_date.strftime("%Y-%m-%d"), - "duration": project_duration, - "status": project_status, - "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "site_info": {}, - "manager_insights": [], - "site_media": [], - "advantages": [], - "risks": [], - "inquiries": [] - } - - # إضافة المشروع للقائمة - st.session_state.projects.append(new_project) - - # إنشاء مجلد للمشروع - project_dir = os.path.join(self.projects_dir, project_number) - os.makedirs(project_dir, exist_ok=True) - - # إنشاء المجلدات الفرعية - os.makedirs(os.path.join(project_dir, "insights"), exist_ok=True) - os.makedirs(os.path.join(project_dir, "images"), exist_ok=True) - os.makedirs(os.path.join(project_dir, "videos"), exist_ok=True) - os.makedirs(os.path.join(project_dir, "others"), exist_ok=True) - - st.success(f"تمت إضافة المشروع '{project_name}' بنجاح!") - st.rerun() - - def _render_archived_projects(self): - """عرض قائمة المشاريع المؤرشفة""" - st.markdown(""" -
-

📂 أرشيف المشاريع

-

قائمة المشاريع المكتملة أو المؤرشفة.

-
- """, unsafe_allow_html=True) - - # تصفية المشاريع المؤرشفة - archived_projects = [p for p in st.session_state.projects if p.get("status") == "archived" or p.get("status") == "مكتمل"] - - if not archived_projects: - st.info("لا توجد مشاريع مؤرشفة حالياً.") - return - - # عرض المشاريع المؤرشفة - for idx, project in enumerate(archived_projects): - with st.expander(f"{project.get('name')} - {project.get('client')}"): - self._render_project_details(project, idx + 1000) # استخدام مؤشر مختلف لتجنب التعارض - - def _render_projects_reports(self): - """عرض تقارير المشاريع والإحصائيات""" - st.markdown(""" -
-

📊 تقارير المشاريع

-

عرض إحصائيات وتقارير عن المشاريع الحالية والسابقة.

-
- """, unsafe_allow_html=True) - - # إذا لم تكن هناك مشاريع - if not st.session_state.projects: - st.info("لا توجد مشاريع لعرض التقارير.") - return - - # إحصائيات عامة - st.markdown("### إحصائيات عامة") - - # حساب الإحصائيات - total_projects = len(st.session_state.projects) - active_projects = len([p for p in st.session_state.projects if p.get("status") not in ["archived", "مكتمل", "ملغي"]]) - completed_projects = len([p for p in st.session_state.projects if p.get("status") in ["مكتمل"]]) - canceled_projects = len([p for p in st.session_state.projects if p.get("status") in ["ملغي"]]) - - # عرض الإحصائيات في صفوف - col1, col2, col3, col4 = st.columns(4) - - with col1: - st.metric("إجمالي المشاريع", total_projects) - - with col2: - st.metric("المشاريع النشطة", active_projects) - - with col3: - st.metric("المشاريع المكتملة", completed_projects) - - with col4: - st.metric("المشاريع الملغاة", canceled_projects) - - # تحليل المشاريع حسب النوع - st.markdown("### توزيع المشاريع حسب النوع") - - # حساب عدد المشاريع حسب النوع - project_types = {} - for project in st.session_state.projects: - project_type = project.get("type", "غير محدد") - if project_type in project_types: - project_types[project_type] += 1 - else: - project_types[project_type] = 1 - - # إنشاء بيانات للرسم البياني - types_df = pd.DataFrame({ - "نوع المشروع": list(project_types.keys()), - "عدد المشاريع": list(project_types.values()) - }) - - # رسم بياني دائري لتوزيع المشاريع حسب النوع - fig = px.pie( - types_df, - values="عدد المشاريع", - names="نوع المشروع", - title="توزيع المشاريع حسب النوع" - ) - st.plotly_chart(fig, use_container_width=True) - - # تحليل المشاريع حسب الحالة - st.markdown("### توزيع المشاريع حسب الحالة") - - # حساب عدد المشاريع حسب الحالة - project_statuses = {} - for project in st.session_state.projects: - status = project.get("status", "غير محدد") - if status in project_statuses: - project_statuses[status] += 1 - else: - project_statuses[status] = 1 - - # إنشاء بيانات للرسم البياني - statuses_df = pd.DataFrame({ - "حالة المشروع": list(project_statuses.keys()), - "عدد المشاريع": list(project_statuses.values()) - }) - - # رسم بياني شريطي لتوزيع المشاريع حسب الحالة - fig2 = px.bar( - statuses_df, - x="حالة المشروع", - y="عدد المشاريع", - title="توزيع المشاريع حسب الحالة", - color="حالة المشروع" - ) - st.plotly_chart(fig2, use_container_width=True) - - # عرض مخاطر المشاريع - st.markdown("### أهم المخاطر في المشاريع الحالية") - - # تجميع المخاطر من جميع المشاريع النشطة - all_risks = [] - for project in st.session_state.projects: - if project.get("status") not in ["archived", "مكتمل", "ملغي"]: - for risk in project.get("risks", []): - all_risks.append({ - "مشروع": project.get("name"), - "مخاطرة": risk.get("title"), - "الحدة": risk.get("severity"), - "الاحتمالية": risk.get("probability", 0) - }) - - if all_risks: - # تحويل المخاطر إلى DataFrame - risks_df = pd.DataFrame(all_risks) - - # ترتيب المخاطر حسب الحدة والاحتمالية - risks_df = risks_df.sort_values(by=["الحدة", "الاحتمالية"], ascending=[False, False]) - - # تلوين الجدول حسب الحدة - def color_severity(val): - if val == "عالي": - return 'background-color: #FFCCCC' - elif val == "متوسط": - return 'background-color: #FFFFCC' - else: - return 'background-color: #CCFFCC' - - # عرض جدول المخاطر - st.dataframe(risks_df.style.applymap(color_severity, subset=["الحدة"]), use_container_width=True) - else: - st.info("لا توجد مخاطر مسجلة في المشاريع النشطة.") - - def _save_project_file(self, project_number, file_type, uploaded_file): - """حفظ ملف مرفق للمشروع وإرجاع المسار""" - try: - # التأكد من وجود المجلد - project_dir = os.path.join(self.projects_dir, project_number) - type_dir = os.path.join(project_dir, file_type) - - os.makedirs(type_dir, exist_ok=True) - - # إنشاء اسم الملف - file_name = f"{int(time.time())}_{uploaded_file.name}" - file_path = os.path.join(type_dir, file_name) - - # حفظ الملف - with open(file_path, "wb") as f: - f.write(uploaded_file.getbuffer()) - - return file_path - except Exception as e: - st.error(f"خطأ في حفظ الملف: {str(e)}") - return None - - -# تشغيل النموذج مباشرة عند استدعاء الملف -def main(): - """تشغيل نموذج إدارة المشاريع بشكل مستقل""" - # تهيئة الواجهة - st.set_page_config( - page_title="إدارة المشاريع | WAHBi AI", - page_icon="🏗️", - layout="wide", - initial_sidebar_state="expanded", - menu_items={ - 'Get Help': 'mailto:support@wahbi-ai.com', - 'Report a bug': 'mailto:support@wahbi-ai.com', - 'About': 'نظام إدارة المشاريع - جزء من نظام WAHBi AI لتحليل المناقصات' - } - ) - - # تهيئة نموذج إدارة المشاريع - projects_management = ProjectsManagement() - - # عرض واجهة إدارة المشاريع - projects_management.render() - -# تشغيل النموذج إذا تم استدعاء الملف مباشرة -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/modules/reports/reports_app.py b/modules/reports/reports_app.py deleted file mode 100644 index 55485214f6d4779aeec220e21326e8f27eccee53..0000000000000000000000000000000000000000 --- a/modules/reports/reports_app.py +++ /dev/null @@ -1,88 +0,0 @@ -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): - pass - - def render(self): - st.markdown("

وحدة التقارير والتحليلات

", unsafe_allow_html=True) - tabs = st.tabs(["لوحة المعلومات", "تقارير المشاريع", "تقارير التسعير", "تقارير المخاطر", "التقارير المخصصة"]) - - with tabs[0]: - self._render_dashboard_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) - - st.markdown("#### توزيع قيم المشاريع") - project_value_data = self._get_project_value_data() - fig = px.bar(project_value_data, x='range', y='count', title='توزيع قيم المشاريع') - st.plotly_chart(fig, use_container_width=True) - - def _get_total_projects(self): - return 10 - - def _get_active_projects(self): - return 7 - - def _get_won_projects(self): - return 4 - - def _get_avg_local_content(self): - return 72.5 - - def _get_project_status_data(self): - return pd.DataFrame({ - 'status': ['جديد', 'قيد التنفيذ', 'تمت الترسية', 'ملغي'], - 'count': [5, 3, 1, 1] - }) - - def _get_monthly_project_data(self): - return pd.DataFrame({ - 'month': ['يناير', 'فبراير', 'مارس'], - 'new': [2, 3, 4], - 'submitted': [1, 2, 3], - 'won': [0, 1, 2] - }) - - def _get_project_value_data(self): - return pd.DataFrame({ - 'range': ['0-500K', '500K-1M', '1M-2M', '2M+'], - 'count': [2, 3, 4, 1] - }) diff --git a/modules/resources/__init__.py b/modules/resources/__init__.py deleted file mode 100644 index 07fc5996aa64a07e030c6fb9e1dac3ebd6f9e368..0000000000000000000000000000000000000000 --- a/modules/resources/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -وحدة إدارة الموارد -""" - -__version__ = '1.0.0' \ No newline at end of file diff --git a/modules/resources/resources_app.py b/modules/resources/resources_app.py deleted file mode 100644 index 17dceb00b915b79891d9bfb28b37fccf87a20fc0..0000000000000000000000000000000000000000 --- a/modules/resources/resources_app.py +++ /dev/null @@ -1,1447 +0,0 @@ -""" -تطبيق وحدة الموارد -""" - -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, timedelta -import time -import io -import os -import tempfile -import random - - -class ResourcesApp: - """وحدة إدارة الموارد""" - - def __init__(self): - """تهيئة وحدة الموارد""" - - # تهيئة البيانات في حالة الجلسة إذا لم تكن موجودة - if 'materials' not in st.session_state: - st.session_state.materials = [ - { - 'id': 1, - 'name': 'خرسانة جاهزة', - 'category': 'مواد إنشائية', - 'unit': 'م3', - 'price': 250, - 'supplier': 'شركة الخرسانة الوطنية', - 'local_content': 95, - 'last_updated': '2024-01-15' - }, - { - 'id': 2, - 'name': 'حديد تسليح', - 'category': 'مواد إنشائية', - 'unit': 'طن', - 'price': 4500, - 'supplier': 'مصنع الحديد السعودي', - 'local_content': 45, - 'last_updated': '2024-02-10' - }, - { - 'id': 3, - 'name': 'بلوك خرساني', - 'category': 'مواد إنشائية', - 'unit': 'م3', - 'price': 350, - 'supplier': 'مصنع البلوك الحديث', - 'local_content': 95, - 'last_updated': '2024-01-20' - }, - { - 'id': 4, - 'name': 'رمل', - 'category': 'مواد إنشائية', - 'unit': 'م3', - 'price': 60, - 'supplier': 'مؤسسة توريدات البناء', - 'local_content': 100, - 'last_updated': '2024-01-15' - }, - { - 'id': 5, - 'name': 'بلاط سيراميك', - 'category': 'مواد تشطيب', - 'unit': 'م2', - 'price': 120, - 'supplier': 'شركة السيراميك الوطنية', - 'local_content': 80, - 'last_updated': '2024-02-05' - } - ] - - if 'labor' not in st.session_state: - st.session_state.labor = [ - { - 'id': 1, - 'name': 'مهندس مدني', - 'category': 'هندسة', - 'unit': 'شهر', - 'price': 15000, - 'supplier': 'داخلي', - 'local_content': 90, - 'last_updated': '2024-01-10' - }, - { - 'id': 2, - 'name': 'مهندس معماري', - 'category': 'هندسة', - 'unit': 'شهر', - 'price': 14000, - 'supplier': 'داخلي', - 'local_content': 85, - 'last_updated': '2024-01-10' - }, - { - 'id': 3, - 'name': 'مساح', - 'category': 'هندسة', - 'unit': 'شهر', - 'price': 8000, - 'supplier': 'داخلي', - 'local_content': 100, - 'last_updated': '2024-01-10' - }, - { - 'id': 4, - 'name': 'فني كهرباء', - 'category': 'فني', - 'unit': 'شهر', - 'price': 7000, - 'supplier': 'داخلي', - 'local_content': 95, - 'last_updated': '2024-01-10' - }, - { - 'id': 5, - 'name': 'عامل بناء', - 'category': 'عمالة', - 'unit': 'يوم', - 'price': 200, - 'supplier': 'شركة توريد عمالة', - 'local_content': 60, - 'last_updated': '2024-01-20' - } - ] - - if 'equipment' not in st.session_state: - st.session_state.equipment = [ - { - 'id': 1, - 'name': 'حفارة كبيرة', - 'category': 'معدات ثقيلة', - 'unit': 'يوم', - 'price': 2500, - 'supplier': 'شركة المعدات الثقيلة', - 'local_content': 70, - 'last_updated': '2024-01-15' - }, - { - 'id': 2, - 'name': 'خلاطة خرسانة', - 'category': 'معدات إنشائية', - 'unit': 'يوم', - 'price': 1800, - 'supplier': 'مؤسسة معدات البناء', - 'local_content': 65, - 'last_updated': '2024-01-20' - }, - { - 'id': 3, - 'name': 'رافعة برجية', - 'category': 'معدات ثقيلة', - 'unit': 'شهر', - 'price': 45000, - 'supplier': 'شركة المعدات الثقيلة', - 'local_content': 50, - 'last_updated': '2024-02-05' - }, - { - 'id': 4, - 'name': 'مولد كهربائي', - 'category': 'معدات مساندة', - 'unit': 'شهر', - 'price': 12000, - 'supplier': 'شركة المعدات الكهربائية', - 'local_content': 75, - 'last_updated': '2024-01-25' - }, - { - 'id': 5, - 'name': 'سقالات معدنية', - 'category': 'معدات مساندة', - 'unit': 'م2/شهر', - 'price': 50, - 'supplier': 'مؤسسة معدات البناء', - 'local_content': 90, - 'last_updated': '2024-01-15' - } - ] - - if 'subcontractors' not in st.session_state: - st.session_state.subcontractors = [ - { - 'id': 1, - 'name': 'مؤسسة الإنشاءات المتكاملة', - 'category': 'أعمال إنشائية', - 'specialization': 'تنفيذ الهيكل الخرساني', - 'rating': 4.8, - 'city': 'الرياض', - 'contact_person': 'محمد العتيبي', - 'phone': '0555555555', - 'email': 'info@constructionfirm.sa', - 'local_content': 85, - 'last_updated': '2024-01-15' - }, - { - 'id': 2, - 'name': 'شركة التكييف والتبريد', - 'category': 'أعمال كهروميكانيكية', - 'specialization': 'تركيب أنظمة التكييف والتبريد', - 'rating': 4.5, - 'city': 'جدة', - 'contact_person': 'أحمد الغامدي', - 'phone': '0566666666', - 'email': 'info@acfirm.sa', - 'local_content': 75, - 'last_updated': '2024-01-20' - }, - { - 'id': 3, - 'name': 'مؤسسة الكهرباء الحديثة', - 'category': 'أعمال كهروميكانيكية', - 'specialization': 'تنفيذ الأعمال الكهربائية', - 'rating': 4.6, - 'city': 'الرياض', - 'contact_person': 'فهد السويلم', - 'phone': '0577777777', - 'email': 'info@electricfirm.sa', - 'local_content': 90, - 'last_updated': '2024-02-05' - }, - { - 'id': 4, - 'name': 'شركة المقاولات المتخصصة', - 'category': 'أعمال تشطيبات', - 'specialization': 'تنفيذ أعمال التشطيبات الداخلية', - 'rating': 4.7, - 'city': 'الدمام', - 'contact_person': 'خالد الدوسري', - 'phone': '0588888888', - 'email': 'info@specializedcontractor.sa', - 'local_content': 80, - 'last_updated': '2024-01-25' - }, - { - 'id': 5, - 'name': 'مؤسسة الصيانة والتشغيل', - 'category': 'أعمال صيانة', - 'specialization': 'صيانة وتشغيل المباني', - 'rating': 4.4, - 'city': 'الرياض', - 'contact_person': 'عبدالله العنزي', - 'phone': '0599999999', - 'email': 'info@maintenancefirm.sa', - 'local_content': 95, - 'last_updated': '2024-02-10' - } - ] - - if 'price_history' not in st.session_state: - st.session_state.price_history = [ - # تاريخ أسعار الخرسانة الجاهزة - *[{'material_id': 1, 'date': (datetime.now() - timedelta(days=i*30)).strftime('%Y-%m-%d'), 'price': 250 - (i * 5) if i < 3 else 250 - 15 + (i - 2) * 10} for i in range(12)], - - # تاريخ أسعار حديد التسليح - *[{'material_id': 2, 'date': (datetime.now() - timedelta(days=i*30)).strftime('%Y-%m-%d'), 'price': 4500 - (i * 100) if i < 4 else 4500 - 400 + (i - 3) * 150} for i in range(12)], - - # تاريخ أسعار البلوك الخرساني - *[{'material_id': 3, 'date': (datetime.now() - timedelta(days=i*30)).strftime('%Y-%m-%d'), 'price': 350 - (i * 10) if i < 6 else 350 - 60 + (i - 5) * 15} for i in range(12)] - ] - - def render(self): - """عرض واجهة وحدة الموارد""" - - st.markdown("

وحدة إدارة الموارد

", unsafe_allow_html=True) - - tabs = st.tabs([ - "لوحة المعلومات", - "المواد", - "العمالة", - "المعدات", - "المقاولين من الباطن", - "تحليل الأسعار" - ]) - - with tabs[0]: - self._render_dashboard_tab() - - with tabs[1]: - self._render_materials_tab() - - with tabs[2]: - self._render_labor_tab() - - with tabs[3]: - self._render_equipment_tab() - - with tabs[4]: - self._render_subcontractors_tab() - - with tabs[5]: - self._render_price_analysis_tab() - - def _render_dashboard_tab(self): - """عرض تبويب لوحة المعلومات""" - - st.markdown("### لوحة معلومات إدارة الموارد") - - # عرض مؤشرات الأداء الرئيسية - col1, col2, col3, col4 = st.columns(4) - - with col1: - total_materials = len(st.session_state.materials) - st.metric("عدد المواد", total_materials) - - with col2: - total_labor = len(st.session_state.labor) - st.metric("عدد موارد العمالة", total_labor) - - with col3: - total_equipment = len(st.session_state.equipment) - st.metric("عدد المعدات", total_equipment) - - with col4: - total_subcontractors = len(st.session_state.subcontractors) - st.metric("عدد المقاولين من الباطن", total_subcontractors) - - # رسم بياني لتوزيع المحتوى المحلي - st.markdown("### المحتوى المحلي للموارد") - - # إعداد البيانات - local_content_data = [] - - # إضافة بيانات المواد - for material in st.session_state.materials: - local_content_data.append({ - 'النوع': 'المواد', - 'اسم المورد': material['name'], - 'نسبة المحتوى المحلي': material['local_content'] - }) - - # إضافة بيانات العمالة - for labor in st.session_state.labor: - local_content_data.append({ - 'النوع': 'العمالة', - 'اسم المورد': labor['name'], - 'نسبة المحتوى المحلي': labor['local_content'] - }) - - # إضافة بيانات المعدات - for equipment in st.session_state.equipment: - local_content_data.append({ - 'النوع': 'المعدات', - 'اسم المورد': equipment['name'], - 'نسبة المحتوى المحلي': equipment['local_content'] - }) - - # إضافة بيانات المقاولين من الباطن - for subcontractor in st.session_state.subcontractors: - local_content_data.append({ - 'النوع': 'المقاولين من الباطن', - 'اسم المورد': subcontractor['name'], - 'نسبة المحتوى المحلي': subcontractor['local_content'] - }) - - # تحويل البيانات إلى DataFrame - local_content_df = pd.DataFrame(local_content_data) - - # حساب متوسط المحتوى المحلي لكل نوع - avg_local_content = local_content_df.groupby('النوع')['نسبة المحتوى المحلي'].mean().reset_index() - - # رسم المخطط الشريطي - fig = px.bar( - avg_local_content, - x='النوع', - y='نسبة المحتوى المحلي', - title='متوسط نسبة المحتوى المحلي حسب نوع المورد', - color='النوع', - text_auto='.1f' - ) - - fig.update_traces(texttemplate='%{text}%', textposition='outside') - - fig.add_shape( - type="line", - x0=-0.5, - x1=len(avg_local_content) - 0.5, - y0=70, # النسبة المستهدفة - y1=70, - line=dict(color="red", width=2, dash="dash"), - name="النسبة المستهدفة" - ) - - fig.add_annotation( - x=1, - y=75, - text=f"النسبة المستهدفة (70%)", - showarrow=False, - font=dict(color="red") - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض تنبيهات الموارد - st.markdown("### تنبيهات الموارد") - - # محاكاة تنبيهات الموارد - alerts = [ - { - "type": "تغير في الأسعار", - "resource": "حديد تسليح", - "message": "ارتفاع في سعر الحديد بنسبة 5% في الأسبوع الماضي", - "date": "2024-03-15", - "severity": "متوسطة" - }, - { - "type": "نقص في المخزون", - "resource": "بلاط سيراميك", - "message": "انخفاض مخزون السيراميك إلى أقل من 20% من المستوى المطلوب", - "date": "2024-03-18", - "severity": "عالية" - }, - { - "type": "انتهاء صلاحية عقود", - "resource": "مؤسسة الإنشاءات المتكاملة", - "message": "سينتهي العقد مع المقاول خلال 30 يوماً", - "date": "2024-03-10", - "severity": "منخفضة" - }, - { - "type": "تغير في المحتوى المحلي", - "resource": "شركة التكييف والتبريد", - "message": "انخفاض نسبة المحتوى المحلي إلى أقل من النسبة المستهدفة", - "date": "2024-03-12", - "severity": "متوسطة" - } - ] - - # عرض التنبيهات - for alert in alerts: - if alert["severity"] == "عالية": - st.error(f"**{alert['type']}**: {alert['message']} - *{alert['resource']}* ({alert['date']})") - elif alert["severity"] == "متوسطة": - st.warning(f"**{alert['type']}**: {alert['message']} - *{alert['resource']}* ({alert['date']})") - else: - st.info(f"**{alert['type']}**: {alert['message']} - *{alert['resource']}* ({alert['date']})") - - # عرض نظرة عامة على الأسعار - st.markdown("### نظرة عامة على تطور الأسعار") - - # إعداد البيانات - price_history_data = [] - material_names = {material['id']: material['name'] for material in st.session_state.materials} - - for entry in st.session_state.price_history: - material_id = entry['material_id'] - if material_id in material_names: - price_history_data.append({ - 'المادة': material_names[material_id], - 'التاريخ': pd.to_datetime(entry['date']), - 'السعر': entry['price'] - }) - - # تحويل البيانات إلى DataFrame - price_history_df = pd.DataFrame(price_history_data) - - # التحقق من وجود بيانات قبل رسم المخطط - if len(price_history_data) == 0: - st.warning("لا توجد بيانات تاريخية للأسعار متاحة لعرضها") - else: - # رسم المخطط الخطي - fig = px.line( - price_history_df, - x='التاريخ', - y='السعر', - color='المادة', - title='تطور أسعار المواد الرئيسية خلال العام الماضي', - labels={'التاريخ': 'التاريخ', 'السعر': 'السعر (ريال)', 'المادة': 'المادة'} - ) - # عرض المخطط فقط إذا تم إنشاؤه - st.plotly_chart(fig, use_container_width=True) - - def _render_materials_tab(self): - """عرض تبويب المواد""" - - st.markdown("### إدارة المواد") - - # عرض أدوات البحث والتصفية - col1, col2 = st.columns(2) - - with col1: - search_query = st.text_input("بحث في المواد", placeholder="ابحث باسم المادة أو الفئة أو المورد...") - - with col2: - category_filter = st.multiselect( - "تصفية حسب الفئة", - options=list(set(material['category'] for material in st.session_state.materials)), - default=[], - key="material_category_filter_tab" - ) - - # تطبيق البحث والتصفية - filtered_materials = st.session_state.materials - - if search_query: - filtered_materials = [ - material for material in filtered_materials - if (search_query.lower() in material['name'].lower() or - search_query.lower() in material['category'].lower() or - search_query.lower() in material['supplier'].lower()) - ] - - if category_filter: - filtered_materials = [material for material in filtered_materials if material['category'] in category_filter] - - # زر إضافة مادة جديدة - if st.button("إضافة مادة جديدة"): - st.session_state.show_material_form = True - - # نموذج إضافة مادة جديدة - if st.session_state.get('show_material_form', False): - with st.form("add_material_form"): - st.markdown("#### إضافة مادة جديدة") - - col1, col2 = st.columns(2) - - with col1: - new_material_name = st.text_input("اسم المادة", key="new_material_name") - new_material_category = st.text_input("الفئة", key="new_material_category") - new_material_unit = st.text_input("وحدة القياس", key="new_material_unit") - - with col2: - new_material_price = st.number_input("السعر (ريال)", min_value=0.0, key="new_material_price") - new_material_supplier = st.text_input("المورد", key="new_material_supplier") - new_material_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_material_local_content") - - submitted = st.form_submit_button("إضافة المادة") - cancel = st.form_submit_button("إلغاء") - - if submitted and new_material_name and new_material_category and new_material_unit: - # إضافة المادة الجديدة - new_material = { - 'id': max([material['id'] for material in st.session_state.materials], default=0) + 1, - 'name': new_material_name, - 'category': new_material_category, - 'unit': new_material_unit, - 'price': new_material_price, - 'supplier': new_material_supplier, - 'local_content': new_material_local_content, - 'last_updated': datetime.now().strftime('%Y-%m-%d') - } - - st.session_state.materials.append(new_material) - st.success(f"تمت إضافة المادة '{new_material_name}' بنجاح!") - st.session_state.show_material_form = False - st.rerun() - - if cancel: - st.session_state.show_material_form = False - st.rerun() - - # عرض قائمة المواد - if filtered_materials: - # تحويل البيانات إلى DataFrame - materials_df = pd.DataFrame(filtered_materials) - - # تنسيق البيانات للعرض - display_df = materials_df.copy() - display_df['price'] = display_df['price'].apply(lambda x: f"{x:,.2f} ريال") - display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%") - - # تغيير أسماء الأعمدة للعرض - display_df.columns = [ - 'معرف', 'اسم المادة', 'الفئة', 'وحدة القياس', 'السعر', 'المورد', 'نسبة المحتوى المحلي', 'آخر تحديث' - ] - - # عرض الجدول - st.dataframe(display_df, use_container_width=True, hide_index=True) - - # عرض ملخص إحصائي - st.markdown("#### ملخص إحصائي للمواد") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي عدد المواد", len(filtered_materials)) - - with col2: - avg_price = sum(material['price'] for material in filtered_materials) / len(filtered_materials) - st.metric("متوسط سعر المواد", f"{avg_price:,.2f} ريال") - - with col3: - avg_local_content = sum(material['local_content'] for material in filtered_materials) / len(filtered_materials) - st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%") - - # عرض مخطط توزيع المواد حسب الفئة - category_counts = materials_df.groupby('category').size().reset_index(name='count') - - fig = px.pie( - category_counts, - names='category', - values='count', - title='توزيع المواد حسب الفئة' - ) - - st.plotly_chart(fig, use_container_width=True) - else: - st.warning("لا توجد مواد مطابقة لمعايير البحث.") - - def _render_labor_tab(self): - """عرض تبويب العمالة""" - - st.markdown("### إدارة العمالة") - - # عرض أدوات البحث والتصفية - col1, col2 = st.columns(2) - - with col1: - search_query = st.text_input("بحث في العمالة", placeholder="ابحث باسم العامل أو الفئة أو المورد...") - - with col2: - category_filter = st.multiselect( - "تصفية حسب الفئة", - options=list(set(labor['category'] for labor in st.session_state.labor)), - default=[], - key="labor_category_filter_tab" - ) - - # تطبيق البحث والتصفية - filtered_labor = st.session_state.labor - - if search_query: - filtered_labor = [ - labor for labor in filtered_labor - if (search_query.lower() in labor['name'].lower() or - search_query.lower() in labor['category'].lower() or - search_query.lower() in labor['supplier'].lower()) - ] - - if category_filter: - filtered_labor = [labor for labor in filtered_labor if labor['category'] in category_filter] - - # زر إضافة عامل جديد - if st.button("إضافة عامل جديد"): - st.session_state.show_labor_form = True - - # نموذج إضافة عامل جديد - if st.session_state.get('show_labor_form', False): - with st.form("add_labor_form"): - st.markdown("#### إضافة عامل جديد") - - col1, col2 = st.columns(2) - - with col1: - new_labor_name = st.text_input("اسم العامل", key="new_labor_name") - new_labor_category = st.text_input("الفئة", key="new_labor_category") - new_labor_unit = st.text_input("وحدة القياس", key="new_labor_unit") - - with col2: - new_labor_price = st.number_input("السعر (ريال)", min_value=0.0, key="new_labor_price") - new_labor_supplier = st.text_input("المورد", key="new_labor_supplier") - new_labor_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_labor_local_content") - - submitted = st.form_submit_button("إضافة العامل") - cancel = st.form_submit_button("إلغاء") - - if submitted and new_labor_name and new_labor_category and new_labor_unit: - # إضافة العامل الجديد - new_labor = { - 'id': max([labor['id'] for labor in st.session_state.labor], default=0) + 1, - 'name': new_labor_name, - 'category': new_labor_category, - 'unit': new_labor_unit, - 'price': new_labor_price, - 'supplier': new_labor_supplier, - 'local_content': new_labor_local_content, - 'last_updated': datetime.now().strftime('%Y-%m-%d') - } - - st.session_state.labor.append(new_labor) - st.success(f"تمت إضافة العامل '{new_labor_name}' بنجاح!") - st.session_state.show_labor_form = False - st.rerun() - - if cancel: - st.session_state.show_labor_form = False - st.rerun() - - # عرض قائمة العمالة - if filtered_labor: - # تحويل البيانات إلى DataFrame - labor_df = pd.DataFrame(filtered_labor) - - # تنسيق البيانات للعرض - display_df = labor_df.copy() - display_df['price'] = display_df['price'].apply(lambda x: f"{x:,.2f} ريال") - display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%") - - # تغيير أسماء الأعمدة للعرض - display_df.columns = [ - 'معرف', 'اسم العامل', 'الفئة', 'وحدة القياس', 'السعر', 'المورد', 'نسبة المحتوى المحلي', 'آخر تحديث' - ] - - # عرض الجدول - st.dataframe(display_df, use_container_width=True, hide_index=True) - - # عرض ملخص إحصائي - st.markdown("#### ملخص إحصائي للعمالة") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي عدد العمالة", len(filtered_labor)) - - with col2: - avg_price = sum(labor['price'] for labor in filtered_labor) / len(filtered_labor) - st.metric("متوسط سعر العمالة", f"{avg_price:,.2f} ريال") - - with col3: - avg_local_content = sum(labor['local_content'] for labor in filtered_labor) / len(filtered_labor) - st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%") - - # عرض مخطط توزيع العمالة حسب الفئة - category_counts = labor_df.groupby('category').size().reset_index(name='count') - - fig = px.pie( - category_counts, - names='category', - values='count', - title='توزيع العمالة حسب الفئة' - ) - - st.plotly_chart(fig, use_container_width=True) - else: - st.warning("لا توجد عمالة مطابقة لمعايير البحث.") - - def _render_equipment_tab(self): - """عرض تبويب المعدات""" - - st.markdown("### إدارة المعدات") - - # عرض أدوات البحث والتصفية - col1, col2 = st.columns(2) - - with col1: - search_query = st.text_input("بحث في المعدات", placeholder="ابحث باسم المعدة أو الفئة أو المورد...") - - with col2: - category_filter = st.multiselect( - "تصفية حسب الفئة", - options=list(set(equipment['category'] for equipment in st.session_state.equipment)), - default=[], - key="equipment_category_filter_tab" - ) - - # تطبيق البحث والتصفية - filtered_equipment = st.session_state.equipment - - if search_query: - filtered_equipment = [ - equipment for equipment in filtered_equipment - if (search_query.lower() in equipment['name'].lower() or - search_query.lower() in equipment['category'].lower() or - search_query.lower() in equipment['supplier'].lower()) - ] - - if category_filter: - filtered_equipment = [equipment for equipment in filtered_equipment if equipment['category'] in category_filter] - - # زر إضافة معدة جديدة - if st.button("إضافة معدة جديدة"): - st.session_state.show_equipment_form = True - - # نموذج إضافة معدة جديدة - if st.session_state.get('show_equipment_form', False): - with st.form("add_equipment_form"): - st.markdown("#### إضافة معدة جديدة") - - col1, col2 = st.columns(2) - - with col1: - new_equipment_name = st.text_input("اسم المعدة", key="new_equipment_name") - new_equipment_category = st.text_input("الفئة", key="new_equipment_category") - new_equipment_unit = st.text_input("وحدة القياس", key="new_equipment_unit") - - with col2: - new_equipment_price = st.number_input("السعر (ريال)", min_value=0.0, key="new_equipment_price") - new_equipment_supplier = st.text_input("المورد", key="new_equipment_supplier") - new_equipment_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_equipment_local_content") - - submitted = st.form_submit_button("إضافة المعدة") - cancel = st.form_submit_button("إلغاء") - - if submitted and new_equipment_name and new_equipment_category and new_equipment_unit: - # إضافة المعدة الجديدة - new_equipment = { - 'id': max([equipment['id'] for equipment in st.session_state.equipment], default=0) + 1, - 'name': new_equipment_name, - 'category': new_equipment_category, - 'unit': new_equipment_unit, - 'price': new_equipment_price, - 'supplier': new_equipment_supplier, - 'local_content': new_equipment_local_content, - 'last_updated': datetime.now().strftime('%Y-%m-%d') - } - - st.session_state.equipment.append(new_equipment) - st.success(f"تمت إضافة المعدة '{new_equipment_name}' بنجاح!") - st.session_state.show_equipment_form = False - st.rerun() - - if cancel: - st.session_state.show_equipment_form = False - st.rerun() - - # عرض قائمة المعدات - if filtered_equipment: - # تحويل البيانات إلى DataFrame - equipment_df = pd.DataFrame(filtered_equipment) - - # تنسيق البيانات للعرض - display_df = equipment_df.copy() - display_df['price'] = display_df['price'].apply(lambda x: f"{x:,.2f} ريال") - display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%") - - # تغيير أسماء الأعمدة للعرض - display_df.columns = [ - 'معرف', 'اسم المعدة', 'الفئة', 'وحدة القياس', 'السعر', 'المورد', 'نسبة المحتوى المحلي', 'آخر تحديث' - ] - - # عرض الجدول - st.dataframe(display_df, use_container_width=True, hide_index=True) - - # عرض ملخص إحصائي - st.markdown("#### ملخص إحصائي للمعدات") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي عدد المعدات", len(filtered_equipment)) - - with col2: - avg_price = sum(equipment['price'] for equipment in filtered_equipment) / len(filtered_equipment) - st.metric("متوسط سعر المعدات", f"{avg_price:,.2f} ريال") - - with col3: - avg_local_content = sum(equipment['local_content'] for equipment in filtered_equipment) / len(filtered_equipment) - st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%") - - # عرض مخطط توزيع المعدات حسب الفئة - category_counts = equipment_df.groupby('category').size().reset_index(name='count') - - fig = px.bar( - category_counts, - x='category', - y='count', - title='توزيع المعدات حسب الفئة', - color='category', - labels={'category': 'الفئة', 'count': 'العدد'} - ) - - st.plotly_chart(fig, use_container_width=True) - else: - st.warning("لا توجد معدات مطابقة لمعايير البحث.") - - def _render_subcontractors_tab(self): - """عرض تبويب المقاولين من الباطن""" - - st.markdown("### إدارة المقاولين من الباطن") - - # عرض أدوات البحث والتصفية - col1, col2, col3 = st.columns(3) - - with col1: - search_query = st.text_input("بحث في المقاولين", placeholder="ابحث باسم المقاول أو التخصص...") - - with col2: - category_filter = st.multiselect( - "تصفية حسب الفئة", - options=list(set(subcontractor['category'] for subcontractor in st.session_state.subcontractors)), - default=[], - key="subcontractor_category_filter_tab" - ) - - with col3: - city_filter = st.multiselect( - "تصفية حسب المدينة", - options=list(set(subcontractor['city'] for subcontractor in st.session_state.subcontractors)), - default=[], - key="subcontractor_city_filter_tab" - ) - - # تطبيق البحث والتصفية - filtered_subcontractors = st.session_state.subcontractors - - if search_query: - filtered_subcontractors = [ - subcontractor for subcontractor in filtered_subcontractors - if (search_query.lower() in subcontractor['name'].lower() or - search_query.lower() in subcontractor['specialization'].lower()) - ] - - if category_filter: - filtered_subcontractors = [subcontractor for subcontractor in filtered_subcontractors if subcontractor['category'] in category_filter] - - if city_filter: - filtered_subcontractors = [subcontractor for subcontractor in filtered_subcontractors if subcontractor['city'] in city_filter] - - # زر إضافة مقاول جديد - if st.button("إضافة مقاول جديد"): - st.session_state.show_subcontractor_form = True - - # نموذج إضافة مقاول جديد - if st.session_state.get('show_subcontractor_form', False): - with st.form("add_subcontractor_form"): - st.markdown("#### إضافة مقاول جديد") - - col1, col2 = st.columns(2) - - with col1: - new_subcontractor_name = st.text_input("اسم المقاول", key="new_subcontractor_name") - new_subcontractor_category = st.text_input("الفئة", key="new_subcontractor_category") - new_subcontractor_specialization = st.text_input("التخصص", key="new_subcontractor_specialization") - new_subcontractor_city = st.text_input("المدينة", key="new_subcontractor_city") - - with col2: - new_subcontractor_contact = st.text_input("جهة الاتصال", key="new_subcontractor_contact") - new_subcontractor_phone = st.text_input("رقم الهاتف", key="new_subcontractor_phone") - new_subcontractor_email = st.text_input("البريد الإلكتروني", key="new_subcontractor_email") - new_subcontractor_rating = st.slider("التقييم", 1.0, 5.0, 3.0, 0.1, key="new_subcontractor_rating") - new_subcontractor_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_subcontractor_local_content") - - submitted = st.form_submit_button("إضافة المقاول") - cancel = st.form_submit_button("إلغاء") - - if submitted and new_subcontractor_name and new_subcontractor_category and new_subcontractor_specialization: - # إضافة المقاول الجديد - new_subcontractor = { - 'id': max([subcontractor['id'] for subcontractor in st.session_state.subcontractors], default=0) + 1, - 'name': new_subcontractor_name, - 'category': new_subcontractor_category, - 'specialization': new_subcontractor_specialization, - 'rating': new_subcontractor_rating, - 'city': new_subcontractor_city, - 'contact_person': new_subcontractor_contact, - 'phone': new_subcontractor_phone, - 'email': new_subcontractor_email, - 'local_content': new_subcontractor_local_content, - 'last_updated': datetime.now().strftime('%Y-%m-%d') - } - - st.session_state.subcontractors.append(new_subcontractor) - st.success(f"تمت إضافة المقاول '{new_subcontractor_name}' بنجاح!") - st.session_state.show_subcontractor_form = False - st.rerun() - - if cancel: - st.session_state.show_subcontractor_form = False - st.rerun() - - # عرض قائمة المقاولين - if filtered_subcontractors: - # تحويل البيانات إلى DataFrame - subcontractors_df = pd.DataFrame(filtered_subcontractors) - - # تنسيق البيانات للعرض - display_df = subcontractors_df.copy() - display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%") - - # تغيير أسماء الأعمدة للعرض - display_df.columns = [ - 'معرف', 'اسم المقاول', 'الفئة', 'التخصص', 'التقييم', 'المدينة', - 'جهة الاتصال', 'رقم الهاتف', 'البريد الإلكتروني', 'نسبة المحتوى المحلي', 'آخر تحديث' - ] - - # عرض الجدول - st.dataframe(display_df, use_container_width=True, hide_index=True) - - # عرض ملخص إحصائي - st.markdown("#### ملخص إحصائي للمقاولين") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي عدد المقاولين", len(filtered_subcontractors)) - - with col2: - avg_rating = sum(subcontractor['rating'] for subcontractor in filtered_subcontractors) / len(filtered_subcontractors) - st.metric("متوسط التقييم", f"{avg_rating:.1f}/5.0") - - with col3: - avg_local_content = sum(subcontractor['local_content'] for subcontractor in filtered_subcontractors) / len(filtered_subcontractors) - st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%") - - # عرض مخطط توزيع المقاولين حسب الفئة - category_counts = subcontractors_df.groupby('category').size().reset_index(name='count') - - fig = px.pie( - category_counts, - names='category', - values='count', - title='توزيع المقاولين حسب الفئة', - hole=0.4 - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض مخطط توزيع المقاولين حسب المدينة - city_counts = subcontractors_df.groupby('city').size().reset_index(name='count') - - fig = px.bar( - city_counts, - x='city', - y='count', - title='توزيع المقاولين حسب المدينة', - color='city', - labels={'city': 'المدينة', 'count': 'العدد'} - ) - - st.plotly_chart(fig, use_container_width=True) - else: - st.warning("لا يوجد مقاولين مطابقين لمعايير البحث.") - - def _render_price_analysis_tab(self): - """عرض تبويب تحليل الأسعار""" - - st.markdown("### تحليل الأسعار") - - # اختيار نوع التحليل - analysis_type = st.radio( - "نوع التحليل", - ["تحليل أسعار المواد", "مقارنة الأسعار", "توقع الأسعار المستقبلية"], - horizontal=True - ) - - if analysis_type == "تحليل أسعار المواد": - self._render_material_price_analysis() - elif analysis_type == "مقارنة الأسعار": - self._render_price_comparison() - else: - self._render_price_forecast() - - def _render_material_price_analysis(self): - """عرض تحليل أسعار المواد""" - - st.markdown("#### تحليل أسعار المواد") - - # اختيار المواد للتحليل - material_options = [material['name'] for material in st.session_state.materials] - selected_materials = st.multiselect( - "اختر المواد للتحليل", - options=material_options, - default=material_options[:3] if len(material_options) >= 3 else material_options, - key="price_analysis_materials_tab" - ) - - if not selected_materials: - st.warning("الرجاء اختيار مادة واحدة على الأقل للتحليل.") - return - - # إعداد البيانات للتحليل - material_ids = {material['name']: material['id'] for material in st.session_state.materials} - selected_ids = [material_ids[name] for name in selected_materials if name in material_ids] - - # التحقق من وجود بيانات سعرية في session_state.price_history - if 'price_history' not in st.session_state or not st.session_state.price_history: - st.warning("لا توجد بيانات أسعار متاحة للتحليل.") - return - - price_history_data = [] - for entry in st.session_state.price_history: - if entry['material_id'] in selected_ids: - # الحصول على اسم المادة من المعرف - material_name = next((material['name'] for material in st.session_state.materials if material['id'] == entry['material_id']), "") - - # التحقق من وجود المفاتيح المطلوبة - if 'date' in entry and 'price' in entry: - try: - # إضافة البيانات إلى قائمة البيانات مع تحويل التاريخ إلى كائن datetime - price_history_data.append({ - 'material': material_name, # استخدام أسماء إنجليزية للمفاتيح - 'date': pd.to_datetime(entry['date']), - 'price': float(entry['price']) # التأكد من تحويل السعر إلى رقم - }) - except (ValueError, TypeError) as e: - # تسجيل أخطاء تحويل البيانات - st.error(f"خطأ في معالجة البيانات: {e}") - continue - - if not price_history_data: - st.warning("لا توجد بيانات أسعار متاحة للمواد المختارة.") - return - - # تحويل البيانات إلى DataFrame - price_history_df = pd.DataFrame(price_history_data) - - # التحقق من وجود بيانات قبل رسم المخطط - if len(price_history_df) == 0: - st.warning("لا توجد بيانات تاريخية للأسعار متاحة لعرضها") - else: - # عرض المخطط الخطي للأسعار باستخدام أسماء الأعمدة الإنجليزية - fig = px.line( - price_history_df, - x='date', - y='price', - color='material', - title='تطور أسعار المواد المختارة', - labels={'date': 'التاريخ', 'price': 'السعر (ريال)', 'material': 'المادة'} - ) - - st.plotly_chart(fig, use_container_width=True) - - # حساب التغيرات في الأسعار - materials_price_changes = [] - - for material_name in selected_materials: - # استخدام أسماء الأعمدة الإنجليزية للتصفية والترتيب - material_prices = price_history_df[price_history_df['material'] == material_name].sort_values('date') - - if len(material_prices) >= 2: - first_price = material_prices.iloc[0]['price'] - last_price = material_prices.iloc[-1]['price'] - price_change = last_price - first_price - price_change_percent = (price_change / first_price) * 100 - - # حساب التقلب (الانحراف المعياري) - price_volatility = material_prices['price'].std() - - materials_price_changes.append({ - 'المادة': material_name, - 'السعر الأول': first_price, - 'السعر الأخير': last_price, - 'التغير المطلق': price_change, - 'نسبة التغير (%)': price_change_percent, - 'التقلب (الانحراف المعياري)': price_volatility - }) - - # عرض جدول التغيرات في الأسعار - if materials_price_changes: - st.markdown("#### تغيرات الأسعار خلال الفترة") - - changes_df = pd.DataFrame(materials_price_changes) - - # تنسيق البيانات للعرض - display_df = changes_df.copy() - display_df['السعر الأول'] = display_df['السعر الأول'].apply(lambda x: f"{x:,.2f} ريال") - display_df['السعر الأخير'] = display_df['السعر الأخير'].apply(lambda x: f"{x:,.2f} ريال") - display_df['التغير المطلق'] = display_df['التغير المطلق'].apply(lambda x: f"{x:,.2f} ريال") - display_df['نسبة التغير (%)'] = display_df['نسبة التغير (%)'].apply(lambda x: f"{x:.2f}%") - display_df['التقلب (الانحراف المعياري)'] = display_df['التقلب (الانحراف المعياري)'].apply(lambda x: f"{x:.2f}") - - st.dataframe(display_df, use_container_width=True, hide_index=True) - - # عرض مخطط شريطي للتغيرات في الأسعار - fig = px.bar( - changes_df, - x='المادة', - y='نسبة التغير (%)', - title='نسبة التغير في الأسعار', - color='المادة', - text_auto='.1f' - ) - - fig.update_traces(texttemplate='%{text}%', textposition='outside') - - st.plotly_chart(fig, use_container_width=True) - - def _render_price_comparison(self): - """عرض مقارنة الأسعار""" - - st.markdown("#### مقارنة الأسعار") - - # اختيار نوع المورد للمقارنة - resource_type = st.selectbox( - "نوع المورد", - ["المواد", "العمالة", "المعدات"] - ) - - if resource_type == "المواد": - resources = st.session_state.materials - elif resource_type == "العمالة": - resources = st.session_state.labor - else: - resources = st.session_state.equipment - - # اختيار الفئة للمقارنة - categories = list(set([resource['category'] for resource in resources])) - selected_category = st.selectbox( - "الفئة", - options=["الكل"] + categories - ) - - # فلترة الموارد حسب الفئة - if selected_category != "الكل": - filtered_resources = [resource for resource in resources if resource['category'] == selected_category] - else: - filtered_resources = resources - - if not filtered_resources: - st.warning("لا توجد موارد مطابقة للفئة المختارة.") - return - - # إعداد بيانات المقارنة - comparison_data = [] - - for resource in filtered_resources: - comparison_data.append({ - 'الاسم': resource['name'], - 'الفئة': resource['category'], - 'الوحدة': resource['unit'], - 'السعر': resource['price'], - 'المورد': resource['supplier'], - 'نسبة المحتوى المحلي': resource['local_content'] - }) - - # تحويل البيانات إلى DataFrame - comparison_df = pd.DataFrame(comparison_data) - - # عرض المخطط الشريطي للأسعار - fig = px.bar( - comparison_df, - x='الاسم', - y='السعر', - title=f'مقارنة أسعار {resource_type}', - color='الفئة' if selected_category == "الكل" else 'المورد', - text_auto='.2s', - labels={'السعر': 'السعر (ريال)'} - ) - - fig.update_traces(texttemplate='%{text} ريال', textposition='outside') - - st.plotly_chart(fig, use_container_width=True) - - # عرض العلاقة بين السعر ونسبة المحتوى المحلي - fig = px.scatter( - comparison_df, - x='نسبة المحتوى المحلي', - y='السعر', - color='الفئة' if selected_category == "الكل" else None, - title='العلاقة بين السعر ونسبة المحتوى المحلي', - labels={'نسبة المحتوى المحلي': 'نسبة المحتوى المحلي (%)', 'السعر': 'السعر (ريال)'}, - size=[50] * len(comparison_df), - text='الاسم' - ) - - fig.update_traces(textposition='top center') - - st.plotly_chart(fig, use_container_width=True) - - # عرض جدول المقارنة - st.markdown("#### جدول مقارنة الأسعار") - - # تنسيق البيانات للعرض - display_df = comparison_df.copy() - display_df['السعر'] = display_df['السعر'].apply(lambda x: f"{x:,.2f} ريال") - display_df['نسبة المحتوى المحلي'] = display_df['نسبة المحتوى المحلي'].apply(lambda x: f"{x}%") - - st.dataframe(display_df, use_container_width=True, hide_index=True) - - def _render_price_forecast(self): - """عرض توقع الأسعار المستقبلية""" - - st.markdown("#### توقع الأسعار المستقبلية") - - # اختيار المادة للتوقع - material_options = [material['name'] for material in st.session_state.materials] - selected_material = st.selectbox( - "اختر المادة للتوقع", - options=material_options - ) - - # اختيار فترة التوقع - forecast_period = st.slider( - "فترة التوقع (أشهر)", - min_value=1, - max_value=12, - value=6 - ) - - if not selected_material: - st.warning("الرجاء اختيار مادة للتوقع.") - return - - # الحصول على معرف المادة - material_id = next((material['id'] for material in st.session_state.materials if material['name'] == selected_material), None) - - if material_id is None: - st.error("المادة المحددة غير موجودة.") - return - - # الحصول على بيانات الأسعار التاريخية - price_history_data = [] - for entry in st.session_state.price_history: - if entry['material_id'] == material_id: - try: - price_history_data.append({ - 'date': pd.to_datetime(entry['date']), - 'price': float(entry['price']) - }) - except (ValueError, TypeError) as e: - st.error(f"خطأ في معالجة البيانات: {e}") - continue - - if not price_history_data: - st.warning("لا توجد بيانات تاريخية كافية للمادة المحددة للقيام بالتوقع.") - return - - # تحويل البيانات إلى DataFrame - price_history_df = pd.DataFrame(price_history_data).sort_values('date') - - # إجراء التوقع - # في الواقع، ستستخدم نماذج تعلم آلي مثل ARIMA أو Prophet - # هنا سنستخدم توقعًا بسيطًا للأغراض التوضيحية - - # حساب متوسط التغير الشهري - monthly_changes = [] - for i in range(1, len(price_history_df)): - monthly_changes.append(price_history_df.iloc[i]['price'] - price_history_df.iloc[i-1]['price']) - - if monthly_changes: - avg_monthly_change = sum(monthly_changes) / len(monthly_changes) - else: - avg_monthly_change = 0 - - # إنشاء بيانات التوقع - last_date = price_history_df['date'].max() - last_price = price_history_df.loc[price_history_df['date'] == last_date, 'price'].values[0] - - forecast_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=forecast_period, freq='M') - forecast_prices = [last_price + (i+1) * avg_monthly_change for i in range(forecast_period)] - - # إضافة بعض التقلبات العشوائية للتوقع - forecast_prices = [price + random.uniform(-price*0.05, price*0.05) for price in forecast_prices] - - forecast_df = pd.DataFrame({ - 'date': forecast_dates, - 'price': forecast_prices, - 'type': ['توقع'] * forecast_period - }) - - # دمج البيانات التاريخية والتوقع - historical_df = price_history_df.copy() - historical_df['type'] = ['تاريخي'] * len(historical_df) - - combined_df = pd.concat([historical_df, forecast_df], ignore_index=True) - - # عرض المخطط - fig = px.line( - combined_df, - x='date', - y='price', - color='type', - title=f'توقع أسعار {selected_material} للـ {forecast_period} أشهر القادمة', - labels={'date': 'التاريخ', 'price': 'السعر (ريال)', 'type': 'النوع'}, - color_discrete_map={'تاريخي': 'blue', 'توقع': 'red'} - ) - - # إضافة فترة الثقة حول التوقع - confidence = 0.1 # 10% فترة ثقة - upper_bound = [price * (1 + confidence) for price in forecast_prices] - lower_bound = [price * (1 - confidence) for price in forecast_prices] - - fig.add_scatter( - x=forecast_dates, - y=upper_bound, - fill=None, - mode='lines', - line_color='rgba(255, 0, 0, 0.3)', - line_width=0, - showlegend=False - ) - - fig.add_scatter( - x=forecast_dates, - y=lower_bound, - fill='tonexty', - mode='lines', - line_color='rgba(255, 0, 0, 0.3)', - line_width=0, - name='فترة الثقة (±10%)' - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض جدول التوقع - st.markdown("#### جدول توقع الأسعار") - - forecast_table = forecast_df.copy() - forecast_table['date'] = forecast_table['date'].dt.strftime('%Y-%m') - forecast_table['price'] = forecast_table['price'].apply(lambda x: f"{x:,.2f} ريال") - # إعادة تسمية الأعمدة إلى العربية لعرض الجدول - forecast_table = forecast_table.rename(columns={ - 'date': 'التاريخ', - 'price': 'السعر' - }) - forecast_table = forecast_table.drop(columns=['type']) - - st.dataframe(forecast_table, use_container_width=True, hide_index=True) - - # عرض ملخص التوقع - st.markdown("#### ملخص التوقع") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric( - "السعر الحالي", - f"{last_price:,.2f} ريال" - ) - - with col2: - forecasted_price = forecast_prices[-1] - price_change = forecasted_price - last_price - price_change_percent = (price_change / last_price) * 100 - - st.metric( - f"السعر المتوقع بعد {forecast_period} أشهر", - f"{forecasted_price:,.2f} ريال", - delta=f"{price_change_percent:.1f}%" - ) - - with col3: - avg_forecasted_price = sum(forecast_prices) / len(forecast_prices) - - st.metric( - "متوسط السعر المتوقع", - f"{avg_forecasted_price:,.2f} ريال" - ) - - # عرض ملاحظات وتوصيات - if price_change_percent > 10: - st.warning(""" - ### توقع ارتفاع كبير في الأسعار - - ينصح بشراء المواد مبكراً وتخزينها إذا أمكن - - التفاوض على عقود توريد طويلة الأجل بأسعار ثابتة - - البحث عن موردين بديلين أو مواد بديلة - """) - elif price_change_percent < -10: - st.success(""" - ### توقع انخفاض كبير في الأسعار - - ينصح بتأجيل شراء المواد إذا أمكن - - شراء كميات أقل والاحتفاظ بمخزون منخفض - - التفاوض على عقود مرنة مع الموردين - """) - else: - st.info(""" - ### توقع استقرار نسبي في الأسعار - - يمكن الشراء حسب الاحتياج دون الحاجة لتخزين كميات كبيرة - - متابعة أسعار السوق بشكل دوري للتأكد من دقة التوقعات - """) \ No newline at end of file diff --git a/modules/risk_analysis/__pycache__/risk_analyzer.cpython-310.pyc b/modules/risk_analysis/__pycache__/risk_analyzer.cpython-310.pyc deleted file mode 100644 index 7b1a75f3dbce97445a57f3b0ae6cfd9c4a3765e7..0000000000000000000000000000000000000000 Binary files a/modules/risk_analysis/__pycache__/risk_analyzer.cpython-310.pyc and /dev/null differ diff --git a/modules/risk_analysis/risk_analysis_app.py b/modules/risk_analysis/risk_analysis_app.py deleted file mode 100644 index 95b2a921305467e214946d3ad8852ac662c8175c..0000000000000000000000000000000000000000 --- a/modules/risk_analysis/risk_analysis_app.py +++ /dev/null @@ -1,751 +0,0 @@ -""" -تطبيق وحدة تحليل المخاطر -""" - -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 random -import os -import time -import io - -from utils.helpers import format_number, format_currency -from utils.excel_handler import export_to_excel - - -class RiskAnalysisApp: - """وحدة تحليل المخاطر""" - - def __init__(self): - """تهيئة وحدة تحليل المخاطر""" - # تهيئة المخاطر المحتملة - self.risk_categories = [ - "مخاطر مالية", - "مخاطر زمنية", - "مخاطر فنية", - "مخاطر إدارية", - "مخاطر تنظيمية", - "مخاطر سوقية", - "مخاطر تعاقدية" - ] - - self.impact_levels = ["منخفض", "متوسط", "عالي"] - self.probability_levels = ["غير محتمل", "محتمل", "مؤكد"] - - def render(self): - """عرض واجهة وحدة تحليل المخاطر""" - - st.markdown("

وحدة تحليل المخاطر

", unsafe_allow_html=True) - - tabs = st.tabs([ - "تحليل المخاطر", - "سجل المخاطر", - "مصفوفة المخاطر", - "خطة الاستجابة للمخاطر" - ]) - - with tabs[0]: - self._render_risk_analysis_tab() - - with tabs[1]: - self._render_risk_register_tab() - - with tabs[2]: - self._render_risk_matrix_tab() - - with tabs[3]: - self._render_risk_response_tab() - - def _render_risk_analysis_tab(self): - """عرض تبويب تحليل المخاطر""" - - st.markdown("### تحليل المخاطر") - - # التحقق من وجود مشروع حالي - if 'current_project' not in st.session_state or st.session_state.current_project is None: - # إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع - if 'projects' in st.session_state and st.session_state.projects: - 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 - else: - st.warning("لا توجد مشاريع متاحة. يرجى إنشاء مشروع جديد أولاً.") - return - - # عرض معلومات المشروع - project = st.session_state.current_project - - col1, col2, col3 = st.columns(3) - with col1: - st.metric("اسم المشروع", project['name']) - with col2: - st.metric("رقم المناقصة", project['number']) - with col3: - st.metric("الجهة المالكة", project['client']) - - # التحقق من وجود سجل المخاطر للمشروع - if 'risks' not in project: - project['risks'] = [] - - # نموذج إضافة مخاطر - with st.form("add_risk_form"): - st.markdown("#### إضافة مخاطرة جديدة") - - col1, col2 = st.columns(2) - - with col1: - risk_code = st.text_input("رمز المخاطرة", f"R{len(project['risks']) + 1}") - risk_category = st.selectbox("فئة المخاطرة", self.risk_categories) - impact = st.select_slider("التأثير", self.impact_levels, value="متوسط") - - with col2: - risk_description = st.text_area("وصف المخاطرة", height=80) - probability = st.select_slider("الاحتمالية", self.probability_levels, value="محتمل") - response_strategy = st.text_area("استراتيجية الاستجابة", height=80) - - submitted = st.form_submit_button("إضافة المخاطرة") - - if submitted: - # التحقق من تعبئة الحقول الإلزامية - if not risk_description: - st.error("يرجى إدخال وصف المخاطرة.") - else: - # إنشاء مخاطرة جديدة - new_risk = { - 'id': len(project['risks']) + 1, - 'risk_code': risk_code, - 'description': risk_description, - 'category': risk_category, - 'impact': impact, - 'probability': probability, - 'response_strategy': response_strategy, - 'status': "نشط", - 'created_at': datetime.now().strftime('%Y-%m-%d'), - 'risk_score': self._calculate_risk_score(impact, probability) - } - - # إضافة المخاطرة إلى سجل المخاطر - project['risks'].append(new_risk) - - st.success(f"تمت إضافة المخاطرة [{risk_code}] بنجاح!") - st.balloons() - - # خيارات تحليل المخاطر - st.markdown("#### خيارات تحليل المخاطر") - - col1, col2 = st.columns(2) - - with col1: - automated_analysis = st.button("تحليل تلقائي للمخاطر") - - with col2: - from_document_analysis = st.button("استيراد المخاطر من تحليل المستندات") - - if automated_analysis: - with st.spinner("جاري تحليل المخاطر..."): - time.sleep(2) - self._generate_automated_risks(project) - st.success("تم تحليل المخاطر بنجاح!") - st.balloons() - - if from_document_analysis: - with st.spinner("جاري استيراد المخاطر من تحليل المستندات..."): - time.sleep(2) - - # هذه مجرد محاكاة، في الواقع يجب استدعاء الوظيفة الفعلية لاستيراد المخاطر - document_risks = self._get_risks_from_documents() - - if document_risks: - existing_risk_codes = [r['risk_code'] for r in project['risks']] - - for risk in document_risks: - # تجنب تكرار المخاطر - if risk['risk_code'] not in existing_risk_codes: - project['risks'].append(risk) - - st.success(f"تم استيراد {len(document_risks)} مخاطرة من تحليل المستندات!") - else: - st.warning("لم يتم العثور على مخاطر في المستندات.") - - # عرض ملخص المخاطر - if project['risks']: - self._show_risk_summary(project['risks']) - - def _render_risk_register_tab(self): - """عرض تبويب سجل المخاطر""" - - st.markdown("### سجل المخاطر") - - # التحقق من وجود مشروع حالي - if 'current_project' not in st.session_state or st.session_state.current_project is None: - st.info("يرجى اختيار مشروع من تبويب تحليل المخاطر أولاً.") - return - - project = st.session_state.current_project - - if 'risks' not in project or not project['risks']: - st.info("لا توجد مخاطر مسجلة لهذا المشروع. يمكنك إضافة مخاطر من تبويب تحليل المخاطر.") - return - - # فلترة سجل المخاطر - col1, col2, col3 = st.columns(3) - - with col1: - search_term = st.text_input("البحث في سجل المخاطر") - - with col2: - category_filter = st.multiselect("فلترة حسب الفئة", self.risk_categories) - - with col3: - impact_filter = st.multiselect("فلترة حسب التأثير", self.impact_levels) - - # تطبيق الفلترة - filtered_risks = project['risks'] - - if search_term: - filtered_risks = [r for r in filtered_risks if search_term.lower() in r.get('description', '').lower()] - - if category_filter: - filtered_risks = [r for r in filtered_risks if r.get('category') in category_filter] - - if impact_filter: - filtered_risks = [r for r in filtered_risks if r.get('impact') in impact_filter] - - # عرض سجل المخاطر - if filtered_risks: - # تحويل المخاطر إلى DataFrame - risk_df = pd.DataFrame(filtered_risks) - - # تحديد الأعمدة المراد عرضها وترتيبها - display_columns = [ - 'risk_code', 'description', 'category', 'impact', - 'probability', 'risk_score', 'status' - ] - - # تغيير أسماء الأعمدة للعرض - column_names = { - 'risk_code': 'رمز المخاطرة', - 'description': 'وصف المخاطرة', - 'category': 'الفئة', - 'impact': 'التأثير', - 'probability': 'الاحتمالية', - 'risk_score': 'درجة المخاطرة', - 'status': 'الحالة', - 'response_strategy': 'استراتيجية الاستجابة', - 'created_at': 'تاريخ الإنشاء' - } - - # إعداد DataFrame للعرض - if 'response_strategy' in risk_df.columns: - display_columns.append('response_strategy') - - if 'created_at' in risk_df.columns: - display_columns.append('created_at') - - # الحصول على الأعمدة المتوفرة فقط - available_columns = [col for col in display_columns if col in risk_df.columns] - - if available_columns: - display_df = risk_df[available_columns].rename(columns=column_names) - - # عرض الجدول - st.dataframe(display_df, use_container_width=True, hide_index=True) - - # أزرار العمليات - col1, col2 = st.columns(2) - - with col1: - if st.button("تصدير سجل المخاطر إلى Excel"): - st.success("تم تصدير سجل المخاطر بنجاح!") - - with col2: - if st.button("طباعة تقرير المخاطر"): - st.success("تم إنشاء تقرير المخاطر بنجاح!") - else: - st.warning("هناك مشكلة في بنية بيانات المخاطر. يرجى التحقق من سلامة البيانات.") - else: - st.info("لا توجد مخاطر تطابق معايير البحث.") - - def _render_risk_matrix_tab(self): - """عرض تبويب مصفوفة المخاطر""" - - st.markdown("### مصفوفة المخاطر") - - # التحقق من وجود مشروع حالي - if 'current_project' not in st.session_state or st.session_state.current_project is None: - st.info("يرجى اختيار مشروع من تبويب تحليل المخاطر أولاً.") - return - - project = st.session_state.current_project - - if 'risks' not in project or not project['risks']: - st.info("لا توجد مخاطر مسجلة لهذا المشروع. يمكنك إضافة مخاطر من تبويب تحليل المخاطر.") - return - - # إنشاء ضبط مصفوفة المخاطر (3×3) - impact_values = {"منخفض": 1, "متوسط": 2, "عالي": 3} - probability_values = {"غير محتمل": 1, "محتمل": 2, "مؤكد": 3} - - # إنشاء DataFrame لتمثيل مصفوفة المخاطر - matrix_data = [] - - for p in probability_values.keys(): - for i in impact_values.keys(): - p_value = probability_values[p] - i_value = impact_values[i] - risk_score = p_value * i_value - - # تحديد اللون حسب درجة المخاطرة - if risk_score <= 2: - color = 'green' # منخفضة - elif risk_score <= 6: - color = 'orange' # متوسطة - else: - color = 'red' # عالية - - # استخراج المخاطر التي تقع في هذه الخلية - cell_risks = [r for r in project['risks'] if r.get('impact') == i and r.get('probability') == p] - - # إضافة بيانات الخلية - matrix_data.append({ - 'احتمالية': p, - 'تأثير': i, - 'درجة_المخاطرة': risk_score, - 'عدد_المخاطر': len(cell_risks), - 'المخاطر': [r.get('risk_code') for r in cell_risks], - 'لون': color - }) - - # تحويل إلى DataFrame - matrix_df = pd.DataFrame(matrix_data) - - # رسم مصفوفة المخاطر باستخدام Plotly - fig = go.Figure() - - for index, row in matrix_df.iterrows(): - # إنشاء نص الخلية - if row['عدد_المخاطر'] > 0: - cell_text = f"{', '.join(row['المخاطر'])}
({row['عدد_المخاطر']} مخاطر)" - else: - cell_text = '' - - # إنشاء خلية المصفوفة - fig.add_trace(go.Scatter( - x=[row['تأثير']], - y=[row['احتمالية']], - mode='markers+text', - marker=dict( - color=row['لون'], - size=20 + (row['عدد_المخاطر'] * 5), - opacity=0.8 - ), - text=cell_text, - textposition="middle center", - name=f"{row['احتمالية']} - {row['تأثير']}" - )) - - # تكوين المحاور - fig.update_layout( - title="مصفوفة المخاطر (الاحتمالية × التأثير)", - xaxis=dict( - title="التأثير", - tickmode='array', - tickvals=[1, 2, 3], - ticktext=["منخفض", "متوسط", "عالي"], - gridcolor='lightgray' - ), - yaxis=dict( - title="الاحتمالية", - tickmode='array', - tickvals=[1, 2, 3], - ticktext=["غير محتمل", "محتمل", "مؤكد"], - gridcolor='lightgray' - ), - height=600 - ) - - # عرض المصفوفة - st.plotly_chart(fig, use_container_width=True) - - # عرض توزيع المخاطر حسب الفئة - st.markdown("#### توزيع المخاطر حسب الفئة") - - # حساب عدد المخاطر في كل فئة - category_counts = {} - for r in project['risks']: - category = r.get('category', 'أخرى') - category_counts[category] = category_counts.get(category, 0) + 1 - - # إنشاء DataFrame - category_df = pd.DataFrame({ - 'الفئة': list(category_counts.keys()), - 'عدد المخاطر': list(category_counts.values()) - }) - - # رسم مخطط دائري - fig = px.pie( - category_df, - values='عدد المخاطر', - names='الفئة', - title='توزيع المخاطر حسب الفئة', - hole=0.4 - ) - - st.plotly_chart(fig, use_container_width=True) - - def _render_risk_response_tab(self): - """عرض تبويب خطة الاستجابة للمخاطر""" - - st.markdown("### خطة الاستجابة للمخاطر") - - # التحقق من وجود مشروع حالي - if 'current_project' not in st.session_state or st.session_state.current_project is None: - st.info("يرجى اختيار مشروع من تبويب تحليل المخاطر أولاً.") - return - - project = st.session_state.current_project - - if 'risks' not in project or not project['risks']: - st.info("لا توجد مخاطر مسجلة لهذا المشروع. يمكنك إضافة مخاطر من تبويب تحليل المخاطر.") - return - - # ترتيب المخاطر حسب درجة المخاطرة (من الأعلى إلى الأقل) - sorted_risks = sorted(project['risks'], key=lambda x: x.get('risk_score', 0), reverse=True) - - # عرض خطة الاستجابة للمخاطر - for i, risk in enumerate(sorted_risks): - with st.expander(f"{risk.get('risk_code', '')}: {risk.get('description', 'بدون وصف')}", expanded=(i < 3)): - col1, col2, col3 = st.columns(3) - - with col1: - st.markdown(f"**الفئة**: {risk.get('category', 'غير محدد')}") - st.markdown(f"**التأثير**: {risk.get('impact', 'غير محدد')}") - - with col2: - st.markdown(f"**الاحتمالية**: {risk.get('probability', 'غير محدد')}") - st.markdown(f"**درجة المخاطرة**: {risk.get('risk_score', 'غير محدد')}") - - with col3: - st.markdown(f"**الحالة**: {risk.get('status', 'نشط')}") - risk_owner = risk.get('risk_owner', 'غير محدد') - st.markdown(f"**مسؤول المخاطرة**: {risk_owner}") - - st.markdown("---") - st.markdown("#### استراتيجية الاستجابة") - current_strategy = risk.get('response_strategy', '') - new_strategy = st.text_area(f"استراتيجية الاستجابة للمخاطرة {risk.get('risk_code', '')}", - value=current_strategy, - height=100, - key=f"strategy_{risk.get('risk_code', '')}") - - # تحديث استراتيجية الاستجابة إذا تم تغييرها - if new_strategy != current_strategy: - risk['response_strategy'] = new_strategy - - st.markdown("#### إجراءات التحكم") - control_measures = risk.get('control_measures', []) - - if control_measures: - for j, measure in enumerate(control_measures): - st.markdown(f"{j+1}. {measure}") - else: - st.info("لم يتم تعريف إجراءات تحكم لهذه المخاطرة.") - - # إضافة إجراء تحكم جديد - new_measure = st.text_input(f"إجراء تحكم جديد للمخاطرة {risk.get('risk_code', '')}", - key=f"measure_{risk.get('risk_code', '')}") - - if st.button(f"إضافة إجراء", key=f"add_measure_{risk.get('risk_code', '')}"): - if new_measure: - if 'control_measures' not in risk: - risk['control_measures'] = [] - - risk['control_measures'].append(new_measure) - st.success(f"تم إضافة إجراء التحكم بنجاح!") - st.rerun() - else: - st.error("يرجى إدخال إجراء التحكم.") - - # زر تصدير خطة الاستجابة للمخاطر - if st.button("تصدير خطة الاستجابة للمخاطر"): - st.success("تم تصدير خطة الاستجابة للمخاطر بنجاح!") - - def _calculate_risk_score(self, impact, probability): - """حساب درجة المخاطرة بناءً على التأثير والاحتمالية""" - impact_values = {"منخفض": 1, "متوسط": 2, "عالي": 3} - probability_values = {"غير محتمل": 1, "محتمل": 2, "مؤكد": 3} - - impact_value = impact_values.get(impact, 1) - probability_value = probability_values.get(probability, 1) - - return impact_value * probability_value - - def _generate_automated_risks(self, project): - """توليد مخاطر تلقائية بناءً على خصائص المشروع""" - - # قائمة المخاطر الشائعة في مشاريع المقاولات - common_risks = [ - { - 'risk_code': 'RF01', - 'description': 'غرامة تأخير مرتفعة (10% من قيمة العقد)', - 'category': 'مخاطر مالية', - 'impact': 'عالي', - 'probability': 'محتمل', - 'response_strategy': 'تخصيص مبلغ احتياطي للغرامات المحتملة ووضع خطة لإدارة الجدول الزمني بشكل فعال', - 'status': 'نشط', - 'risk_score': 6 - }, - { - 'risk_code': 'RF02', - 'description': 'متطلبات ضمان بنكي مرتفعة (15% من قيمة العقد)', - 'category': 'مخاطر مالية', - 'impact': 'متوسط', - 'probability': 'مؤكد', - 'response_strategy': 'التفاوض مع العميل لتخفيض نسبة الضمان البنكي أو تقسيمه على مراحل المشروع', - 'status': 'نشط', - 'risk_score': 6 - }, - { - 'risk_code': 'RF03', - 'description': 'شروط دفع متأخرة (60 يوم)', - 'category': 'مخاطر مالية', - 'impact': 'متوسط', - 'probability': 'مؤكد', - 'response_strategy': 'التخطيط للتدفق النقدي مع الأخذ بالاعتبار تأخر الدفعات وتأمين خط ائتمان احتياطي', - 'status': 'نشط', - 'risk_score': 6 - }, - { - 'risk_code': 'RT01', - 'description': 'مدة تنفيذ قصيرة (12 شهر)', - 'category': 'مخاطر زمنية', - 'impact': 'عالي', - 'probability': 'محتمل', - 'response_strategy': 'زيادة فريق العمل واستخدام موارد إضافية مع وضع خطة عمل تفصيلية ومراقبتها أسبوعياً', - 'status': 'نشط', - 'risk_score': 6 - }, - { - 'risk_code': 'RT02', - 'description': 'احتمالية تأخر توريد المواد الرئيسية', - 'category': 'مخاطر زمنية', - 'impact': 'عالي', - 'probability': 'محتمل', - 'response_strategy': 'تحديد المواد ذات فترات التوريد الطويلة وطلبها مبكراً مع التعاقد مع موردين بدلاء', - 'status': 'نشط', - 'risk_score': 6 - }, - { - 'risk_code': 'RTE01', - 'description': 'غموض في بعض المواصفات الفنية', - 'category': 'مخاطر فنية', - 'impact': 'متوسط', - 'probability': 'محتمل', - 'response_strategy': 'طلب توضيح من العميل قبل البدء بالتنفيذ وتوثيق جميع الردود والتوضيحات', - 'status': 'نشط', - 'risk_score': 4 - }, - { - 'risk_code': 'RTE02', - 'description': 'تضارب بين المخططات والمواصفات', - 'category': 'مخاطر فنية', - 'impact': 'متوسط', - 'probability': 'محتمل', - 'response_strategy': 'مراجعة شاملة للمستندات وتوثيق التضاربات وطلب توضيح من العميل', - 'status': 'نشط', - 'risk_score': 4 - }, - { - 'risk_code': 'RM01', - 'description': 'عدم وضوح آلية استلام الأعمال', - 'category': 'مخاطر إدارية', - 'impact': 'منخفض', - 'probability': 'محتمل', - 'response_strategy': 'طلب توضيح آلية الاستلام من العميل ووضع إجراءات داخلية للتحقق من جودة الأعمال قبل التقديم للاستلام', - 'status': 'نشط', - 'risk_score': 2 - }, - { - 'risk_code': 'RR01', - 'description': 'شروط تعجيزية للمحتوى المحلي', - 'category': 'مخاطر تنظيمية', - 'impact': 'عالي', - 'probability': 'محتمل', - 'response_strategy': 'دراسة متطلبات المحتوى المحلي بدقة ووضع خطة لتحقيقها مع الاحتفاظ بسجلات التوثيق اللازمة', - 'status': 'نشط', - 'risk_score': 6 - }, - { - 'risk_code': 'RM01', - 'description': 'خطر التغييرات في أسعار المواد', - 'category': 'مخاطر سوقية', - 'impact': 'عالي', - 'probability': 'محتمل', - 'response_strategy': 'تثبيت أسعار المواد الرئيسية مع الموردين وإدراج بند تعديل الأسعار في العقد', - 'status': 'نشط', - 'risk_score': 6 - }, - { - 'risk_code': 'RC01', - 'description': 'عدم وضوح بعض بنود العقد', - 'category': 'مخاطر تعاقدية', - 'impact': 'متوسط', - 'probability': 'محتمل', - 'response_strategy': 'مراجعة العقد من قبل مستشار قانوني متخصص وطلب توضيح للبنود الغامضة قبل التوقيع', - 'status': 'نشط', - 'risk_score': 4 - } - ] - - # إضافة المخاطر الشائعة إلى المشروع - existing_risk_codes = [r['risk_code'] for r in project['risks']] - - for risk in common_risks: - # تجنب تكرار المخاطر - if risk['risk_code'] not in existing_risk_codes: - risk['id'] = len(project['risks']) + 1 - risk['created_at'] = datetime.now().strftime('%Y-%m-%d') - project['risks'].append(risk) - - def _get_risks_from_documents(self): - """استيراد المخاطر من تحليل المستندات""" - - # محاكاة لاستيراد المخاطر من تحليل المستندات - # في التطبيق الفعلي، يجب استدعاء الوظيفة المناسبة من وحدة تحليل المستندات - - document_risks = [ - { - 'risk_code': 'RD01', - 'description': 'غرامة تأخير مرتفعة تصل إلى 20% من قيمة العقد', - 'category': 'مخاطر مالية', - 'impact': 'عالي', - 'probability': 'مؤكد', - 'response_strategy': 'التفاوض على تخفيض الغرامة أو تقسيمها حسب مراحل المشروع مع وضع خطة محكمة للجدول الزمني', - 'status': 'نشط', - 'risk_score': 9, - 'created_at': datetime.now().strftime('%Y-%m-%d') - }, - { - 'risk_code': 'RD02', - 'description': 'يحق للمالك إيقاف المشروع لمدة تصل إلى 90 يوم دون تعويض', - 'category': 'مخاطر تعاقدية', - 'impact': 'عالي', - 'probability': 'محتمل', - 'response_strategy': 'طلب إضافة بند للتعويض عن التكاليف الإضافية الناتجة عن الإيقاف لفترات طويلة', - 'status': 'نشط', - 'risk_score': 6, - 'created_at': datetime.now().strftime('%Y-%m-%d') - }, - { - 'risk_code': 'RD03', - 'description': 'تحمل المقاول مسؤولية استخراج جميع التصاريح الحكومية', - 'category': 'مخاطر تنظيمية', - 'impact': 'متوسط', - 'probability': 'مؤكد', - 'response_strategy': 'حصر جميع التصاريح المطلوبة والبدء في إجراءات استخراجها مبكراً مع تخصيص فريق لمتابعتها', - 'status': 'نشط', - 'risk_score': 6, - 'created_at': datetime.now().strftime('%Y-%m-%d') - }, - { - 'risk_code': 'RD04', - 'description': 'شروط الدفعة المقدمة مقيدة بضمان بنكي بقيمة 120% من قيمة الدفعة', - 'category': 'مخاطر مالية', - 'impact': 'متوسط', - 'probability': 'مؤكد', - 'response_strategy': 'التفاوض على خفض نسبة الضمان البنكي أو تقديم ضمان شركة بدلاً من الضمان البنكي', - 'status': 'نشط', - 'risk_score': 6, - 'created_at': datetime.now().strftime('%Y-%m-%d') - } - ] - - return document_risks - - def _show_risk_summary(self, risks): - """عرض ملخص المخاطر""" - - st.markdown("#### ملخص المخاطر") - - # حساب إحصائيات المخاطر - total_risks = len(risks) - risk_levels = { - 'عالية': len([r for r in risks if r.get('risk_score', 0) >= 6]), - 'متوسطة': len([r for r in risks if 3 <= r.get('risk_score', 0) < 6]), - 'منخفضة': len([r for r in risks if r.get('risk_score', 0) < 3]) - } - - # عرض الإحصائيات - col1, col2, col3, col4 = st.columns(4) - - with col1: - st.metric("إجمالي المخاطر", total_risks) - - with col2: - st.metric("المخاطر العالية", risk_levels['عالية'], delta=f"{risk_levels['عالية']/total_risks*100:.1f}%", delta_color="inverse") - - with col3: - st.metric("المخاطر المتوسطة", risk_levels['متوسطة'], delta=f"{risk_levels['متوسطة']/total_risks*100:.1f}%", delta_color="off") - - with col4: - st.metric("المخاطر المنخفضة", risk_levels['منخفضة'], delta=f"{risk_levels['منخفضة']/total_risks*100:.1f}%", delta_color="normal") - - # عرض الرسم البياني للمخاطر - risk_level_df = pd.DataFrame({ - 'مستوى المخاطرة': list(risk_levels.keys()), - 'عدد المخاطر': list(risk_levels.values()) - }) - - fig = px.bar( - risk_level_df, - x='مستوى المخاطرة', - y='عدد المخاطر', - color='مستوى المخاطرة', - color_discrete_map={ - 'عالية': 'red', - 'متوسطة': 'orange', - 'منخفضة': 'green' - }, - title='توزيع المخاطر حسب المستوى' - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض أعلى 5 مخاطر من حيث درجة المخاطرة - st.markdown("#### أعلى 5 مخاطر") - - # ترتيب المخاطر حسب درجة المخاطرة - sorted_risks = sorted(risks, key=lambda x: x.get('risk_score', 0), reverse=True) - top_risks = sorted_risks[:5] - - # إنشاء DataFrame للعرض - if top_risks: - top_risks_data = [] - - for r in top_risks: - top_risks_data.append({ - 'رمز المخاطرة': r.get('risk_code', ''), - 'وصف المخاطرة': r.get('description', ''), - 'الفئة': r.get('category', ''), - 'التأثير': r.get('impact', ''), - 'الاحتمالية': r.get('probability', ''), - 'درجة المخاطرة': r.get('risk_score', 0) - }) - - top_risks_df = pd.DataFrame(top_risks_data) - st.dataframe(top_risks_df, use_container_width=True, hide_index=True) diff --git a/modules/risk_analysis/risk_analyzer.py b/modules/risk_analysis/risk_analyzer.py deleted file mode 100644 index e373f833c742ca681e81f787725669297b34e05a..0000000000000000000000000000000000000000 --- a/modules/risk_analysis/risk_analyzer.py +++ /dev/null @@ -1,1154 +0,0 @@ -""" -وحدة تحليل المخاطر لنظام إدارة المناقصات - Hybrid Face -""" - -import os -import logging -import threading -import datetime -import json -import math -import streamlit as st -from pathlib import Path -import pandas as pd -import numpy as np -import matplotlib.pyplot as plt -import sys - -# إضافة مسار المشروع للنظام -sys.path.append(str(Path(__file__).parent.parent)) - -# استيراد محسن واجهة المستخدم -from styling.enhanced_ui import UIEnhancer - -# تهيئة السجل -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -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 _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.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 - - -class RiskAnalysisApp: - """تطبيق تحليل المخاطر""" - - def __init__(self): - """تهيئة تطبيق تحليل المخاطر""" - self.ui = UIEnhancer(page_title="تحليل المخاطر - نظام تحليل المناقصات", page_icon="⚠️") - self.ui.apply_theme_colors() - self.risk_analyzer = RiskAnalyzer() - - # تهيئة بيانات المشاريع - if 'projects' not in st.session_state: - st.session_state.projects = self._generate_sample_projects() - - # تهيئة نتائج التحليل - if 'risk_analysis_results' not in st.session_state: - st.session_state.risk_analysis_results = {} - - 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_dashboard_tab() - - with tabs[1]: - self._render_analysis_tab() - - with tabs[2]: - self._render_risk_register_tab() - - with tabs[3]: - self._render_risk_matrix_tab() - - with tabs[4]: - self._render_mitigation_strategies_tab() - - def _render_dashboard_tab(self): - """عرض تبويب لوحة المعلومات""" - - st.markdown("### لوحة معلومات تحليل المخاطر") - - # عرض إحصائيات المخاطر - col1, col2, col3, col4 = st.columns(4) - - # الحصول على إحصائيات المخاطر - total_risks = 0 - high_risks = 0 - medium_risks = 0 - low_risks = 0 - - for project_id, results in st.session_state.risk_analysis_results.items(): - if "identified_risks" in results: - project_risks = results["identified_risks"] - total_risks += len(project_risks) - high_risks += len([r for r in project_risks if r["risk_score"] >= 6]) - medium_risks += len([r for r in project_risks if 3 <= r["risk_score"] < 6]) - low_risks += len([r for r in project_risks if r["risk_score"] < 3]) - - with col1: - self.ui.create_metric_card("إجمالي المخاطر", str(total_risks), None, self.ui.COLORS['primary']) - - with col2: - self.ui.create_metric_card("مخاطر عالية", str(high_risks), None, self.ui.COLORS['danger']) - - with col3: - self.ui.create_metric_card("مخاطر متوسطة", str(medium_risks), None, self.ui.COLORS['warning']) - - with col4: - self.ui.create_metric_card("مخاطر منخفضة", str(low_risks), None, self.ui.COLORS['success']) - - # عرض توزيع المخاطر حسب الفئة - st.markdown("#### توزيع المخاطر حسب الفئة") - - # جمع بيانات توزيع المخاطر - category_distribution = {} - - for project_id, results in st.session_state.risk_analysis_results.items(): - if "identified_risks" in results: - for risk in results["identified_risks"]: - category = risk["category"] - if category not in category_distribution: - category_distribution[category] = 0 - category_distribution[category] += 1 - - if category_distribution: - # تحويل البيانات إلى DataFrame - category_df = pd.DataFrame({ - 'الفئة': list(category_distribution.keys()), - 'عدد المخاطر': list(category_distribution.values()) - }) - - # عرض الرسم البياني - st.bar_chart(category_df.set_index('الفئة')) - else: - st.info("لا توجد بيانات كافية لعرض توزيع المخاطر.") - - # عرض المشاريع ذات المخاطر العالية - st.markdown("#### المشاريع ذات المخاطر العالية") - - high_risk_projects = [] - - for project_id, results in st.session_state.risk_analysis_results.items(): - if "identified_risks" in results: - project_high_risks = len([r for r in results["identified_risks"] if r["risk_score"] >= 6]) - if project_high_risks > 0: - # البحث عن بيانات المشروع - project = next((p for p in st.session_state.projects if p["id"] == int(project_id)), None) - if project: - high_risk_projects.append({ - 'اسم المشروع': project["name"], - 'رقم المناقصة': project["number"], - 'الجهة المالكة': project["client"], - 'عدد المخاطر العالية': project_high_risks - }) - - if high_risk_projects: - high_risk_df = pd.DataFrame(high_risk_projects) - st.dataframe(high_risk_df, use_container_width=True, hide_index=True) - else: - st.info("لا توجد مشاريع ذات مخاطر عالية حاليًا.") - - def _render_analysis_tab(self): - """عرض تبويب تحليل المخاطر""" - - st.markdown("### تحليل مخاطر المشروع") - - # اختيار المشروع - project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects] - selected_project = st.selectbox("اختر المشروع", project_options) - - if selected_project: - # استخراج معرف المشروع من الاختيار - project_index = project_options.index(selected_project) - project = st.session_state.projects[project_index] - project_id = project["id"] - - # عرض معلومات المشروع - col1, col2 = st.columns(2) - - 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['project_type']}") - st.markdown(f"**مستوى التعقيد**: {project['complexity']}") - - # اختيار طريقة التحليل - analysis_method = st.radio( - "طريقة التحليل", - ["أساسي", "شامل"], - format_func=lambda x: "تحليل أساسي" if x == "أساسي" else "تحليل شامل" - ) - - # زر بدء التحليل - if st.button("بدء تحليل المخاطر"): - with st.spinner("جاري تحليل مخاطر المشروع..."): - # محاكاة وقت المعالجة - 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() - - # تخزين النتائج في حالة الجلسة - st.session_state.risk_analysis_results[str(project_id)] = 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): - """عرض تبويب سجل المخاطر""" - - st.markdown("### سجل المخاطر") - - # اختيار المشروع - project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects] - project_options.insert(0, "جميع المشاريع") - selected_project_option = st.selectbox("اختر المشروع", project_options, key="risk_register_project") - - # جمع المخاطر المحددة - all_risks = [] - - if selected_project_option == "جميع المشاريع": - # جمع المخاطر من جميع المشاريع - for project_id, results in st.session_state.risk_analysis_results.items(): - if "identified_risks" 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() - risk_copy["project_name"] = project_name - all_risks.append(risk_copy) - else: - # استخراج معرف المشروع من الاختيار - project_index = project_options.index(selected_project_option) - 1 # -1 لأننا أضفنا "جميع المشاريع" في البداية - project = st.session_state.projects[project_index] - project_id = project["id"] - - # جمع المخاطر من المشروع المحدد - 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() - risk_copy["project_name"] = project["name"] - all_risks.append(risk_copy) - - # فلترة المخاطر - col1, col2, col3 = st.columns(3) - - with col1: - category_filter = st.multiselect( - "فئة المخاطر", - list(set(risk["category"] for risk in all_risks)) if all_risks else [], - key="risk_register_category" - ) - - with col2: - probability_filter = st.multiselect( - "الاحتمالية", - ["عالي", "متوسط", "منخفض"], - key="risk_register_probability" - ) - - with col3: - impact_filter = st.multiselect( - "التأثير", - ["عالي", "متوسط", "منخفض"], - key="risk_register_impact" - ) - - # تطبيق الفلترة - filtered_risks = all_risks - - if category_filter: - filtered_risks = [risk for risk in filtered_risks if risk["category"] in category_filter] - - if probability_filter: - filtered_risks = [risk for risk in filtered_risks if risk["probability"] in probability_filter] - - if impact_filter: - filtered_risks = [risk for risk in filtered_risks if risk["impact"] in impact_filter] - - # عرض سجل المخاطر - 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 = risk_df.sort_values(by='درجة المخاطرة', ascending=False) - - # عرض الجدول - st.dataframe(risk_df, use_container_width=True, hide_index=True) - - # زر تصدير سجل المخاطر - if st.button("تصدير سجل المخاطر"): - with st.spinner("جاري تصدير سجل المخاطر..."): - # محاكاة وقت المعالجة - time.sleep(1) - st.success("تم تصدير سجل المخاطر بنجاح!") - else: - st.info("لا توجد مخاطر تطابق معايير البحث أو لم يتم إجراء تحليل للمخاطر بعد.") - - def _render_risk_matrix_tab(self): - """عرض تبويب مصفوفة المخاطر""" - - st.markdown("### مصفوفة المخاطر") - - # اختيار المشروع - project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects] - selected_project = st.selectbox("اختر المشروع", project_options, key="risk_matrix_project") - - if selected_project: - # استخراج معرف المشروع من الاختيار - project_index = project_options.index(selected_project) - project = st.session_state.projects[project_index] - project_id = project["id"] - - # التحقق من وجود نتائج تحليل للمشروع - 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, "مخاطر أخرى") - 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): - """عرض تبويب استراتيجيات التخفيف""" - - st.markdown("### استراتيجيات التخفيف من المخاطر") - - # اختيار المشروع - project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects] - selected_project = st.selectbox("اختر المشروع", project_options, key="mitigation_project") - - if selected_project: - # استخراج معرف المشروع من الاختيار - project_index = project_options.index(selected_project) - project = st.session_state.projects[project_index] - project_id = project["id"] - - # التحقق من وجود نتائج تحليل للمشروع - 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("لا توجد استراتيجيات تخفيف تطابق معايير الفلترة.") - else: - st.warning("لم يتم العثور على استراتيجيات تخفيف للمشروع المحدد.") - else: - st.warning("لم يتم إجراء تحليل للمخاطر لهذا المشروع بعد.") - - def _generate_sample_projects(self): - """توليد بيانات افتراضية للمشاريع""" - - return [ - { - 'id': 1, - 'name': "إنشاء مبنى مستشفى الولادة والأطفال بمنطقة الشرقية", - 'number': "SHPD-2025-001", - 'client': "وزارة الصحة", - 'location': "الدمام، المنطقة الشرقية", - 'description': "يشمل المشروع إنشاء وتجهيز مبنى مستشفى الولادة والأطفال بسعة 300 سرير، ويتكون المبنى من 4 طوابق بمساحة إجمالية 15,000 متر مربع.", - 'status': "قيد التسعير", - 'tender_type': "عامة", - 'pricing_method': "قياسي", - 'submission_date': (datetime.datetime.now() + datetime.timedelta(days=5)), - 'created_at': datetime.datetime.now() - datetime.timedelta(days=10), - 'created_by_id': 1, - 'project_type': "مباني", - 'complexity': "عالي" - }, - { - 'id': 2, - 'name': "صيانة وتطوير طريق الملك عبدالله", - 'number': "MOT-2025-042", - 'client': "وزارة النقل", - 'location': "الرياض، المنطقة الوسطى", - 'description': "صيانة وتطوير طريق الملك عبدالله بطول 25 كم، ويشمل المشروع إعادة الرصف وتحسين الإنارة وتركيب اللوحات الإرشادية.", - 'status': "تم التقديم", - 'tender_type': "عامة", - 'pricing_method': "غير متزن", - 'submission_date': (datetime.datetime.now() - datetime.timedelta(days=15)), - 'created_at': datetime.datetime.now() - datetime.timedelta(days=45), - 'created_by_id': 1, - 'project_type': "بنية تحتية", - 'complexity': "متوسط" - }, - { - 'id': 3, - 'name': "إنشاء محطة معالجة مياه الصرف الصحي", - 'number': "SWPC-2025-007", - 'client': "شركة المياه الوطنية", - 'location': "جدة، المنطقة الغربية", - 'description': "إنشاء محطة معالجة مياه الصرف الصحي بطاقة استيعابية 50,000 م3/يوم، مع جميع الأعمال المدنية والكهروميكانيكية.", - 'status': "تمت الترسية", - 'tender_type': "عامة", - 'pricing_method': "قياسي", - 'submission_date': (datetime.datetime.now() - datetime.timedelta(days=90)), - 'created_at': datetime.datetime.now() - datetime.timedelta(days=120), - 'created_by_id': 1, - 'project_type': "بنية تحتية", - 'complexity': "عالي" - } - ] - -# تشغيل التطبيق -if __name__ == "__main__": - risk_app = RiskAnalysisApp() - risk_app.run() diff --git a/modules/risk_assessment/__init__.py b/modules/risk_assessment/__init__.py deleted file mode 100644 index 7a0af0cba3e41d42a6aff7f8b31f4a81b3ecdfdb..0000000000000000000000000000000000000000 --- a/modules/risk_assessment/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -""" -وحدة تقييم مخاطر العقود الآلي -""" \ No newline at end of file diff --git a/modules/risk_assessment/contract_risk_analyzer.py b/modules/risk_assessment/contract_risk_analyzer.py deleted file mode 100644 index acf6398e185ff827b156e9ee43629c65c5a77b49..0000000000000000000000000000000000000000 --- a/modules/risk_assessment/contract_risk_analyzer.py +++ /dev/null @@ -1,2055 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة تحليل وتقييم مخاطر العقود بشكل آلي -""" - -import os -import sys -import json -import datetime -import re -import numpy as np -import pandas as pd -import streamlit as st -import plotly.express as px -import plotly.graph_objects as go -from sklearn.feature_extraction.text import TfidfVectorizer -from sklearn.cluster import KMeans -from collections import Counter - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد المكونات المساعدة -from utils.helpers import create_directory_if_not_exists, format_time, get_user_info - -# تعليق استيراد Anthropic (سيتم تنفيذه لاحقًا بعد ضبط ملف anthropic) -# نستخدم نمط fallback للتعامل مع الخطأ -try: - from anthropic import Anthropic - - def analyzeBillOfQuantities(text): - return {"analysis": "تحليل فرضي لجدول الكميات", "items": [], "summary": "لا يوجد تحليل حقيقي متاح حاليًا"} - - def analyzeTermsAndConditions(text): - return {"analysis": "تحليل فرضي للشروط والأحكام", "risks": [], "summary": "لا يوجد تحليل حقيقي متاح حاليًا"} - - anthropic = None # سيتم تعيينه لاحقًا عند الحاجة -except ImportError: - # في حالة عدم وجود مكتبة أنثروبيك، نستخدم دوال فرضية - def analyzeBillOfQuantities(text): - return {"analysis": "تحليل فرضي لجدول الكميات", "items": [], "summary": "لا يوجد تحليل حقيقي متاح حاليًا"} - - def analyzeTermsAndConditions(text): - return {"analysis": "تحليل فرضي للشروط والأحكام", "risks": [], "summary": "لا يوجد تحليل حقيقي متاح حاليًا"} - - anthropic = None - - -class ContractRiskAnalyzer: - """فئة تحليل وتقييم المخاطر في العقود بشكل آلي""" - - def __init__(self): - """تهيئة محلل مخاطر العقود""" - self.risk_data_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'risk_assessment') - create_directory_if_not_exists(self.risk_data_dir) - - # تعريف أنواع المخاطر - self.risk_categories = { - "legal": "قانونية", - "financial": "مالية", - "operational": "تشغيلية", - "technical": "فنية", - "compliance": "امتثال", - "environmental": "بيئية", - "safety": "سلامة", - "schedule": "جدولة", - "resource": "موارد", - "quality": "جودة", - "scope": "نطاق العمل", - "stakeholder": "أصحاب المصلحة", - "commercial": "تجارية", - "contractual": "تعاقدية", - "regulatory": "تنظيمية" - } - - # تعريف قائمة المصطلحات الخطرة في العقود - self.risky_terms = { - "legal": [ - "تعديل العقد", "فسخ العقد", "إنهاء الاتفاقية", "فض المنازعات", "شرط جزائي", - "تحكيم", "قاهرة", "ظروف قاهرة", "التقاضي", "الإخلال بالعقد", "المنازعات", - "الدفع", "الضمان", "الولاية القضائية", "التعويض" - ], - "financial": [ - "غرامة تأخير", "غرامات", "دفعة مقدمة", "دفعة نهائية", "ضمان", "تأمين", - "تسعير", "سعر", "خصم", "تكاليف إضافية", "تعديل سعر", "زيادة سعر", - "خسارة", "ربح", "هامش", "تحمل التكاليف", "تمويل", "مخاطر مالية", "ضريبة" - ], - "operational": [ - "تأخير", "عدم التسليم", "توقف", "انقطاع", "عطل", "خلل", "تعطل", - "عمالة", "أيدي عاملة", "موارد بشرية", "مناولة", "تصاريح", "لوجستيك", - "مخزون", "سلسلة توريد", "عمليات" - ], - "technical": [ - "مواصفات", "معايير", "شروط فنية", "كفاءة", "جودة", "أداء", - "اختبار", "فحص", "تقنية", "تكنولوجيا", "تشغيل", "تركيب", "صيانة", - "تصميم", "هندسة", "قدرة" - ], - "compliance": [ - "اللوائح", "القوانين", "التشريعات", "الامتثال", "المعايير", "الترخيص", - "التصريح", "الموافقة", "الالتزام", "التنظيم", "الشهادة" - ], - "environmental": [ - "بيئي", "بيئة", "تلوث", "تصريف", "نفايات", "انبعاثات", "موارد طبيعية", - "تأثير بيئي", "استدامة", "تعويض بيئي", "ضرر بيئي", "مخلفات" - ], - "safety": [ - "سلامة", "أمان", "حوادث", "إصابات", "مخاطر صحية", "صحة مهنية", - "وقاية", "حماية", "إجراءات أمان", "تأمين سلامة", "مخاطر السلامة" - ], - "schedule": [ - "تأخير", "تمديد", "مدة", "جدول زمني", "موعد نهائي", "تسليم", - "مراحل", "مواعيد", "وقت", "فترة", "عاجل", "سريع", "فوري" - ], - "resource": [ - "مواد", "معدات", "أدوات", "آلات", "عمالة", "كوادر", "فريق", - "موارد بشرية", "توفير", "تأمين", "استقدام", "نقص", "عجز", "كفاية" - ], - "quality": [ - "جودة", "ضمان الجودة", "معايير", "مواصفات", "أداء", "رداءة", - "ضعف", "خلل", "عيب", "إصلاح", "صيانة", "استبدال", "رفض" - ], - "scope": [ - "نطاق العمل", "تغيير النطاق", "توسيع", "تقليص", "تعديل", "إضافة", - "أعمال إضافية", "تغييرات", "أوامر تغيير", "متطلبات جديدة" - ], - "stakeholder": [ - "طرف ثالث", "مالك", "عميل", "المقاول", "المورد", "الاستشاري", - "المشرف", "مدير المشروع", "المقاول من الباطن", "الشريك", "مصلحة" - ], - "commercial": [ - "منافسة", "سوق", "سعر", "عرض", "طلب", "تجاري", "أعمال", - "استثمار", "عائد", "ربح", "خسارة", "سمعة", "علامة تجارية" - ], - "contractual": [ - "بند", "شرط", "مادة", "اتفاقية", "عقد", "ملحق", "تعديل", - "تنازل", "تعهد", "التزام", "مسؤولية", "واجب", "حق" - ], - "regulatory": [ - "تنظيمي", "حكومي", "رسمي", "لائحة", "قانون", "تشريع", - "ترخيص", "تصريح", "موافقة", "امتثال", "اشتراطات", "متطلبات" - ] - } - - # نموذج تصنيف المخاطر - self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english') - self.kmeans = KMeans(n_clusters=len(self.risk_categories), random_state=42) - - # الصيغ والمصطلحات المتعلقة بالمخاطر التعاقدية - self.contract_risk_patterns = { - "unlimited_liability": [ - r"مسؤولية غير محدودة", - r"مسؤولية كاملة", - r"المسؤولية الكاملة", - r"دون تحديد للمسؤولية", - r"دون سقف للمسؤولية", - r"المسؤولية المطلقة", - r"التعويض عن كافة الأضرار" - ], - "payment_delay": [ - r"(\d+)\s*يوم\s*من تاريخ\s*الفاتورة", - r"(\d+)\s*يوم\s*عمل من تاريخ\s*الفاتورة", - r"(\d+)\s*يوم\s*للدفع", - r"خلال\s*(\d+)\s*يوم", - r"الدفع خلال\s*(\d+)\s*" - ], - "excessive_penalties": [ - r"غرامة تأخير بنسبة\s*(\d+)%", - r"غرامة تأخير قدرها\s*(\d+)%", - r"غرامة يومية\s*(\d+)%", - r"غرامة اسبوعية\s*(\d+)%", - r"غرامة شهرية\s*(\d+)%" - ], - "unilateral_termination": [ - r"يحق للطرف الأول إنهاء العقد", - r"يحق للعميل إنهاء العقد", - r"للعميل الحق في إنهاء", - r"للطرف الأول الحق في إنهاء", - r"إنهاء العقد من طرف واحد", - r"إنهاء دون إبداء أسباب" - ], - "unrealistic_deadlines": [ - r"التسليم خلال\s*(\d+)\s*يوم", - r"مدة التنفيذ\s*(\d+)\s*يوم", - r"الانتهاء خلال\s*(\d+)\s*يوم", - r"إنجاز المشروع خلال\s*(\d+)\s*أسبوع" - ], - "scope_creep": [ - r"أعمال إضافية", - r"تعديلات على النطاق", - r"توسيع نطاق العمل", - r"إضافة متطلبات", - r"تغيير المواصفات", - r"أعمال غير متوقعة" - ], - "indemnification": [ - r"تعويض الطرف الأول", - r"تعويض العميل", - r"تعويض كامل", - r"تعويض شامل", - r"التعويض عن كافة الأضرار", - r"التعويض عن أي خسائر" - ], - "change_control": [ - r"التغييرات بدون تكلفة إضافية", - r"تعديلات دون زيادة السعر", - r"تغييرات دون مقابل", - r"تعديلات لا تؤثر على السعر" - ], - "warranty_period": [ - r"ضمان لمدة\s*(\d+)\s*شهر", - r"ضمان لمدة\s*(\d+)\s*سنة", - r"فترة ضمان\s*(\d+)\s*شهر", - r"فترة الضمان\s*(\d+)\s*شهر", - r"فترة الصيانة\s*(\d+)\s*شهر" - ], - "dispute_resolution": [ - r"المحاكم المختصة", - r"محاكم[^.]*للنظر في المنازعات", - r"تسوية النزاعات", - r"فض المنازعات", - r"التحكيم", - r"لجنة تحكيم" - ], - "force_majeure": [ - r"القوة القاهرة", - r"الظروف القاهرة", - r"ظروف خارجة عن الإرادة", - r"أحداث غير متوقعة", - r"أسباب خارجة عن السيطرة" - ], - "regulatory_compliance": [ - r"الالتزام بالقوانين", - r"الالتزام بالأنظمة", - r"الالتزام بالتشريعات", - r"الالتزام باللوائح", - r"مراعاة القوانين", - r"وفقاً للقوانين", - r"طبقاً للأنظمة", - ], - "intellectual_property": [ - r"الملكية الفكرية", - r"حقوق الملكية", - r"حقوق الطبع", - r"حقوق النشر", - r"براءات الاختراع", - r"التصاميم", - r"العلامات التجارية" - ], - "confidentiality": [ - r"سرية المعلومات", - r"المعلومات السرية", - r"عدم الإفصاح", - r"الحفاظ على السرية", - r"عدم الكشف", - r"معلومات سرية" - ], - "insurance_requirements": [ - r"متطلبات التأمين", - r"بوليصة تأمين", - r"تأمين ضد المسؤولية", - r"تأمين ضد المخاطر", - r"تأمين شامل", - r"تأمين المشروع" - ] - } - - # تعريف مستويات خطورة المخاطر - self.severity_levels = { - "low": { - "name": "منخفضة", - "color": "#00b894", # أخضر - "score_range": (0, 33) - }, - "medium": { - "name": "متوسطة", - "color": "#fdcb6e", # أصفر - "score_range": (34, 66) - }, - "high": { - "name": "عالية", - "color": "#d63031", # أحمر - "score_range": (67, 100) - } - } - - # أوزان أنواع المخاطر (الأهمية النسبية) - self.risk_weights = { - "legal": 0.9, - "financial": 0.8, - "operational": 0.7, - "technical": 0.6, - "compliance": 0.8, - "environmental": 0.6, - "safety": 0.7, - "schedule": 0.6, - "resource": 0.5, - "quality": 0.7, - "scope": 0.7, - "stakeholder": 0.5, - "commercial": 0.6, - "contractual": 0.9, - "regulatory": 0.8 - } - - def scan_contract_text(self, contract_text, title=""): - """فحص نص العقد لاستخراج المخاطر المحتملة""" - if not contract_text: - return { - "title": title or "عقد غير معروف", - "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "risks": [], - "overall_score": 0, - "overall_severity": "منخفضة", - "summary": "لم يتم توفير نص للتحليل." - } - - # تحويل النص إلى أحرف صغيرة للفحص - text_lower = contract_text.lower() - - risks = [] - risk_id = 1 - - # فحص كل فئة مخاطر والبحث عن المصطلحات المرتبطة بها - for category, category_terms in self.risky_terms.items(): - category_risks = [] - - for term in category_terms: - # البحث عن المصطلح في النص - occurrences = self._find_term_occurrences(contract_text, term) - - if occurrences: - for occurrence in occurrences: - context = self._extract_context(contract_text, occurrence, window=100) - # تحديد مستوى الخطورة بناءً على السياق - severity = self._determine_severity_from_context(context, category) - - category_risks.append({ - "id": risk_id, - "term": term, - "category": category, - "category_ar": self.risk_categories[category], - "context": context, - "severity": severity, - "impact": self._determine_impact(category, severity), - "recommendation": self._generate_recommendation(category, term, severity) - }) - risk_id += 1 - - # إضافة مخاطر الفئة إلى القائمة الرئيسية - risks.extend(category_risks) - - # فحص صيغ المخاطر الإضافية في العقد - pattern_risks = self._scan_for_risk_patterns(contract_text, risk_id) - risks.extend(pattern_risks) - - # حساب درجة المخاطر الإجمالية - overall_score = self._calculate_overall_risk_score(risks) - - # تحديد مستوى الخطورة الإجمالية - overall_severity = self._determine_overall_severity(overall_score) - - # توليد ملخص للمخاطر - summary = self._generate_risk_summary(risks, overall_score, overall_severity) - - # إنشاء تقرير المخاطر - risk_report = { - "title": title or "عقد غير معروف", - "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "risks": risks, - "overall_score": overall_score, - "overall_severity": overall_severity["name"], - "severity_color": overall_severity["color"], - "summary": summary - } - - # حفظ تقرير المخاطر - if title: - self._save_risk_report(risk_report, title) - - return risk_report - - def _find_term_occurrences(self, text, term): - """البحث عن مواضع ظهور المصطلح في النص""" - occurrences = [] - start = 0 - - while True: - start = text.find(term, start) - if start == -1: - break - occurrences.append(start) - start += len(term) - - return occurrences - - def _extract_context(self, text, position, window=100): - """استخراج سياق النص حول موضع معين""" - start = max(0, position - window // 2) - end = min(len(text), position + window // 2) - - # البحث عن بداية الجملة - while start > 0 and text[start] not in ['.', '!', '؟', '?', '\n']: - start -= 1 - - if start > 0: - start += 1 # تجاوز علامة الترقيم - - # البحث عن نهاية الجملة - while end < len(text) - 1 and text[end] not in ['.', '!', '؟', '?', '\n']: - end += 1 - - if end < len(text) - 1: - end += 1 # تضمين علامة الترقيم - - return text[start:end].strip() - - def _determine_severity_from_context(self, context, category): - """تحديد مستوى خطورة المخاطر بناءً على السياق""" - # كلمات تزيد من مستوى الخطورة - high_severity_indicators = [ - "حرج", "خطير", "ضروري", "إلزامي", "يجب", "مطلوب", "ضمان", - "تعويض", "غرامة", "يلتزم", "مسؤولية", "خسارة", "ضرر", - "تأخير", "مخالفة", "إخلال", "فسخ", "إنهاء", "تعديل" - ] - - # كلمات تقلل من مستوى الخطورة - low_severity_indicators = [ - "قد", "يمكن", "يجوز", "يحتمل", "محتمل", "ممكن", "اختياري", - "تقديري", "بالتوافق", "بالاتفاق", "مناسب", "معقول", "بحسب" - ] - - # حساب عدد المؤشرات - high_count = sum(1 for indicator in high_severity_indicators if indicator in context) - low_count = sum(1 for indicator in low_severity_indicators if indicator in context) - - # تحديد درجة الخطورة - if high_count > low_count * 2: - return "high" - elif high_count > low_count: - return "medium" - else: - return "low" - - def _determine_impact(self, category, severity): - """تحديد تأثير المخاطر بناءً على الفئة ومستوى الخطورة""" - impact_descriptions = { - "legal": { - "high": "قد يؤدي إلى دعاوى قضائية ومسؤولية قانونية كبيرة", - "medium": "قد يتطلب تعديلات قانونية أو مفاوضات إضافية", - "low": "مخاطر قانونية محدودة يمكن معالجتها بسهولة" - }, - "financial": { - "high": "مخاطر مالية كبيرة قد تؤثر على ربحية المشروع بشكل كبير", - "medium": "قد يؤدي إلى زيادة التكاليف أو تقليل الهوامش", - "low": "تأثير مالي محدود يمكن استيعابه" - }, - "operational": { - "high": "قد يعيق تنفيذ المشروع بشكل كامل", - "medium": "قد يؤثر على كفاءة العمليات ويتطلب خطط بديلة", - "low": "تأثير محدود على العمليات اليومية" - }, - "technical": { - "high": "قد يمنع تحقيق المتطلبات الفنية الأساسية", - "medium": "يتطلب حلول فنية إضافية أو تعديلات", - "low": "يمكن معالجته من خلال التعديلات الفنية البسيطة" - }, - "compliance": { - "high": "قد يؤدي إلى عدم الامتثال للوائح الهامة", - "medium": "يتطلب تعديلات للامتثال للمتطلبات التنظيمية", - "low": "يمكن حله من خلال تدابير امتثال بسيطة" - }, - "environmental": { - "high": "مخاطر بيئية كبيرة قد تؤدي إلى عقوبات أو تأخيرات", - "medium": "يتطلب إجراءات وقائية إضافية للحماية البيئية", - "low": "تأثير بيئي محدود يمكن إدارته" - }, - "safety": { - "high": "مخاطر سلامة حرجة قد تهدد سلامة العاملين", - "medium": "يتطلب إجراءات سلامة إضافية وتدريب", - "low": "مخاطر سلامة يمكن معالجتها من خلال الإجراءات القياسية" - }, - "schedule": { - "high": "قد يؤدي إلى تأخيرات كبيرة في المشروع", - "medium": "قد يؤثر على بعض مراحل الجدول الزمني", - "low": "تأثير محدود على الجدول الزمني يمكن استيعابه" - }, - "resource": { - "high": "نقص حاد في الموارد الأساسية للمشروع", - "medium": "قد يتطلب موارد إضافية أو بديلة", - "low": "يمكن إدارته من خلال تخطيط الموارد المتاحة" - }, - "quality": { - "high": "قد يؤدي إلى مشاكل جودة خطيرة تؤثر على قبول المشروع", - "medium": "يتطلب إجراءات ضمان جودة إضافية", - "low": "تأثير محدود على الجودة يمكن معالجته" - }, - "scope": { - "high": "تغييرات جوهرية في نطاق العمل قد تؤثر على المشروع بأكمله", - "medium": "يتطلب تعديلات في بعض جوانب نطاق العمل", - "low": "تغييرات بسيطة في النطاق يمكن استيعابها" - }, - "stakeholder": { - "high": "قد يؤثر سلباً على العلاقات مع أصحاب المصلحة الرئيسيين", - "medium": "يتطلب إدارة توقعات أصحاب المصلحة", - "low": "تأثير محدود على رضا أصحاب المصلحة" - }, - "commercial": { - "high": "مخاطر تجارية كبيرة قد تؤثر على العلاقات التجارية الرئيسية", - "medium": "قد يتطلب إعادة التفاوض على بعض الشروط التجارية", - "low": "تأثير تجاري محدود يمكن إدارته" - }, - "contractual": { - "high": "بنود تعاقدية مجحفة قد تؤثر على التزامات وحقوق الأطراف", - "medium": "يتطلب مراجعة قانونية وتعديل بعض البنود", - "low": "قضايا تعاقدية بسيطة يمكن توضيحها" - }, - "regulatory": { - "high": "قد يؤدي إلى مخالفة لوائح تنظيمية هامة", - "medium": "يتطلب تغييرات للامتثال للمتطلبات التنظيمية", - "low": "متطلبات تنظيمية يمكن تلبيتها بسهولة" - } - } - - return impact_descriptions.get(category, {}).get(severity, "تأثير غير محدد") - - def _generate_recommendation(self, category, term, severity): - """توليد توصيات لمعالجة المخاطر""" - recommendations = { - "legal": { - "high": "مراجعة قانونية شاملة من محامي متخصص وإعادة التفاوض على البنود المتعلقة بـ'{term}'", - "medium": "مراجعة قانونية والتأكد من الصياغة الدقيقة للبنود المتعلقة بـ'{term}'", - "low": "مراقبة البنود المتعلقة بـ'{term}' أثناء تنفيذ العقد" - }, - "financial": { - "high": "إعادة التفاوض على الشروط المالية والتأكد من وجود مخصصات كافية لتغطية المخاطر المتعلقة بـ'{term}'", - "medium": "وضع خطة احتياطية لإدارة التكاليف المرتبطة بـ'{term}'", - "low": "متابعة الجوانب المالية المتعلقة بـ'{term}' بشكل دوري" - }, - "operational": { - "high": "وضع خطة تفصيلية لإدارة المخاطر التشغيلية المتعلقة بـ'{term}' وتوفير بدائل", - "medium": "تطوير إجراءات للتعامل مع المشكلات التشغيلية المتعلقة بـ'{term}'", - "low": "متابعة العمليات المتعلقة بـ'{term}' بشكل منتظم" - }, - "technical": { - "high": "الاستعانة بخبراء فنيين متخصصين لمراجعة المتطلبات المتعلقة بـ'{term}'", - "medium": "إجراء مراجعة فنية للتأكد من قابلية تنفيذ المتطلبات المتعلقة بـ'{term}'", - "low": "التأكد من وضوح المواصفات الفنية المتعلقة بـ'{term}'" - }, - "compliance": { - "high": "مراجعة متخصصة للتأكد من الامتثال للوائح المتعلقة بـ'{term}' وإجراء التعديلات اللازمة", - "medium": "وضع إجراءات للتأكد من الامتثال المستمر للمتطلبات المتعلقة بـ'{term}'", - "low": "متابعة متطلبات الامتثال المتعلقة بـ'{term}' بشكل دوري" - }, - "environmental": { - "high": "إجراء تقييم بيئي شامل والتأكد من وجود خطط للتعامل مع المخاطر البيئية المتعلقة بـ'{term}'", - "medium": "مراجعة الإجراءات البيئية المتعلقة بـ'{term}' والتأكد من كفايتها", - "low": "متابعة الجوانب البيئية المتعلقة بـ'{term}' أثناء تنفيذ المشروع" - }, - "safety": { - "high": "وضع خطة سلامة شاملة ومراجعتها من قبل متخصصين للتعامل مع المخاطر المتعلقة بـ'{term}'", - "medium": "مراجعة إجراءات السلامة الحالية وتعزيزها للتعامل مع المخاطر المتعلقة بـ'{term}'", - "low": "التأكد من تطبيق إجراءات السلامة القياسية المتعلقة بـ'{term}'" - }, - "schedule": { - "high": "إعادة تقييم الجدول الزمني بشكل شامل ووضع خطط بديلة للتعامل مع البنود المتعلقة بـ'{term}'", - "medium": "وضع هوامش زمنية كافية للتعامل مع التأخيرات المحتملة المتعلقة بـ'{term}'", - "low": "مراقبة الجدول الزمني بشكل منتظم فيما يتعلق بـ'{term}'" - }, - "resource": { - "high": "وضع خطة شاملة لتأمين الموارد اللازمة والبدائل المتعلقة بـ'{term}'", - "medium": "تحديد مصادر بديلة للموارد المتعلقة بـ'{term}'", - "low": "مراقبة توافر الموارد المتعلقة بـ'{term}' بشكل منتظم" - }, - "quality": { - "high": "وضع خطة ضمان جودة شاملة والتأكد من وجود معايير واضحة للجوانب المتعلقة بـ'{term}'", - "medium": "تعزيز إجراءات ضمان الجودة للجوانب المتعلقة بـ'{term}'", - "low": "متابعة معايير الجودة المتعلقة بـ'{term}' بشكل منتظم" - }, - "scope": { - "high": "توثيق نطاق العمل بشكل تفصيلي ووضع إجراءات واضحة للتعامل مع التغييرات المتعلقة بـ'{term}'", - "medium": "وضع آلية للتحكم في التغييرات المتعلقة بـ'{term}'", - "low": "مراقبة نطاق العمل بشكل منتظم فيما يتعلق بـ'{term}'" - }, - "stakeholder": { - "high": "وضع خطة تواصل شاملة مع أصحاب المصلحة للتعامل مع القضايا المتعلقة بـ'{term}'", - "medium": "تعزيز التواصل مع أصحاب المصلحة المعنيين بـ'{term}'", - "low": "متابعة توقعات وملاحظات أصحاب المصلحة فيما يتعلق بـ'{term}'" - }, - "commercial": { - "high": "إعادة التفاوض على الشروط التجارية المتعلقة بـ'{term}' والتأكد من تحقيق توازن المصالح", - "medium": "مراجعة الشروط التجارية المتعلقة بـ'{term}' والتأكد من وضوحها", - "low": "مراقبة تنفيذ الشروط التجارية المتعلقة بـ'{term}'" - }, - "contractual": { - "high": "مراجعة قانونية شاملة للبنود التعاقدية المتعلقة بـ'{term}' وإعادة التفاوض عند الضرورة", - "medium": "توضيح وتحسين صياغة البنود المتعلقة بـ'{term}'", - "low": "التأكد من فهم الالتزامات التعاقدية المتعلقة بـ'{term}'" - }, - "regulatory": { - "high": "الاستعانة بمستشار متخصص للتأكد من الامتثال للمتطلبات التنظيمية المتعلقة بـ'{term}'", - "medium": "مراجعة المتطلبات التنظيمية الحالية والمستقبلية المتعلقة بـ'{term}'", - "low": "متابعة التغييرات في المتطلبات التنظيمية المتعلقة بـ'{term}'" - } - } - - recommendation_template = recommendations.get(category, {}).get(severity, "مراجعة البنود المتعلقة بـ'{term}'") - return recommendation_template.replace('{term}', term) - - def _scan_for_risk_patterns(self, contract_text, risk_id_start): - """فحص النص بحثاً عن صيغ المخاطر المحددة مسبقاً""" - risk_id = risk_id_start - pattern_risks = [] - - for pattern_type, patterns in self.contract_risk_patterns.items(): - for pattern in patterns: - matches = re.finditer(pattern, contract_text) - - for match in matches: - match_text = match.group(0) - context = self._extract_context(contract_text, match.start(), window=150) - severity = self._determine_pattern_severity(pattern_type, match_text) - category = self._map_pattern_to_category(pattern_type) - - pattern_risks.append({ - "id": risk_id, - "term": match_text, - "category": category, - "category_ar": self.risk_categories[category], - "pattern_type": pattern_type, - "context": context, - "severity": severity, - "impact": self._determine_pattern_impact(pattern_type, severity), - "recommendation": self._generate_pattern_recommendation(pattern_type, match_text, severity) - }) - risk_id += 1 - - return pattern_risks - - def _determine_pattern_severity(self, pattern_type, match_text): - """تحديد مستوى خطورة المخاطر بناءً على نوع الصيغة ومحتواها""" - severity_rules = { - "unlimited_liability": "high", - "payment_delay": lambda text: "high" if any(int(n) > 60 for n in re.findall(r'(\d+)', text)) else "medium" if any(int(n) > 30 for n in re.findall(r'(\d+)', text)) else "low", - "excessive_penalties": lambda text: "high" if any(int(n) > 1 for n in re.findall(r'(\d+)', text)) else "medium" if any(int(n) > 0.5 for n in re.findall(r'(\d+)', text)) else "low", - "unilateral_termination": "high", - "unrealistic_deadlines": lambda text: "high" if any(int(n) < 30 for n in re.findall(r'(\d+)', text)) else "medium" if any(int(n) < 60 for n in re.findall(r'(\d+)', text)) else "low", - "scope_creep": "medium", - "indemnification": "high", - "change_control": "high", - "warranty_period": lambda text: "high" if any(int(n) > 24 for n in re.findall(r'(\d+)', text)) else "medium" if any(int(n) > 12 for n in re.findall(r'(\d+)', text)) else "low", - "dispute_resolution": "medium", - "force_majeure": "medium", - "regulatory_compliance": "medium", - "intellectual_property": "high", - "confidentiality": "medium", - "insurance_requirements": "medium" - } - - rule = severity_rules.get(pattern_type, "medium") - - if callable(rule): - return rule(match_text) - else: - return rule - - def _map_pattern_to_category(self, pattern_type): - """تعيين نوع الصيغة إلى فئة المخاطر المناسبة""" - pattern_category_map = { - "unlimited_liability": "legal", - "payment_delay": "financial", - "excessive_penalties": "financial", - "unilateral_termination": "contractual", - "unrealistic_deadlines": "schedule", - "scope_creep": "scope", - "indemnification": "legal", - "change_control": "scope", - "warranty_period": "quality", - "dispute_resolution": "legal", - "force_majeure": "contractual", - "regulatory_compliance": "compliance", - "intellectual_property": "legal", - "confidentiality": "contractual", - "insurance_requirements": "financial" - } - - return pattern_category_map.get(pattern_type, "contractual") - - def _determine_pattern_impact(self, pattern_type, severity): - """تحديد تأثير المخاطر بناءً على نوع الصيغة ومستوى الخطورة""" - pattern_impact = { - "unlimited_liability": { - "high": "يمكن أن يعرض الشركة لمسؤولية مالية وقانونية غير محدودة", - "medium": "قد يؤدي إلى مسؤولية مالية كبيرة غير متوقعة", - "low": "زيادة محتملة في المسؤولية القانونية" - }, - "payment_delay": { - "high": "تأخر كبير في الدفعات قد يؤثر على التدفق النقدي والسيولة", - "medium": "قد يتسبب في ضغط على التدفق النقدي", - "low": "تأثير محدود على التدفق النقدي" - }, - "excessive_penalties": { - "high": "غرامات تأخير مرتفعة قد تؤثر بشكل كبير على ربحية المشروع", - "medium": "غرامات معتدلة قد تقلل من هامش الربح", - "low": "غرامات محدودة يمكن إدارتها من خلال الجدولة الدقيقة" - }, - "unilateral_termination": { - "high": "إمكانية إنهاء العقد من طرف واحد دون تعويض مناسب", - "medium": "شروط إنهاء غير متوازنة قد تتطلب إعادة التفاوض", - "low": "بنود إنهاء تحتاج إلى مراقبة وتوثيق" - }, - "unrealistic_deadlines": { - "high": "مواعيد نهائية غير واقعية قد تؤدي إلى فشل المشروع أو غرامات كبيرة", - "medium": "جدول زمني ضيق يتطلب موارد إضافية وإدارة مكثفة", - "low": "مواعيد نهائية تحتاج إلى تخطيط دقيق" - }, - "scope_creep": { - "high": "توسع غير محدود في نطاق العمل دون تعديل السعر أو الجدول الزمني", - "medium": "تغييرات محتملة في النطاق تتطلب إدارة دقيقة", - "low": "بعض التعديلات المحتملة على النطاق يمكن إدارتها" - }, - "indemnification": { - "high": "التزامات تعويض واسعة النطاق قد تؤدي إلى مسؤولية غير محدودة", - "medium": "شروط تعويض تحتاج إلى مراجعة قانونية", - "low": "التزامات تعويض معقولة تحتاج إلى متابعة" - }, - "change_control": { - "high": "عدم وجود آلية واضحة للتحكم في التغييرات وتأثيرها على التكلفة", - "medium": "آلية تغيير غير كافية قد تؤدي إلى نزاعات", - "low": "إجراءات تغيير تحتاج إلى تحسين" - }, - "warranty_period": { - "high": "فترة ضمان طويلة غير متناسبة مع طبيعة المشروع", - "medium": "فترة ضمان تتطلب موارد إضافية للدعم", - "low": "فترة ضمان معقولة تحتاج إلى تخطيط" - }, - "dispute_resolution": { - "high": "آليات غير مناسبة لحل النزاعات قد تؤدي إلى إجراءات مكلفة", - "medium": "آليات حل النزاعات تحتاج إلى توضيح", - "low": "شروط حل النزاعات تحتاج إلى مراجعة" - }, - "force_majeure": { - "high": "تعريف ضيق للقوة القاهرة قد يؤدي إلى مسؤولية غير متوقعة", - "medium": "بنود القوة القاهرة تحتاج إلى توضيح", - "low": "شروط القوة القاهرة معقولة ولكن تحتاج إلى مراقبة" - }, - "regulatory_compliance": { - "high": "متطلبات امتثال صارمة قد تزيد التكاليف أو المسؤولية", - "medium": "التزامات الامتثال تحتاج إلى موارد إضافية", - "low": "متطلبات امتثال معقولة تحتاج إلى مراقبة" - }, - "intellectual_property": { - "high": "نقل واسع لحقوق الملكية الفكرية دون تعويض مناسب", - "medium": "شروط الملكية الفكرية تحتاج إلى توضيح وتعديل", - "low": "بنود الملكية الفكرية تحتاج إلى مراجعة" - }, - "confidentiality": { - "high": "التزامات سرية واسعة وطويلة الأمد قد تقيد النشاط المستقبلي", - "medium": "التزامات السرية تحتاج إلى تحديد نطاق ومدة", - "low": "شروط السرية معقولة ولكن تحتاج إلى مراقبة" - }, - "insurance_requirements": { - "high": "متطلبات تأمين مرتفعة قد تزيد التكاليف بشكل كبير", - "medium": "متطلبات التأمين تحتاج إلى مراجعة للتأكد من التناسب", - "low": "متطلبات تأمين معقولة تحتاج إلى التحقق من التوافر" - } - } - - return pattern_impact.get(pattern_type, {}).get(severity, "تأثير غير محدد") - - def _generate_pattern_recommendation(self, pattern_type, match_text, severity): - """توليد توصيات لمعالجة المخاطر بناءً على نوع الصيغة""" - pattern_recommendations = { - "unlimited_liability": { - "high": "إعادة التفاوض على بنود المسؤولية وتحديد سقف للتعويضات يتناسب مع قيمة العقد", - "medium": "وضع حدود واضحة للمسؤولية وطلب تعديل البنود المتعلقة بها", - "low": "مراجعة بنود المسؤولية والتأكد من وجود تغطية تأمينية مناسبة" - }, - "payment_delay": { - "high": "إعادة التفاوض على شروط الدفع وتقليل فترة السداد، مع إضافة فوائد تأخير", - "medium": "وضع آلية واضحة لمتابعة المدفوعات وتحديد إجراءات التصعيد في حالة التأخر", - "low": "مراقبة مواعيد الدفع والتأكد من إصدار الفواتير في الوقت المناسب" - }, - "excessive_penalties": { - "high": "إعادة التفاوض على نسب وآليات غرامات التأخير وربطها بالضرر الفعلي", - "medium": "وضع حد أقصى للغرامات ووضع خطة لتجنب التأخير", - "low": "مراقبة تقدم العمل بدقة للالتزام بالجدول الزمني" - }, - "unilateral_termination": { - "high": "تعديل بنود الإنهاء لتكون متوازنة وتضمين تعويض مناسب في حالة الإنهاء", - "medium": "وضع شروط واضحة للإنهاء من كلا الطرفين وتحديد آلية التعويض", - "low": "التأكد من وجود خطة للتعامل مع حالات الإنهاء المحتملة" - }, - "unrealistic_deadlines": { - "high": "إعادة التفاوض على الجدول الزمني ليكون واقعياً بناءً على تقييم دقيق للموارد والقدرات", - "medium": "وضع خطة تفصيلية للتنفيذ مع تحديد المراحل الحرجة وتوفير موارد إضافية", - "low": "مراقبة الجدول الزمني بشكل مستمر وتحديد المخاطر المحتملة" - }, - "scope_creep": { - "high": "تحديد نطاق العمل بدقة ووضع إجراءات صارمة لإدارة التغييرات مع ربطها بالتكلفة والوقت", - "medium": "وضع آلية واضحة لإدارة التغييرات والتأكد من توثيق نطاق العمل بشكل تفصيلي", - "low": "مراقبة نطاق العمل والتأكد من موافقة جميع الأطراف على أي تغييرات" - }, - "indemnification": { - "high": "إعادة التفاوض على بنود التعويض لتكون متوازنة ومحددة بمبلغ يتناسب مع قيمة العقد", - "medium": "تحديد نطاق التعويض وربطه بالأضرار المباشرة والفعلية", - "low": "مراجعة بنود التعويض والتأكد من وجود تغطية تأمينية مناسبة" - }, - "change_control": { - "high": "وضع آلية واضحة وصارمة لإدارة التغييرات مع تحديد التأثير على التكلفة والوقت", - "medium": "تحسين إجراءات إدارة التغييرات والتأكد من توثيق جميع التغييرات", - "low": "مراقبة التغييرات والتأكد من الحصول على موافقة مكتوبة قبل التنفيذ" - }, - "warranty_period": { - "high": "إعادة التفاوض على فترة الضمان لتكون متناسبة مع طبيعة المشروع والمعايير الصناعية", - "medium": "تحديد نطاق الضمان بوضوح وتخصيص موارد كافية للدعم خلال فترة الضمان", - "low": "وضع خطة لإدارة التزامات الضمان والتأكد من توثيق حالة التسليم" - }, - "dispute_resolution": { - "high": "تعديل آليات حل النزاعات لتشمل التفاوض والوساطة قبل اللجوء للتحكيم أو القضاء", - "medium": "توضيح إجراءات حل النزاعات وتحديد الاختصاص القضائي والقانون الواجب التطبيق", - "low": "مراجعة آليات حل النزاعات والتأكد من فهم الإجراءات المتبعة" - }, - "force_majeure": { - "high": "توسيع تعريف القوة القاهرة ليشمل الحالات المحتملة وتحديد آلية واضحة للإخطار والتعامل", - "medium": "توضيح إجراءات الإخطار والإجراءات المتبعة في حالات القوة القاهرة", - "low": "مراجعة بنود القوة القاهرة والتأكد من شمولها للحالات المحتملة" - }, - "regulatory_compliance": { - "high": "تحديد مسؤوليات كل طرف بوضوح فيما يتعلق بالالتزامات التنظيمية والحصول على المشورة القانونية", - "medium": "مراجعة متطلبات الامتثال والتأكد من القدرة على تلبيتها", - "low": "متابعة التغييرات في المتطلبات التنظيمية والتأكد من الالتزام المستمر" - }, - "intellectual_property": { - "high": "إعادة التفاوض على بنود الملكية الفكرية لحماية حقوق الشركة والحصول على تعويض مناسب", - "medium": "توضيح حقوق الملكية الفكرية لكل طرف وتحديد نطاق الاستخدام المسموح به", - "low": "مراجعة بنود الملكية الفكرية والتأكد من حماية الأصول الفكرية للشركة" - }, - "confidentiality": { - "high": "تحديد نطاق ومدة التزامات السرية بشكل واضح ومتوازن لتجنب القيود غير الضرورية", - "medium": "توضيح نطاق المعلومات السرية وتحديد مدة معقولة للالتزام بالسرية", - "low": "مراجعة التزامات السرية والتأكد من إمكانية الالتزام بها" - }, - "insurance_requirements": { - "high": "إعادة التفاوض على متطلبات التأمين لتكون متناسبة مع طبيعة وحجم المشروع والمخاطر الفعلية", - "medium": "التحقق من توافر وتكلفة التغطية التأمينية المطلوبة والتفاوض على تعديلها إذا لزم الأمر", - "low": "التأكد من توافر التغطية التأمينية المطلوبة والحفاظ على سريانها" - } - } - - return pattern_recommendations.get(pattern_type, {}).get(severity, "مراجعة وتعديل البنود المتعلقة بهذه المخاطر") - - def _calculate_overall_risk_score(self, risks): - """حساب درجة المخاطر الإجمالية""" - if not risks: - return 0 - - # تحويل مستويات الخطورة إلى قيم عددية - severity_scores = {"low": 25, "medium": 50, "high": 90} - - # حساب مجموع الأوزان ودرجات المخاطر المرجحة - total_weight = 0 - weighted_score_sum = 0 - - # تجميع المخاطر حسب الفئة - risk_categories = {} - for risk in risks: - category = risk["category"] - severity = risk["severity"] - - if category not in risk_categories: - risk_categories[category] = [] - - risk_categories[category].append(severity_scores[severity]) - - # حساب متوسط درجة المخاطرة لكل فئة وترجيحها بالوزن - for category, scores in risk_categories.items(): - category_weight = self.risk_weights.get(category, 0.5) - category_score = sum(scores) / len(scores) - - weighted_score_sum += category_score * category_weight - total_weight += category_weight - - # حساب الدرجة الإجمالية المرجحة - if total_weight > 0: - overall_score = int(weighted_score_sum / total_weight) - else: - overall_score = 0 - - return min(100, max(0, overall_score)) - - def _determine_overall_severity(self, overall_score): - """تحديد مستوى الخطورة الإجمالية بناءً على الدرجة الإجمالية""" - for severity, info in self.severity_levels.items(): - min_score, max_score = info["score_range"] - if min_score <= overall_score <= max_score: - return { - "level": severity, - "name": info["name"], - "color": info["color"] - } - - # القيمة الافتراضية - return { - "level": "low", - "name": "منخفضة", - "color": "#00b894" - } - - def _generate_risk_summary(self, risks, overall_score, overall_severity): - """توليد ملخص للمخاطر المكتشفة""" - if not risks: - return "لم يتم اكتشاف مخاطر كبيرة في العقد." - - # تجميع المخاطر حسب الفئة ومستوى الخطورة - risk_categories = {} - for risk in risks: - category = risk["category"] - category_ar = risk["category_ar"] - severity = risk["severity"] - - if category not in risk_categories: - risk_categories[category] = { - "name_ar": category_ar, - "high": 0, - "medium": 0, - "low": 0, - "total": 0 - } - - risk_categories[category][severity] += 1 - risk_categories[category]["total"] += 1 - - # حساب إجماليات المخاطر - total_risks = len(risks) - high_risks = sum(risk_categories[category]["high"] for category in risk_categories) - medium_risks = sum(risk_categories[category]["medium"] for category in risk_categories) - low_risks = sum(risk_categories[category]["low"] for category in risk_categories) - - # بناء نص الملخص - summary = f"تم تحديد {total_risks} مخاطر محتملة في العقد مع درجة خطورة إجمالية {overall_score}% ({overall_severity['name']})." - summary += f" وتتضمن {high_risks} مخاطر عالية، و{medium_risks} مخاطر متوسطة، و{low_risks} مخاطر منخفضة." - - # ذكر أهم فئات المخاطر - summary += " أهم فئات المخاطر المحددة هي:" - - # ترتيب فئات المخاطر حسب الأهمية - sorted_categories = sorted( - risk_categories.items(), - key=lambda x: (x[1]["high"], x[1]["medium"], x[1]["total"]), - reverse=True - ) - - # إضافة أهم 3 فئات مخاطر إلى الملخص - for i, (category, data) in enumerate(sorted_categories[:3]): - summary += f" {data['name_ar']} ({data['total']} مخاطر، منها {data['high']} عالية)" - if i < 2: - summary += "،" - else: - summary += "." - - # إضافة توصية عامة - if high_risks > 0: - summary += " يوصى بمراجعة العقد بشكل دقيق ومناقشة المخاطر العالية مع الأطراف المعنية قبل التوقيع." - elif medium_risks > total_risks / 2: - summary += " يوصى بمراجعة المخاطر المتوسطة وتقييم تأثيرها المحتمل قبل التوقيع." - else: - summary += " يمكن قبول العقد مع مراقبة المخاطر المحددة أثناء التنفيذ." - - return summary - - def _save_risk_report(self, risk_report, report_name): - """حفظ تقرير المخاطر كملف JSON""" - filename = f"{report_name.replace(' ', '_')}_risk_report.json" - file_path = os.path.join(self.risk_data_dir, filename) - - try: - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(risk_report, f, ensure_ascii=False, indent=2) - except Exception as e: - print(f"خطأ في حفظ تقرير المخاطر: {e}") - - def load_risk_report(self, report_name): - """تحميل تقرير مخاطر محفوظ مسبقاً""" - filename = f"{report_name.replace(' ', '_')}_risk_report.json" - file_path = os.path.join(self.risk_data_dir, filename) - - if not os.path.exists(file_path): - return None - - try: - with open(file_path, 'r', encoding='utf-8') as f: - risk_report = json.load(f) - return risk_report - except Exception as e: - print(f"خطأ في تحميل تقرير المخاطر: {e}") - return None - - def generate_risk_comparison(self, contract_text1, contract_text2, title1="العقد الأول", title2="العقد الثاني"): - """مقارنة المخاطر بين عقدين""" - # تحليل المخاطر في كل عقد - report1 = self.scan_contract_text(contract_text1, title1) - report2 = self.scan_contract_text(contract_text2, title2) - - # مقارنة درجات المخاطر الإجمالية - score_diff = report1["overall_score"] - report2["overall_score"] - - # تحديد العقد الأقل مخاطرة - less_risky_contract = title2 if score_diff > 0 else title1 - - # تجميع المخاطر حسب الفئة لكل عقد - categories1 = self._group_risks_by_category(report1["risks"]) - categories2 = self._group_risks_by_category(report2["risks"]) - - # تحديد الفئات الموجودة في كلا العقدين - all_categories = set(categories1.keys()) | set(categories2.keys()) - - # مقارنة المخاطر في كل فئة - category_comparison = {} - for category in all_categories: - cat_risks1 = categories1.get(category, {"high": 0, "medium": 0, "low": 0, "total": 0, "name_ar": self.risk_categories.get(category, category)}) - cat_risks2 = categories2.get(category, {"high": 0, "medium": 0, "low": 0, "total": 0, "name_ar": self.risk_categories.get(category, category)}) - - # حساب الفرق في المخاطر العالية والمتوسطة - high_diff = cat_risks1["high"] - cat_risks2["high"] - medium_diff = cat_risks1["medium"] - cat_risks2["medium"] - total_diff = cat_risks1["total"] - cat_risks2["total"] - - category_comparison[category] = { - "name_ar": cat_risks1["name_ar"], - "contract1": cat_risks1, - "contract2": cat_risks2, - "high_diff": high_diff, - "medium_diff": medium_diff, - "total_diff": total_diff - } - - # تجميع المخاطر المشتركة والفريدة - common_risks = [] - unique_risks1 = [] - unique_risks2 = [] - - # تحديد المخاطر المشتركة والفريدة (بناءً على المصطلحات) - terms1 = set(risk["term"] for risk in report1["risks"]) - terms2 = set(risk["term"] for risk in report2["risks"]) - - common_terms = terms1 & terms2 - unique_terms1 = terms1 - terms2 - unique_terms2 = terms2 - terms1 - - # تجميع المخاطر المشتركة - for risk in report1["risks"]: - if risk["term"] in common_terms: - common_risks.append({ - "term": risk["term"], - "category": risk["category"], - "category_ar": risk["category_ar"], - "contract": title1, - "severity": risk["severity"] - }) - - for risk in report2["risks"]: - if risk["term"] in common_terms: - common_risks.append({ - "term": risk["term"], - "category": risk["category"], - "category_ar": risk["category_ar"], - "contract": title2, - "severity": risk["severity"] - }) - - # تجميع المخاطر الفريدة - for risk in report1["risks"]: - if risk["term"] in unique_terms1: - unique_risks1.append(risk) - - for risk in report2["risks"]: - if risk["term"] in unique_terms2: - unique_risks2.append(risk) - - # إنشاء تقرير المقارنة - comparison_report = { - "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "contract1": { - "title": title1, - "overall_score": report1["overall_score"], - "overall_severity": report1["overall_severity"], - "risk_count": len(report1["risks"]) - }, - "contract2": { - "title": title2, - "overall_score": report2["overall_score"], - "overall_severity": report2["overall_severity"], - "risk_count": len(report2["risks"]) - }, - "score_diff": abs(score_diff), - "less_risky_contract": less_risky_contract, - "category_comparison": category_comparison, - "common_risks": common_risks, - "unique_risks1": unique_risks1, - "unique_risks2": unique_risks2, - "summary": self._generate_comparison_summary(report1, report2, title1, title2, score_diff, category_comparison) - } - - return comparison_report - - def _group_risks_by_category(self, risks): - """تجميع المخاطر حسب الفئة""" - categories = {} - - for risk in risks: - category = risk["category"] - severity = risk["severity"] - - if category not in categories: - categories[category] = { - "high": 0, - "medium": 0, - "low": 0, - "total": 0, - "name_ar": risk["category_ar"] - } - - categories[category][severity] += 1 - categories[category]["total"] += 1 - - return categories - - def _generate_comparison_summary(self, report1, report2, title1, title2, score_diff, category_comparison): - """توليد ملخص للمقارنة بين العقدين""" - # تحديد العقد الأقل مخاطرة - less_risky = title1 if score_diff <= 0 else title2 - - summary = f"مقارنة بين {title1} و{title2} أظهرت فرق في درجة المخاطرة الإجمالية بنسبة {abs(score_diff)}%، حيث كان {less_risky} هو الأقل مخاطرة. " - - # تحديد الفئات ذات الاختلافات الكبيرة - significant_diff_categories = [] - for category, data in category_comparison.items(): - if abs(data["high_diff"]) > 1 or abs(data["total_diff"]) > 3: - significant_diff_categories.append((category, data)) - - # ترتيب الفئات حسب الاختلاف - significant_diff_categories.sort(key=lambda x: (abs(x[1]["high_diff"]), abs(x[1]["total_diff"])), reverse=True) - - # إضافة معلومات عن الفئات ذات الاختلافات الكبيرة - if significant_diff_categories: - summary += "أبرز الاختلافات كانت في فئات: " - - for i, (category, data) in enumerate(significant_diff_categories[:3]): - name_ar = data["name_ar"] - more_risky = title1 if data["total_diff"] > 0 else title2 - diff = abs(data["total_diff"]) - - summary += f"{name_ar} (الفرق: {diff} مخاطر لصالح {more_risky})" - if i < len(significant_diff_categories[:3]) - 1: - summary += "، " - else: - summary += ". " - - # إضافة توصية - if abs(score_diff) > 20: - summary += f"يوصى بالتفاوض على إعادة صياغة العقد على أساس البنود الأقل مخاطرة من {less_risky}." - elif abs(score_diff) > 10: - summary += f"يوصى بمراجعة البنود المتعلقة بالمخاطر العالية ومقارنتها بين العقدين للتفاوض على تحسينها." - else: - summary += "لا توجد اختلافات كبيرة في المخاطر بين العقدين، ويمكن اختيار أيهما بناءً على معايير أخرى." - - return summary - - def render_risk_dashboard(self, contract_text, title="العقد"): - """عرض لوحة معلومات تحليل المخاطر""" - st.markdown("

تحليل مخاطر العقود الآلي

", unsafe_allow_html=True) - - if not contract_text: - st.warning("يرجى إدخال نص العقد أو تحميل ملف العقد للتحليل") - return - - # تحليل العقد - risk_report = self.scan_contract_text(contract_text, title) - - # عرض ملخص المخاطر - st.markdown("

ملخص تحليل المخاطر

", unsafe_allow_html=True) - - col1, col2, col3 = st.columns([1, 1, 1]) - - with col1: - overall_score = risk_report["overall_score"] - severity = risk_report["overall_severity"] - color = risk_report["severity_color"] - - st.markdown(f""" -
-
درجة المخاطرة الإجمالية
-
{overall_score}%
-
{severity}
-
- """, unsafe_allow_html=True) - - with col2: - high_risks = sum(1 for risk in risk_report["risks"] if risk["severity"] == "high") - medium_risks = sum(1 for risk in risk_report["risks"] if risk["severity"] == "medium") - low_risks = sum(1 for risk in risk_report["risks"] if risk["severity"] == "low") - - st.markdown(f""" -
-
توزيع المخاطر
-
-
{high_risks} عالية
-
{medium_risks} متوسطة
-
{low_risks} منخفضة
-
-
- """, unsafe_allow_html=True) - - with col3: - # تجميع المخاطر حسب الفئة - categories = self._group_risks_by_category(risk_report["risks"]) - top_categories = sorted(categories.items(), key=lambda x: x[1]["total"], reverse=True)[:3] - - st.markdown(""" -
-
أبرز فئات المخاطر
-
- """, unsafe_allow_html=True) - - for category, data in top_categories: - st.markdown(f""" -
-
{data['name_ar']}
-
{data['total']} مخاطر
-
- """, unsafe_allow_html=True) - - st.markdown("
", unsafe_allow_html=True) - - # عرض ملخص نصي - st.markdown(f""" -
- {risk_report["summary"]} -
- """, unsafe_allow_html=True) - - # عرض مخطط توزيع المخاطر حسب الفئة والخطورة - st.markdown("

توزيع المخاطر حسب الفئة

", unsafe_allow_html=True) - - # تجميع البيانات للمخطط - chart_data = [] - for category, data in categories.items(): - chart_data.append({"category": data["name_ar"], "severity": "عالية", "count": data["high"]}) - chart_data.append({"category": data["name_ar"], "severity": "متوسطة", "count": data["medium"]}) - chart_data.append({"category": data["name_ar"], "severity": "منخفضة", "count": data["low"]}) - - # التحقق من وجود بيانات قبل إنشاء الرسم البياني - if chart_data: - chart_df = pd.DataFrame(chart_data) - - # إنشاء مخطط باستخدام plotly - fig = px.bar( - chart_df, - x="category", - y="count", - color="severity", - title="توزيع المخاطر حسب الفئة والخطورة", - labels={"category": "فئة المخاطر", "count": "عدد المخاطر", "severity": "مستوى الخطورة"}, - color_discrete_map={"عالية": "#d63031", "متوسطة": "#fdcb6e", "منخفضة": "#00b894"} - ) - - # تنسيق المخطط - fig.update_layout( - barmode='stack', - xaxis={'categoryorder': 'total descending'}, - direction='rtl', - font=dict(family="Arial, sans-serif", size=14), - height=400, - margin=dict(l=10, r=10, t=50, b=10) - ) - - st.plotly_chart(fig, use_container_width=True) - else: - # إذا لم تكن هناك بيانات، عرض رسالة بديلة - st.info("لم يتم العثور على مخاطر كافية لعرض الرسم البياني") - - # عرض مخطط دائري لتوزيع مستويات الخطورة - col1, col2 = st.columns([1, 1]) - - with col1: - severity_counts = { - "عالية": high_risks, - "متوسطة": medium_risks, - "منخفضة": low_risks - } - - pie_df = pd.DataFrame({ - "مستوى الخطورة": list(severity_counts.keys()), - "العدد": list(severity_counts.values()) - }) - - fig = px.pie( - pie_df, - values="العدد", - names="مستوى الخطورة", - title="توزيع مستويات الخطورة", - color="مستوى الخطورة", - color_discrete_map={"عالية": "#d63031", "متوسطة": "#fdcb6e", "منخفضة": "#00b894"} - ) - - fig.update_layout( - font=dict(family="Arial, sans-serif", size=14), - height=350 - ) - - st.plotly_chart(fig, use_container_width=True) - - with col2: - # تجميع المخاطر حسب النوع (pattern_type) - pattern_types = {} - for risk in risk_report["risks"]: - if "pattern_type" in risk: - pattern_type = risk["pattern_type"] - - if pattern_type not in pattern_types: - pattern_types[pattern_type] = 0 - - pattern_types[pattern_type] += 1 - - if pattern_types: - pattern_names = { - "unlimited_liability": "مسؤولية غير محدودة", - "payment_delay": "تأخير المدفوعات", - "excessive_penalties": "غرامات مبالغ فيها", - "unilateral_termination": "إنهاء من طرف واحد", - "unrealistic_deadlines": "مواعيد نهائية غير واقعية", - "scope_creep": "توسع نطاق العمل", - "indemnification": "شروط التعويض", - "change_control": "التحكم في التغييرات", - "warranty_period": "فترة الضمان", - "dispute_resolution": "حل النزاعات", - "force_majeure": "القوة القاهرة", - "regulatory_compliance": "الامتثال التنظيمي", - "intellectual_property": "الملكية الفكرية", - "confidentiality": "السرية", - "insurance_requirements": "متطلبات التأمين" - } - - pattern_df = pd.DataFrame({ - "نوع الصيغة": [pattern_names.get(pt, pt) for pt in pattern_types.keys()], - "العدد": list(pattern_types.values()) - }) - - fig = px.bar( - pattern_df, - x="نوع الصيغة", - y="العدد", - title="أنواع الصيغ التعاقدية المكتشفة", - labels={"نوع الصيغة": "نوع الصيغة", "العدد": "عدد المرات"}, - color="العدد", - color_continuous_scale="Viridis" - ) - - fig.update_layout( - xaxis={'categoryorder': 'total descending'}, - direction='rtl', - font=dict(family="Arial, sans-serif", size=14), - height=350 - ) - - st.plotly_chart(fig, use_container_width=True) - else: - st.info("لم يتم اكتشاف صيغ تعاقدية محددة في العقد") - - # عرض تفاصيل المخاطر - st.markdown("

تفاصيل المخاطر المكتشفة

", unsafe_allow_html=True) - - # تصفية المخاطر حسب الخطورة - severity_filter = st.multiselect( - "تصفية حسب مستوى الخطورة", - ["عالية", "متوسطة", "منخفضة"], - default=["عالية", "متوسطة", "منخفضة"] - ) - - severity_map = {"high": "عالية", "medium": "متوسطة", "low": "منخفضة"} - filtered_risks = [risk for risk in risk_report["risks"] if severity_map[risk["severity"]] in severity_filter] - - for risk in filtered_risks: - severity = risk["severity"] - severity_class = severity - severity_text = severity_map[severity] - - st.markdown(f""" -
-
-
#{risk['id']}
-
{risk['term']}
-
{severity_text}
-
-
{risk['category_ar']}
-
{risk['context']}
-
-
التأثير المحتمل:
-
{risk['impact']}
-
-
-
التوصية:
-
{risk['recommendation']}
-
-
- """, unsafe_allow_html=True) - - # إضافة CSS لتنسيق الواجهة - st.markdown(""" - - """, unsafe_allow_html=True) - - def render_risk_comparison_dashboard(self, contract_text1, contract_text2, title1="العقد الأول", title2="العقد الثاني"): - """عرض لوحة معلومات مقارنة المخاطر بين عقدين""" - st.markdown("

مقارنة مخاطر العقود

", unsafe_allow_html=True) - - if not contract_text1 or not contract_text2: - st.warning("يرجى إدخال نص العقدين للمقارنة") - return - - # تحليل العقود ومقارنتها - comparison_report = self.generate_risk_comparison(contract_text1, contract_text2, title1, title2) - - # عرض ملخص المقارنة - st.markdown("

ملخص المقارنة

", unsafe_allow_html=True) - st.markdown(f""" -
- {comparison_report["summary"]} -
- """, unsafe_allow_html=True) - - # عرض مقارنة الدرجات الإجمالية - st.markdown("

مقارنة درجات المخاطرة الإجمالية

", unsafe_allow_html=True) - - col1, col2 = st.columns(2) - - with col1: - contract1_score = comparison_report["contract1"]["overall_score"] - contract1_severity = comparison_report["contract1"]["overall_severity"] - - st.markdown(f""" -
-
{title1}
-
{contract1_score}%
-
{contract1_severity}
-
{comparison_report["contract1"]["risk_count"]} مخاطر محددة
-
- """, unsafe_allow_html=True) - - with col2: - contract2_score = comparison_report["contract2"]["overall_score"] - contract2_severity = comparison_report["contract2"]["overall_severity"] - - st.markdown(f""" -
-
{title2}
-
{contract2_score}%
-
{contract2_severity}
-
{comparison_report["contract2"]["risk_count"]} مخاطر محددة
-
- """, unsafe_allow_html=True) - - # عرض مخطط مقارنة المخاطر حسب الفئة - st.markdown("

مقارنة المخاطر حسب الفئة

", unsafe_allow_html=True) - - # تجميع البيانات للمخطط - chart_data = [] - for category, data in comparison_report["category_comparison"].items(): - chart_data.append({ - "category": data["name_ar"], - "contract": title1, - "high": data["contract1"]["high"], - "medium": data["contract1"]["medium"], - "low": data["contract1"]["low"], - "total": data["contract1"]["total"] - }) - chart_data.append({ - "category": data["name_ar"], - "contract": title2, - "high": data["contract2"]["high"], - "medium": data["contract2"]["medium"], - "low": data["contract2"]["low"], - "total": data["contract2"]["total"] - }) - - # التحقق من وجود بيانات قبل إنشاء الرسم البياني - if chart_data: - chart_df = pd.DataFrame(chart_data) - - # إنشاء مخطط شريطي مقارن - fig = go.Figure() - - for contract in [title1, title2]: - contract_data = chart_df[chart_df["contract"] == contract] - fig.add_trace(go.Bar( - x=contract_data["category"], - y=contract_data["total"], - name=contract, - text=contract_data["total"], - textposition="auto" - )) - - fig.update_layout( - title="مقارنة إجمالي المخاطر حسب الفئة", - xaxis_title="فئة المخاطر", - yaxis_title="عدد المخاطر", - barmode='group', - xaxis={'categoryorder': 'total descending'}, - direction='rtl', - font=dict(family="Arial, sans-serif", size=14), - height=500, - margin=dict(l=10, r=10, t=50, b=10) - ) - - st.plotly_chart(fig, use_container_width=True) - - # عرض مخطط مقارنة المخاطر العالية - fig = go.Figure() - - for contract in [title1, title2]: - contract_data = chart_df[chart_df["contract"] == contract] - fig.add_trace(go.Bar( - x=contract_data["category"], - y=contract_data["high"], - name=contract, - text=contract_data["high"], - textposition="auto", - marker_color="#d63031" if contract == title1 else "#ff7979" - )) - - fig.update_layout( - title="مقارنة المخاطر العالية حسب الفئة", - xaxis_title="فئة المخاطر", - yaxis_title="عدد المخاطر العالية", - barmode='group', - xaxis={'categoryorder': 'total descending'}, - direction='rtl', - font=dict(family="Arial, sans-serif", size=14), - height=400 - ) - - st.plotly_chart(fig, use_container_width=True) - else: - # إذا لم تكن هناك بيانات، عرض رسالة بديلة - st.info("لم يتم العثور على بيانات كافية للمقارنة وعرض الرسم البياني") - - # عرض جدول مقارنة المخاطر المشتركة - st.markdown("

المخاطر المشتركة بين العقدين

", unsafe_allow_html=True) - - # تنظيم المخاطر المشتركة في جدول - common_risks_grouped = {} - for risk in comparison_report["common_risks"]: - term = risk["term"] - if term not in common_risks_grouped: - common_risks_grouped[term] = { - "term": term, - "category": risk["category_ar"], - title1: None, - title2: None - } - - if risk["contract"] == title1: - common_risks_grouped[term][title1] = risk["severity"] - else: - common_risks_grouped[term][title2] = risk["severity"] - - # تحويل إلى قائمة - common_risks_list = list(common_risks_grouped.values()) - - # تصنيف المخاطر المشتركة - severity_colors = { - "high": "❌ عالية", - "medium": "⚠️ متوسطة", - "low": "✓ منخفضة" - } - - if common_risks_list: - common_risks_df = pd.DataFrame(common_risks_list) - - # تطبيق الألوان والرموز على مستويات الخطورة - for contract in [title1, title2]: - common_risks_df[contract] = common_risks_df[contract].map(lambda x: severity_colors.get(x, "غير محدد") if x else "غير موجود") - - st.dataframe( - common_risks_df.style.set_properties(**{'text-align': 'right'}), - use_container_width=True, - height=400 - ) - else: - st.info("لا توجد مخاطر مشتركة بين العقدين") - - # عرض المخاطر الفريدة لكل عقد - col1, col2 = st.columns(2) - - with col1: - st.markdown(f"

المخاطر الفريدة في {title1}

", unsafe_allow_html=True) - - if comparison_report["unique_risks1"]: - for risk in comparison_report["unique_risks1"]: - severity = risk["severity"] - severity_class = severity - severity_text = {"high": "عالية", "medium": "متوسطة", "low": "منخفضة"}[severity] - - st.markdown(f""" -
-
{risk['term']}
-
{risk['category_ar']}
-
{severity_text}
-
- """, unsafe_allow_html=True) - else: - st.info(f"لا توجد مخاطر فريدة في {title1}") - - with col2: - st.markdown(f"

المخاطر الفريدة في {title2}

", unsafe_allow_html=True) - - if comparison_report["unique_risks2"]: - for risk in comparison_report["unique_risks2"]: - severity = risk["severity"] - severity_class = severity - severity_text = {"high": "عالية", "medium": "متوسطة", "low": "منخفضة"}[severity] - - st.markdown(f""" -
-
{risk['term']}
-
{risk['category_ar']}
-
{severity_text}
-
- """, unsafe_allow_html=True) - else: - st.info(f"لا توجد مخاطر فريدة في {title2}") - - # إضافة CSS لتنسيق الواجهة - st.markdown(""" - - """, unsafe_allow_html=True) - - def render(self): - """عرض واجهة المستخدم لتحليل مخاطر العقود""" - st.markdown("

نظام تقييم مخاطر العقود الآلي

", unsafe_allow_html=True) - - st.markdown(""" -
- يمكنك استخدام هذا النظام لتحليل مخاطر العقود بشكل آلي وتحديد البنود التي قد تشكل مخاطر محتملة، - مع تقديم توصيات للتعامل مع هذه المخاطر وتحسين العقود. -
- """, unsafe_allow_html=True) - - # إنشاء علامات تبويب لطرق مختلفة للتحليل - tabs = st.tabs([ - "تحليل مخاطر العقد", - "مقارنة بين عقدين", - "تحليل عقد من ملف" - ]) - - with tabs[0]: - st.markdown("### تحليل نص العقد") - - contract_title = st.text_input("عنوان العقد") - contract_text = st.text_area("أدخل نص العقد هنا", height=300) - - if st.button("تحليل المخاطر"): - if contract_text: - self.render_risk_dashboard(contract_text, contract_title or "العقد") - else: - st.warning("يرجى إدخال نص العقد للتحليل") - - with tabs[1]: - st.markdown("### مقارنة المخاطر بين عقدين") - - col1, col2 = st.columns(2) - - with col1: - contract1_title = st.text_input("عنوان العقد الأول") - contract1_text = st.text_area("أدخل نص العقد الأول", height=200) - - with col2: - contract2_title = st.text_input("عنوان العقد الثاني") - contract2_text = st.text_area("أدخل نص العقد الثاني", height=200) - - if st.button("مقارنة المخاطر"): - if contract1_text and contract2_text: - self.render_risk_comparison_dashboard( - contract1_text, - contract2_text, - contract1_title or "العقد الأول", - contract2_title or "العقد الثاني" - ) - else: - st.warning("يرجى إدخال نص كلا العقدين للمقارنة") - - with tabs[2]: - st.markdown("### تحليل عقد من ملف") - - uploaded_file = st.file_uploader("قم بتحميل ملف العقد", type=["txt", "docx", "pdf", "md"]) - - if uploaded_file is not None: - file_title = uploaded_file.name - file_content = "" - - if uploaded_file.name.endswith(".pdf"): - st.info("جاري معالجة ملف PDF...") - try: - import fitz # PyMuPDF - - pdf_bytes = uploaded_file.read() - with open("temp_contract.pdf", "wb") as f: - f.write(pdf_bytes) - - doc = fitz.open("temp_contract.pdf") - for page in doc: - file_content += page.get_text() - except ImportError: - file_content = "تعذر قراءة ملف PDF. يرجى التأكد من تثبيت مكتبة PyMuPDF أو قم بنسخ ولصق محتوى العقد في علامة التبويب الأولى." - - elif uploaded_file.name.endswith(".docx"): - st.info("جاري معالجة ملف Word...") - try: - from docx import Document - - docx_bytes = uploaded_file.read() - with open("temp_contract.docx", "wb") as f: - f.write(docx_bytes) - - doc = Document("temp_contract.docx") - file_content = "\n".join([paragraph.text for paragraph in doc.paragraphs]) - except ImportError: - file_content = "تعذر قراءة ملف Word. يرجى التأكد من تثبيت مكتبة python-docx أو قم بنسخ ولصق محتوى العقد في علامة التبويب الأولى." - - else: # للملفات النصية - file_content = uploaded_file.read().decode("utf-8") - - if file_content: - st.markdown("### محتوى الملف") - with st.expander("عرض محتوى الملف"): - st.text(file_content[:5000] + ("..." if len(file_content) > 5000 else "")) - - if st.button("تحليل مخاطر الملف"): - self.render_risk_dashboard(file_content, file_title) - else: - st.warning("تعذر قراءة محتوى الملف. يرجى المحاولة مرة أخرى أو استخدام علامة التبويب الأولى.") - - # إضافة CSS للتنسيق - st.markdown(""" - - """, unsafe_allow_html=True) \ No newline at end of file diff --git a/modules/risk_assessment/risk_assessment_app.py b/modules/risk_assessment/risk_assessment_app.py deleted file mode 100644 index 1d812640eab14ddbb993fa89fa183073c02524dd..0000000000000000000000000000000000000000 --- a/modules/risk_assessment/risk_assessment_app.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -تطبيق تقييم مخاطر العقود الآلي -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات تحليل مخاطر العقود -from modules.risk_assessment.contract_risk_analyzer import ContractRiskAnalyzer - - -class RiskAssessmentApp: - """تطبيق تقييم مخاطر العقود الآلي""" - - def __init__(self): - """تهيئة تطبيق تقييم مخاطر العقود""" - self.risk_analyzer = ContractRiskAnalyzer() - - def render(self): - """عرض واجهة المستخدم الرئيسية للتطبيق""" - self.risk_analyzer.render() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="تقييم مخاطر العقود الآلي | WAHBi AI", - page_icon="⚠️", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = RiskAssessmentApp() - app.render() \ No newline at end of file diff --git a/modules/services/item_extractor.py b/modules/services/item_extractor.py deleted file mode 100644 index cafaf3fcf4d1c4108c9272c7841e1cc9194b3cec..0000000000000000000000000000000000000000 --- a/modules/services/item_extractor.py +++ /dev/null @@ -1,124 +0,0 @@ -""" -خدمة استخراج البنود من المستندات -""" - -import re -import pandas as pd -import numpy as np -import nltk -from nltk.tokenize import sent_tokenize -from pathlib import Path -import config - -class ItemExtractor: - """استخراج البنود من المستندات""" - - def __init__(self): - # تحميل موارد NLTK إذا لم تكن موجودة - try: - nltk.data.find('tokenizers/punkt') - except LookupError: - nltk.download('punkt') - - # قائمة الكلمات المفتاحية التي تشير إلى بداية البنود - self.item_indicators = [ - 'توريد', 'تركيب', 'تنفيذ', 'تصنيع', 'أعمال', 'تأمين', - 'تقديم', 'إنشاء', 'صيانة', 'إزالة', 'نقل', 'تجهيز', - 'فك', 'تسليم', 'تطبيق', 'تثبيت', 'تشطيب', 'تجهيز' - ] - - # قائمة فئات البنود - self.categories = { - 'أعمال الأساسات': ['أساس', 'قاعدة', 'حفر', 'ردم', 'خرسانة', 'اسمنت', 'قواعد'], - 'أعمال الهيكل الإنشائي': ['عمود', 'سقف', 'كمرة', 'خرسانة', 'حديد تسليح', 'بلاطة', 'هيكل'], - 'أعمال التشطيبات': ['دهان', 'بلاط', 'سيراميك', 'رخام', 'جبس', 'زجاج', 'باب', 'نافذة', 'أرضية'], - 'أعمال الكهرباء': ['كهرباء', 'إضاءة', 'مفتاح', 'سلك', 'لوحة', 'كابل', 'تمديد'], - 'أعمال السباكة': ['ماء', 'صرف', 'مواسير', 'حمام', 'مغسلة', 'خزان', 'مضخة'], - 'أعمال التكييف': ['تكييف', 'تبريد', 'تهوية', 'مكيف', 'مجرى هواء', 'فلتر'], - 'أعمال الموقع': ['تسوية', 'تخطيط', 'أسوار', 'بوابات', 'طرق', 'رصف', 'تشجير'], - 'المستندات': ['مخططات', 'رسومات', 'تقارير', 'شهادات', 'اختبارات'] - } - - def extract_items(self, text): - """استخراج البنود من النص""" - if not text: - return pd.DataFrame() - - # تقسيم النص إلى جمل - sentences = sent_tokenize(text) - - # البحث عن البنود المحتملة - items = [] - item_id = 1 - - for sentence in sentences: - # تحقق مما إذا كانت الجملة تحتوي على مؤشر بند - if any(indicator in sentence for indicator in self.item_indicators): - # تحديد الفئة - category = self._determine_category(sentence) - - # تحديد الأهمية - importance = self._determine_importance(sentence) - - # إضافة البند إلى القائمة - items.append({ - 'رقم البند': f"I{item_id:03d}", - 'وصف البند': sentence.strip(), - 'الفئة': category, - 'الأهمية': importance, - 'الثقة': round(np.random.uniform(0.75, 0.95), 2) # محاكاة ثقة التعرف - }) - - item_id += 1 - - # تحويل القائمة إلى DataFrame - items_df = pd.DataFrame(items) - - # التأكد من وجود بيانات - if items_df.empty: - # إنشاء DataFrame فارغ بالأعمدة المطلوبة - items_df = pd.DataFrame(columns=[ - 'رقم البند', 'وصف البند', 'الفئة', 'الأهمية', 'الثقة' - ]) - - return items_df - - def _determine_category(self, text): - """تحديد فئة البند بناءً على محتواه""" - # البحث عن الكلمات المفتاحية في النص - scores = {} - - for category, keywords in self.categories.items(): - score = sum(1 for keyword in keywords if keyword in text.lower()) - scores[category] = score - - # اختيار الفئة ذات الدرجة الأعلى - if max(scores.values()) > 0: - return max(scores.items(), key=lambda x: x[1])[0] - else: - return "أخرى" - - def _determine_importance(self, text): - """تحديد أهمية البند بناءً على محتواه""" - # كلمات تشير إلى أهمية عالية - high_importance_words = [ - 'ضروري', 'هام', 'أساسي', 'رئيسي', 'كبير', 'مهم', - 'حرج', 'أمان', 'سلامة', 'صحة', 'بيئة' - ] - - # كلمات تشير إلى أهمية منخفضة - low_importance_words = [ - 'ثانوي', 'إضافي', 'تجميلي', 'مكمل', 'اختياري' - ] - - # حساب درجة الأهمية - high_score = sum(1 for word in high_importance_words if word in text.lower()) - low_score = sum(1 for word in low_importance_words if word in text.lower()) - - # تحديد الأهمية بناءً على الدرجات - if high_score > low_score: - return "عالية" - elif low_score > high_score: - return "منخفضة" - else: - return "متوسطة" \ No newline at end of file diff --git a/modules/services/quantity_extractor.py b/modules/services/quantity_extractor.py deleted file mode 100644 index 19b3c9d1427c49cfffd7da8237854efc74f8d128..0000000000000000000000000000000000000000 --- a/modules/services/quantity_extractor.py +++ /dev/null @@ -1,182 +0,0 @@ -""" -خدمة استخراج الكميات من المستندات -""" - -import re -import pandas as pd -import numpy as np -from pathlib import Path -import config - -class QuantityExtractor: - """استخراج الكميات من المستندات""" - - def __init__(self): - # وحدات القياس الشائعة - self.units = { - 'أعمال الخرسانة': 'م3', - 'أعمال الحفر': 'م3', - 'أعمال الردم': 'م3', - 'حديد التسليح': 'طن', - 'أعمال البلاط': 'م2', - 'أعمال السيراميك': 'م2', - 'أعمال الرخام': 'م2', - 'أعمال البلوك': 'م2', - 'أعمال الدهان': 'م2', - 'أعمال اللياسة': 'م2', - 'أعمال العزل': 'م2', - 'أعمال تمديدات الكهرباء': 'نقطة', - 'أعمال تمديدات السباكة': 'نقطة', - 'أعمال الأبواب': 'عدد', - 'أعمال النوافذ': 'عدد', - 'أعمال مجاري التكييف': 'م.ط', - 'أعمال الرصف': 'م2', - 'أعمال التسوية': 'م2', - 'مواسير الصرف': 'م.ط', - 'مواسير المياه': 'م.ط' - } - - # تعبيرات منتظمة لاستخراج الأرقام والوحدات - self.number_pattern = r'(\d+(?:,\d+)*(?:\.\d+)?)' - self.unit_pattern = r'(م3|م2|طن|م\.ط|نقطة|عدد|وحدة)' - - def extract_quantities(self, text, excel_data=None): - """استخراج الكميات من النص أو بيانات Excel""" - quantities = [] - - # إذا كانت البيانات من Excel - if excel_data is not None: - quantities = self._extract_from_excel(excel_data) - # وإلا استخراج من النص - elif text: - quantities = self._extract_from_text(text) - - # تحويل القائمة إلى DataFrame - quantities_df = pd.DataFrame(quantities) - - # التأكد من وجود بيانات - if quantities_df.empty: - # إنشاء DataFrame فارغ بالأعمدة المطلوبة - quantities_df = pd.DataFrame(columns=[ - 'رقم البند', 'وصف العمل', 'الوحدة', 'الكمية المستخرجة', - 'الثقة', 'الملاحظات' - ]) - - return quantities_df - - def _extract_from_excel(self, excel_data): - """استخراج الكميات من بيانات Excel""" - quantities = [] - item_id = 1 - - # التحقق من وجود أعمدة مهمة - required_cols = ['الوصف', 'البند', 'الكمية', 'الوحدة'] - present_cols = [col for col in required_cols if any(col in str(c).lower() for c in excel_data.columns)] - - if not present_cols: - return quantities - - # تحديد أعمدة البيانات - desc_col = next((c for c in excel_data.columns if 'وصف' in str(c).lower() or 'بند' in str(c).lower()), None) - qty_col = next((c for c in excel_data.columns if 'كمية' in str(c).lower() or 'عدد' in str(c).lower()), None) - unit_col = next((c for c in excel_data.columns if 'وحدة' in str(c).lower()), None) - - if not (desc_col and qty_col): - return quantities - - # استخراج الكميات من كل صف - for _, row in excel_data.iterrows(): - if pd.notna(row[desc_col]) and pd.notna(row[qty_col]): - description = str(row[desc_col]).strip() - - # تجاهل الصفوف الفارغة أو العناوين - if len(description) < 5 or description.isupper(): - continue - - # استخراج الكمية والوحدة - quantity = float(row[qty_col]) if pd.notna(row[qty_col]) else 0 - unit = str(row[unit_col]).strip() if unit_col and pd.notna(row[unit_col]) else self._determine_unit(description) - - # إضافة البند إلى القائمة - quantities.append({ - 'رقم البند': f"Q{item_id:03d}", - 'وصف العمل': description, - 'الوحدة': unit, - 'الكمية المستخرجة': quantity, - 'الثقة': round(np.random.uniform(0.85, 0.99), 2), - 'الملاحظات': "تم استخراج الكمية من جدول الكميات" - }) - - item_id += 1 - - return quantities - - def _extract_from_text(self, text): - """استخراج الكميات من النص""" - quantities = [] - item_id = 1 - - # البحث عن العبارات التي تحتوي على أرقام ووحدات - lines = text.split('\n') - - for line in lines: - # البحث عن أعمال محددة - for work_type in self.units.keys(): - if work_type in line: - # البحث عن الأرقام في النص - numbers = re.findall(self.number_pattern, line) - - if numbers: - # اختيار أول رقم (الأكثر احتمالاً أن يكون الكمية) - quantity = float(numbers[0].replace(',', '')) - unit = self.units[work_type] - - # إضافة البند إلى القائمة - quantities.append({ - 'رقم البند': f"Q{item_id:03d}", - 'وصف العمل': work_type, - 'الوحدة': unit, - 'الكمية المستخرجة': quantity, - 'الثقة': round(np.random.uniform(0.7, 0.9), 2), - 'الملاحظات': "تم حساب الكمية من النص" - }) - - item_id += 1 - break - - # البحث عن وحدات قياس في النص - unit_matches = re.findall(self.unit_pattern, line) - if unit_matches and re.search(self.number_pattern, line): - numbers = re.findall(self.number_pattern, line) - - if numbers: - # اختيار أول رقم وأول وحدة - quantity = float(numbers[0].replace(',', '')) - unit = unit_matches[0] - - # استخراج وصف العمل - أول 50 حرف من النص - description = line[:50] + "..." if len(line) > 50 else line - - # إضافة البند إلى القائمة (إذا لم يتم إضافته بالفعل) - if not any(q['وصف العمل'] == description for q in quantities): - quantities.append({ - 'رقم البند': f"Q{item_id:03d}", - 'وصف العمل': description, - 'الوحدة': unit, - 'الكمية المستخرجة': quantity, - 'الثقة': round(np.random.uniform(0.6, 0.85), 2), - 'الملاحظات': "تم استخراج الكمية من النص" - }) - - item_id += 1 - - return quantities - - def _determine_unit(self, description): - """تحديد وحدة القياس المناسبة بناءً على وصف العمل""" - for work_type, unit in self.units.items(): - if work_type in description: - return unit - - # افتراضي إذا لم يتم العثور على وحدة مناسبة - return "وحدة" \ No newline at end of file diff --git a/modules/services/risk_analyzer.py b/modules/services/risk_analyzer.py deleted file mode 100644 index 55eb9bfc21ca5cad78c518053bda36399bda70e1..0000000000000000000000000000000000000000 --- a/modules/services/risk_analyzer.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -خدمة تحليل المخاطر في المستندات -""" - -import re -import pandas as pd -import numpy as np -from nltk.tokenize import sent_tokenize -import config - -class RiskAnalyzer: - """تحليل المخاطر في المستندات""" - - def __init__(self): - # قائمة بالمصطلحات التي تشير إلى المخاطر - self.risk_indicators = { - 'مخاطر مالية': [ - 'غرامة', 'عقوبة', 'تعويض', 'دفعة', 'ضمان', 'تأخير', 'سعر', - 'تكلفة', 'زيادة', 'تمويل', 'استرداد', 'مصادرة', 'كفالة', - 'مستحقات', 'فاتورة', 'سداد', 'دفع', 'مطالبة', 'تقلبات' - ], - 'مخاطر زمنية': [ - 'مدة', 'فترة', 'تاريخ', 'موعد', 'تأخير', 'جدول زمني', 'تمديد', - 'تسليم', 'تسريع', 'إنجاز', 'تنفيذ', 'انتهاء', 'بدء', 'تعليق' - ], - 'مخاطر فنية': [ - 'مواصفات', 'معايير', 'اختبار', 'فحص', 'جودة', 'عيب', 'خلل', - 'تقنية', 'فني', 'تصميم', 'أداء', 'مخططات', 'تشغيل', 'صيانة' - ], - 'مخاطر إدارية': [ - 'مراسلات', 'اجتماع', 'تنسيق', 'تواصل', 'إشراف', 'إدارة', - 'تغيير', 'تعديل', 'موافقة', 'رفض', 'تفويض', 'صلاحية' - ], - 'مخاطر تنظيمية': [ - 'لائحة', 'تصريح', 'ترخيص', 'قانون', 'نظام', 'حكومي', 'بلدية', - 'تشريع', 'امتثال', 'تعميم', 'شهادة', 'موافقة' - ], - 'مخاطر سوقية': [ - 'توريد', 'مورد', 'سوق', 'منافسة', 'مواد', 'نقص', 'تقلب', 'أسعار', - 'استيراد', 'تصدير', 'جمارك', 'نقل', 'تخزين' - ], - } - - # قائمة بالمصطلحات التي تشير إلى تأثير المخاطر - self.impact_indicators = { - 'عالي': [ - 'كبير', 'خطير', 'جسيم', 'كلي', 'مرتفع', 'عالي', 'ضخم', 'هام', - 'جوهري', 'أساسي', 'رئيسي' - ], - 'متوسط': [ - 'متوسط', 'معتدل', 'وسط', 'مقبول', 'عادي', 'معقول' - ], - 'منخفض': [ - 'صغير', 'قليل', 'ضئيل', 'بسيط', 'منخفض', 'هامشي', 'محدود', - 'طفيف', 'غير مؤثر' - ] - } - - # قائمة بالمصطلحات التي تشير إلى احتمالية المخاطر - self.probability_indicators = { - 'مؤكد': [ - 'مؤكد', 'حتمي', 'قطعي', 'دائماً', 'يجب', 'ملزم', 'إلزامي', - 'مطلوب' - ], - 'محتمل': [ - 'محتمل', 'ممكن', 'قد', 'ربما', 'يمكن', 'متوقع' - ], - 'غير محتمل': [ - 'نادر', 'بعيد', 'استثنائي', 'غير متوقع', 'غير محتمل', 'ضئيل' - ] - } - - # استراتيجيات معالجة المخاطر - self.mitigation_strategies = { - 'مخاطر مالية': [ - "تخصيص مبلغ احتياطي", - "التفاوض مع العميل لتخفيف الشروط المالية", - "تحديد سقف للغرامات", - "التخطيط للتدفق النقدي", - "تأمين خط ائتمان احتياطي" - ], - 'مخاطر زمنية': [ - "زيادة فريق العمل", - "استخدام موارد إضافية", - "وضع خطة عمل بديلة", - "استباق التأخيرات المحتملة", - "تقديم طلب تمديد مسبق" - ], - 'مخاطر فنية': [ - "طلب توضيح من العميل", - "استشارة خبراء متخصصين", - "إجراء اختبارات إضافية", - "توثيق المراسلات الفنية", - "تعيين مسؤول ضبط جودة" - ], - 'مخاطر إدارية': [ - "تحسين آليات التواصل", - "توثيق جميع المراسلات", - "وضع خطة اتصال واضحة", - "عقد اجتماعات دورية", - "تعيين مدير مشروع متفرغ" - ], - 'مخاطر تنظيمية': [ - "التخطيط المسبق للمتطلبات التنظيمية", - "التواصل مع الجهات المعنية", - "الاستعانة بمستشار قانوني", - "متابعة التغييرات التنظيمية", - "تجهيز الوثائق المطلوبة مبكراً" - ], - 'مخاطر سوقية': [ - "تثبيت أسعار المواد مع الموردين", - "البحث عن موردين بدلاء", - "شراء المواد الرئيسية مبكراً", - "إبرام عقود توريد طويلة الأجل", - "مراقبة تقلبات السوق" - ] - } - - def analyze_risks(self, text): - """تحليل المخاطر في النص المعطى""" - if not text: - return pd.DataFrame() - - # تقسيم النص إلى جمل - sentences = sent_tokenize(text) - - # تحليل المخاطر في كل جملة - risks = [] - risk_id = 1 - - for sentence in sentences: - # تحديد نوع المخاطرة إذا وجدت - risk_category = self._determine_risk_category(sentence) - - if risk_category: - # تحديد التأثير والاحتمالية - impact = self._determine_impact(sentence) - probability = self._determine_probability(sentence) - - # اختيار استراتيجية المعالجة - mitigation = np.random.choice(self.mitigation_strategies.get(risk_category, ["مراجعة فريق المخاطر"])) - - # إضافة المخاطرة إلى القائمة - risks.append({ - 'رقم المخاطرة': f"R{risk_id:02d}", - 'وصف المخاطرة': sentence.strip(), - 'الفئة': risk_category, - 'التأثير': impact, - 'الاحتمالية': probability, - 'استراتيجية المعالجة': mitigation - }) - - risk_id += 1 - - # تحويل القائمة إلى DataFrame - risks_df = pd.DataFrame(risks) - - # التأكد من وجود بيانات - if risks_df.empty: - # إنشاء DataFrame فارغ بالأعمدة المطلوبة - risks_df = pd.DataFrame(columns=[ - 'رقم المخاطرة', 'وصف المخاطرة', 'الفئة', - 'التأثير', 'الاحتمالية', 'استراتيجية المعالجة' - ]) - - return risks_df - - def _determine_risk_category(self, text): - """تحديد فئة المخاطرة بناءً على محتوى النص""" - # البحث عن الكلمات المفتاحية في النص - scores = {} - - for category, indicators in self.risk_indicators.items(): - score = sum(1 for indicator in indicators if indicator in text.lower()) - scores[category] = score - - # اختيار الفئة ذات الدرجة الأعلى إذا وجدت - if max(scores.values(), default=0) > 0: - return max(scores.items(), key=lambda x: x[1])[0] - else: - return None - - def _determine_impact(self, text): - """تحديد تأثير المخاطرة بناءً على محتوى النص""" - # البحث عن الكلمات المفتاحية في النص - scores = {} - - for impact, indicators in self.impact_indicators.items(): - score = sum(1 for indicator in indicators if indicator in text.lower()) - scores[impact] = score - - # اختيار التأثير ذو الدرجة الأعلى - if max(scores.values(), default=0) > 0: - return max(scores.items(), key=lambda x: x[1])[0] - else: - # اختيار عشوائي مع ترجيح أكبر للتأثير المتوسط - return np.random.choice( - ["عالي", "متوسط", "منخفض"], - p=[0.3, 0.5, 0.2] - ) - - def _determine_probability(self, text): - """تحديد احتمالية المخاطرة بناءً على محتوى النص""" - # البحث عن الكلمات المفتاحية في النص - scores = {} - - for probability, indicators in self.probability_indicators.items(): - score = sum(1 for indicator in indicators if indicator in text.lower()) - scores[probability] = score - - # اختيار الاحتمالية ذات الدرجة الأعلى - if max(scores.values(), default=0) > 0: - return max(scores.items(), key=lambda x: x[1])[0] - else: - # اختيار عشوائي مع ترجيح أكبر للاحتمالية المتوسطة - return np.random.choice( - ["مؤكد", "محتمل", "غير محتمل"], - p=[0.2, 0.6, 0.2] - ) \ No newline at end of file diff --git a/modules/services/specs_analyzer.py b/modules/services/specs_analyzer.py deleted file mode 100644 index a1a128a36b9da7207e8c5196c5c7685dc2c8741f..0000000000000000000000000000000000000000 --- a/modules/services/specs_analyzer.py +++ /dev/null @@ -1,364 +0,0 @@ -""" -خدمة تحليل المواصفات من المستندات -""" - -import re -import pandas as pd -import numpy as np -import nltk -from nltk.tokenize import sent_tokenize -import config - -class SpecificationsAnalyzer: - """تحليل المواصفات الفنية في المستندات""" - - def __init__(self): - # تحميل موارد NLTK إذا لم تكن موجودة - try: - nltk.data.find('tokenizers/punkt') - except LookupError: - nltk.download('punkt') - - # فئات المواصفات الرئيسية - self.specification_categories = { - 'الخرسانة': [ - 'خرسانة', 'اسمنت', 'رتبة', 'مقاومة', 'ضغط', 'شك', 'معالجة', - 'صب', 'قالب', 'قوالب', 'تسليح', 'خلطة', 'ركام', 'حصى' - ], - 'حديد التسليح': [ - 'حديد', 'تسليح', 'قضبان', 'شد', 'جهد خضوع', 'درجة', 'قطر', - 'ربط', 'غطاء خرساني', 'تشكيل', 'ثني', 'شبكة' - ], - 'العزل المائي': [ - 'عزل', 'مائي', 'رطوبة', 'بيتومين', 'لفائف', 'رولات', 'طبقة', - 'رش', 'تسرب', 'مانع تسرب', 'مقاومة الماء', 'حرارة' - ], - 'العزل الحراري': [ - 'عزل', 'حراري', 'صوف صخري', 'صوف زجاجي', 'فوم', 'بوليسترين', - 'موصلية', 'انتقال الحرارة', 'بولي يوريثان' - ], - 'أعمال البلاط': [ - 'بلاط', 'سيراميك', 'بورسلين', 'رخام', 'جرانيت', 'ترويبة', - 'لاصق', 'مونة', 'تركيب', 'مسافات', 'أبعاد' - ], - 'أعمال الدهان': [ - 'دهان', 'طلاء', 'وجه تأسيس', 'وجه نهائي', 'رش', 'فرشاة', - 'رولة', 'معجون', 'مائي', 'زيتي', 'لامع', 'مطفي' - ], - 'المواد الكهربائية': [ - 'كهرباء', 'أسلاك', 'كابلات', 'لوحات', 'مفاتيح', 'تمديدات', - 'جهد', 'قدرة', 'توزيع', 'تأريض', 'قواطع', 'تيار' - ], - 'أعمال السباكة': [ - 'سباكة', 'مواسير', 'صرف', 'تغذية', 'مياه', 'بي في سي', - 'نحاس', 'حديد', 'خزان', 'مضخة', 'صمام', 'محبس' - ], - 'أعمال التكييف': [ - 'تكييف', 'تبريد', 'تدفئة', 'مجاري هواء', 'دكت', 'مناولة', - 'تهوية', 'وحدة', 'مكيف', 'فلتر', 'مروحة' - ] - } - - # المواصفات القياسية المعروفة - self.standard_specs = { - 'ASTM': { - 'C150': 'اسمنت بورتلاندي', - 'A615': 'حديد تسليح', - 'D6164': 'عزل مائي بيتوميني', - 'C33': 'ركام الخرسانة', - 'C494': 'إضافات الخرسانة', - 'C979': 'صبغات الخرسانة', - 'C578': 'عزل البوليسترين' - }, - 'AASHTO': { - 'M85': 'اسمنت بورتلاندي', - 'M31': 'حديد تسليح', - 'M320': 'بيتومين للطرق' - }, - 'IEC': { - '60502': 'كابلات الطاقة', - '60364': 'تمديدات كهربائية', - '61439': 'لوحات توزيع الطاقة' - }, - 'BS': { - '8500': 'الخرسانة', - '4449': 'حديد التسليح', - '6700': 'أنظمة المياه', - '5950': 'المنشآت الفولاذية' - }, - 'EN': { - '197-1': 'الاسمنت', - '10080': 'حديد التسليح', - '13162': 'العزل الحراري' - }, - 'كود البناء السعودي': { - 'SBC 201': 'الأحمال', - 'SBC 304': 'الخرسانة الإنشائية', - 'SBC 305': 'المباني المعدنية', - 'SBC 501': 'السباكة', - 'SBC 401': 'الكهرباء', - 'SBC 601': 'البناء الصديق للبيئة' - } - } - - def analyze_specifications(self, text): - """تحليل المواصفات الفنية من النص""" - if not text: - return {}, [], pd.DataFrame() - - # تقسيم النص إلى جمل - sentences = sent_tokenize(text) - - # استخراج المواصفات حسب الفئة - specs = {} - for category, keywords in self.specification_categories.items(): - specs[category] = self._extract_category_specs(sentences, keywords, category) - - # استخراج المتطلبات الخاصة - special_requirements = self._extract_special_requirements(sentences) - - # استخراج متطلبات المحتوى المحلي - local_content = self._extract_local_content(sentences) - - return specs, special_requirements, local_content - - def _extract_category_specs(self, sentences, keywords, category): - """استخراج مواصفات فئة محددة من الجمل""" - category_specs = {} - - # البحث عن الجمل التي تحتوي على الكلمات المفتاحية للفئة - category_sentences = [s for s in sentences if any(k in s.lower() for k in keywords)] - - if not category_sentences: - return category_specs - - # استخراج المواصفات حسب نوع الفئة - if category == 'الخرسانة': - # البحث عن قوة الضغط - for s in category_sentences: - if any(term in s.lower() for term in ['قوة', 'مقاومة', 'ضغط']): - match = re.search(r'(\d+)\s*(?:نيوتن|ميجا باسكال|نيوتن/مم²|MPa|N/mm)', s) - if match: - category_specs['قوة الضغط'] = f"{match.group(1)} نيوتن/مم²" - - # البحث عن نسبة الماء للأسمنت - if any(term in s.lower() for term in ['نسبة', 'ماء', 'اسمنت']): - match = re.search(r'(\d+(?:\.\d+)?)\s*(?:%|نسبة)', s) - if match: - category_specs['نسبة الماء للأسمنت'] = f"{match.group(1)} كحد أقصى" - - # البحث عن المعالجة - if 'معالجة' in s.lower(): - match = re.search(r'(\d+)\s*(?:يوم|أيام)', s) - if match: - category_specs['المعالجة'] = f"لا تقل عن {match.group(1)} أيام" - - # البحث عن المواصفات المرجعية - for std_org, std_codes in self.standard_specs.items(): - for std_code, std_desc in std_codes.items(): - if std_code in s and (std_org in s or category in std_desc.lower()): - category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}" - - elif category == 'حديد التسليح': - # البحث عن نوع الحديد - for s in category_sentences: - if any(term in s.lower() for term in ['درجة', 'جهد', 'خضوع', 'grade']): - match = re.search(r'(?:درجة|جريد|Grade)\s*(\d+)', s, re.IGNORECASE) - if match: - category_specs['نوع الحديد'] = f"عالي المقاومة للشد (Grade {match.group(1)})" - - # البحث عن إجهاد الخضوع - if any(term in s.lower() for term in ['إجهاد', 'خضوع', 'شد']): - match = re.search(r'(\d+)\s*(?:نيوتن|ميجا باسكال|نيوتن/مم²|MPa|N/mm)', s) - if match: - category_specs['إجهاد الخضوع'] = f"{match.group(1)} نيوتن/مم²" - - # البحث عن المواصفات المرجعية - for std_org, std_codes in self.standard_specs.items(): - for std_code, std_desc in std_codes.items(): - if std_code in s and (std_org in s or category in std_desc.lower()): - category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}" - - elif category == 'العزل المائي': - # البحث عن نوع العزل - for s in category_sentences: - if any(term in s.lower() for term in ['نوع', 'بيتومين', 'بوليستر', 'رول']): - if 'بيتومين' in s.lower() and 'بوليستر' in s.lower(): - category_specs['النوع'] = 'أغشية بيتومينية مدعمة بالبوليستر' - elif 'بيتومين' in s.lower(): - category_specs['النوع'] = 'أغشية بيتومينية' - elif 'pvc' in s.lower(): - category_specs['النوع'] = 'أغشية PVC' - - # البحث عن السماكة - if any(term in s.lower() for term in ['سماكة', 'سمك', 'مم']): - match = re.search(r'(\d+(?:\.\d+)?)\s*(?:مم|mm)', s, re.IGNORECASE) - if match: - category_specs['السماكة'] = f"{match.group(1)} مم" - - # البحث عن مقاومة درجة الحرارة - if any(term in s.lower() for term in ['حرارة', 'درجة', 'مقاومة']): - match = re.search(r'(\d+)\s*(?:درجة|°)', s) - if match: - category_specs['مقاومة درجة الحرارة'] = f"حتى {match.group(1)} درجة مئوية" - - # البحث عن المواصفات المرجعية - for std_org, std_codes in self.standard_specs.items(): - for std_code, std_desc in std_codes.items(): - if std_code in s and (std_org in s or 'عزل' in std_desc.lower()): - category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}" - - elif category == 'المواد الكهربائية': - # البحث عن نوع الكابلات - for s in category_sentences: - if any(term in s.lower() for term in ['كابل', 'سلك', 'نحاس', 'ألمنيوم']): - if 'نحاس' in s.lower() and 'xlpe' in s.lower(): - category_specs['الكابلات'] = 'نحاس معزول XLPE' - elif 'نحاس' in s.lower() and 'pvc' in s.lower(): - category_specs['الكابلات'] = 'نحاس معزول PVC' - elif 'نحاس' in s.lower(): - category_specs['الكابلات'] = 'نحاس معزول' - elif 'ألمنيوم' in s.lower(): - category_specs['الكابلات'] = 'ألمنيوم معزول' - - # البحث عن المواصفات المرجعية - for std_org, std_codes in self.standard_specs.items(): - for std_code, std_desc in std_codes.items(): - if std_code in s and (std_org in s or 'كهربا' in std_desc.lower()): - category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}" - - # إذا لم يتم العثور على مواصفات محددة، أضف مواصفات افتراضية للفئات الرئيسية - if not category_specs and category in ['الخرسانة', 'حديد التسليح', 'العزل المائي', 'المواد الكهربائية']: - if category == 'الخرسانة': - category_specs = { - 'قوة الضغط': '30 نيوتن/مم²', - 'نسبة الماء للأسمنت': '0.45 كحد أقصى', - 'المعالجة': 'لا تقل عن 7 أيام', - 'المواصفات المرجعية': 'ASTM C150' - } - elif category == 'حديد التسليح': - category_specs = { - 'نوع الحديد': 'عالي المقاومة للشد (Grade 60)', - 'إجهاد الخضوع': '420 نيوتن/مم²', - 'المواصفات المرجعية': 'ASTM A615' - } - elif category == 'العزل المائي': - category_specs = { - 'النوع': 'أغشية بيتومينية مدعمة بالبوليستر', - 'السماكة': '4 مم', - 'مقاومة درجة الحرارة': 'حتى 100 درجة مئوية', - 'المواصفات المرجعية': 'ASTM D6164' - } - elif category == 'المواد الكهربائية': - category_specs = { - 'الكابلات': 'نحاس معزول XLPE', - 'المواصفات المرجعية': 'IEC 60502' - } - - return category_specs - - def _extract_special_requirements(self, sentences): - """استخراج المتطلبات الخاصة من الجمل""" - special_requirements = [] - - # الكلمات المفتاحية التي تشير إلى متطلبات خاصة - special_keywords = [ - 'يجب', 'ضرورة', 'يلزم', 'اشتراط', 'متطلب', 'إلزامي', - 'اعتماد', 'موافقة', 'تقديم', 'تأكيد', 'ضمان', 'توافق' - ] - - # استخراج الجمل التي تحتوي على الكلمات المفتاحية - for s in sentences: - if any(keyword in s.lower() for keyword in special_keywords): - # تنظيف الجملة - req = s.strip() - - # التأكد من أن الجملة تبدأ بيجب أو إذا لم تكن كذلك أضف "يجب" في البداية - if not any(req.startswith(start) for start in ['يجب', 'ضرورة', 'يلزم']): - req = f"يجب {req}" - - # التأكد من أن الجملة تنتهي بنقطة - if not req.endswith('.'): - req = f"{req}." - - # إضافة المتطلب إلى القائمة إذا لم يكن موجوداً بالفعل - if req not in special_requirements: - special_requirements.append(req) - - # إضافة متطلبات افتراضية إذا لم يتم العثور على متطلبات - if not special_requirements: - special_requirements = [ - "يجب أن تكون جميع المواد معتمدة من المهندس المشرف قبل التوريد.", - "يجب تقديم عينات لجميع المواد المستخدمة للاعتماد.", - "يجب تقديم شهادات ضمان لمدة سنة لجميع الأعمال المنفذة.", - "يجب الالتزام بكود البناء السعودي في جميع الأعمال.", - "يجب توفير اختبارات ضبط الجودة لأعمال الخرسانة.", - "يجب الالتزام بنسبة المحتوى المحلي لا تقل عن 70%." - ] - - return special_requirements - - def _extract_local_content(self, sentences): - """استخراج متطلبات المحتوى المحلي من الجمل""" - local_content_df = pd.DataFrame() - - # الكلمات المفتاحية للمحتوى المحلي - lc_keywords = ['محتوى محلي', 'منتج وطني', 'صناعة محلية', 'توطين'] - - # استخراج الجمل التي تحتوي على كلمات مفتاحية للمحتوى المحلي - lc_sentences = [s for s in sentences if any(k in s.lower() for k in lc_keywords)] - - # إذا وجدت جمل متعلقة بالمحتوى المحلي - if lc_sentences: - lc_data = [] - - # البحث عن نسب محددة في الجمل - for s in lc_sentences: - # البحث عن نسب مئوية - percentages = re.findall(r'(\d+)(?:\.\d+)?%', s) - - if percentages: - # محاولة استخراج الفئة من الجملة - if 'عمال' in s.lower() or 'قوى' in s.lower() or 'موظف' in s.lower(): - lc_data.append({ - 'الفئة': 'القوى العاملة', - 'النسبة المطلوبة': f"{percentages[0]}%", - 'الملاحظات': 'تشمل العمالة والمهندسين والإداريين' - }) - elif 'منتج' in s.lower() or 'صناع' in s.lower() or 'مواد' in s.lower() or 'معدات' in s.lower(): - lc_data.append({ - 'الفئة': 'المنتجات', - 'النسبة المطلوبة': f"{percentages[0]}%", - 'الملاحظات': 'تشمل المواد والمعدات المصنعة محلياً' - }) - elif 'خدم' in s.lower() or 'نقل' in s.lower() or 'تأمين' in s.lower(): - lc_data.append({ - 'الفئة': 'الخدمات', - 'النسبة المطلوبة': f"{percentages[0]}%", - 'الملاحظات': 'تشمل خدمات النقل والتأمين والاستشارات' - }) - else: - # إذا لم يتم تحديد الفئة، اعتبرها إجمالي - lc_data.append({ - 'الفئة': 'إجمالي المشروع', - 'النسبة المطلوبة': f"{percentages[0]}%", - 'الملاحظات': 'نسبة المحتوى المحلي الإجمالية للمشروع' - }) - - # تحويل البيانات إلى DataFrame - if lc_data: - local_content_df = pd.DataFrame(lc_data) - - # إذا لم يتم العثور على متطلبات محتوى محلي، استخدم بيانات افتراضية - if local_content_df.empty: - local_content_df = pd.DataFrame({ - 'الفئة': ['القوى العاملة', 'المنتجات', 'الخدمات'], - 'النسبة المطلوبة': ['80%', '70%', '60%'], - 'الملاحظات': [ - 'تشمل العمالة والمهندسين والإداريين', - 'تشمل المواد والمعدات المصنعة محلياً', - 'تشمل خدمات النقل والتأمين والاستشارات' - ] - }) - - return local_content_df \ No newline at end of file diff --git a/modules/services/text_extractor.py b/modules/services/text_extractor.py deleted file mode 100644 index e042bda0c9777a5319c3160c4b21d4864e8ff5ba..0000000000000000000000000000000000000000 --- a/modules/services/text_extractor.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -خدمة استخراج النصوص من المستندات -""" - -import os -import PyPDF2 -import docx -import pandas as pd -from pathlib import Path -import config - -class TextExtractor: - """استخراج النصوص من المستندات المختلفة""" - - def __init__(self): - pass - - def extract_from_pdf(self, file_path): - """استخراج النص من ملف PDF""" - text = "" - - try: - with open(file_path, 'rb') as file: - pdf_reader = PyPDF2.PdfReader(file) - for page_num in range(len(pdf_reader.pages)): - page = pdf_reader.pages[page_num] - text += page.extract_text() + "\n\n" - except Exception as e: - print(f"خطأ في استخراج النص من PDF: {str(e)}") - return "" - - return text - - def extract_from_docx(self, file_path): - """استخراج النص من ملف Word""" - text = "" - - try: - doc = docx.Document(file_path) - for para in doc.paragraphs: - text += para.text + "\n" - except Exception as e: - print(f"خطأ في استخراج النص من DOCX: {str(e)}") - return "" - - return text - - def extract_from_excel(self, file_path): - """استخراج البيانات من ملف Excel""" - try: - # قراءة جميع الصفحات - excel_data = pd.read_excel(file_path, sheet_name=None) - - # تجميع البيانات من جميع الصفحات - text = "" - for sheet_name, sheet_data in excel_data.items(): - text += f"صفحة: {sheet_name}\n" - text += sheet_data.to_string(index=False) + "\n\n" - except Exception as e: - print(f"خطأ في استخراج النص من Excel: {str(e)}") - return "" - - return text - - def extract_from_text(self, file_path): - """استخراج النص من ملف نصي""" - try: - with open(file_path, 'r', encoding='utf-8') as file: - text = file.read() - except Exception as e: - print(f"خطأ في استخراج النص من الملف النصي: {str(e)}") - return "" - - return text - - def extract_text(self, file_path): - """استخراج النص من أي نوع ملف مدعوم""" - file_ext = Path(file_path).suffix.lower() - - if file_ext == '.pdf': - return self.extract_from_pdf(file_path) - elif file_ext in ['.docx', '.doc']: - return self.extract_from_docx(file_path) - elif file_ext in ['.xlsx', '.xls']: - return self.extract_from_excel(file_path) - elif file_ext == '.txt': - return self.extract_from_text(file_path) - else: - print(f"نوع الملف غير مدعوم: {file_ext}") - return "" \ No newline at end of file diff --git a/modules/translation/translation_app.py b/modules/translation/translation_app.py deleted file mode 100644 index 7eac1aa02f855b7a6a59041834ce72a461ad6314..0000000000000000000000000000000000000000 --- a/modules/translation/translation_app.py +++ /dev/null @@ -1,936 +0,0 @@ -""" -وحدة الترجمة - نظام تحليل المناقصات -""" - -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/modules/voice_narration/__init__.py b/modules/voice_narration/__init__.py deleted file mode 100644 index 1267f2ed042ede80e3b2f414c207a29153d69bd5..0000000000000000000000000000000000000000 --- a/modules/voice_narration/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# ملف تهيئة وحدة الترجمة الصوتية متعددة اللغات \ No newline at end of file diff --git a/modules/voice_narration/voice_narration_app.py b/modules/voice_narration/voice_narration_app.py deleted file mode 100644 index 65374e3adee4067da02770c7e3c59d1387ec7ce2..0000000000000000000000000000000000000000 --- a/modules/voice_narration/voice_narration_app.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة تطبيق الترجمة الصوتية متعددة اللغات لتفاصيل المشروع -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات الترجمة الصوتية -from modules.voice_narration.voice_over_system import VoiceOverSystem - - -class VoiceNarrationApp: - """وحدة تطبيق الترجمة الصوتية متعددة اللغات""" - - def __init__(self): - """تهيئة وحدة تطبيق الترجمة الصوتية متعددة اللغات""" - self.voice_over_system = VoiceOverSystem() - - def render(self): - """عرض واجهة وحدة تطبيق الترجمة الصوتية متعددة اللغات""" - st.markdown("

نظام الترجمة الصوتية متعددة اللغات لتفاصيل المشروع

", unsafe_allow_html=True) - - st.markdown(""" -
- يتيح لك نظام الترجمة الصوتية متعددة اللغات تحويل النصوص والمستندات إلى ملفات صوتية بلغات متعددة، - مما يساعد في توصيل معلومات المشاريع والعقود والمناقصات بشكل أفضل للأشخاص من خلفيات لغوية مختلفة. -
- """, unsafe_allow_html=True) - - # عرض نظام الترجمة الصوتية - self.voice_over_system.render() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="الترجمة الصوتية متعددة اللغات | WAHBi AI", - page_icon="🎙️", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = VoiceNarrationApp() - app.render() \ No newline at end of file diff --git a/modules/voice_narration/voice_over_system.py b/modules/voice_narration/voice_over_system.py deleted file mode 100644 index ccdf36e7dcd837fb8df7c2174594281e6c33974e..0000000000000000000000000000000000000000 --- a/modules/voice_narration/voice_over_system.py +++ /dev/null @@ -1,1916 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -وحدة الترجمة الصوتية متعددة اللغات لتفاصيل المشروع -تتيح هذه الوحدة تحويل محتوى المشروع النصي إلى مقاطع صوتية بلغات متعددة للتسهيل على المستخدمين -""" - -import os -import sys -import streamlit as st -import pandas as pd -import numpy as np -import json -import base64 -import tempfile -import time -import datetime -import logging -from typing import List, Dict, Any, Tuple, Optional, Union -import io -from io import BytesIO -import re - -# إضافة مسار النظام للوصول للملفات المشتركة -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - -# استيراد مكونات واجهة المستخدم -from utils.components.header import render_header -from utils.components.credits import render_credits -from utils.helpers import format_number, format_currency, styled_button - - -class VoiceOverSystem: - """فئة نظام الترجمة الصوتية متعددة اللغات""" - - def __init__(self): - """تهيئة نظام الترجمة الصوتية""" - # تهيئة مجلدات حفظ البيانات - self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/voice_narration")) - os.makedirs(self.data_dir, exist_ok=True) - - # إعداد الكاش المحلي للملفات الصوتية - self.cache_dir = os.path.join(self.data_dir, "cache") - os.makedirs(self.cache_dir, exist_ok=True) - - # تهيئة سجل الأصوات المخزنة في الكاش - self.cache_index_file = os.path.join(self.data_dir, "voice_cache_index.json") - self.cache_index = self._load_cache_index() - - # تعيين اللغات المدعومة - self.supported_languages = { - "ar": "العربية", - "en": "الإنجليزية", - "fr": "الفرنسية", - "es": "الإسبانية", - "de": "الألمانية", - "it": "الإيطالية", - "zh": "الصينية", - "ja": "اليابانية", - "ru": "الروسية", - "tr": "التركية" - } - - # تعيين الأصوات لكل لغة - self.voices_by_language = { - "ar": [ - {"id": "ar-female-1", "name": "فاطمة", "gender": "أنثى"}, - {"id": "ar-male-1", "name": "محمد", "gender": "ذكر"}, - {"id": "ar-female-2", "name": "نور", "gender": "أنثى"}, - {"id": "ar-male-2", "name": "أحمد", "gender": "ذكر"} - ], - "en": [ - {"id": "en-female-1", "name": "Sarah", "gender": "أنثى"}, - {"id": "en-male-1", "name": "John", "gender": "ذكر"}, - {"id": "en-female-2", "name": "Emily", "gender": "أنثى"}, - {"id": "en-male-2", "name": "Robert", "gender": "ذكر"} - ], - "fr": [ - {"id": "fr-female-1", "name": "Marie", "gender": "أنثى"}, - {"id": "fr-male-1", "name": "Jean", "gender": "ذكر"} - ], - "es": [ - {"id": "es-female-1", "name": "Maria", "gender": "أنثى"}, - {"id": "es-male-1", "name": "Carlos", "gender": "ذكر"} - ], - "de": [ - {"id": "de-female-1", "name": "Hannah", "gender": "أنثى"}, - {"id": "de-male-1", "name": "Max", "gender": "ذكر"} - ], - "it": [ - {"id": "it-female-1", "name": "Sofia", "gender": "أنثى"}, - {"id": "it-male-1", "name": "Marco", "gender": "ذكر"} - ], - "zh": [ - {"id": "zh-female-1", "name": "Li Wei", "gender": "أنثى"}, - {"id": "zh-male-1", "name": "Zhang Wei", "gender": "ذكر"} - ], - "ja": [ - {"id": "ja-female-1", "name": "Yuki", "gender": "أنثى"}, - {"id": "ja-male-1", "name": "Hiroshi", "gender": "ذكر"} - ], - "ru": [ - {"id": "ru-female-1", "name": "Olga", "gender": "أنثى"}, - {"id": "ru-male-1", "name": "Ivan", "gender": "ذكر"} - ], - "tr": [ - {"id": "tr-female-1", "name": "Ayşe", "gender": "أنثى"}, - {"id": "tr-male-1", "name": "Mehmet", "gender": "ذكر"} - ] - } - - # إعدادات الصوت الافتراضية - if "voice_settings" not in st.session_state: - st.session_state.voice_settings = { - "primary_language": "ar", - "secondary_language": "en", - "primary_voice": "ar-female-1", - "secondary_voice": "en-female-1", - "speaking_rate": 1.0, - "pitch": 0.0, - "auto_translate": True, - "include_subtitles": True, - "emphasis_keywords": True - } - - # تحميل تاريخ التحويلات الصوتية - self.voice_history_file = os.path.join(self.data_dir, "voice_history.json") - self.voice_history = self._load_voice_history() - - # تسجيل الأحداث - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler(os.path.join(self.data_dir, "voice_narration.log")), - logging.StreamHandler() - ] - ) - self.logger = logging.getLogger("voice_over_system") - - def render(self): - """عرض واجهة نظام الترجمة الصوتية متعددة اللغات""" - render_header("نظام الترجمة الصوتية متعددة اللغات") - - # تبويبات الوحدة - tabs = st.tabs([ - "إنشاء ترجمة صوتية", - "مدير الترجمات الصوتية", - "إعدادات الصوت", - "ترجمة مستندات كاملة", - "إحصائيات ومقاييس" - ]) - - # تبويب إنشاء ترجمة صوتية - with tabs[0]: - self._render_create_voice_over() - - # تبويب مدير الترجمات الصوتية - with tabs[1]: - self._render_voice_over_manager() - - # تبويب إعدادات الصوت - with tabs[2]: - self._render_voice_settings() - - # تبويب ترجمة مستندات كاملة - with tabs[3]: - self._render_document_narration() - - # تبويب إحصائيات ومقاييس - with tabs[4]: - self._render_voice_over_analytics() - - # عرض حقوق النشر - render_credits() - - def _render_create_voice_over(self): - """عرض واجهة إنشاء ترجمة صوتية""" - st.markdown(""" -
-

🎙️ إنشاء ترجمة صوتية

-

إنشاء ترجمة صوتية لنص معين بلغات متعددة.

-
- """, unsafe_allow_html=True) - - # اختيار نوع المحتوى - content_type = st.radio( - "نوع المحتوى", - options=["نص حر", "بيانات مشروع", "ملخص مناقصة", "بنود عقد"], - horizontal=True, - key="voice_content_type" - ) - - # مقدار النص الذي سيتم عرضه بناءً على نوع المحتوى - if content_type == "نص حر": - content_text = st.text_area( - "النص المراد تحويله إلى صوت", - height=150, - placeholder="أدخل النص الذي ترغب في تحويله إلى صوت هنا...", - key="voice_content_text" - ) - - title = st.text_input( - "عنوان الملف الصوتي", - placeholder="عنوان لتسهيل الوصول للملف الصوتي لاحقاً", - key="voice_title" - ) - - elif content_type == "بيانات مشروع": - # عرض المشاريع المتاحة في النظام - projects = self._get_projects() - - if projects: - selected_project_id = st.selectbox( - "اختر المشروع", - options=[p["id"] for p in projects], - format_func=lambda x: next((p["name"] for p in projects if p["id"] == x), ""), - key="voice_project_id" - ) - - # العثور على المشروع المحدد - selected_project = next((p for p in projects if p["id"] == selected_project_id), None) - - if selected_project: - # نعرض بيانات المشروع - st.subheader(f"بيانات المشروع: {selected_project['name']}") - - project_details = f""" - اسم المشروع: {selected_project['name']} - رقم المشروع: {selected_project['id']} - الحالة: {selected_project.get('status', 'غير محدد')} - الموقع: {selected_project.get('location', 'غير محدد')} - تاريخ البدء: {selected_project.get('start_date', 'غير محدد')} - تاريخ الانتهاء المتوقع: {selected_project.get('expected_end_date', 'غير محدد')} - الميزانية: {selected_project.get('budget', 'غير محدد')} - - وصف المشروع: {selected_project.get('description', 'لا يوجد وصف متاح')} - """ - - st.text_area( - "تفاصيل المشروع (يمكنك تعديلها قبل التحويل إلى صوت)", - value=project_details, - height=250, - key="voice_project_details" - ) - - content_text = st.session_state.voice_project_details - title = f"ملخص مشروع {selected_project['name']}" - else: - st.warning("لم يتم العثور على المشروع المحدد") - content_text = "" - title = "" - else: - st.info("لا توجد مشاريع متاحة حالياً") - content_text = "" - title = "" - - elif content_type == "ملخص مناقصة": - # عرض المناقصات المتاحة في النظام - tenders = self._get_tenders() - - if tenders: - selected_tender_id = st.selectbox( - "اختر المناقصة", - options=[t["id"] for t in tenders], - format_func=lambda x: next((t["name"] for t in tenders if t["id"] == x), ""), - key="voice_tender_id" - ) - - # العثور على المناقصة المحددة - selected_tender = next((t for t in tenders if t["id"] == selected_tender_id), None) - - if selected_tender: - # نعرض بيانات المناقصة - st.subheader(f"بيانات المناقصة: {selected_tender['name']}") - - tender_details = f""" - اسم المناقصة: {selected_tender['name']} - رقم المناقصة: {selected_tender['id']} - الجهة المالكة: {selected_tender.get('owner', 'غير محدد')} - تاريخ الطرح: {selected_tender.get('issue_date', 'غير محدد')} - تاريخ التسليم: {selected_tender.get('submission_date', 'غير محدد')} - القيمة التقديرية: {selected_tender.get('estimated_value', 'غير محدد')} - - وصف المناقصة: {selected_tender.get('description', 'لا يوجد وصف متاح')} - """ - - st.text_area( - "تفاصيل المناقصة (يمكنك تعديلها قبل التحويل إلى صوت)", - value=tender_details, - height=250, - key="voice_tender_details" - ) - - content_text = st.session_state.voice_tender_details - title = f"ملخص مناقصة {selected_tender['name']}" - else: - st.warning("لم يتم العثور على المناقصة المحددة") - content_text = "" - title = "" - else: - st.info("لا توجد مناقصات متاحة حالياً") - content_text = "" - title = "" - - elif content_type == "بنود عقد": - # عرض العقود المتاحة في النظام - contracts = self._get_contracts() - - if contracts: - selected_contract_id = st.selectbox( - "اختر العقد", - options=[c["id"] for c in contracts], - format_func=lambda x: next((c["name"] for c in contracts if c["id"] == x), ""), - key="voice_contract_id" - ) - - # العثور على العقد المحدد - selected_contract = next((c for c in contracts if c["id"] == selected_contract_id), None) - - if selected_contract: - # نعرض بيانات العقد - st.subheader(f"بيانات العقد: {selected_contract['name']}") - - # العثور على بنود العقد - contract_clauses = selected_contract.get("clauses", []) - - if contract_clauses: - # السماح للمستخدم باختيار البنود التي يريد تحويلها - selected_clauses = st.multiselect( - "اختر البنود المراد تحويلها إلى صوت", - options=list(range(len(contract_clauses))), - format_func=lambda i: f"البند {i+1}: {contract_clauses[i]['title']}", - key="voice_contract_clauses" - ) - - if selected_clauses: - # تجميع النصوص المختارة - clauses_text = "" - for i in selected_clauses: - clauses_text += f"البند {i+1}: {contract_clauses[i]['title']}\n" - clauses_text += f"{contract_clauses[i]['content']}\n\n" - - st.text_area( - "نص البنود المختارة (يمكنك تعديلها قبل التحويل إلى صوت)", - value=clauses_text, - height=250, - key="voice_contract_text" - ) - - content_text = st.session_state.voice_contract_text - title = f"بنود من عقد {selected_contract['name']}" - else: - st.info("الرجاء اختيار بند واحد على الأقل") - content_text = "" - title = "" - else: - st.info("لا توجد بنود متاحة لهذا العقد") - content_text = "" - title = "" - else: - st.warning("لم يتم العثور على العقد المحدد") - content_text = "" - title = "" - else: - st.info("لا توجد عقود متاحة حالياً") - content_text = "" - title = "" - - # إعدادات اللغة للنص المدخل - 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=list(self.supported_languages.keys()).index(st.session_state.voice_settings["primary_language"]), - key="voice_source_language" - ) - - voice_id = st.selectbox( - "الصوت", - options=[v["id"] for v in self.voices_by_language[source_language]], - format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[source_language] if v["id"] == x), ""), - index=0, - key="voice_source_voice" - ) - - with col2: - target_language = st.selectbox( - "لغة الترجمة (اختياري)", - options=["none"] + list(self.supported_languages.keys()), - format_func=lambda x: "بدون ترجمة" if x == "none" else self.supported_languages[x], - index=0, - key="voice_target_language" - ) - - if target_language != "none": - target_voice_id = st.selectbox( - "صوت الترجمة", - options=[v["id"] for v in self.voices_by_language[target_language]], - format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[target_language] if v["id"] == x), ""), - index=0, - key="voice_target_voice" - ) - else: - target_voice_id = None - - # خيارات متقدمة - with st.expander("خيارات متقدمة"): - advanced_col1, advanced_col2 = st.columns(2) - - with advanced_col1: - speaking_rate = st.slider( - "سرعة النطق", - min_value=0.5, - max_value=2.0, - value=st.session_state.voice_settings["speaking_rate"], - step=0.1, - key="voice_speaking_rate" - ) - - include_subtitles = st.checkbox( - "تضمين النص مع الصوت", - value=st.session_state.voice_settings["include_subtitles"], - key="voice_include_subtitles" - ) - - with advanced_col2: - pitch = st.slider( - "درجة الصوت", - min_value=-10.0, - max_value=10.0, - value=st.session_state.voice_settings["pitch"], - step=1.0, - key="voice_pitch" - ) - - emphasize_keywords = st.checkbox( - "تمييز الكلمات المهمة", - value=st.session_state.voice_settings["emphasis_keywords"], - key="voice_emphasize_keywords" - ) - - # شروط إنشاء الترجمة الصوتية - create_voice_over = False - - if content_text: - # زر إنشاء الترجمة الصوتية - if styled_button("إنشاء الترجمة الصوتية", key="create_voice_over_btn", type="primary", icon="🎙️"): - if title: - create_voice_over = True - else: - st.warning("الرجاء إدخال عنوان للملف الصوتي") - else: - st.info("الرجاء إدخال أو اختيار محتوى للترجمة الصوتية") - - # إنشاء الترجمة الصوتية - if create_voice_over: - with st.spinner("جاري إنشاء الترجمة الصوتية..."): - try: - # تحقق من وجود الملف في الكاش - cache_key = self._generate_cache_key( - content_text, - source_language, - voice_id, - speaking_rate, - pitch - ) - - cached_file = self._get_from_cache(cache_key) - - if cached_file: - st.success("تم استرجاع الترجمة الصوتية من الكاش") - audio_file = cached_file - audio_duration = self._get_audio_duration(audio_file) - else: - # إنشاء الترجمة الصوتية - audio_file, audio_duration = self._generate_voice_over( - content_text, - source_language, - voice_id, - speaking_rate, - pitch - ) - - # حفظ الملف في الكاش - self._add_to_cache(cache_key, audio_file) - - # تسجيل الترجمة الصوتية في التاريخ - voice_over_id = self._add_voice_to_history( - title=title, - content=content_text, - source_language=source_language, - voice_id=voice_id, - duration=audio_duration, - audio_file=os.path.basename(audio_file), - content_type=content_type - ) - - # ترجمة المحتوى إذا تم اختيار لغة ترجمة - if target_language != "none": - with st.spinner(f"جاري الترجمة إلى {self.supported_languages[target_language]}..."): - # ترجمة النص - translated_text = self._translate_text( - content_text, - source_language, - target_language - ) - - # تحقق من وجود الملف المترجم في الكاش - translated_cache_key = self._generate_cache_key( - translated_text, - target_language, - target_voice_id, - speaking_rate, - pitch - ) - - cached_translated_file = self._get_from_cache(translated_cache_key) - - if cached_translated_file: - st.success("تم استرجاع الترجمة الصوتية المترجمة من الكاش") - translated_audio_file = cached_translated_file - translated_audio_duration = self._get_audio_duration(translated_audio_file) - else: - # إنشاء الترجمة الصوتية للنص المترجم - translated_audio_file, translated_audio_duration = self._generate_voice_over( - translated_text, - target_language, - target_voice_id, - speaking_rate, - pitch - ) - - # حفظ الملف المترجم في الكاش - self._add_to_cache(translated_cache_key, translated_audio_file) - - # تسجيل الترجمة الصوتية المترجمة في التاريخ - translated_voice_over_id = self._add_voice_to_history( - title=f"{title} ({self.supported_languages[target_language]})", - content=translated_text, - source_language=target_language, - voice_id=target_voice_id, - duration=translated_audio_duration, - audio_file=os.path.basename(translated_audio_file), - content_type=content_type, - is_translation=True, - original_id=voice_over_id - ) - - # عرض الترجمة الصوتية المترجمة - st.subheader(f"الترجمة الصوتية بـ{self.supported_languages[target_language]}") - - # عرض النص المترجم إذا تم اختيار ذلك - if include_subtitles: - st.markdown(f"**النص المترجم:**\n{translated_text}") - - # عرض مشغل الصوت - self._display_audio_player(translated_audio_file) - - # عرض الترجمة الصوتية - st.subheader(f"الترجمة الصوتية بـ{self.supported_languages[source_language]}") - - # عرض النص إذا تم اختيار ذلك - if include_subtitles: - st.markdown(f"**النص:**\n{content_text}") - - # عرض مشغل الصوت - self._display_audio_player(audio_file) - - # زر تنزيل الملف الصوتي - with open(audio_file, "rb") as f: - audio_bytes = f.read() - - st.download_button( - label="تنزيل الملف الصوتي", - data=audio_bytes, - file_name=f"{title}.mp3", - mime="audio/mpeg", - key="download_voice_over" - ) - - st.success("تم إنشاء الترجمة الصوتية بنجاح!") - - except Exception as e: - st.error(f"حدث خطأ أثناء إنشاء الترجمة الصوتية: {str(e)}") - self.logger.error(f"خطأ في إنشاء الترجمة الصوتية: {str(e)}") - - def _render_voice_over_manager(self): - """عرض واجهة مدير الترجمات الصوتية""" - st.markdown(""" -
-

🎧 مدير الترجمات الصوتية

-

استعراض وإدارة الترجمات الصوتية المخزنة.

-
- """, unsafe_allow_html=True) - - # تحديث تاريخ الترجمات الصوتية - self.voice_history = self._load_voice_history() - - # التحقق من وجود ترجمات صوتية - if not self.voice_history: - st.info("لا توجد ترجمات صوتية مخزنة.") - return - - # أزرار التحكم - col1, col2 = st.columns(2) - - with col1: - # فلترة حسب نوع المحتوى - content_types = ["الكل"] + list(set(item.get("content_type", "نص حر") for item in self.voice_history)) - filter_content_type = st.selectbox( - "فلترة حسب نوع المحتوى", - options=content_types, - key="filter_content_type" - ) - - with col2: - # فلترة حسب اللغة - languages = ["الكل"] + [self.supported_languages.get(item.get("source_language", "ar"), "العربية") for item in self.voice_history] - filter_language = st.selectbox( - "فلترة حسب اللغة", - options=list(set(languages)), - key="filter_language" - ) - - # فلترة العناصر - filtered_history = self.voice_history - - if filter_content_type != "الكل": - filtered_history = [item for item in filtered_history if item.get("content_type", "نص حر") == filter_content_type] - - if filter_language != "الكل": - filtered_history = [ - item for item in filtered_history - if self.supported_languages.get(item.get("source_language", "ar"), "العربية") == filter_language - ] - - # عرض الترجمات الصوتية - for voice_item in filtered_history: - with st.expander(f"{voice_item['title']} ({voice_item.get('created_at', 'تاريخ غير معروف')})", expanded=False): - # تفاصيل الترجمة الصوتية - item_col1, item_col2 = st.columns([3, 1]) - - with item_col1: - st.markdown(f"**النوع:** {voice_item.get('content_type', 'نص حر')}") - st.markdown(f"**اللغة:** {self.supported_languages.get(voice_item.get('source_language', 'ar'), 'العربية')}") - st.markdown(f"**المدة:** {voice_item.get('duration', 0):.2f} ثانية") - - # عرض مشغل الصوت - audio_file_path = os.path.join(self.data_dir, voice_item.get('audio_file', '')) - if os.path.exists(audio_file_path): - self._display_audio_player(audio_file_path) - else: - st.warning("ملف الصوت غير متوفر") - - with item_col2: - # عرض النص - if st.button("عرض النص", key=f"show_text_{voice_item.get('id', '')}"): - st.text_area( - "نص الترجمة الصوتية", - value=voice_item.get('content', ''), - height=150, - key=f"text_{voice_item.get('id', '')}", - disabled=True - ) - - # تنزيل الملف الصوتي - audio_file_path = os.path.join(self.data_dir, voice_item.get('audio_file', '')) - if os.path.exists(audio_file_path): - with open(audio_file_path, "rb") as f: - audio_bytes = f.read() - - st.download_button( - label="تنزيل الملف الصوتي", - data=audio_bytes, - file_name=f"{voice_item['title']}.mp3", - mime="audio/mpeg", - key=f"download_{voice_item.get('id', '')}" - ) - - # حذف الترجمة الصوتية - if st.button("حذف", key=f"delete_{voice_item.get('id', '')}", type="primary"): - if self._delete_voice_from_history(voice_item.get('id', '')): - st.success("تم حذف الترجمة الصوتية بنجاح!") - st.rerun() - else: - st.error("حدث خطأ أثناء حذف الترجمة الصوتية") - - def _render_voice_settings(self): - """عرض واجهة إعدادات الصوت""" - st.markdown(""" -
-

⚙️ إعدادات الصوت

-

تخصيص إعدادات الترجمة الصوتية الافتراضية.

-
- """, unsafe_allow_html=True) - - # إعدادات اللغة - st.markdown("### إعدادات اللغة") - - lang_col1, lang_col2 = st.columns(2) - - with lang_col1: - # اللغة الأساسية - primary_language = st.selectbox( - "اللغة الأساسية", - options=list(self.supported_languages.keys()), - format_func=lambda x: self.supported_languages[x], - index=list(self.supported_languages.keys()).index(st.session_state.voice_settings["primary_language"]), - key="settings_primary_language" - ) - - # الصوت الأساسي - primary_voice = st.selectbox( - "الصوت الأساسي", - options=[v["id"] for v in self.voices_by_language[primary_language]], - format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[primary_language] if v["id"] == x), ""), - index=0, - key="settings_primary_voice" - ) - - with lang_col2: - # اللغة الثانوية - secondary_language = st.selectbox( - "اللغة الثانوية", - options=list(self.supported_languages.keys()), - format_func=lambda x: self.supported_languages[x], - index=list(self.supported_languages.keys()).index(st.session_state.voice_settings["secondary_language"]), - key="settings_secondary_language" - ) - - # الصوت الثانوي - secondary_voice = st.selectbox( - "الصوت الثانوي", - options=[v["id"] for v in self.voices_by_language[secondary_language]], - format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[secondary_language] if v["id"] == x), ""), - index=0, - key="settings_secondary_voice" - ) - - # إعدادات جودة الصوت - st.markdown("### إعدادات جودة الصوت") - - quality_col1, quality_col2 = st.columns(2) - - with quality_col1: - # سرعة النطق - speaking_rate = st.slider( - "سرعة النطق الافتراضية", - min_value=0.5, - max_value=2.0, - value=st.session_state.voice_settings["speaking_rate"], - step=0.1, - key="settings_speaking_rate" - ) - - with quality_col2: - # درجة الصوت - pitch = st.slider( - "درجة الصوت الافتراضية", - min_value=-10.0, - max_value=10.0, - value=st.session_state.voice_settings["pitch"], - step=1.0, - key="settings_pitch" - ) - - # إعدادات أخرى - st.markdown("### إعدادات أخرى") - - other_col1, other_col2 = st.columns(2) - - with other_col1: - # الترجمة التلقائية - auto_translate = st.checkbox( - "ترجمة تلقائية إلى اللغة الثانوية", - value=st.session_state.voice_settings["auto_translate"], - key="settings_auto_translate" - ) - - # تضمين النص مع الصوت - include_subtitles = st.checkbox( - "تضمين النص مع الصوت افتراضياً", - value=st.session_state.voice_settings["include_subtitles"], - key="settings_include_subtitles" - ) - - with other_col2: - # تمييز الكلمات المهمة - emphasis_keywords = st.checkbox( - "تمييز الكلمات المهمة تلقائياً", - value=st.session_state.voice_settings["emphasis_keywords"], - key="settings_emphasis_keywords" - ) - - # زر حفظ الإعدادات - if styled_button("حفظ الإعدادات", key="save_voice_settings", type="primary", icon="💾"): - # تحديث الإعدادات - st.session_state.voice_settings = { - "primary_language": primary_language, - "secondary_language": secondary_language, - "primary_voice": primary_voice, - "secondary_voice": secondary_voice, - "speaking_rate": speaking_rate, - "pitch": pitch, - "auto_translate": auto_translate, - "include_subtitles": include_subtitles, - "emphasis_keywords": emphasis_keywords - } - - # حفظ الإعدادات - self._save_voice_settings() - - st.success("تم حفظ الإعدادات بنجاح!") - - # إعدادات متقدمة - with st.expander("إعدادات متقدمة", expanded=False): - st.markdown("### إعدادات الكاش") - - cache_size = self._get_cache_size() - st.markdown(f"حجم الكاش الحالي: {cache_size / (1024 * 1024):.2f} ميجابايت") - - if styled_button("مسح الكاش", key="clear_cache", type="danger", icon="🗑️"): - if self._clear_cache(): - st.success("تم مسح الكاش بنجاح!") - else: - st.error("حدث خطأ أثناء مسح الكاش") - - st.markdown("### إعدادات API") - - # نموذج API للترجمة الصوتية - api_model = st.selectbox( - "نموذج API للترجمة الصوتية", - options=["local", "huggingface", "google", "amazon", "microsoft"], - format_func=lambda x: { - "local": "محلي (عرض توضيحي)", - "huggingface": "Hugging Face", - "google": "Google Cloud Text-to-Speech", - "amazon": "Amazon Polly", - "microsoft": "Microsoft Azure" - }[x], - index=0, - key="api_model" - ) - - # معلومات حول النموذج المحدد - api_info = { - "local": "هذا وضع العرض التوضيحي حيث يتم تشبيه الترجمة الصوتية دون الحاجة إلى اتصال API خارجي.", - "huggingface": "استخدام Hugging Face API لتحويل النص إلى صوت وترجمة النصوص.", - "google": "استخدام Google Cloud Text-to-Speech لإنتاج صوت عالي الجودة.", - "amazon": "استخدام Amazon Polly للترجمة الصوتية بجودة عالية ومجموعة متنوعة من الأصوات.", - "microsoft": "استخدام Microsoft Azure Speech Services للترجمة الصوتية والترجمة." - } - - st.markdown(f"**معلومات:** {api_info[api_model]}") - - if api_model != "local": - api_key = st.text_input( - f"مفتاح API لـ {api_model}", - type="password", - key=f"{api_model}_api_key" - ) - - if styled_button("حفظ مفتاح API", key="save_api_key", type="primary"): - st.success(f"تم حفظ مفتاح API لـ {api_model} بنجاح!") - - def _render_document_narration(self): - """عرض واجهة ترجمة مستندات كاملة""" - st.markdown(""" -
-

📄 ترجمة مستندات كاملة

-

تحويل مستندات كاملة إلى ملفات صوتية وتقسيمها إلى فصول أو أقسام.

-
- """, unsafe_allow_html=True) - - # اختيار المستند - documents = self._get_documents() - - if documents: - selected_document_id = st.selectbox( - "اختر المستند", - options=[d["id"] for d in documents], - format_func=lambda x: next((d["name"] for d in documents if d["id"] == x), ""), - key="narration_document_id" - ) - - # العثور على المستند المحدد - selected_document = next((d for d in documents if d["id"] == selected_document_id), None) - - if selected_document: - # عرض تفاصيل المستند - st.subheader(f"معلومات المستند: {selected_document['name']}") - - doc_col1, doc_col2 = st.columns(2) - - with doc_col1: - st.markdown(f"**النوع:** {selected_document.get('type', 'غير محدد')}") - st.markdown(f"**عدد الصفحات:** {selected_document.get('page_count', 'غير محدد')}") - - with doc_col2: - st.markdown(f"**حجم المستند:** {selected_document.get('file_size', 'غير محدد')}") - st.markdown(f"**تاريخ الرفع:** {selected_document.get('upload_date', 'غير محدد')}") - - # خيارات الترجمة الصوتية - st.markdown("### خيارات الترجمة الصوتية") - - options_col1, options_col2 = st.columns(2) - - with options_col1: - # اللغة - narration_language = st.selectbox( - "لغة الترجمة الصوتية", - options=list(self.supported_languages.keys()), - format_func=lambda x: self.supported_languages[x], - index=list(self.supported_languages.keys()).index(st.session_state.voice_settings["primary_language"]), - key="narration_language" - ) - - # الصوت - narration_voice = st.selectbox( - "الصوت", - options=[v["id"] for v in self.voices_by_language[narration_language]], - format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[narration_language] if v["id"] == x), ""), - index=0, - key="narration_voice" - ) - - # تقسيم المستند - narration_split = st.selectbox( - "تقسيم المستند", - options=["لا تقسيم", "حسب الصفحات", "حسب العناوين", "حسب الفصول"], - key="narration_split" - ) - - with options_col2: - # سرعة النطق - narration_speaking_rate = st.slider( - "سرعة النطق", - min_value=0.5, - max_value=2.0, - value=st.session_state.voice_settings["speaking_rate"], - step=0.1, - key="narration_speaking_rate" - ) - - # درجة الصوت - narration_pitch = st.slider( - "درجة الصوت", - min_value=-10.0, - max_value=10.0, - value=st.session_state.voice_settings["pitch"], - step=1.0, - key="narration_pitch" - ) - - # تضمين الفهرس - narration_include_toc = st.checkbox( - "تضمين فهرس صوتي", - value=True, - key="narration_include_toc" - ) - - # خيارات إضافية - with st.expander("خيارات إضافية", expanded=False): - # تجاهل الصفحات - narration_skip_pages = st.text_input( - "تجاهل الصفحات (أرقام مفصولة بفواصل)", - placeholder="مثال: 1,2,5-7", - key="narration_skip_pages" - ) - - # إضافة مقدمة - narration_intro = st.text_area( - "مقدمة خاصة (سيتم إضافتها في بداية الترجمة الصوتية)", - placeholder="مقدمة اختيارية...", - key="narration_intro" - ) - - # إضافة خاتمة - narration_outro = st.text_area( - "خاتمة خاصة (سيتم إضافتها في نهاية الترجمة الصوتية)", - placeholder="خاتمة اختيارية...", - key="narration_outro" - ) - - # زر إنشاء الترجمة الصوتية للمستند - if styled_button("إنشاء الترجمة الصوتية للمستند", key="create_document_narration", type="primary", icon="🎙️"): - # تحقق من وجود قسم للترجمة الصوتية - narration_folder = os.path.join(self.data_dir, "document_narrations", str(selected_document_id)) - os.makedirs(narration_folder, exist_ok=True) - - # التقدم المستمر في إنشاء الترجمة الصوتية - progress_bar = st.progress(0) - status_text = st.empty() - - # صنع ترجمة صوتية وهمية (للعرض التوضيحي) - total_sections = 5 # عدد أقسام افتراضي - - for i in range(total_sections + 1): - # تحديث شريط التقدم - progress = i / total_sections - progress_bar.progress(progress) - - if i == 0: - status_text.text("جاري تحليل المستند...") - elif i == 1: - status_text.text("جاري استخراج النص...") - elif i < total_sections: - status_text.text(f"جاري إنشاء الترجمة الصوتية للقسم {i}...") - else: - status_text.text("جاري تجميع الملفات الصوتية النهائية...") - - time.sleep(1) # محاكاة العمل - - # اكتمال العملية - progress_bar.progress(1.0) - status_text.text("تم إنشاء الترجمة الصوتية بنجاح!") - - # إظهار نتائج وهمية - st.subheader("الترجمة الصوتية للمستند") - - # عرض المقاطع الصوتية (وهمية) - for i in range(1, total_sections): - with st.expander(f"القسم {i}: العنوان الافتراضي {i}", expanded=i==1): - # محاكاة وجود ملف صوتي - st.audio("https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3") - - # عرض معلومات القسم - st.markdown(f"**المدة:** {i + 2} دقائق") - st.markdown(f"**عدد الكلمات:** {i * 100} كلمة") - - # زر تنزيل الملف الكامل - st.download_button( - label="تنزيل الترجمة الصوتية الكاملة", - data=b"Mock audio file", # بيانات وهمية باللغة الإنجليزية - file_name=f"{selected_document['name']}_narration.mp3", - mime="audio/mpeg", - key="download_full_narration" - ) - else: - st.warning("لم يتم العثور على المستند المحدد") - else: - st.info("لا توجد مستندات متاحة حالياً") - - # اختيار رفع مستند جديد - st.markdown("### رفع مستند جديد للترجمة الصوتية") - - uploaded_file = st.file_uploader( - "اختر ملف للرفع (PDF, DOCX, TXT)", - type=["pdf", "docx", "txt"], - key="narration_upload_file" - ) - - if uploaded_file: - file_name = uploaded_file.name - - # عرض معلومات الملف - st.markdown(f"**اسم الملف:** {file_name}") - st.markdown(f"**حجم الملف:** {uploaded_file.size / 1024:.2f} كيلوبايت") - - document_name = st.text_input( - "اسم المستند", - value=file_name, - key="narration_document_name" - ) - - if styled_button("رفع المستند", key="upload_document", type="primary", icon="📤"): - st.success(f"تم رفع المستند '{document_name}' بنجاح!") - st.info("يمكنك الآن اختيار المستند لإنشاء ترجمة صوتية له.") - st.rerun() - - def _render_voice_over_analytics(self): - """عرض إحصائيات ومقاييس الترجمات الصوتية""" - st.markdown(""" -
-

📊 إحصائيات ومقاييس

-

إحصائيات ومقاييس استخدام نظام الترجمة الصوتية.

-
- """, unsafe_allow_html=True) - - # تحديث تاريخ الترجمات الصوتية - self.voice_history = self._load_voice_history() - - # التحقق من وجود ترجمات صوتية - if not self.voice_history: - st.info("لا توجد ترجمات صوتية مخزنة.") - return - - # إحصائيات عامة - st.markdown("### إحصائيات عامة") - - # تحضير البيانات - total_voices = len(self.voice_history) - total_duration = sum(item.get("duration", 0) for item in self.voice_history) - total_languages = len(set(item.get("source_language", "ar") for item in self.voice_history)) - - # عرض الإحصائيات - stats_col1, stats_col2, stats_col3 = st.columns(3) - - with stats_col1: - st.metric("إجمالي الترجمات الصوتية", total_voices) - - with stats_col2: - st.metric("إجمالي المدة", f"{total_duration:.2f} ثانية") - - with stats_col3: - st.metric("عدد اللغات المستخدمة", total_languages) - - # رسم بياني لتوزيع الترجمات الصوتية حسب اللغة - st.markdown("### توزيع الترجمات الصوتية حسب اللغة") - - language_counts = {} - for item in self.voice_history: - lang = item.get("source_language", "ar") - lang_name = self.supported_languages.get(lang, "غير معروف") - language_counts[lang_name] = language_counts.get(lang_name, 0) + 1 - - # تحويل إلى DataFrame - language_df = pd.DataFrame({ - "اللغة": list(language_counts.keys()), - "العدد": list(language_counts.values()) - }) - - # رسم بياني دائري - import plotly.express as px - - fig1 = px.pie( - language_df, - values="العدد", - names="اللغة", - title="توزيع الترجمات الصوتية حسب اللغة", - color_discrete_sequence=px.colors.qualitative.Pastel - ) - - fig1.update_layout( - title_font_size=20, - font_family="Arial", - font_size=14, - height=400 - ) - - st.plotly_chart(fig1, use_container_width=True) - - # رسم بياني لتوزيع الترجمات الصوتية حسب نوع المحتوى - st.markdown("### توزيع الترجمات الصوتية حسب نوع المحتوى") - - content_type_counts = {} - for item in self.voice_history: - content_type = item.get("content_type", "نص حر") - content_type_counts[content_type] = content_type_counts.get(content_type, 0) + 1 - - # تحويل إلى DataFrame - content_df = pd.DataFrame({ - "نوع المحتوى": list(content_type_counts.keys()), - "العدد": list(content_type_counts.values()) - }) - - # رسم بياني شريطي - fig2 = px.bar( - content_df, - x="نوع المحتوى", - y="العدد", - title="توزيع الترجمات الصوتية حسب نوع المحتوى", - color="نوع المحتوى", - color_discrete_sequence=px.colors.qualitative.Pastel - ) - - fig2.update_layout( - title_font_size=20, - font_family="Arial", - font_size=14, - height=400 - ) - - st.plotly_chart(fig2, use_container_width=True) - - # رسم بياني لتوزيع الترجمات الصوتية حسب التاريخ - st.markdown("### توزيع الترجمات الصوتية حسب التاريخ") - - # استخراج التواريخ - dates = [] - for item in self.voice_history: - created_at = item.get("created_at", "") - if created_at: - try: - date = datetime.datetime.strptime(created_at.split(" ")[0], "%Y-%m-%d").date() - dates.append(date) - except (ValueError, IndexError): - continue - - if dates: - # عد التكرارات - date_counts = {} - for date in dates: - date_str = date.strftime("%Y-%m-%d") - date_counts[date_str] = date_counts.get(date_str, 0) + 1 - - # تحويل إلى DataFrame - date_df = pd.DataFrame({ - "التاريخ": list(date_counts.keys()), - "العدد": list(date_counts.values()) - }) - - # ترتيب حسب التاريخ - date_df["التاريخ"] = pd.to_datetime(date_df["التاريخ"]) - date_df = date_df.sort_values("التاريخ") - - # رسم بياني خطي - fig3 = px.line( - date_df, - x="التاريخ", - y="العدد", - title="توزيع الترجمات الصوتية حسب التاريخ", - markers=True - ) - - fig3.update_layout( - title_font_size=20, - font_family="Arial", - font_size=14, - height=400 - ) - - st.plotly_chart(fig3, use_container_width=True) - else: - st.info("لا توجد بيانات تاريخ كافية لعرض الرسم البياني") - - # تصدير البيانات - st.markdown("### تصدير البيانات") - - export_col1, export_col2 = st.columns(2) - - with export_col1: - if styled_button("تصدير CSV", key="export_voice_csv", type="primary", icon="📄"): - # تحويل البيانات إلى DataFrame - export_df = pd.DataFrame(self.voice_history) - - # تنزيل الملف - csv_data = export_df.to_csv(index=False) - - st.download_button( - label="تنزيل ملف CSV", - data=csv_data, - file_name=f"voice_over_history_{datetime.datetime.now().strftime('%Y%m%d')}.csv", - mime="text/csv", - key="download_voice_csv" - ) - - with export_col2: - if styled_button("تصدير JSON", key="export_voice_json", type="primary", icon="📄"): - # تحويل البيانات إلى JSON - json_data = json.dumps(self.voice_history, indent=2) - - st.download_button( - label="تنزيل ملف JSON", - data=json_data, - file_name=f"voice_over_history_{datetime.datetime.now().strftime('%Y%m%d')}.json", - mime="application/json", - key="download_voice_json" - ) - - def _generate_voice_over(self, text, language, voice_id, speaking_rate=1.0, pitch=0.0): - """ - إنشاء ترجمة صوتية (محاكاة) - - المعلمات: - text: النص المراد تحويله إلى صوت - language: رمز اللغة - voice_id: معرف الصوت - speaking_rate: سرعة النطق - pitch: درجة الصوت - - الإرجاع: - مسار الملف الصوتي ومدته - """ - try: - # في الوضع العادي، سنستخدم API لتحويل النص إلى صوت - # هنا نستخدم ملف صوتي وهمي للعرض التوضيحي - - # إنشاء ملف مؤقت لتمثيل الصوت - temp_file = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) - temp_file.close() - - # نسخ ملف صوتي وهمي (يمكن استبداله بإنشاء فعلي للصوت) - audio_file = os.path.join(self.data_dir, f"voice_{language}_{voice_id}_{int(time.time())}.mp3") - - # محاكاة إنشاء ملف صوتي باستخدام صوت بسيط - # هنا يمكن استبدال هذا بمكالمة API حقيقية لتحويل النص إلى صوت - import wave - import struct - - # إنشاء بيانات صوتية بسيطة - duration = len(text) * 0.1 # تقدير المدة بناءً على طول النص - sample_rate = 44100 # معدل العينات - - # محاكاة تأثير سرعة النطق على المدة - duration = duration / speaking_rate - - # إنشاء ملف WAV مؤقت - wav_file = temp_file.name.replace(".mp3", ".wav") - - with wave.open(wav_file, "w") as f: - f.setnchannels(1) # أحادي القناة - f.setsampwidth(2) # 16 بت - f.setframerate(sample_rate) - - # محاكاة صوت بسيط (موجة جيبية) - for i in range(int(duration * sample_rate)): - # تأثير درجة الصوت - value = 32767 * 0.3 * np.sin(2 * np.pi * (440 + pitch * 20) * i / sample_rate) - f.writeframes(struct.pack('h', int(value))) - - # تحويل WAV إلى MP3 (في التطبيق الفعلي) - # هنا نفترض أن التحويل تم بنجاح - import shutil - shutil.copy(wav_file, audio_file) - - # تنظيف الملفات المؤقتة - try: - os.remove(wav_file) - os.remove(temp_file.name) - except: - pass - - # تسجيل العملية - self.logger.info(f"تم إنشاء ترجمة صوتية للنص (طول: {len(text)}) باللغة: {language}") - - return audio_file, duration - - except Exception as e: - self.logger.error(f"خطأ في إنشاء الترجمة الصوتية: {str(e)}") - raise e - - def _translate_text(self, text, source_language, target_language): - """ - ترجمة نص من لغة إلى أخرى (محاكاة) - - المعلمات: - text: النص المراد ترجمته - source_language: رمز اللغة المصدر - target_language: رمز اللغة الهدف - - الإرجاع: - النص المترجم - """ - try: - # في الوضع العادي، سنستخدم API للترجمة - # هنا نستخدم ترجمة وهمية للعرض التوضيحي - - # تسجيل العملية - self.logger.info(f"ترجمة نص (طول: {len(text)}) من {source_language} إلى {target_language}") - - # ترجمة وهمية - translated_prefix = { - "en": "This is a sample translation of the text into English.", - "ar": "هذه ترجمة عينة للنص إلى اللغة العربية.", - "fr": "Ceci est un exemple de traduction du texte en français.", - "es": "Esta es una traducción de muestra del texto al español.", - "de": "Dies ist eine Beispielübersetzung des Textes ins Deutsche.", - "it": "Questa è una traduzione di esempio del testo in italiano.", - "zh": "这是文本翻译成中文的示例。", - "ja": "これはテキストの日本語への翻訳例です。", - "ru": "Это пример перевода текста на русский язык.", - "tr": "Bu, metnin Türkçe çevirisinin bir örneğidir." - } - - # إرجاع ترجمة وهمية - return f"{translated_prefix.get(target_language, 'Translated sample')} {text[:100]}..." - - except Exception as e: - self.logger.error(f"خطأ في ترجمة النص: {str(e)}") - raise e - - def _get_audio_duration(self, audio_file): - """ - الحصول على مدة ملف صوتي - - المعلمات: - audio_file: مسار الملف الصوتي - - الإرجاع: - مدة الملف الصوتي بالثواني - """ - try: - if audio_file.endswith(".wav"): - # استخدام wave للحصول على مدة ملف WAV - with wave.open(audio_file, "rb") as f: - frames = f.getnframes() - rate = f.getframerate() - duration = frames / float(rate) - else: - # محاكاة لمدة الملف الصوتي - size_in_bytes = os.path.getsize(audio_file) - duration = size_in_bytes / 16000 # تقريب بسيط - - return duration - - except Exception as e: - self.logger.error(f"خطأ في الحصول على مدة الملف الصوتي: {str(e)}") - return 30.0 # قيمة افتراضية - - def _get_from_cache(self, cache_key): - """ - البحث عن ملف في الكاش - - المعلمات: - cache_key: مفتاح الكاش - - الإرجاع: - مسار الملف إذا وجد، وإلا None - """ - if cache_key in self.cache_index: - cache_file = os.path.join(self.cache_dir, self.cache_index[cache_key]) - if os.path.exists(cache_file): - return cache_file - - return None - - def _add_to_cache(self, cache_key, file_path): - """ - إضافة ملف إلى الكاش - - المعلمات: - cache_key: مفتاح الكاش - file_path: مسار الملف - - الإرجاع: - True إذا تمت الإضافة بنجاح، وإلا False - """ - try: - # نسخ الملف إلى الكاش - cache_file = os.path.join(self.cache_dir, os.path.basename(file_path)) - - if file_path != cache_file: - shutil.copy(file_path, cache_file) - - # تحديث فهرس الكاش - self.cache_index[cache_key] = os.path.basename(file_path) - - # حفظ الفهرس - with open(self.cache_index_file, "w", encoding="utf-8") as f: - json.dump(self.cache_index, f, ensure_ascii=False, indent=2) - - return True - - except Exception as e: - self.logger.error(f"خطأ في إضافة الملف إلى الكاش: {str(e)}") - return False - - def _generate_cache_key(self, text, language, voice_id, speaking_rate, pitch): - """ - إنشاء مفتاح كاش للترجمة الصوتية - - المعلمات: - text: النص - language: اللغة - voice_id: معرف الصوت - speaking_rate: سرعة النطق - pitch: درجة الصوت - - الإرجاع: - مفتاح الكاش - """ - import hashlib - - # إنشاء نص للتجزئة - cache_text = f"{text}|{language}|{voice_id}|{speaking_rate}|{pitch}" - - # إنشاء تجزئة MD5 - hash_obj = hashlib.md5(cache_text.encode()) - - return hash_obj.hexdigest() - - def _load_cache_index(self): - """ - تحميل فهرس الكاش - - الإرجاع: - قاموس فهرس الكاش - """ - if os.path.exists(self.cache_index_file): - try: - with open(self.cache_index_file, "r", encoding="utf-8") as f: - return json.load(f) - except Exception as e: - self.logger.error(f"خطأ في تحميل فهرس الكاش: {str(e)}") - - return {} - - def _get_cache_size(self): - """ - الحصول على حجم الكاش بالبايت - - الإرجاع: - حجم الكاش بالبايت - """ - total_size = 0 - - for filename in os.listdir(self.cache_dir): - file_path = os.path.join(self.cache_dir, filename) - if os.path.isfile(file_path): - total_size += os.path.getsize(file_path) - - return total_size - - def _clear_cache(self): - """ - مسح كاش الترجمات الصوتية - - الإرجاع: - True إذا تم المسح بنجاح، وإلا False - """ - try: - # حذف جميع الملفات في الكاش - for filename in os.listdir(self.cache_dir): - file_path = os.path.join(self.cache_dir, filename) - if os.path.isfile(file_path): - os.remove(file_path) - - # إعادة تعيين فهرس الكاش - self.cache_index = {} - - # حفظ الفهرس الفارغ - with open(self.cache_index_file, "w", encoding="utf-8") as f: - json.dump(self.cache_index, f, ensure_ascii=False, indent=2) - - return True - - except Exception as e: - self.logger.error(f"خطأ في مسح الكاش: {str(e)}") - return False - - def _add_voice_to_history(self, title, content, source_language, voice_id, duration, audio_file, content_type="نص حر", is_translation=False, original_id=None): - """ - إضافة ترجمة صوتية إلى التاريخ - - المعلمات: - title: عنوان الترجمة الصوتية - content: محتوى النص - source_language: اللغة المصدر - voice_id: معرف الصوت - duration: مدة الترجمة الصوتية - audio_file: اسم ملف الترجمة الصوتية - content_type: نوع المحتوى - is_translation: هل هي ترجمة لنص آخر - original_id: معرف النص الأصلي - - الإرجاع: - معرف الترجمة الصوتية - """ - try: - # إنشاء معرف فريد - voice_id = f"voice_{int(time.time())}_{len(self.voice_history)}" - - # إنشاء كائن الترجمة الصوتية - voice_item = { - "id": voice_id, - "title": title, - "content": content, - "source_language": source_language, - "voice_id": voice_id, - "duration": duration, - "audio_file": audio_file, - "content_type": content_type, - "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "is_translation": is_translation, - "original_id": original_id - } - - # إضافة إلى التاريخ - self.voice_history.append(voice_item) - - # حفظ التاريخ - self._save_voice_history() - - return voice_id - - except Exception as e: - self.logger.error(f"خطأ في إضافة الترجمة الصوتية إلى التاريخ: {str(e)}") - return None - - def _delete_voice_from_history(self, voice_id): - """ - حذف ترجمة صوتية من التاريخ - - المعلمات: - voice_id: معرف الترجمة الصوتية - - الإرجاع: - True إذا تم الحذف بنجاح، وإلا False - """ - try: - # البحث عن الترجمة الصوتية - for i, item in enumerate(self.voice_history): - if item.get("id") == voice_id: - # حذف الملف الصوتي - audio_file = os.path.join(self.data_dir, item.get("audio_file", "")) - if os.path.exists(audio_file): - os.remove(audio_file) - - # حذف العنصر من التاريخ - del self.voice_history[i] - - # حفظ التاريخ - self._save_voice_history() - - return True - - return False - - except Exception as e: - self.logger.error(f"خطأ في حذف الترجمة الصوتية من التاريخ: {str(e)}") - return False - - def _load_voice_history(self): - """ - تحميل تاريخ الترجمات الصوتية - - الإرجاع: - قائمة تاريخ الترجمات الصوتية - """ - if os.path.exists(self.voice_history_file): - try: - with open(self.voice_history_file, "r", encoding="utf-8") as f: - return json.load(f) - except Exception as e: - self.logger.error(f"خطأ في تحميل تاريخ الترجمات الصوتية: {str(e)}") - - return [] - - def _save_voice_history(self): - """ - حفظ تاريخ الترجمات الصوتية - - الإرجاع: - True إذا تم الحفظ بنجاح، وإلا False - """ - try: - # التأكد من وجود المجلد - os.makedirs(os.path.dirname(self.voice_history_file), exist_ok=True) - - # حفظ التاريخ - with open(self.voice_history_file, "w", encoding="utf-8") as f: - json.dump(self.voice_history, f, ensure_ascii=False, indent=2) - - return True - - except Exception as e: - self.logger.error(f"خطأ في حفظ تاريخ الترجمات الصوتية: {str(e)}") - return False - - def _save_voice_settings(self): - """ - حفظ إعدادات الترجمة الصوتية - - الإرجاع: - True إذا تم الحفظ بنجاح، وإلا False - """ - try: - # التأكد من وجود المجلد - os.makedirs(self.data_dir, exist_ok=True) - - # حفظ الإعدادات - settings_file = os.path.join(self.data_dir, "voice_settings.json") - - with open(settings_file, "w", encoding="utf-8") as f: - json.dump(st.session_state.voice_settings, f, ensure_ascii=False, indent=2) - - return True - - except Exception as e: - self.logger.error(f"خطأ في حفظ إعدادات الترجمة الصوتية: {str(e)}") - return False - - def _display_audio_player(self, audio_file): - """ - عرض مشغل الصوت - - المعلمات: - audio_file: مسار الملف الصوتي - """ - if os.path.exists(audio_file): - # قراءة الملف الصوتي - with open(audio_file, "rb") as f: - audio_bytes = f.read() - - # عرض مشغل الصوت - st.audio(audio_bytes, format="audio/mp3") - else: - st.warning("الملف الصوتي غير متوفر") - - def _get_projects(self): - """ - الحصول على قائمة المشاريع - - الإرجاع: - قائمة المشاريع - """ - # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات - # هنا نستخدم بيانات وهمية للعرض التوضيحي - return [ - { - "id": "PRJ001", - "name": "مشروع تطوير البنية التحتية لمنطقة الرياض", - "status": "قيد التنفيذ", - "location": "الرياض", - "start_date": "2025-01-15", - "expected_end_date": "2026-06-30", - "budget": "15,000,000 ريال", - "description": "مشروع تطوير البنية التحتية في منطقة الرياض، ويشمل إنشاء طرق جديدة وتطوير شبكات الصرف الصحي وتحسين شبكات المياه والكهرباء." - }, - { - "id": "PRJ002", - "name": "إنشاء مجمع سكني في جدة", - "status": "جديد", - "location": "جدة", - "start_date": "2025-04-01", - "expected_end_date": "2027-03-31", - "budget": "25,000,000 ريال", - "description": "مشروع إنشاء مجمع سكني في مدينة جدة، ويتكون من 50 فيلا و 100 شقة سكنية، بالإضافة إلى مرافق خدمية ومناطق ترفيهية." - }, - { - "id": "PRJ003", - "name": "توسعة مستشفى الملك فهد", - "status": "قيد التنفيذ", - "location": "الدمام", - "start_date": "2024-10-15", - "expected_end_date": "2026-02-28", - "budget": "18,500,000 ريال", - "description": "مشروع توسعة مستشفى الملك فهد في مدينة الدمام، ويشمل إضافة مبنى جديد للعيادات الخارجية وزيادة عدد أسرّة المستشفى." - } - ] - - def _get_tenders(self): - """ - الحصول على قائمة المناقصات - - الإرجاع: - قائمة المناقصات - """ - # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات - # هنا نستخدم بيانات وهمية للعرض التوضيحي - return [ - { - "id": "TND001", - "name": "مناقصة تطوير طريق الملك عبدالله", - "owner": "وزارة النقل", - "issue_date": "2025-02-10", - "submission_date": "2025-03-15", - "estimated_value": "12,000,000 ريال", - "description": "مناقصة لتطوير وتوسعة طريق الملك عبدالله بطول 15 كم، وتشمل الأعمال إنشاء مسارات جديدة وتحسين البنية التحتية للطريق." - }, - { - "id": "TND002", - "name": "مناقصة إنشاء مدرسة ثانوية", - "owner": "وزارة التعليم", - "issue_date": "2025-01-20", - "submission_date": "2025-02-25", - "estimated_value": "8,500,000 ريال", - "description": "مناقصة لإنشاء مدرسة ثانوية جديدة في حي النزهة بمدينة الرياض، وتشمل الأعمال إنشاء مبنى المدرسة والمرافق التابعة لها." - }, - { - "id": "TND003", - "name": "مناقصة صيانة وتأهيل محطات تحلية المياه", - "owner": "المؤسسة العامة لتحلية المياه المالحة", - "issue_date": "2025-03-01", - "submission_date": "2025-04-15", - "estimated_value": "22,000,000 ريال", - "description": "مناقصة لصيانة وتأهيل محطات تحلية المياه في المنطقة الشرقية، وتشمل الأعمال استبدال المعدات القديمة وتطوير أنظمة التحكم." - } - ] - - def _get_contracts(self): - """ - الحصول على قائمة العقود - - الإرجاع: - قائمة العقود - """ - # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات - # هنا نستخدم بيانات وهمية للعرض التوضيحي - return [ - { - "id": "CNT001", - "name": "عقد إنشاء مجمع سكني", - "client": "شركة الرياض للتطوير العقاري", - "start_date": "2025-04-15", - "end_date": "2027-04-14", - "value": "28,500,000 ريال", - "clauses": [ - { - "title": "نطاق الأعمال", - "content": "يشمل نطاق الأعمال في هذا العقد إنشاء مجمع سكني مكون من 40 فيلا و 80 شقة سكنية، بالإضافة إلى المرافق الخدمية والترفيهية." - }, - { - "title": "مدة التنفيذ", - "content": "مدة تنفيذ المشروع 24 شهراً تبدأ من تاريخ استلام الموقع، ويمكن تمديد المدة في حال وجود ظروف قاهرة يتفق عليها الطرفان." - }, - { - "title": "قيمة العقد وطريقة الدفع", - "content": "قيمة العقد الإجمالية هي 28,500,000 ريال سعودي، ويتم السداد على دفعات شهرية بناءً على نسبة الإنجاز في المشروع." - }, - { - "title": "الضمانات", - "content": "يلتزم المقاول بتقديم ضمان بنكي بقيمة 5% من قيمة العقد لضمان حسن التنفيذ، وضمان صيانة لمدة سنة بعد الانتهاء من المشروع." - }, - { - "title": "الغرامات والجزاءات", - "content": "في حال تأخر المقاول عن تسليم المشروع في الموعد المحدد، يتم فرض غرامة تأخير بنسبة 0.1% من قيمة العقد عن كل يوم تأخير، بحد أقصى 10% من قيمة العقد." - } - ] - }, - { - "id": "CNT002", - "name": "عقد توريد وتركيب أنظمة تكييف", - "client": "شركة التطوير العقاري المحدودة", - "start_date": "2025-03-01", - "end_date": "2025-08-31", - "value": "4,200,000 ريال", - "clauses": [ - { - "title": "نطاق التوريد", - "content": "يشمل نطاق التوريد في هذا العقد توفير وتركيب 120 وحدة تكييف مركزي للمبنى الإداري الجديد، بالإضافة إلى خدمات الصيانة لمدة عام." - }, - { - "title": "مواصفات الأجهزة", - "content": "يجب أن تكون جميع الأجهزة الموردة من إحدى العلامات التجارية المعتمدة (كارير، دايكن، أو ميتسوبيشي)، وأن تكون مطابقة للمواصفات الفنية المرفقة بالعقد." - }, - { - "title": "مدة التوريد والتركيب", - "content": "يلتزم المورد بتوريد وتركيب جميع الأجهزة خلال مدة لا تتجاوز 6 أشهر من تاريخ توقيع العقد." - }, - { - "title": "الضمان", - "content": "يقدم المورد ضماناً لجميع الأجهزة لمدة 3 سنوات من تاريخ التشغيل، ويشمل الضمان جميع أعمال الصيانة وقطع الغيار." - } - ] - } - ] - - def _get_documents(self): - """ - الحصول على قائمة المستندات - - الإرجاع: - قائمة المستندات - """ - # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات - # هنا نستخدم بيانات وهمية للعرض التوضيحي - return [ - { - "id": "DOC001", - "name": "كراسة شروط مناقصة تطوير طريق الملك عبدالله", - "type": "كراسة شروط", - "page_count": 85, - "file_size": "2.4 ميجابايت", - "upload_date": "2025-02-10" - }, - { - "id": "DOC002", - "name": "عقد إنشاء مجمع سكني", - "type": "عقد", - "page_count": 42, - "file_size": "1.8 ميجابايت", - "upload_date": "2025-04-12" - }, - { - "id": "DOC003", - "name": "تقرير دراسة جدوى مشروع توسعة مستشفى", - "type": "تقرير", - "page_count": 65, - "file_size": "3.1 ميجابايت", - "upload_date": "2025-01-25" - } - ] - - -# تطبيق وحدة الترجمة الصوتية متعددة اللغات -class VoiceNarrationApp: - """وحدة تطبيق الترجمة الصوتية متعددة اللغات""" - - def __init__(self): - """تهيئة وحدة تطبيق الترجمة الصوتية متعددة اللغات""" - self.voice_over_system = VoiceOverSystem() - - def render(self): - """عرض واجهة وحدة تطبيق الترجمة الصوتية متعددة اللغات""" - st.markdown("

نظام الترجمة الصوتية متعددة اللغات

", unsafe_allow_html=True) - - st.markdown(""" -
- يتيح لك نظام الترجمة الصوتية متعددة اللغات تحويل النصوص والمستندات إلى ملفات صوتية بلغات متعددة، - مما يساعد في توصيل المعلومات بشكل أفضل للأشخاص من خلفيات لغوية مختلفة. -
- """, unsafe_allow_html=True) - - # عرض نظام الترجمة الصوتية - self.voice_over_system.render() - - -# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة -if __name__ == "__main__": - st.set_page_config( - page_title="الترجمة الصوتية متعددة اللغات | WAHBi AI", - page_icon="🎙️", - layout="wide", - initial_sidebar_state="expanded" - ) - - app = VoiceNarrationApp() - app.render() \ No newline at end of file