diff --git "a/modules/pricing/pricing_app.py" "b/modules/pricing/pricing_app.py" --- "a/modules/pricing/pricing_app.py" +++ "b/modules/pricing/pricing_app.py" @@ -1,1111 +1,1747 @@ import streamlit as st import pandas as pd -import numpy as np -import random import matplotlib.pyplot as plt -import arabic_reshaper -from bidi.algorithm import get_display -import json -import os +import base64 from datetime import datetime +import os +import json +import csv +import io + +# استيراد الوحدات المخصصة +from data_storage import DataStorage +from export_utils import ( + export_to_excel, export_to_pdf, export_local_content_report, + export_risk_report, get_download_link +) + +# محاولة استيراد المكتبات الإضافية +try: + import arabic_reshaper + from bidi.algorithm import get_display + ARABIC_SUPPORT = True +except ImportError: + ARABIC_SUPPORT = False + st.warning("لم يتم العثور على مكتبات دعم اللغة العربية. بعض الميزات قد لا تعمل بشكل صحيح.") class PricingApp: def __init__(self): """تهيئة التطبيق""" - # إضافة CSS للتنسيق - self._add_custom_css() - - # تهيئة حالة الجلسة - if 'projects' not in st.session_state: - st.session_state.projects = self._load_sample_projects() - - if 'current_project_id' not in st.session_state: - st.session_state.current_project_id = None + # تهيئة مدير التخزين + self.storage = DataStorage() - if 'current_tab' not in st.session_state: - st.session_state.current_tab = "boq" + # تهيئة حالة الجلسة إذا لم تكن موجودة + if 'initialized' not in st.session_state: + self._initialize_session_state() - if 'item_analysis_edited' not in st.session_state: - st.session_state.item_analysis_edited = False + # تعيين نمط CSS + self._set_css() - def _is_projects_dict(self): - """التحقق مما إذا كان projects قاموساً أم قائمة""" - return isinstance(st.session_state.projects, dict) + def _initialize_session_state(self): + """تهيئة حالة الجلسة وتحميل البيانات من التخزين""" + # تحميل البيانات من ملفات JSON + projects = self.storage.load_projects() + boq_items = self.storage.load_boq_items() + pricing_history = self.storage.load_pricing_history() + risks = self.storage.load_risks() + + # تعيين حالة الجلسة + st.session_state.projects = projects + st.session_state.boq_items = boq_items + st.session_state.saved_pricing = pricing_history + st.session_state.risks = risks + st.session_state.current_project = None + st.session_state.pricing_step = "project_info" + st.session_state.initialized = True - def _add_custom_css(self): - """إضافة CSS مخصص للتطبيق""" + def _set_css(self): + """تعيين نمط CSS للتطبيق""" st.markdown(""" """, unsafe_allow_html=True) - def _update_total_price(self): - """تحديث السعر الإجمالي تلقائياً عند تغيير الكمية أو سعر الوحدة""" - # سيتم استدعاء هذه الدالة عند تغيير قيمة الكمية أو سعر الوحدة - # يتم تحديث البيانات في الجدول وإعادة حساب المجموع الكلي - st.session_state.item_analysis_edited = True - - def render(self): - """دالة العرض الرئيسية للتوافق مع واجهة التطبيق الرئيسي""" - # استدعاء دالة run لتنفيذ وظائف التطبيق - self.run() - def run(self): """تشغيل التطبيق""" - st.title("وحدة التسعير المتكاملة") + st.title("نظام تسعير المشاريع") - # عرض الشريط العلوي - self._render_top_bar() + # عرض شريط التنقل الجانبي + self._render_sidebar() - # عرض المحتوى حسب التبويب المحدد - if st.session_state.current_tab == "boq": - self._render_bill_of_quantities() - elif st.session_state.current_tab == "item_analysis": - self._render_item_price_analysis() - - def _render_top_bar(self): - """عرض الشريط العلوي""" - cols = st.columns([1, 1, 1, 1, 1]) + # تحديد الخطوة الحالية + current_step = st.session_state.pricing_step - with cols[0]: - if st.button("جدول الكميات", use_container_width=True): - st.session_state.current_tab = "boq" - st.rerun() + # تحديث قائمة الخطوات + steps = { + "project_info": self._render_project_info_step, + "boq": self._render_boq_step, + "cost_analysis": self._render_cost_analysis_step, + "pricing_strategies": self._render_pricing_strategies_step, + "review": self._render_review_step, + "local_content": self._render_local_content_step, + "risk_assessment": self._render_risk_assessment_step + } - with cols[1]: - if st.button("تحليل الأسعار", use_container_width=True): - if st.session_state.current_project_id is not None: - st.session_state.current_tab = "item_analysis" + # عرض الخطوة الحالية + if current_step in steps: + steps[current_step]() + else: + st.error(f"خطوة غير معروفة: {current_step}") + + def _render_sidebar(self): + """عرض شريط التنقل الجانبي""" + with st.sidebar: + st.header("المشاريع") + + # عرض قائمة المشاريع + if st.session_state.projects: + project_names = [p['name'] for p in st.session_state.projects] + project_ids = [p['id'] for p in st.session_state.projects] + + selected_index = 0 + if st.session_state.current_project: + if st.session_state.current_project in project_ids: + selected_index = project_ids.index(st.session_state.current_project) + + selected_project = st.selectbox( + "اختر مشروعًا", + project_names, + index=selected_index + ) + + selected_project_id = project_ids[project_names.index(selected_project)] + + if st.button("فتح المشروع"): + st.session_state.current_project = selected_project_id + st.session_state.pricing_step = "project_info" st.rerun() - else: - st.warning("الرجاء اختيار مشروع أولاً") - - with cols[4]: - if st.button("إنشاء مشروع جديد", use_container_width=True): - self._create_new_project() + + if st.button("حذف المشروع"): + self.storage.delete_project(selected_project_id) + + # تحديث حالة الجلسة + st.session_state.projects = self.storage.load_projects() + st.session_state.boq_items = self.storage.load_boq_items() + + if st.session_state.current_project == selected_project_id: + st.session_state.current_project = None + + st.success(f"تم حذف المشروع: {selected_project}") + st.rerun() + + if st.button("إنشاء مشروع جديد"): + st.session_state.current_project = None + st.session_state.pricing_step = "project_info" + st.rerun() + + # عرض معلومات المشروع الحالي + if st.session_state.current_project: + st.divider() + st.subheader("المشروع الحالي") + + project_info = self._get_current_project_info() + if project_info: + st.write(f"**الاسم:** {project_info['name']}") + st.write(f"**العميل:** {project_info['client']}") + st.write(f"**القيمة التقديرية:** {project_info['estimated_value']:,.2f} ريال") + + # عرض خطوات التسعير + st.divider() + st.subheader("خطوات التسعير") + + steps = [ + ("project_info", "معلومات المشروع"), + ("boq", "جدول الكميات"), + ("cost_analysis", "تحليل التكلفة"), + ("pricing_strategies", "استراتيجيات التسعير"), + ("review", "المراجعة النهائية"), + ("local_content", "المحتوى المحلي"), + ("risk_assessment", "تقييم المخاطر") + ] + + for step_id, step_name in steps: + if st.button(step_name, key=f"nav_{step_id}"): + st.session_state.pricing_step = step_id + st.rerun() + + # عرض معلومات إضافية + st.divider() + st.caption("نظام تسعير المشاريع - الإصدار 2.0") - def _create_new_project(self): - """إنشاء مشروع جديد""" - # إنشاء مشروع جديد - new_project = { - "id": f"proj_{len(st.session_state.projects) + 1}", - "name": f"مشروع رقم {len(st.session_state.projects) + 1}", - "client": "عميل قياسي", - "date": datetime.now().strftime("%Y-%m-%d"), - "budget": 1000000.00, - "items": [] - } + def _get_current_project_info(self): + """الحصول على معلومات المشروع الحالي""" + if not st.session_state.current_project: + return None - # إضافة المشروع الجديد حسب نوع هيكل البيانات - if self._is_projects_dict(): - # إذا كان قاموساً - project_id = new_project["id"] - st.session_state.projects[project_id] = new_project - st.session_state.current_project_id = project_id - else: - # إذا كان قائمة - st.session_state.projects.append(new_project) - st.session_state.current_project_id = len(st.session_state.projects) - 1 + for project in st.session_state.projects: + if project['id'] == st.session_state.current_project: + return project - st.session_state.current_tab = "boq" - st.rerun() + return None - def _render_bill_of_quantities(self): - """عرض جدول الكميات""" - st.header("جدول الكميات") - - # اختيار المشروع - if self._is_projects_dict(): - # إذا كان قاموساً - project_options = {proj["name"]: proj_id for proj_id, proj in st.session_state.projects.items()} - else: - # إذا كان قائمة - project_options = {proj["name"]: i for i, proj in enumerate(st.session_state.projects)} + def _render_project_info_step(self): + """عرض خطوة معلومات المشروع""" + st.markdown('
', unsafe_allow_html=True) + st.markdown('
الخطوة 1: معلومات المشروع
', unsafe_allow_html=True) - # التحقق من وجود مشاريع - if not project_options: - st.warning("لا توجد مشاريع. قم بإنشاء مشروع جديد أولاً.") - return + # التحقق مما إذا كان هناك مشروع حالي + project_info = self._get_current_project_info() - selected_project_name = st.selectbox( - "اختر المشروع", - options=list(project_options.keys()), - index=0 if st.session_state.current_project_id is None else list(project_options.keys()).index(self._get_current_project()["name"]) - ) + # إنشاء نموذج إدخال معلومات المشروع + with st.form(key="project_info_form"): + # حقول النموذج + project_name = st.text_input( + "اسم المشروع", + value=project_info['name'] if project_info else "" + ) + + client = st.text_input( + "العميل", + value=project_info['client'] if project_info else "" + ) + + location = st.text_input( + "الموقع", + value=project_info.get('location', "") if project_info else "" + ) + + tender_number = st.text_input( + "رقم المناقصة", + value=project_info.get('tender_number', "") if project_info else "" + ) + + estimated_value = st.number_input( + "القيمة التقديرية (ريال)", + min_value=0.0, + value=float(project_info['estimated_value']) if project_info else 0.0, + step=1000.0 + ) + + deadline = st.date_input( + "الموعد النهائي", + value=datetime.strptime(project_info['deadline'], "%Y-%m-%d").date() if project_info and 'deadline' in project_info else datetime.now().date() + ) + + contract_duration = st.text_input( + "مدة العقد", + value=project_info.get('contract_duration', "") if project_info else "" + ) + + pricing_type = st.selectbox( + "نوع التسعير", + ["قياسي", "تنافسي", "مخصص"], + index=["قياسي", "تنافسي", "مخصص"].index(project_info['pricing_type']) if project_info and 'pricing_type' in project_info else 0 + ) + + # زر الإرسال + submit_button = st.form_submit_button("حفظ معلومات المشروع") + + if submit_button: + # التحقق من صحة البيانات + if not project_name or not client or estimated_value <= 0: + st.error("يرجى ملء جميع الحقول المطلوبة") + else: + # إنشاء كائن المشروع + project_data = { + 'name': project_name, + 'client': client, + 'location': location, + 'tender_number': tender_number, + 'estimated_value': estimated_value, + 'deadline': deadline.strftime("%Y-%m-%d"), + 'contract_duration': contract_duration, + 'pricing_type': pricing_type + } + + if project_info: + # تحديث المشروع الحالي + self.storage.update_project(project_info['id'], project_data) + + # تحديث حالة الجلسة + for i, p in enumerate(st.session_state.projects): + if p['id'] == project_info['id']: + st.session_state.projects[i].update(project_data) + break + + st.success(f"تم تحديث معلومات المشروع: {project_name}") + else: + # إنشاء معرف فريد للمشروع + project_id = f"project_{len(st.session_state.projects) + 1}" + project_data['id'] = project_id + + # إضافة المشروع إلى قائمة المشاريع + added_project = self.storage.add_project(project_data) + st.session_state.projects.append(added_project) + st.session_state.current_project = project_id + + st.success(f"تم إنشاء مشروع جديد: {project_name}") - st.session_state.current_project_id = project_options[selected_project_name] - current_project = self._get_current_project() + # زر الانتقال إلى الخطوة التالية + if project_info: + if st.button("متابعة إلى جدول الكميات ⬅️", type="primary"): + st.session_state.pricing_step = "boq" + st.rerun() - # عرض معلومات المشروع - col1, col2, col3 = st.columns(3) - with col1: - st.write(f"**العميل:** {current_project['client']}") - with col2: - st.write(f"**التاريخ:** {current_project['date']}") - with col3: - st.write(f"**الميزانية:** {current_project['budget']:,.2f} ريال") + st.markdown('
', unsafe_allow_html=True) + + def _render_boq_step(self): + """عرض خطوة جدول الكميات""" + st.markdown('
', unsafe_allow_html=True) + st.markdown('
الخطوة 2: جدول الكميات
', unsafe_allow_html=True) - # إضافة بند جديد - with st.expander("إضافة بند جديد"): - with st.form("add_item_form"): - col1, col2 = st.columns(2) - with col1: - new_item_name = st.text_input("اسم البند") - new_item_unit = st.text_input("الوحدة") - with col2: - new_item_quantity = st.number_input("الكمية", min_value=0.0, step=0.1) - new_item_price = st.number_input("سعر الوحدة (ريال)", min_value=0.0, step=0.1) - - submitted = st.form_submit_button("إضافة البند") - if submitted and new_item_name and new_item_unit: - new_item = { - "id": f"item_{len(current_project['items']) + 1}", - "name": new_item_name, - "unit": new_item_unit, - "quantity": float(new_item_quantity), - "unit_price": float(new_item_price), - "total_price": float(new_item_quantity) * float(new_item_price), - "materials": [], - "labor": [], - "equipment": [], - "subcontractors": [] - } - current_project["items"].append(new_item) - st.success("تمت إضافة البند بنجاح") - st.rerun() + project_info = self._get_current_project_info() + if not project_info: + st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") + if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): + st.session_state.pricing_step = "project_info" + st.rerun() + return + + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] # عرض جدول الكميات - if current_project["items"]: - items_df = pd.DataFrame([ + if project_items: + st.subheader("جدول الكميات") + + # إنشاء DataFrame للعرض + df = pd.DataFrame([ { - "م": i + 1, - "اسم البند": item["name"], - "الوحدة": item["unit"], - "الكمية": item["quantity"], - "سعر الوحدة (ريال)": item["unit_price"], - "السعر الإجمالي (ريال)": item["total_price"], - "تحليل": f'تحليل' + 'الكود': item['code'], + 'الوصف': item['description'], + 'الوحدة': item['unit'], + 'الكمية': item['quantity'], + 'سعر الوحدة': item['unit_price'], + 'السعر الإجمالي': item['total_price'], + 'نوع المورد': item.get('resource_type', '-') } - for i, item in enumerate(current_project["items"]) + for item in project_items ]) - # إضافة صف المجموع - total_price = sum(item["total_price"] for item in current_project["items"]) - total_row = pd.DataFrame([{ - "م": "", - "اسم البند": "المجموع", - "الوحدة": "", - "الكمية": "", - "سعر الوحدة (ريال)": "", - "السعر الإجمالي (ريال)": total_price, - "تحليل": "" - }]) + # عرض الجدول + st.dataframe(df) - # استخدام pd.concat بدلاً من append - items_df = pd.concat([items_df, total_row], ignore_index=True) - - st.write(items_df.to_html(escape=False, index=False), unsafe_allow_html=True) + # عرض إجمالي التكلفة + total_cost = sum(item['total_price'] for item in project_items) + st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال") + else: + st.info("لا توجد بنود في جدول الكميات. يرجى إضافة بنود باستخدام النموذج أدناه.") + + # نموذج إضافة بند جديد + with st.expander("إضافة بند جديد", expanded=not project_items): + with st.form(key="add_item_form"): + # حقول النموذج + code = st.text_input("الكود") + description = st.text_area("الوصف") + unit = st.text_input("الوحدة") + quantity = st.number_input("الكمية", min_value=0.0, step=0.1) + unit_price = st.number_input("سعر الوحدة (ريال)", min_value=0.0, step=0.1) + resource_type = st.selectbox( + "نوع المورد", + ["مواد", "عمالة", "معدات", "خدمات", "أخرى"] + ) + + # حساب السعر الإجمالي + total_price = quantity * unit_price + st.metric("السعر الإجمالي", f"{total_price:,.2f} ريال") + + # زر الإرسال + submit_button = st.form_submit_button("إضافة البند") + + if submit_button: + # التحقق من صحة البيانات + if not code or not description or not unit or quantity <= 0 or unit_price <= 0: + st.error("يرجى ملء جميع الحقول المطلوبة") + else: + # إنشاء كائن البند + item_id = f"item_{len(st.session_state.boq_items) + 1}" + item_data = { + 'id': item_id, + 'project_id': st.session_state.current_project, + 'code': code, + 'description': description, + 'unit': unit, + 'quantity': quantity, + 'unit_price': unit_price, + 'total_price': total_price, + 'resource_type': resource_type + } + + # إضافة البند إلى قائمة البنود + added_item = self.storage.add_boq_item(item_data) + st.session_state.boq_items.append(added_item) + + st.success(f"تم إضافة البند: {code}") + st.rerun() + + # نموذج استيراد بنود من ملف CSV + with st.expander("استيراد من CSV"): + uploaded_file = st.file_uploader("اختر ملف CSV", type="csv") - # معالجة النقر على رابط التحليل - for i in range(len(current_project["items"])): - if st.button(f"تحليل البند {i+1}", key=f"analyze_btn_{i}", use_container_width=True): - st.session_state.current_item_index = i - st.session_state.current_tab = "item_analysis" + if uploaded_file is not None: + try: + # قراءة الملف + csv_data = uploaded_file.read().decode('utf-8') + csv_reader = csv.reader(io.StringIO(csv_data)) + + # تخطي الصف الأول (العناوين) + headers = next(csv_reader) + + # قراءة البنود + imported_items = [] + for row in csv_reader: + if len(row) >= 6: + code = row[0] + description = row[1] + unit = row[2] + quantity = float(row[3]) + unit_price = float(row[4]) + resource_type = row[5] if len(row) > 5 else "مواد" + + total_price = quantity * unit_price + + item_id = f"item_{len(st.session_state.boq_items) + len(imported_items) + 1}" + item_data = { + 'id': item_id, + 'project_id': st.session_state.current_project, + 'code': code, + 'description': description, + 'unit': unit, + 'quantity': quantity, + 'unit_price': unit_price, + 'total_price': total_price, + 'resource_type': resource_type + } + + imported_items.append(item_data) + + if imported_items: + if st.button("استيراد البنود", key="import_items_btn"): + # إضافة البنود إلى قائمة البنود + for item_data in imported_items: + added_item = self.storage.add_boq_item(item_data) + st.session_state.boq_items.append(added_item) + + st.success(f"تم استيراد {len(imported_items)} بند") + st.rerun() + else: + st.error("لم يتم العثور على بنود صالحة في الملف") + + except Exception as e: + st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}") + + # أزرار التنقل بين الخطوات + col1, col2 = st.columns(2) + with col1: + if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info_btn"): + st.session_state.pricing_step = "project_info" + st.rerun() + + with col2: + if project_items: + if st.button("متابعة إلى تحليل التكلفة ⬅️", key="continue_to_cost_analysis", type="primary"): + st.session_state.pricing_step = "cost_analysis" st.rerun() - else: - st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود جديدة.") - - def _get_current_project(self): - """الحصول على المشروع الحالي بناءً على نوع هيكل البيانات""" - if st.session_state.current_project_id is None: - return None + else: + st.warning("يجب إضافة بند واحد على الأقل للمتابعة") - if self._is_projects_dict(): - # إذا كان قاموساً - return st.session_state.projects[st.session_state.current_project_id] - else: - # إذا كان قائمة - return st.session_state.projects[int(st.session_state.current_project_id)] + st.markdown('
', unsafe_allow_html=True) - def _render_item_price_analysis(self): - """عرض تحليل سعر البند""" - if st.session_state.current_project_id is None or "current_item_index" not in st.session_state: - st.warning("الرجاء اختيار مشروع وبند أولاً") + def _render_cost_analysis_step(self): + """عرض خطوة تحليل التكلفة""" + st.markdown('
', unsafe_allow_html=True) + st.markdown('
الخطوة 3: تحليل التكلفة
', unsafe_allow_html=True) + + project_info = self._get_current_project_info() + if not project_info: + st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") + if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): + st.session_state.pricing_step = "project_info" + st.rerun() + return + + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + + if not project_items: + st.warning("لا توجد بنود في جدول الكميات. يرجى العودة إلى الخطوة السابقة لإضافة البنود.") + + col1, col2 = st.columns(2) + with col1: + if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq_btn"): + st.session_state.pricing_step = "boq" + st.rerun() return - current_project = self._get_current_project() - current_item = current_project["items"][st.session_state.current_item_index] + # تحليل التكلفة + st.subheader("تحليل التكلفة حسب نوع المورد") - st.header(f"تحليل سعر البند: {current_item['name']}") + # حساب التكلفة حسب نوع المورد + resource_types = {} + for item in project_items: + resource_type = item.get('resource_type', 'أخرى') + if resource_type in resource_types: + resource_types[resource_type] += item['total_price'] + else: + resource_types[resource_type] = item['total_price'] - # معلومات البند - col1, col2, col3, col4 = st.columns(4) + # عرض التكلفة حسب نوع المورد + resource_df = pd.DataFrame({ + 'نوع المورد': list(resource_types.keys()), + 'التكلفة': list(resource_types.values()) + }) + + # إضافة نسبة التكلفة + total_cost = sum(resource_types.values()) + resource_df['النسبة'] = resource_df['التكلفة'] / total_cost * 100 + + # عرض الجدول + st.dataframe(resource_df) + + # عرض الرسم البياني + fig, ax = plt.subplots(figsize=(10, 6)) + ax.pie( + resource_df['التكلفة'], + labels=resource_df['نوع المورد'], + autopct='%1.1f%%', + startangle=90 + ) + ax.axis('equal') + st.pyplot(fig) + + # عرض إجمالي التكلفة + st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال") + + # أزرار التنقل بين الخطوات + col1, col2 = st.columns(2) with col1: - st.write(f"**الوحدة:** {current_item['unit']}") + if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq"): + st.session_state.pricing_step = "boq" + st.rerun() + with col2: - st.write(f"**الكمية:** {current_item['quantity']}") - with col3: - st.write(f"**سعر الوحدة:** {current_item['unit_price']} ريال") - with col4: - st.write(f"**السعر الإجمالي:** {current_item['total_price']} ريال") - - # زر حفظ التغييرات - if st.session_state.item_analysis_edited: - if st.button("حفظ التغييرات", type="primary", use_container_width=True): - # تحديث السعر الإجمالي للبند - materials_total = sum(material.get("total_price", 0) for material in current_item["materials"]) - labor_total = sum(labor.get("total_price", 0) for labor in current_item["labor"]) - equipment_total = sum(equipment.get("total_price", 0) for equipment in current_item["equipment"]) - subcontractors_total = sum(sub.get("total_price", 0) for sub in current_item["subcontractors"]) - - direct_cost = materials_total + labor_total + equipment_total + subcontractors_total - current_item["unit_price"] = direct_cost / current_item["quantity"] if current_item["quantity"] > 0 else 0 - current_item["total_price"] = current_item["unit_price"] * current_item["quantity"] - - st.session_state.item_analysis_edited = False - st.success("تم حفظ التغييرات بنجاح") + if st.button("متابعة إلى استراتيجيات التسعير ⬅️", key="continue_to_pricing_strategies", type="primary"): + st.session_state.pricing_step = "pricing_strategies" st.rerun() - # تحليل المواد - st.subheader("تحليل المواد") + st.markdown('
', unsafe_allow_html=True) + + def _render_pricing_strategies_step(self): + """عرض خطوة استراتيجيات التسعير""" + st.markdown('
', unsafe_allow_html=True) + st.markdown('
الخطوة 4: استراتيجيات التسعير
', unsafe_allow_html=True) - # إضافة مادة جديدة - with st.expander("إضافة مادة جديدة"): - with st.form("add_material_form"): - col1, col2 = st.columns(2) - with col1: - new_material_name = st.text_input("اسم المادة") - new_material_unit = st.text_input("الوحدة") - with col2: - new_material_quantity = st.number_input("الكمية", min_value=0.0, step=0.1) - new_material_price = st.number_input("سعر الوحدة (ريال)", min_value=0.0, step=0.1) - - submitted = st.form_submit_button("إضافة المادة") - if submitted and new_material_name and new_material_unit: - new_material = { - "id": f"material_{len(current_item['materials']) + 1}", - "name": new_material_name, - "unit": new_material_unit, - "quantity": float(new_material_quantity), - "unit_price": float(new_material_price), - "total_price": float(new_material_quantity) * float(new_material_price) - } - current_item["materials"].append(new_material) - st.session_state.item_analysis_edited = True - st.success("تمت إضافة المادة بنجاح") + project_info = self._get_current_project_info() + if not project_info: + st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") + if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): + st.session_state.pricing_step = "project_info" + st.rerun() + return + + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + + if not project_items: + st.warning("لا توجد بنود في جدول الكميات. يرجى العودة إلى الخطوة السابقة لإضافة البنود.") + + col1, col2 = st.columns(2) + with col1: + if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq_btn"): + st.session_state.pricing_step = "boq" st.rerun() + return - # عرض جدول المواد - if current_item["materials"]: - # تحويل البيانات إلى DataFrame - materials_data = [] - for i, material in enumerate(current_item["materials"]): - materials_data.append({ - "م": i + 1, - "اسم المادة": material["name"], - "الوحدة": material["unit"], - "الكمية": material["quantity"], - "سعر الوحدة (ريال)": material["unit_price"], - "السعر الإجمالي (ريال)": material["total_price"], - "الإجراءات": f' ' - }) + # حساب إجمالي التكلفة + total_cost = sum(item['total_price'] for item in project_items) + + # عرض استراتيجيات التسعير + st.subheader("استراتيجيات التسعير") + + # اختيار استراتيجية التسعير + pricing_strategy = st.selectbox( + "اختر استراتيجية التسعير", + ["قياسي", "تنافسي", "مخصص"], + index=["قياسي", "تنافسي", "مخصص"].index(project_info['pricing_type']) if 'pricing_type' in project_info else 0 + ) + + # تعيين نسبة الربح حسب استراتيجية التسعير + if pricing_strategy == "قياسي": + profit_percentage = st.slider("نسبة الربح", min_value=10, max_value=30, value=20, step=1) + st.info("استراتيجية التسعير القياسية تستخدم نسبة ربح ثابتة على إجمالي التكلفة.") + elif pricing_strategy == "تنافسي": + profit_percentage = st.slider("نسبة الربح", min_value=5, max_value=20, value=10, step=1) + st.info("استراتيجية التسعير التنافسية تستخدم نسبة ربح منخفضة لزيادة القدرة التنافسية.") + else: # مخصص + profit_percentage = st.slider("نسبة الربح", min_value=0, max_value=50, value=15, step=1) + st.info("استراتيجية التسعير المخصصة تتيح تحديد نسبة الربح بحرية.") + + # حساب السعر النهائي + profit_amount = total_cost * (profit_percentage / 100) + final_price = total_cost + profit_amount + + # عرض ملخص التسعير + st.subheader("ملخص التسعير") + + col1, col2, col3 = st.columns(3) + + with col1: + st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال") + + with col2: + st.metric("مبلغ الربح", f"{profit_amount:,.2f} ريال") + + with col3: + st.metric("السعر النهائي", f"{final_price:,.2f} ريال") + + # زر تطبيق استراتيجية التسعير + if st.button("تطبيق استراتيجية التسعير", key="apply_pricing_strategy"): + # تحديث نوع التسعير في المشروع + self.storage.update_project(project_info['id'], {'pricing_type': pricing_strategy}) - materials_df = pd.DataFrame(materials_data) + # تحديث حالة الجلسة + for i, p in enumerate(st.session_state.projects): + if p['id'] == project_info['id']: + st.session_state.projects[i]['pricing_type'] = pricing_strategy + break - # إضافة صف المجموع - materials_total = sum(material["total_price"] for material in current_item["materials"]) - total_row = pd.DataFrame([{ - "م": "", - "اسم المادة": "المجموع", - "الوحدة": "", - "الكمية": "", - "سعر الوحدة (ريال)": "", - "السعر الإجمالي (ريال)": materials_total, - "الإجراءات": "" - }]) + # حفظ التسعير في سجل التسعير + pricing_data = { + 'project_id': project_info['id'], + 'pricing_strategy': pricing_strategy, + 'profit_percentage': profit_percentage, + 'total_cost': total_cost, + 'profit_amount': profit_amount, + 'final_price': final_price + } - # استخدام pd.concat بدلاً من append - materials_df = pd.concat([materials_df, total_row], ignore_index=True) + self.storage.save_pricing(pricing_data) - # عرض الجدول القابل للتعديل - edited_materials_df = st.data_editor( - materials_df, - column_config={ - "م": st.column_config.NumberColumn("م", width="small"), - "اسم المادة": st.column_config.TextColumn("اسم المادة"), - "الوحدة": st.column_config.TextColumn("الوحدة", width="small"), - "الكمية": st.column_config.NumberColumn("الكمية", format="%.2f", width="small"), - "سعر الوحدة (ريال)": st.column_config.NumberColumn("سعر الوحدة (ريال)", format="%.2f"), - "السعر الإجمالي (ريال)": st.column_config.NumberColumn("السعر الإجمالي (ريال)", format="%.2f"), - "الإجراءات": st.column_config.TextColumn("الإجراءات", width="medium"), - }, - hide_index=True, - key="materials_table", - on_change=self._update_total_price, - disabled=["م", "اسم المادة", "الوحدة", "السعر الإجمالي (ريال)", "الإجراءات"] - ) + st.success("تم تطبيق استراتيجية التسعير بنجاح") + + # أزرار التنقل بين الخطوات + col1, col2 = st.columns(2) + with col1: + if st.button("العودة إلى تحليل التكلفة ➡️", key="back_to_cost_analysis"): + st.session_state.pricing_step = "cost_analysis" + st.rerun() + + with col2: + if st.button("متابعة إلى المراجعة النهائية ⬅️", key="continue_to_review", type="primary"): + st.session_state.pricing_step = "review" + st.rerun() + + st.markdown('
', unsafe_allow_html=True) + + def _render_review_step(self): + """عرض خطوة المراجعة النهائية""" + st.markdown('
', unsafe_allow_html=True) + st.markdown('
الخطوة 5: المراجعة النهائية
', unsafe_allow_html=True) + + project_info = self._get_current_project_info() + if not project_info: + st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") + if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): + st.session_state.pricing_step = "project_info" + st.rerun() + return + + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + + if not project_items: + st.warning("لا توجد بنود في جدول الكميات. يرجى العودة إلى الخطوة السابقة لإضافة البنود.") - # تحديث البيانات بعد التعديل - for i, material in enumerate(current_item["materials"]): - if i < len(edited_materials_df) - 1: # تجاهل صف المجموع - material["quantity"] = edited_materials_df.iloc[i]["الكمية"] - material["unit_price"] = edited_materials_df.iloc[i]["سعر الوحدة (ريال)"] - material["total_price"] = material["quantity"] * material["unit_price"] + col1, col2 = st.columns(2) + with col1: + if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq_btn"): + st.session_state.pricing_step = "boq" + st.rerun() + return + + # الحصول على آخر تسعير للمشروع + pricing_history = self.storage.get_project_pricing_history(st.session_state.current_project) + latest_pricing = pricing_history[-1] if pricing_history else None + + # عرض معلومات المشروع + st.subheader("معلومات المشروع") + + col1, col2 = st.columns(2) + + with col1: + st.write(f"**اسم المشروع:** {project_info['name']}") + st.write(f"**العميل:** {project_info['client']}") + st.write(f"**الموقع:** {project_info.get('location', '-')}") + st.write(f"**رقم المناقصة:** {project_info.get('tender_number', '-')}") + + with col2: + st.write(f"**القيمة التقديرية:** {project_info['estimated_value']:,.2f} ريال") + st.write(f"**الموعد النهائي:** {project_info['deadline']}") + st.write(f"**مدة العقد:** {project_info.get('contract_duration', '-')}") + st.write(f"**نوع التسعير:** {project_info['pricing_type']}") + + # عرض ملخص التسعير + st.subheader("ملخص التسعير") + + # حساب إجمالي التكلفة + total_cost = sum(item['total_price'] for item in project_items) + + if latest_pricing: + profit_percentage = latest_pricing['profit_percentage'] + profit_amount = latest_pricing['profit_amount'] + final_price = latest_pricing['final_price'] + else: + # استخدام قيم افتراضية إذا لم يكن هناك تسعير سابق + profit_percentage = 15 + profit_amount = total_cost * (profit_percentage / 100) + final_price = total_cost + profit_amount + + col1, col2, col3 = st.columns(3) + + with col1: + st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال") + + with col2: + st.metric("مبلغ الربح", f"{profit_amount:,.2f} ريال", f"{profit_percentage}%") + + with col3: + st.metric("السعر النهائي", f"{final_price:,.2f} ريال") + + # عرض جدول الكميات + st.subheader("جدول الكميات") + + # إنشاء DataFrame للعرض + df = pd.DataFrame([ + { + 'الكود': item['code'], + 'الوصف': item['description'], + 'الوحدة': item['unit'], + 'الكمية': item['quantity'], + 'سعر الوحدة': item['unit_price'], + 'السعر الإجمالي': item['total_price'], + 'نوع المورد': item.get('resource_type', '-') + } + for item in project_items + ]) + + # عرض الجدول + st.dataframe(df) + + # خيارات التصدير + st.subheader("تصدير البيانات") + + export_options = st.multiselect( + "اختر خيارات التصدير", + [ + "جدول الكميات (CSV)", + "جدول الكميات (Excel)", + "تقرير كامل (PDF)", + "تقرير المحتوى المحلي (Excel)", + "تقرير المخاطر (Excel)" + ], + default=["جدول الكميات (CSV)"], + key="export_options" + ) + + if st.button("تصدير البيانات", key="export_data_btn", type="primary"): + if "جدول الكميات (CSV)" in export_options: + # إنشاء ملف CSV للتصدير + csv_buffer = io.StringIO() + csv_writer = csv.writer(csv_buffer) + + # كتابة العناوين + csv_writer.writerow(['الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي', 'نوع المورد']) + + # كتابة البيانات + for item in project_items: + csv_writer.writerow([ + item['code'], + item['description'], + item['unit'], + item['quantity'], + item['unit_price'], + item['total_price'], + item.get('resource_type', '-') + ]) + + # تحويل البيانات إلى base64 + b64 = base64.b64encode(csv_buffer.getvalue().encode('utf-8')).decode() + href = f'تحميل CSV' + st.markdown(href, unsafe_allow_html=True) - # معالجة أزرار التعديل والحذف - for i in range(len(current_item["materials"])): - col1, col2 = st.columns([1, 1]) - with col1: - if st.button(f"تعديل المادة {i+1}", key=f"edit_material_btn_{i}"): - st.session_state[f"edit_material_{i}"] = True + if "جدول الكميات (Excel)" in export_options: + # إنشاء ملف Excel للتصدير + excel_file = export_to_excel( + project_items, + project_info, + f"/tmp/boq_{st.session_state.current_project}.xlsx" + ) - with col2: - if st.button(f"حذف المادة {i+1}", key=f"delete_material_btn_{i}"): - st.session_state[f"delete_material_{i}"] = True + # عرض رابط التنزيل + href = get_download_link( + excel_file, + "تحميل Excel", + "excel" + ) + st.markdown(href, unsafe_allow_html=True) + + if "تقرير كامل (PDF)" in export_options: + # إنشاء ملف PDF للتصدير + pdf_file = export_to_pdf( + project_items, + project_info, + f"/tmp/report_{st.session_state.current_project}.pdf" + ) - # نموذج التعديل - if st.session_state.get(f"edit_material_{i}", False): - with st.form(f"edit_material_form_{i}"): - material = current_item["materials"][i] - col1, col2 = st.columns(2) - with col1: - material_name = st.text_input("اسم المادة", value=material["name"]) - material_unit = st.text_input("الوحدة", value=material["unit"]) - with col2: - material_quantity = st.number_input("الكمية", min_value=0.0, step=0.1, value=material["quantity"]) - material_price = st.number_input("سعر الوحدة (ريال)", min_value=0.0, step=0.1, value=material["unit_price"]) - - col1, col2 = st.columns(2) - with col1: - if st.form_submit_button("حفظ التعديلات"): - material["name"] = material_name - material["unit"] = material_unit - material["quantity"] = float(material_quantity) - material["unit_price"] = float(material_price) - material["total_price"] = material["quantity"] * material["unit_price"] - st.session_state.item_analysis_edited = True - st.session_state[f"edit_material_{i}"] = False - st.rerun() - with col2: - if st.form_submit_button("إلغاء"): - st.session_state[f"edit_material_{i}"] = False - st.rerun() + # عرض رابط التنزيل + href = get_download_link( + pdf_file, + "تحميل PDF", + "pdf" + ) + st.markdown(href, unsafe_allow_html=True) + + if "تقرير المحتوى المحلي (Excel)" in export_options: + # إنشاء تقرير المحتوى المحلي + local_content_file = export_local_content_report( + project_items, + project_info, + f"/tmp/local_content_{st.session_state.current_project}.xlsx" + ) - # تأكيد الحذف - if st.session_state.get(f"delete_material_{i}", False): - st.warning(f"هل أنت متأكد من حذف المادة: {current_item['materials'][i]['name']}؟") - col1, col2 = st.columns(2) - with col1: - if st.button("نعم، حذف", key=f"confirm_delete_material_{i}"): - current_item["materials"].pop(i) - st.session_state.item_analysis_edited = True - st.session_state[f"delete_material_{i}"] = False - st.rerun() - with col2: - if st.button("إلغاء", key=f"cancel_delete_material_{i}"): - st.session_state[f"delete_material_{i}"] = False - st.rerun() + # عرض رابط التنزيل + href = get_download_link( + local_content_file, + "تحميل تقرير المحتوى المحلي", + "excel" + ) + st.markdown(href, unsafe_allow_html=True) - # عرض المجموع الكلي للمواد - st.markdown(f"
إجمالي تكلفة المواد: {materials_total:,.2f} ريال
", unsafe_allow_html=True) - else: - st.info("لا توجد مواد مضافة لهذا البند") + if "تقرير المخاطر (Excel)" in export_options: + # الحصول على مخاطر المشروع + project_risks = [risk for risk in st.session_state.risks if risk['project_id'] == st.session_state.current_project] + + if project_risks: + # إنشاء تقرير المخاطر + risk_report_file = export_risk_report( + project_risks, + project_info, + total_cost, + f"/tmp/risk_report_{st.session_state.current_project}.xlsx" + ) + + # عرض رابط التنزيل + href = get_download_link( + risk_report_file, + "تحميل تقرير المخاطر", + "excel" + ) + st.markdown(href, unsafe_allow_html=True) + else: + st.warning("لا توجد مخاطر مسجلة للمشروع. لا يمكن إنشاء تقرير المخاطر.") - # تحليل العمالة - st.subheader("تحليل العمالة") + # أزرار التنقل بين الخطوات + col1, col2, col3 = st.columns(3) + with col1: + if st.button("العودة إلى استراتيجيات التسعير ➡️", key="back_to_pricing_strategies"): + st.session_state.pricing_step = "pricing_strategies" + st.rerun() - # إضافة عمالة جديدة - with st.expander("إضافة عمالة جديدة"): - with st.form("add_labor_form"): - col1, col2 = st.columns(2) - with col1: - new_labor_name = st.text_input("نوع العمالة") - new_labor_unit = st.text_input("الوحدة", value="يوم") - with col2: - new_labor_quantity = st.number_input("عدد الأيام", min_value=0.0, step=0.5) - new_labor_price = st.number_input("الأجر اليومي (ريال)", min_value=0.0, step=10.0) - - submitted = st.form_submit_button("إضافة العمالة") - if submitted and new_labor_name: - new_labor = { - "id": f"labor_{len(current_item['labor']) + 1}", - "name": new_labor_name, - "unit": new_labor_unit, - "quantity": float(new_labor_quantity), - "unit_price": float(new_labor_price), - "total_price": float(new_labor_quantity) * float(new_labor_price) - } - current_item["labor"].append(new_labor) - st.session_state.item_analysis_edited = True - st.success("تمت إضافة العمالة بنجاح") + with col2: + if st.button("متابعة إلى المحتوى المحلي ⬅️", key="continue_to_local_content", type="primary"): + st.session_state.pricing_step = "local_content" + st.rerun() + + with col3: + if st.button("إنهاء وعودة للصفحة الرئيسية", key="go_to_home"): + st.session_state.pricing_step = "project_info" + st.rerun() + + st.markdown('
', unsafe_allow_html=True) + + def _render_local_content_step(self): + """عرض خطوة احتساب المحتوى المحلي""" + st.markdown('
', unsafe_allow_html=True) + st.markdown('
الخطوة 6: المحتوى المحلي
', unsafe_allow_html=True) + + project_info = self._get_current_project_info() + if not project_info: + st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") + if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): + st.session_state.pricing_step = "project_info" + st.rerun() + return + + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + + if not project_items: + st.warning("لا توجد بنود في جدول الكميات. يرجى العودة إلى الخطوة السابقة لإضافة البنود.") + + col1, col2 = st.columns(2) + with col1: + if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq_btn"): + st.session_state.pricing_step = "boq" st.rerun() + return + + # تعيين النسبة المستهدفة للمحتوى المحلي + local_content_target = st.slider( + "النسبة المستهدفة للمحتوى المحلي (%)", + min_value=0, + max_value=100, + value=project_info.get('local_content_target', 40), + step=5 + ) + + # تحديث النسبة المستهدفة في المشروع + if local_content_target != project_info.get('local_content_target', 40): + self.storage.update_project(project_info['id'], {'local_content_target': local_content_target}) + + # تحديث حالة الجلسة + for i, p in enumerate(st.session_state.projects): + if p['id'] == project_info['id']: + st.session_state.projects[i]['local_content_target'] = local_content_target + break + + # عرض جدول البنود مع معلومات المحتوى المحلي + st.subheader("تحديث معلومات المحتوى المحلي") + + # إنشاء DataFrame للعرض + df = pd.DataFrame([ + { + 'معرف': item['id'], + 'الكود': item['code'], + 'الوصف': item['description'], + 'نوع المورد': item.get('resource_type', '-'), + 'مورد محلي': item.get('is_local_supplier', False), + 'نسبة المحتوى المحلي (%)': item.get('local_content_percentage', 0), + 'السعر الإجمالي': item['total_price'] + } + for item in project_items + ]) + + # عرض الجدول + edited_df = st.data_editor( + df, + column_config={ + 'معرف': st.column_config.TextColumn(disabled=True), + 'الكود': st.column_config.TextColumn(disabled=True), + 'الوصف': st.column_config.TextColumn(disabled=True), + 'نوع المورد': st.column_config.TextColumn(disabled=True), + 'مورد محلي': st.column_config.CheckboxColumn( + "مورد محلي", + help="حدد إذا كان المورد محلي" + ), + 'نسبة المحتوى المحلي (%)': st.column_config.NumberColumn( + "نسبة المحتوى المحلي (%)", + min_value=0, + max_value=100, + step=5, + format="%d %%" + ), + 'السعر الإجمالي': st.column_config.NumberColumn( + "السعر الإجمالي", + format="%,.2f ريال", + disabled=True + ) + }, + hide_index=True, + use_container_width=True, + key="local_content_editor" + ) - # عرض جدول العمالة - if current_item["labor"]: - # تحويل البيانات إلى DataFrame - labor_data = [] - for i, labor in enumerate(current_item["labor"]): - labor_data.append({ - "م": i + 1, - "نوع العمالة": labor["name"], - "الوحدة": labor["unit"], - "عدد الأيام": labor["quantity"], - "الأجر اليومي (ريال)": labor["unit_price"], - "الإجمالي (ريال)": labor["total_price"], - "الإجراءات": f' ' + # تحديث معلومات المحتوى المحلي + if st.button("حفظ معلومات المحتوى المحلي", key="save_local_content"): + for i, row in edited_df.iterrows(): + item_id = row['معرف'] + is_local_supplier = row['مورد محلي'] + local_content_percentage = row['نسبة المحتوى المحلي (%)'] + + # تحديث البند + self.storage.update_boq_item(item_id, { + 'is_local_supplier': is_local_supplier, + 'local_content_percentage': local_content_percentage }) + + # تحديث حالة الجلسة + for j, item in enumerate(st.session_state.boq_items): + if item['id'] == item_id: + st.session_state.boq_items[j]['is_local_supplier'] = is_local_supplier + st.session_state.boq_items[j]['local_content_percentage'] = local_content_percentage + break + + st.success("تم حفظ معلومات المحتوى المحلي بنجاح") + + # حساب المحتوى المحلي + if st.button("حساب المحتوى المحلي", key="calculate_local_content"): + # تحديث بنود المشروع بعد التعديل + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] - labor_df = pd.DataFrame(labor_data) + # حساب ملخص المحتوى المحلي + local_content_summary = self._calculate_local_content_summary(project_info['id']) - # إضافة صف المجموع - labor_total = sum(labor["total_price"] for labor in current_item["labor"]) - total_row = pd.DataFrame([{ - "م": "", - "نوع العمالة": "المجموع", - "الوحدة": "", - "عدد الأيام": "", - "الأجر اليومي (ريال)": "", - "الإجمالي (ريال)": labor_total, - "الإجراءات": "" - }]) + # تحديث ملخص المحتوى المحلي في المشروع + self.storage.update_project(project_info['id'], {'local_content_summary': local_content_summary}) - # استخدام pd.concat بدلاً من append - labor_df = pd.concat([labor_df, total_row], ignore_index=True) + # تحديث حالة الجلسة + for i, p in enumerate(st.session_state.projects): + if p['id'] == project_info['id']: + st.session_state.projects[i]['local_content_summary'] = local_content_summary + break - # عرض الجدول القابل للتعديل - edited_labor_df = st.data_editor( - labor_df, - column_config={ - "م": st.column_config.NumberColumn("م", width="small"), - "نوع العمالة": st.column_config.TextColumn("نوع العمالة"), - "الوحدة": st.column_config.TextColumn("الوحدة", width="small"), - "عدد الأيام": st.column_config.NumberColumn("عدد الأيام", format="%.1f", width="small"), - "الأجر اليومي (ريال)": st.column_config.NumberColumn("الأجر اليومي (ريال)", format="%.2f"), - "الإجمالي (ريال)": st.column_config.NumberColumn("الإجمالي (ريال)", format="%.2f"), - "الإجراءات": st.column_config.TextColumn("الإجراءات", width="medium"), - }, - hide_index=True, - key="labor_table", - on_change=self._update_total_price, - disabled=["م", "نوع العمالة", "الوحدة", "الإجمالي (ريال)", "الإجراءات"] - ) + st.success("تم حساب المحتوى المحلي بنجاح") + st.rerun() + + # عرض ملخص المحتوى المحلي + st.subheader("ملخص المحتوى المحلي") + + # الحصول على ملخص المحتوى المحلي + local_content_summary = project_info.get('local_content_summary', { + 'total_percentage': 0, + 'by_category': { + 'materials': 0, + 'labor': 0, + 'services': 0, + 'equipment': 0 + } + }) + + # عرض النسبة الإجمالية للمحتوى المحلي + total_percentage = local_content_summary.get('total_percentage', 0) + + # تحديد لون المؤشر حسب النسبة المستهدفة + if total_percentage >= local_content_target: + delta_color = "normal" # أخضر + else: + delta_color = "inverse" # أحمر + + st.metric( + "النسبة الإجمالية للمحتوى المحلي", + f"{total_percentage:.1f}%", + f"{total_percentage - local_content_target:.1f}% من المستهدف", + delta_color=delta_color + ) + + # عرض النسب حسب الفئة + st.subheader("المحتوى المحلي حسب الفئة") + + by_category = local_content_summary.get('by_category', {}) + + col1, col2, col3, col4 = st.columns(4) + + with col1: + st.metric("المواد", f"{by_category.get('materials', 0):.1f}%") + + with col2: + st.metric("العمالة", f"{by_category.get('labor', 0):.1f}%") + + with col3: + st.metric("الخدمات", f"{by_category.get('services', 0):.1f}%") + + with col4: + st.metric("المعدات", f"{by_category.get('equipment', 0):.1f}%") + + # عرض الرسم البياني + if by_category: + fig, ax = plt.subplots(figsize=(10, 6)) + categories = list(by_category.keys()) + percentages = list(by_category.values()) - # تحديث البيانات بعد التعديل - for i, labor in enumerate(current_item["labor"]): - if i < len(edited_labor_df) - 1: # تجاهل صف المجموع - labor["quantity"] = edited_labor_df.iloc[i]["عدد الأيام"] - labor["unit_price"] = edited_labor_df.iloc[i]["الأجر اليومي (ريال)"] - labor["total_price"] = labor["quantity"] * labor["unit_price"] + ax.bar(categories, percentages) + ax.set_ylabel('النسبة المئوية (%)') + ax.set_title('المحتوى المحلي حسب الفئة') + ax.axhline(y=local_content_target, color='r', linestyle='-', label=f'المستهدف ({local_content_target}%)') + ax.legend() - # معالجة أزرار التعديل والحذف - for i in range(len(current_item["labor"])): - col1, col2 = st.columns([1, 1]) - with col1: - if st.button(f"تعديل العمالة {i+1}", key=f"edit_labor_btn_{i}"): - st.session_state[f"edit_labor_{i}"] = True - - with col2: - if st.button(f"حذف العمالة {i+1}", key=f"delete_labor_btn_{i}"): - st.session_state[f"delete_labor_{i}"] = True - - # نموذج التعديل - if st.session_state.get(f"edit_labor_{i}", False): - with st.form(f"edit_labor_form_{i}"): - labor = current_item["labor"][i] - col1, col2 = st.columns(2) - with col1: - labor_name = st.text_input("نوع العمالة", value=labor["name"]) - labor_unit = st.text_input("الوحدة", value=labor["unit"]) - with col2: - labor_quantity = st.number_input("عدد الأيام", min_value=0.0, step=0.5, value=labor["quantity"]) - labor_price = st.number_input("الأجر اليومي (ريال)", min_value=0.0, step=10.0, value=labor["unit_price"]) - - col1, col2 = st.columns(2) - with col1: - if st.form_submit_button("حفظ التعديلات"): - labor["name"] = labor_name - labor["unit"] = labor_unit - labor["quantity"] = float(labor_quantity) - labor["unit_price"] = float(labor_price) - labor["total_price"] = labor["quantity"] * labor["unit_price"] - st.session_state.item_analysis_edited = True - st.session_state[f"edit_labor_{i}"] = False - st.rerun() - with col2: - if st.form_submit_button("إلغاء"): - st.session_state[f"edit_labor_{i}"] = False - st.rerun() - - # تأكيد الحذف - if st.session_state.get(f"delete_labor_{i}", False): - st.warning(f"هل أنت متأكد من حذف العمالة: {current_item['labor'][i]['name']}؟") - col1, col2 = st.columns(2) - with col1: - if st.button("نعم، حذف", key=f"confirm_delete_labor_{i}"): - current_item["labor"].pop(i) - st.session_state.item_analysis_edited = True - st.session_state[f"delete_labor_{i}"] = False - st.rerun() - with col2: - if st.button("إلغاء", key=f"cancel_delete_labor_{i}"): - st.session_state[f"delete_labor_{i}"] = False - st.rerun() + st.pyplot(fig) + + # تصدير تقرير المحتوى المحلي + if st.button("تصدير تقرير المحتوى المحلي", key="export_local_content_report", type="primary"): + # إنشاء تقرير المحتوى المحلي + local_content_file = export_local_content_report( + project_items, + project_info, + f"/tmp/local_content_{st.session_state.current_project}.xlsx" + ) - # عرض المجموع الكلي للعمالة - st.markdown(f"
إجمالي تكلفة العمالة: {labor_total:,.2f} ريال
", unsafe_allow_html=True) - else: - st.info("لا توجد عمالة مضافة لهذا البند") + # عرض رابط التنزيل + href = get_download_link( + local_content_file, + "تحميل تقرير المحتوى المحلي", + "excel" + ) + st.markdown(href, unsafe_allow_html=True) - # تحليل المعدات - st.subheader("تحليل المعدات") + # أزرار التنقل بين الخطوات + col1, col2 = st.columns(2) + with col1: + if st.button("العودة إلى المراجعة النهائية ➡️", key="back_to_review"): + st.session_state.pricing_step = "review" + st.rerun() - # إضافة معدات جديدة - with st.expander("إضافة معدات جديدة"): - with st.form("add_equipment_form"): - col1, col2 = st.columns(2) - with col1: - new_equipment_name = st.text_input("نوع المعدات") - new_equipment_unit = st.text_input("الوحدة", value="يوم") - with col2: - new_equipment_quantity = st.number_input("عدد الأيام", min_value=0.0, step=0.5, key="new_equipment_quantity") - new_equipment_price = st.number_input("التكلفة اليومية (ريال)", min_value=0.0, step=10.0, key="new_equipment_price") - - submitted = st.form_submit_button("إضافة المعدات") - if submitted and new_equipment_name: - new_equipment = { - "id": f"equipment_{len(current_item['equipment']) + 1}", - "name": new_equipment_name, - "unit": new_equipment_unit, - "quantity": float(new_equipment_quantity), - "unit_price": float(new_equipment_price), - "total_price": float(new_equipment_quantity) * float(new_equipment_price) - } - current_item["equipment"].append(new_equipment) - st.session_state.item_analysis_edited = True - st.success("تمت إضافة المعدات بنجاح") + with col2: + if st.button("متابعة إلى تقييم المخاطر ⬅️", key="continue_to_risk_assessment", type="primary"): + st.session_state.pricing_step = "risk_assessment" + st.rerun() + + st.markdown('
', unsafe_allow_html=True) + + def _calculate_local_content_summary(self, project_id): + """حساب ملخص المحتوى المحلي للمشروع""" + # الحصول على بنود المشروع + project_items = [item for item in st.session_state.boq_items if item['project_id'] == project_id] + + if not project_items: + return { + 'total_percentage': 0, + 'by_category': { + 'materials': 0, + 'labor': 0, + 'services': 0, + 'equipment': 0 + } + } + + # حساب إجمالي التكلفة + total_cost = sum(item['total_price'] for item in project_items) + + if total_cost == 0: + return { + 'total_percentage': 0, + 'by_category': { + 'materials': 0, + 'labor': 0, + 'services': 0, + 'equipment': 0 + } + } + + # حساب المحتوى المحلي الإجمالي + local_content_value = sum(item['total_price'] * item.get('local_content_percentage', 0) / 100 for item in project_items) + total_percentage = local_content_value / total_cost * 100 + + # حساب المحتوى المحلي حسب الفئة + categories = {} + for category in ['materials', 'labor', 'services', 'equipment']: + category_items = [item for item in project_items if item.get('resource_type', '-').lower() == category] + + if category_items: + category_cost = sum(item['total_price'] for item in category_items) + category_local_content = sum(item['total_price'] * item.get('local_content_percentage', 0) / 100 for item in category_items) + category_percentage = category_local_content / category_cost * 100 if category_cost > 0 else 0 + categories[category] = category_percentage + else: + categories[category] = 0 + + # إنشاء ملخص المحتوى المحلي + summary = { + 'total_percentage': total_percentage, + 'by_category': categories + } + + return summary + + def _render_risk_assessment_step(self): + """عرض خطوة تقييم المخاطر""" + st.markdown('
', unsafe_allow_html=True) + st.markdown('
الخطوة 7: تقييم المخاطر
', unsafe_allow_html=True) + + project_info = self._get_current_project_info() + if not project_info: + st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") + if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): + st.session_state.pricing_step = "project_info" + st.rerun() + return + + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + + if not project_items: + st.warning("لا توجد بنود في جدول الكميات. يرجى العودة إلى الخطوة السابقة لإضافة البنود.") + + col1, col2 = st.columns(2) + with col1: + if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq_btn"): + st.session_state.pricing_step = "boq" st.rerun() + return - # عرض جدول المعدات - if current_item["equipment"]: - # تحويل البيانات إلى DataFrame - equipment_data = [] - for i, equipment in enumerate(current_item["equipment"]): - equipment_data.append({ - "م": i + 1, - "نوع المعدات": equipment["name"], - "الوحدة": equipment["unit"], - "عدد الأيام": equipment["quantity"], - "التكلفة اليومية (ريال)": equipment["unit_price"], - "الإجمالي (ريال)": equipment["total_price"], - "الإجراءات": f' ' - }) + # حساب إجمالي التكلفة + total_cost = sum(item['total_price'] for item in project_items) + + # الحصول على مخاطر المشروع الحالي + project_risks = [risk for risk in st.session_state.risks if risk['project_id'] == st.session_state.current_project] + + # استخدام علامات تبويب لتنظيم العرض + tabs = st.tabs(["ملخص المخاطر", "قائمة المخاطر", "إضافة مخاطرة جديدة", "تقرير المخاطر"]) + + with tabs[0]: + # عرض ملخص المخاطر + st.subheader("ملخص المخاطر") - equipment_df = pd.DataFrame(equipment_data) + # حساب ملخص المخاطر + risk_summary = self._calculate_risk_summary(project_info['id'], total_cost) - # إضافة صف المجموع - equipment_total = sum(equipment["total_price"] for equipment in current_item["equipment"]) - total_row = pd.DataFrame([{ - "م": "", - "نوع المعدات": "المجموع", - "الوحدة": "", - "عدد الأيام": "", - "التكلفة اليومية (ريال)": "", - "الإجمالي (ريال)": equipment_total, - "الإجراءات": "" - }]) + # عرض الإحصائيات الرئيسية + col1, col2, col3 = st.columns(3) - # استخدام pd.concat بدلاً من append - equipment_df = pd.concat([equipment_df, total_row], ignore_index=True) + with col1: + st.metric("إجمالي عدد المخاطر", risk_summary['total_risks']) - # عرض الجدول القابل للتعديل - edited_equipment_df = st.data_editor( - equipment_df, - column_config={ - "م": st.column_config.NumberColumn("م", width="small"), - "نوع المعدات": st.column_config.TextColumn("نوع المعدات"), - "الوحدة": st.column_config.TextColumn("الوحدة", width="small"), - "عدد الأيام": st.column_config.NumberColumn("عدد الأيام", format="%.1f", width="small"), - "التكلفة اليومية (ريال)": st.column_config.NumberColumn("التكلفة اليومية (ريال)", format="%.2f"), - "الإجمالي (ريال)": st.column_config.NumberColumn("الإجمالي (ريال)", format="%.2f"), - "الإجراءات": st.column_config.TextColumn("الإجراءات", width="medium"), - }, - hide_index=True, - key="equipment_table", - on_change=self._update_total_price, - disabled=["م", "نوع المعدات", "الوحدة", "الإجمالي (ريال)", "الإجراءات"] - ) + with col2: + st.metric("احتياطي المخاطر المقترح", f"{risk_summary['risk_contingency']:,.2f} ريال") - # تحديث البيانات بعد التعديل - for i, equipment in enumerate(current_item["equipment"]): - if i < len(edited_equipment_df) - 1: # تجاهل صف المجموع - equipment["quantity"] = edited_equipment_df.iloc[i]["عدد الأيام"] - equipment["unit_price"] = edited_equipment_df.iloc[i]["التكلفة اليومية (ريال)"] - equipment["total_price"] = equipment["quantity"] * equipment["unit_price"] + with col3: + st.metric("نسبة احتياطي المخاطر", f"{risk_summary['risk_contingency_percentage']:.1f}%") - # معالجة أزرار التعديل والحذف - for i in range(len(current_item["equipment"])): - col1, col2 = st.columns([1, 1]) - with col1: - if st.button(f"تعديل المعدات {i+1}", key=f"edit_equipment_btn_{i}"): - st.session_state[f"edit_equipment_{i}"] = True + # عرض توزيع المخاطر حسب الشدة + st.subheader("توزيع المخاطر حسب الشدة") + + col1, col2, col3 = st.columns(3) + + with col1: + st.metric("مخاطر عالية", risk_summary['high_risks'], delta_color="inverse") + + with col2: + st.metric("مخاطر متوسطة", risk_summary['medium_risks'], delta_color="off") + + with col3: + st.metric("مخاطر منخفضة", risk_summary['low_risks'], delta_color="normal") + + # عرض رسم بياني للمخاطر + if project_risks: + st.subheader("توزيع المخاطر حسب الفئة") - with col2: - if st.button(f"حذف المعدات {i+1}", key=f"delete_equipment_btn_{i}"): - st.session_state[f"delete_equipment_{i}"] = True + # تجميع المخاطر حسب الفئة + category_counts = {} + for risk in project_risks: + category = risk['category'] + if category in category_counts: + category_counts[category] += 1 + else: + category_counts[category] = 1 + + # إنشاء DataFrame للرسم البياني + chart_data = pd.DataFrame({ + "الفئة": list(category_counts.keys()), + "عدد المخاطر": list(category_counts.values()) + }) + + # عرض الرسم البياني + st.bar_chart(chart_data, x="الفئة", y="عدد المخاطر") - # نموذج التعديل - if st.session_state.get(f"edit_equipment_{i}", False): - with st.form(f"edit_equipment_form_{i}"): - equipment = current_item["equipment"][i] + # عرض مصفوفة المخاطر + st.subheader("مصفوفة المخاطر") + + # إنشاء مصفوفة المخاطر + risk_matrix = self._create_risk_matrix(project_risks) + + # عرض المصفوفة + st.dataframe( + risk_matrix, + column_config={ + "": st.column_config.TextColumn(""), + "1": st.column_config.TextColumn("1 - ضئيل"), + "2": st.column_config.TextColumn("2 - منخفض"), + "3": st.column_config.TextColumn("3 - متوسط"), + "4": st.column_config.TextColumn("4 - عالي"), + "5": st.column_config.TextColumn("5 - حرج") + }, + hide_index=True + ) + else: + st.info("لا توجد مخاطر مسجلة للمشروع. يرجى إضافة مخاطر لعرض التحليل.") + + with tabs[1]: + # عرض قائمة المخاطر + st.subheader("قائمة المخاطر") + + if project_risks: + # عرض جدول المخاطر + risk_df = pd.DataFrame([ + { + 'معرف': risk['id'], + 'اسم المخاطرة': risk['name'], + 'الفئة': risk['category'], + 'الاحتمالية': risk['probability'], + 'التأثير': risk['impact'], + 'درجة المخاطرة': risk['risk_score'], + 'التأثير المالي': risk['cost_impact'], + 'التأثير على الجدول': risk['schedule_impact'], + 'الحالة': risk['status'] + } + for risk in project_risks + ]) + + # تنسيق الجدول + st.dataframe( + risk_df, + column_config={ + 'معرف': st.column_config.NumberColumn(disabled=True), + 'اسم المخاطرة': st.column_config.TextColumn(), + 'الفئة': st.column_config.TextColumn(), + 'الاحتمالية': st.column_config.NumberColumn(format="%d"), + 'التأثير': st.column_config.NumberColumn(format="%d"), + 'درجة المخاطرة': st.column_config.NumberColumn(format="%d"), + 'التأثير المالي': st.column_config.NumberColumn(format="%,.2f ريال"), + 'التأثير على الجدول': st.column_config.NumberColumn(format="%d يوم"), + 'الحالة': st.column_config.TextColumn() + }, + hide_index=True, + use_container_width=True + ) + + # عرض تفاصيل المخاطر + st.subheader("تفاصيل المخاطر") + + # اختيار مخاطرة لعرض تفاصيلها + selected_risk_id = st.selectbox( + "اختر مخاطرة لعرض التفاصيل", + options=[risk['id'] for risk in project_risks], + format_func=lambda x: next((risk['name'] for risk in project_risks if risk['id'] == x), ""), + key="selected_risk_id" + ) + + # عرض تفاصيل المخاطرة المختارة + if selected_risk_id: + selected_risk = next((risk for risk in project_risks if risk['id'] == selected_risk_id), None) + + if selected_risk: col1, col2 = st.columns(2) + with col1: - equipment_name = st.text_input("نوع المعدات", value=equipment["name"]) - equipment_unit = st.text_input("الوحدة", value=equipment["unit"]) + st.markdown(f"**اسم المخاطرة:** {selected_risk['name']}") + st.markdown(f"**الفئة:** {selected_risk['category']}") + st.markdown(f"**الاحتمالية:** {selected_risk['probability']}") + st.markdown(f"**التأثير:** {selected_risk['impact']}") + st.markdown(f"**درجة المخاطرة:** {selected_risk['risk_score']}") + with col2: - equipment_quantity = st.number_input("عدد الأيام", min_value=0.0, step=0.5, value=equipment["quantity"]) - equipment_price = st.number_input("التكلفة اليومية (ريال)", min_value=0.0, step=10.0, value=equipment["unit_price"]) + st.markdown(f"**التأثير المالي:** {selected_risk['cost_impact']:,.2f} ريال") + st.markdown(f"**التأثير على الجدول الزمني:** {selected_risk['schedule_impact']} يوم") + st.markdown(f"**الحالة:** {selected_risk['status']}") + st.markdown(f"**تاريخ التحديث:** {selected_risk.get('updated_at', '-')}") + st.markdown(f"**الوصف:** {selected_risk.get('description', '-')}") + st.markdown(f"**خطة التخفيف:** {selected_risk.get('mitigation_plan', '-')}") + st.markdown(f"**خطة الطوارئ:** {selected_risk.get('contingency_plan', '-')}") + + # أزرار تعديل وحذف المخاطرة col1, col2 = st.columns(2) + with col1: - if st.form_submit_button("حفظ التعديلات"): - equipment["name"] = equipment_name - equipment["unit"] = equipment_unit - equipment["quantity"] = float(equipment_quantity) - equipment["unit_price"] = float(equipment_price) - equipment["total_price"] = equipment["quantity"] * equipment["unit_price"] - st.session_state.item_analysis_edited = True - st.session_state[f"edit_equipment_{i}"] = False + if st.button("تعديل المخاطرة", key="edit_risk_btn"): + st.session_state.edit_risk_id = selected_risk_id st.rerun() + with col2: - if st.form_submit_button("إلغاء"): - st.session_state[f"edit_equipment_{i}"] = False + if st.button("حذف المخاطرة", key="delete_risk_btn"): + # حذف المخاطرة + self.storage.delete_risk(selected_risk_id) + + # تحديث حالة الجلسة + st.session_state.risks = [r for r in st.session_state.risks if r['id'] != selected_risk_id] + + # إعادة حساب ملخص المخاطر + self._calculate_risk_summary(project_info['id'], total_cost) + + st.success(f"تم حذف المخاطرة '{selected_risk['name']}' بنجاح") st.rerun() - # تأكيد الحذف - if st.session_state.get(f"delete_equipment_{i}", False): - st.warning(f"هل أنت متأكد من حذف المعدات: {current_item['equipment'][i]['name']}؟") - col1, col2 = st.columns(2) - with col1: - if st.button("نعم، حذف", key=f"confirm_delete_equipment_{i}"): - current_item["equipment"].pop(i) - st.session_state.item_analysis_edited = True - st.session_state[f"delete_equipment_{i}"] = False - st.rerun() - with col2: - if st.button("إلغاء", key=f"cancel_delete_equipment_{i}"): - st.session_state[f"delete_equipment_{i}"] = False - st.rerun() - - # عرض المجموع الكلي للمعدات - st.markdown(f"
إجمالي تكلفة المعدات: {equipment_total:,.2f} ريال
", unsafe_allow_html=True) - else: - st.info("لا توجد معدات مضافة لهذا البند") - - # تحليل المقاولين من الباطن - st.subheader("تحليل المقاولين من الباطن") + # نموذج تعديل المخاطرة + if 'edit_risk_id' in st.session_state: + edit_risk_id = st.session_state.edit_risk_id + edit_risk = next((risk for risk in project_risks if risk['id'] == edit_risk_id), None) + + if edit_risk: + st.subheader(f"تعديل المخاطرة: {edit_risk['name']}") + + with st.form(key="edit_risk_form"): + # حقول النموذج + risk_name = st.text_input("اسم المخاطرة", value=edit_risk['name']) + risk_description = st.text_area("وصف المخاطرة", value=edit_risk.get('description', '')) + + col1, col2 = st.columns(2) + + with col1: + risk_category = st.selectbox( + "فئة المخاطرة", + options=["مالية", "فنية", "تعاقدية", "تنظيمية", "بيئية", "أمنية", "أخرى"], + index=["مالية", "فنية", "تعاقدية", "تنظيمية", "بيئية", "أمنية", "أخرى"].index(edit_risk['category']) if edit_risk['category'] in ["مالية", "فنية", "تعاقدية", "تنظيمية", "بيئية", "أمنية", "أخرى"] else 0 + ) + + risk_probability = st.slider( + "احتمالية حدوث المخاطرة", + min_value=1, + max_value=5, + value=edit_risk['probability'], + help="1: ضئيلة جداً، 5: شبه مؤكدة" + ) + + risk_impact = st.slider( + "تأثير المخاطرة", + min_value=1, + max_value=5, + value=edit_risk['impact'], + help="1: ضئيل جداً، 5: كارثي" + ) + + with col2: + risk_cost_impact = st.number_input( + "التأثير المالي (ريال)", + min_value=0.0, + value=float(edit_risk['cost_impact']), + step=1000.0 + ) + + risk_schedule_impact = st.number_input( + "التأثير على الجدول الزمني (أيام)", + min_value=0, + value=edit_risk['schedule_impact'], + step=1 + ) + + risk_status = st.selectbox( + "حالة المخاطرة", + options=["نشطة", "مغلقة", "تم التخفيف", "حدثت"], + index=["نشطة", "مغلقة", "تم التخفيف", "حدثت"].index(edit_risk['status']) if edit_risk['status'] in ["نشطة", "مغلقة", "تم التخفيف", "حدثت"] else 0 + ) + + risk_mitigation_plan = st.text_area("خطة التخفيف", value=edit_risk.get('mitigation_plan', '')) + risk_contingency_plan = st.text_area("خطة الطوارئ", value=edit_risk.get('contingency_plan', '')) + + # حساب درجة المخاطرة + risk_score = risk_probability * risk_impact + st.info(f"درجة المخاطرة: {risk_score}") + + # أزرار الإجراءات + col1, col2 = st.columns(2) + + with col1: + cancel_button = st.form_submit_button("إلغاء") + + with col2: + submit_button = st.form_submit_button("حفظ التغييرات") + + if cancel_button: + # إلغاء التعديل + del st.session_state.edit_risk_id + st.rerun() + + if submit_button: + # التحقق من صحة البيانات + if not risk_name: + st.error("يجب إدخال اسم المخاطرة") + else: + # تحديث المخاطرة + updated_risk = { + 'name': risk_name, + 'description': risk_description, + 'category': risk_category, + 'probability': risk_probability, + 'impact': risk_impact, + 'risk_score': risk_score, + 'cost_impact': risk_cost_impact, + 'schedule_impact': risk_schedule_impact, + 'status': risk_status, + 'mitigation_plan': risk_mitigation_plan, + 'contingency_plan': risk_contingency_plan + } + + # تحديث المخاطرة في التخزين + self.storage.update_risk(edit_risk_id, updated_risk) + + # تحديث حالة الجلسة + for i, r in enumerate(st.session_state.risks): + if r['id'] == edit_risk_id: + st.session_state.risks[i].update(updated_risk) + break + + # إعادة حساب ملخص المخاطر + self._calculate_risk_summary(project_info['id'], total_cost) + + # إزالة معرف التعديل + del st.session_state.edit_risk_id + + st.success(f"تم تحديث المخاطرة '{risk_name}' بنجاح") + st.rerun() + else: + st.info("لا توجد مخاطر مسجلة للمشروع. يرجى إضافة مخاطر باستخدام النموذج أدناه.") - # إضافة مقاول جديد - with st.expander("إضافة مقاول من الباطن"): - with st.form("add_subcontractor_form"): + with tabs[2]: + # نموذج إضافة مخاطرة جديدة + st.subheader("إضافة مخاطرة جديدة") + + with st.form(key="add_risk_form"): + # حقول النموذج + risk_name = st.text_input("اسم المخاطرة") + risk_description = st.text_area("وصف المخاطرة") + col1, col2 = st.columns(2) + with col1: - new_sub_name = st.text_input("اسم المقاول") - new_sub_work = st.text_input("نوع العمل") + risk_category = st.selectbox( + "فئة المخاطرة", + options=["مالية", "فنية", "تعاقدية", "تنظيمية", "بيئية", "أمنية", "أخرى"] + ) + + risk_probability = st.slider( + "احتمالية حدوث المخاطرة", + min_value=1, + max_value=5, + value=3, + help="1: ضئيلة جداً، 5: شبه مؤكدة" + ) + + risk_impact = st.slider( + "تأثير المخاطرة", + min_value=1, + max_value=5, + value=3, + help="1: ضئيل جداً، 5: كارثي" + ) + with col2: - new_sub_price = st.number_input("التكلفة الإجمالية (ريال)", min_value=0.0, step=100.0) + risk_cost_impact = st.number_input( + "التأثير المالي (ريال)", + min_value=0.0, + value=0.0, + step=1000.0 + ) + + risk_schedule_impact = st.number_input( + "التأثير على الجدول الزمني (أيام)", + min_value=0, + value=0, + step=1 + ) + + risk_status = st.selectbox( + "حالة المخاطرة", + options=["نشطة", "مغلقة", "تم التخفيف", "حدثت"], + index=0 + ) - submitted = st.form_submit_button("إضافة مقاول") - if submitted and new_sub_name and new_sub_work: - new_sub = { - "id": f"sub_{len(current_item['subcontractors']) + 1}", - "name": new_sub_name, - "work": new_sub_work, - "quantity": 1, - "unit_price": float(new_sub_price), - "total_price": float(new_sub_price) - } - current_item["subcontractors"].append(new_sub) - st.session_state.item_analysis_edited = True - st.success("تمت إضافة المقاول بنجاح") - st.rerun() + risk_mitigation_plan = st.text_area("خطة التخفيف") + risk_contingency_plan = st.text_area("خطة الطوارئ") + + # حساب درجة المخاطرة + risk_score = risk_probability * risk_impact + st.info(f"درجة المخاطرة: {risk_score}") + + # زر الإرسال + submit_button = st.form_submit_button("إضافة المخاطرة") + + if submit_button: + # التحقق من صحة البيانات + if not risk_name: + st.error("يجب إدخال اسم المخاطرة") + else: + # إنشاء معرف فريد + risk_id = f"risk_{len(st.session_state.risks) + 1}" + + # إنشاء كائن المخاطرة + new_risk = { + 'id': risk_id, + 'project_id': project_info['id'], + 'name': risk_name, + 'description': risk_description, + 'category': risk_category, + 'probability': risk_probability, + 'impact': risk_impact, + 'risk_score': risk_score, + 'cost_impact': risk_cost_impact, + 'schedule_impact': risk_schedule_impact, + 'status': risk_status, + 'mitigation_plan': risk_mitigation_plan, + 'contingency_plan': risk_contingency_plan + } + + # إضافة المخاطرة إلى التخزين + added_risk = self.storage.add_risk(new_risk) + + # تحديث حالة الجلسة + st.session_state.risks.append(added_risk) + + # إعادة حساب ملخص المخاطر + self._calculate_risk_summary(project_info['id'], total_cost) + + st.success(f"تم إضافة المخاطرة '{risk_name}' بنجاح") + st.rerun() - # عرض جدول المقاولين - if current_item["subcontractors"]: - # تحويل البيانات إلى DataFrame - subs_data = [] - for i, sub in enumerate(current_item["subcontractors"]): - subs_data.append({ - "م": i + 1, - "اسم المقاول": sub["name"], - "نوع العمل": sub["work"], - "التكلفة الإجمالية (ريال)": sub["total_price"], - "الإجراءات": f' ' - }) + with tabs[3]: + # تقرير المخاطر + st.subheader("تقرير المخاطر") - subs_df = pd.DataFrame(subs_data) - - # إضافة صف المجموع - subs_total = sum(sub["total_price"] for sub in current_item["subcontractors"]) - total_row = pd.DataFrame([{ - "م": "", - "اسم المقاول": "المجموع", - "نوع العمل": "", - "التكلفة الإجمالية (ريال)": subs_total, - "الإجراءات": "" - }]) - - # استخدام pd.concat بدلاً من append - subs_df = pd.concat([subs_df, total_row], ignore_index=True) - - # عرض الجدول القابل للتعديل - edited_subs_df = st.data_editor( - subs_df, - column_config={ - "م": st.column_config.NumberColumn("م", width="small"), - "اسم المقاول": st.column_config.TextColumn("اسم المقاول"), - "نوع العمل": st.column_config.TextColumn("نوع العمل"), - "التكلفة الإجمالية (ريال)": st.column_config.NumberColumn("التكلفة الإجمالية (ريال)", format="%.2f"), - "الإجراءات": st.column_config.TextColumn("الإجراءات", width="medium"), - }, - hide_index=True, - key="subs_table", - on_change=self._update_total_price, - disabled=["م", "اسم المقاول", "نوع العمل", "الإجراءات"] - ) - - # تحديث البيانات بعد التعديل - for i, sub in enumerate(current_item["subcontractors"]): - if i < len(edited_subs_df) - 1: # تجاهل صف المجموع - sub["total_price"] = edited_subs_df.iloc[i]["التكلفة الإجمالية (ريال)"] - sub["unit_price"] = sub["total_price"] # للمقاولين، سعر الوحدة = السعر الإجمالي - - # معالجة أزرار التعديل والحذف - for i in range(len(current_item["subcontractors"])): - col1, col2 = st.columns([1, 1]) - with col1: - if st.button(f"تعديل المقاول {i+1}", key=f"edit_sub_btn_{i}"): - st.session_state[f"edit_sub_{i}"] = True + if project_risks: + # عرض ملخص التقرير + risk_summary = project_info.get('risk_summary', self._calculate_risk_summary(project_info['id'], total_cost)) - with col2: - if st.button(f"حذف المقاول {i+1}", key=f"delete_sub_btn_{i}"): - st.session_state[f"delete_sub_{i}"] = True + st.markdown(f""" + ### ملخص تقرير المخاطر لمشروع: {project_info['name']} - # نموذج التعديل - if st.session_state.get(f"edit_sub_{i}", False): - with st.form(f"edit_sub_form_{i}"): - sub = current_item["subcontractors"][i] - col1, col2 = st.columns(2) - with col1: - sub_name = st.text_input("اسم المقاول", value=sub["name"]) - sub_work = st.text_input("نوع العمل", value=sub["work"]) - with col2: - sub_price = st.number_input("التكلفة الإجمالية (ريال)", min_value=0.0, step=100.0, value=sub["total_price"]) - - col1, col2 = st.columns(2) - with col1: - if st.form_submit_button("حفظ التعديلات"): - sub["name"] = sub_name - sub["work"] = sub_work - sub["total_price"] = float(sub_price) - sub["unit_price"] = sub["total_price"] - st.session_state.item_analysis_edited = True - st.session_state[f"edit_sub_{i}"] = False - st.rerun() - with col2: - if st.form_submit_button("إلغاء"): - st.session_state[f"edit_sub_{i}"] = False - st.rerun() + - **إجمالي عدد المخاطر:** {risk_summary['total_risks']} + - **المخاطر العالية:** {risk_summary['high_risks']} + - **المخاطر المتوسطة:** {risk_summary['medium_risks']} + - **المخاطر المنخفضة:** {risk_summary['low_risks']} + - **إجمالي التأثير المالي:** {risk_summary['total_cost_impact']:,.2f} ريال + - **إجمالي التأثير على الجدول الزمني:** {risk_summary['total_schedule_impact']} يوم + - **احتياطي المخاطر المقترح:** {risk_summary['risk_contingency']:,.2f} ريال ({risk_summary['risk_contingency_percentage']:.1f}% من التكلفة الإجمالية) + """) - # تأكيد الحذف - if st.session_state.get(f"delete_sub_{i}", False): - st.warning(f"هل أنت متأكد من حذف المقاول: {current_item['subcontractors'][i]['name']}؟") - col1, col2 = st.columns(2) - with col1: - if st.button("نعم، حذف", key=f"confirm_delete_sub_{i}"): - current_item["subcontractors"].pop(i) - st.session_state.item_analysis_edited = True - st.session_state[f"delete_sub_{i}"] = False - st.rerun() - with col2: - if st.button("إلغاء", key=f"cancel_delete_sub_{i}"): - st.session_state[f"delete_sub_{i}"] = False - st.rerun() - - # عرض المجموع الكلي للمقاولين - st.markdown(f"
إجمالي تكلفة المقاولين من الباطن: {subs_total:,.2f} ريال
", unsafe_allow_html=True) - else: - st.info("لا يوجد مقاولون من الباطن مضافون لهذا البند") + # عرض أعلى المخاطر + st.subheader("أعلى المخاطر") + + # ترتيب المخاطر حسب درجة المخاطرة + sorted_risks = sorted(project_risks, key=lambda x: x['risk_score'], reverse=True) + + for i, risk in enumerate(sorted_risks[:5]): # عرض أعلى 5 مخاطر + risk_color = "🔴" if risk['risk_score'] >= 15 else "🟠" if risk['risk_score'] >= 8 else "🟢" + st.markdown(f""" + {risk_color} **{risk['name']}** (درجة المخاطرة: {risk['risk_score']}) + - **الفئة:** {risk['category']} + - **الاحتمالية:** {risk['probability']}/5, **التأثير:** {risk['impact']}/5 + - **التأثير المالي:** {risk['cost_impact']:,.2f} ريال + - **خطة التخفيف:** {risk.get('mitigation_plan', '-')} + """) + + # زر تصدير التقرير + if st.button("تصدير تقرير المخاطر", key="export_risk_report_btn", type="primary"): + # إنشاء تقرير المخاطر + risk_report_file = export_risk_report( + project_risks, + project_info, + total_cost, + f"/tmp/risk_report_{st.session_state.current_project}.xlsx" + ) + + # عرض رابط التنزيل + href = get_download_link( + risk_report_file, + "تحميل تقرير المخاطر", + "excel" + ) + st.markdown(href, unsafe_allow_html=True) + else: + st.info("لا توجد مخاطر مسجلة للمشروع. يرجى إضافة مخاطر لإنشاء التقرير.") - # ملخص التكلفة - st.subheader("ملخص التكلفة") + # أزرار التنقل بين الخطوات + col1, col2 = st.columns(2) + with col1: + if st.button("العودة إلى المحتوى المحلي ➡️", key="back_to_local_content"): + st.session_state.pricing_step = "local_content" + st.rerun() - # حساب التكاليف - materials_total = sum(material.get("total_price", 0) for material in current_item["materials"]) - labor_total = sum(labor.get("total_price", 0) for labor in current_item["labor"]) - equipment_total = sum(equipment.get("total_price", 0) for equipment in current_item["equipment"]) - subcontractors_total = sum(sub.get("total_price", 0) for sub in current_item["subcontractors"]) + with col2: + if st.button("إنهاء وعودة للصفحة الرئيسية", key="go_to_home", type="primary"): + st.session_state.pricing_step = "project_info" + st.rerun() - direct_cost = materials_total + labor_total + equipment_total + subcontractors_total - overhead_profit = max(0, current_item["total_price"] - direct_cost) # تجنب القيم السالبة + st.markdown('
', unsafe_allow_html=True) + + def _calculate_risk_summary(self, project_id, total_cost): + """حساب ملخص المخاطر للمشروع""" + # الحصول على مخاطر المشروع + project_risks = [risk for risk in st.session_state.risks if risk['project_id'] == project_id] - # عرض ملخص التكلفة - st.markdown("
", unsafe_allow_html=True) + # حساب عدد المخاطر حسب الشدة + high_risks = sum(1 for risk in project_risks if risk['risk_score'] >= 15) + medium_risks = sum(1 for risk in project_risks if 8 <= risk['risk_score'] < 15) + low_risks = sum(1 for risk in project_risks if risk['risk_score'] < 8) - col1, col2 = st.columns(2) - with col1: - 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) - st.markdown("

المصاريف العمومية والربح:

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

إجمالي سعر البند:

", unsafe_allow_html=True) + # حساب إجمالي التأثير المالي والجدول الزمني + total_cost_impact = sum(risk['cost_impact'] for risk in project_risks) + total_schedule_impact = sum(risk['schedule_impact'] for risk in project_risks) - with col2: - st.markdown(f"

{materials_total:,.2f} ريال

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

{labor_total:,.2f} ريال

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

{equipment_total:,.2f} ريال

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

{subcontractors_total:,.2f} ريال

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

{direct_cost:,.2f} ريال

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

{overhead_profit:,.2f} ريال

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

{current_item['total_price']:,.2f} ريال

", unsafe_allow_html=True) - - st.markdown("
", unsafe_allow_html=True) - - # رسم بياني للتكاليف - if direct_cost > 0: - st.subheader("توزيع التكاليف") - - # إعداد البيانات للرسم البياني - labels = ['المواد', 'العمالة', 'المعدات', 'المقاولين', 'الربح'] - sizes = [materials_total, labor_total, equipment_total, subcontractors_total, overhead_profit] - colors = ['#ff9999', '#66b3ff', '#99ff99', '#ffcc99', '#c2c2f0'] - - # رسم الرسم البياني - fig, ax = plt.subplots(figsize=(10, 6)) - wedges, texts, autotexts = ax.pie( - sizes, - labels=[get_display(arabic_reshaper.reshape(label)) for label in labels], - autopct='%1.1f%%', - startangle=90, - colors=colors - ) + # حساب احتياطي المخاطر المقترح + # استخدام صيغة بسيطة: مجموع (احتمالية * تأثير مالي) لكل المخاطر + risk_contingency = sum(risk['probability'] / 5 * risk['cost_impact'] for risk in project_risks) + + # حساب نسبة احتياطي المخاطر من التكلفة الإجمالية + risk_contingency_percentage = (risk_contingency / total_cost * 100) if total_cost > 0 else 0 + + # إنشاء ملخص + summary = { + 'total_risks': len(project_risks), + 'high_risks': high_risks, + 'medium_risks': medium_risks, + 'low_risks': low_risks, + 'total_cost_impact': total_cost_impact, + 'total_schedule_impact': total_schedule_impact, + 'risk_contingency': risk_contingency, + 'risk_contingency_percentage': risk_contingency_percentage + } + + # تحديث ملخص المخاطر في المشروع + self.storage.update_project(project_id, {'risk_summary': summary}) + + # تحديث حالة الجلسة + for i, p in enumerate(st.session_state.projects): + if p['id'] == project_id: + st.session_state.projects[i]['risk_summary'] = summary + break + + return summary + + def _create_risk_matrix(self, risks): + """إنشاء مصفوفة المخاطر""" + # إنشاء مصفوفة فارغة + matrix = { + "": ["5 - شبه مؤكدة", "4 - محتملة", "3 - ممكنة", "2 - مستبعدة", "1 - نادرة"], + "1": ["", "", "", "", ""], + "2": ["", "", "", "", ""], + "3": ["", "", "", "", ""], + "4": ["", "", "", "", ""], + "5": ["", "", "", "", ""] + } + + # تجميع المخاطر حسب الاحتمالية والتأثير + risk_counts = {} + for risk in risks: + probability = risk['probability'] + impact = risk['impact'] + key = f"{probability}_{impact}" - # تنسيق النص - for text in texts: - text.set_fontsize(12) - for autotext in autotexts: - autotext.set_fontsize(10) - autotext.set_color('white') + if key in risk_counts: + risk_counts[key] += 1 + else: + risk_counts[key] = 1 + + # ملء المصفوفة + for key, count in risk_counts.items(): + probability, impact = map(int, key.split('_')) + row_index = 5 - probability # عكس الترتيب (5 في الأعلى) + col_key = str(impact) - ax.axis('equal') - plt.title(get_display(arabic_reshaper.reshape('توزيع تكاليف البند')), fontsize=16) + # تحديد لون الخلية بناءً على درجة المخاطرة + risk_score = probability * impact + if risk_score >= 15: + cell_value = f"🔴 {count}" # أحمر للمخاطر العالية + elif risk_score >= 8: + cell_value = f"🟠 {count}" # برتقالي للمخاطر المتوسطة + else: + cell_value = f"🟢 {count}" # أخضر للمخاطر المنخفضة - st.pyplot(fig) - - def _load_sample_projects(self): - """تحميل مشاريع نموذجية""" - return { - "proj_1": { - "id": "proj_1", - "name": "مشروع إنشاء مبنى سكني", - "client": "شركة الإعمار", - "date": "2025-06-24", - "budget": 901000000.00, - "items": [ - { - "id": "item_1", - "name": "أعمال الحفر", - "unit": "م3", - "quantity": 1500.0, - "unit_price": 35.0, - "total_price": 52500.0, - "materials": [ - { - "id": "material_1", - "name": "رمل", - "unit": "م3", - "quantity": 50.0, - "unit_price": 120.0, - "total_price": 6000.0 - } - ], - "labor": [ - { - "id": "labor_1", - "name": "عامل حفر", - "unit": "يوم", - "quantity": 30.0, - "unit_price": 150.0, - "total_price": 4500.0 - } - ], - "equipment": [ - { - "id": "equipment_1", - "name": "حفارة", - "unit": "يوم", - "quantity": 15.0, - "unit_price": 1200.0, - "total_price": 18000.0 - } - ], - "subcontractors": [] - }, - { - "id": "item_2", - "name": "أعمال الخرسانة", - "unit": "م3", - "quantity": 800.0, - "unit_price": 450.0, - "total_price": 360000.0, - "materials": [ - { - "id": "material_1", - "name": "أسمنت", - "unit": "طن", - "quantity": 120.0, - "unit_price": 600.0, - "total_price": 72000.0 - }, - { - "id": "material_2", - "name": "رمل", - "unit": "م3", - "quantity": 400.0, - "unit_price": 120.0, - "total_price": 48000.0 - }, - { - "id": "material_3", - "name": "حصى", - "unit": "م3", - "quantity": 600.0, - "unit_price": 150.0, - "total_price": 90000.0 - } - ], - "labor": [ - { - "id": "labor_1", - "name": "عامل خرسانة", - "unit": "يوم", - "quantity": 200.0, - "unit_price": 200.0, - "total_price": 40000.0 - }, - { - "id": "labor_2", - "name": "فني خرسانة", - "unit": "يوم", - "quantity": 50.0, - "unit_price": 350.0, - "total_price": 17500.0 - } - ], - "equipment": [ - { - "id": "equipment_1", - "name": "خلاطة خرسانة", - "unit": "يوم", - "quantity": 40.0, - "unit_price": 800.0, - "total_price": 32000.0 - }, - { - "id": "equipment_2", - "name": "مضخة خرسانة", - "unit": "يوم", - "quantity": 20.0, - "unit_price": 1500.0, - "total_price": 30000.0 - } - ], - "subcontractors": [] - }, - { - "id": "item_3", - "name": "أعمال البناء", - "unit": "م2", - "quantity": 2000.0, - "unit_price": 220.0, - "total_price": 440000.0, - "materials": [], - "labor": [], - "equipment": [], - "subcontractors": [ - { - "id": "sub_1", - "name": "مقاول بناء", - "work": "بناء جدران", - "quantity": 1, - "unit_price": 250000.0, - "total_price": 250000.0 - } - ] - } - ] - } - } + matrix[col_key][row_index] = cell_value + + # تحويل إلى DataFrame + return pd.DataFrame(matrix) -# تشغيل التطبيق عند تنفيذ الملف مباشرة +# تشغيل التطبيق if __name__ == "__main__": app = PricingApp() - app.run() + app.run() \ No newline at end of file