Spaces:
Paused
Paused
| """ | |
| تطبيق وحدة تحليل المخاطر | |
| """ | |
| 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("<h1 class='module-title'>وحدة تحليل المخاطر</h1>", 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['المخاطر'])}<br>({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.experimental_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) | |